diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml new file mode 100644 index 000000000..8bf18c4ac --- /dev/null +++ b/.github/workflows/validate-pr.yml @@ -0,0 +1,58 @@ +name: Validate PR + +on: + pull_request: + types: [opened, edited, synchronize, labeled, unlabeled] + +jobs: + validate: + runs-on: ubuntu-latest + permissions: + pull-requests: read + steps: + - name: Ensure PR has a valid type + uses: actions/github-script@v7 + with: + script: | + const title = context.payload.pull_request.title; + const prefixMatch = title.match(/^(\w+)(?:\([^)]*\))?(!)?:/); + const prefix = prefixMatch?.[1]?.toLowerCase(); + + const validPrefixes = [ + 'feat', 'fix', 'perf', 'test', 'docs', + 'build', 'ci', 'refactor', 'task', 'chore', 'style', + ]; + + const hasValidPrefix = validPrefixes.includes(prefix); + + const { data: labels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + }); + + const typeLabels = [ + '[Type] Enhancement', + '[Type] Bug', + '[Type] Performance', + '[Type] Automated Testing', + '[Type] Developer Documentation', + '[Type] Build Tooling', + '[Type] Task', + '[Type] Breaking Change', + '[Type] Regression', + ]; + + const hasTypeLabel = labels.some((l) => typeLabels.includes(l.name)); + + if (hasValidPrefix || hasTypeLabel) { + const method = hasValidPrefix ? 'Conventional Commits prefix' : 'type label'; + core.info(`✅ PR has a valid type via ${method}`); + return; + } + + core.setFailed( + 'PR must have either a Conventional Commits title prefix (e.g. feat:, fix:, docs:) ' + + 'or a [Type] label applied manually.\n\n' + + 'See docs/code/developer-workflows.md for details.' + );