name: PR Title Check on: pull_request: types: [opened, edited, synchronize, reopened] branches-ignore: - release - beta 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; const author = context.payload.pull_request.user.login; // Skip bot PRs (e.g. dependabot) - they have their own title format if (author === 'dependabot[bot]') { return; } // 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` ); 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 for angle brackets not wrapped in backticks. // Astro docs MDX treats bare < as JSX component opening tags. const stripped = title.replace(/`[^`]*`/g, ''); if (/[<>]/.test(stripped)) { core.setFailed( 'PR title contains `<` or `>` not wrapped in backticks.\n' + 'Astro docs MDX interprets bare `<` as JSX components.\n' + 'Please wrap angle brackets with backticks, e.g.: [component] Add `` support' ); 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} ` ); }