diff --git a/.github/workflows/title-checker.yml b/.github/workflows/title-checker.yml new file mode 100644 index 00000000..ff3fef00 --- /dev/null +++ b/.github/workflows/title-checker.yml @@ -0,0 +1,99 @@ +name: PR Title Checker + +on: + pull_request_target: + types: [opened, edited, reopened] + +jobs: + check-title: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Validate PR Title and Apply Labels + uses: actions/github-script@v7 + with: + script: | + const title = context.payload.pull_request.title; + const { owner, repo } = context.repo; + const pr_number = context.payload.pull_request.number; + + const regex = /^([a-z-]+)(?:\(([^)]+)\))?(!?): ([A-Z].+)$/; + const match = title.match(regex); + + if (!match) { + core.setFailed("PR title does not follow format: 'type(scope)!: description'"); + return; + } + + const [_, type, scope, breaking, __] = match; + + const displayTitle = `${title} (#${pr_number})`; + const maxLength = 72; + + if (displayTitle.length > maxLength) { + core.setFailed( + `PR title is too long by ${displayTitle.length-maxLength} characters.` + ); + return; + } + + const repoLabels = await github.rest.issues.listLabelsForRepo({ + owner, + repo, + per_page: 100, + }); + + const labelNames = repoLabels.data.map(l => l.name); + const labelsToAdd = []; + + const typeLabel = `cc: ${type}`; + if (!labelNames.includes(typeLabel)) { + core.setFailed(`Invalid type: "${type}". No label "${typeLabel}" found in repo.`); + return; + } + labelsToAdd.push(typeLabel); + + if (scope) { + const scopeLabel = `package: ${scope}`; + if (!labelNames.includes(scopeLabel)) { + core.setFailed(`Invalid scope: "${scope}". No label "${scopeLabel}" found in repo.`); + return; + } + labelsToAdd.push(scopeLabel); + } + + if (breaking === '!') { + if (!labelNames.includes('breaking-change')) { + core.setFailed('No "breaking-change" label found in repo.'); + return; + } + labelsToAdd.push('breaking-change'); + } + + const prLabels = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: pr_number, + per_page: 100, + }); + const managedPrefixes = ['cc: ', 'package: ']; + const labelsToRemove = prLabels.data + .map(l => l.name) + .filter(n => managedPrefixes.some(p => n.startsWith(p)) || n === 'breaking-change'); + for (const label of labelsToRemove) { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: pr_number, + name: label + }); + } + + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: pr_number, + labels: labelsToAdd + }); + console.log(`Successfully added labels: ${labelsToAdd.join(', ')}`);