name: PR Title Check on: pull_request: types: [opened, edited, synchronize, reopened] permissions: contents: read pull-requests: read jobs: check: name: Validate PR title runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | const { detectComponents, hasCoreChanges, hasDashboardChanges, hasGitHubActionsChanges, } = require('./.github/scripts/detect-tags.js'); const title = context.payload.pull_request.title; // Block titles starting with "word:" or "word(scope):" patterns const commitStylePattern = /^\w+(\(.*?\))?[!]?\s*:/; if (commitStylePattern.test(title)) { core.setFailed( `PR title should not start with a "prefix:" style format.\n` + `Please use the format: [component] Brief description\n` + `Example: [pn532] Add health checking and auto-reset` ); return; } // Get changed files to detect tags const files = await github.paginate(github.rest.pulls.listFiles, { owner: context.repo.owner, repo: context.repo.repo, pull_number: context.issue.number, }); const filenames = files.map(f => f.filename); // Detect tags from changed files using shared logic const tags = new Set(); for (const comp of detectComponents(filenames)) { tags.add(comp); } if (hasCoreChanges(filenames)) tags.add('core'); if (hasDashboardChanges(filenames)) tags.add('dashboard'); if (hasGitHubActionsChanges(filenames)) tags.add('ci'); if (tags.size === 0) { return; } // Check title starts with [tag] prefix const bracketPattern = /^\[\w+\]/; if (!bracketPattern.test(title)) { const suggestion = [...tags].map(c => `[${c}]`).join(''); // Skip if the suggested prefix would be too long for a readable title if (suggestion.length > 40) { return; } core.setFailed( `PR modifies: ${[...tags].join(', ')}\n` + `Title must start with a [tag] prefix.\n` + `Suggested: ${suggestion} ` ); }