diff --git a/.github/actions/breaking-pr-check/action.yml b/.github/actions/breaking-pr-check/action.yml new file mode 100644 index 00000000000..f54443a4f43 --- /dev/null +++ b/.github/actions/breaking-pr-check/action.yml @@ -0,0 +1,6 @@ +name: Validate Breaking Change PR +description: Validate breaking change PR title and description + +runs: + using: node20 + main: index.js diff --git a/.github/actions/breaking-pr-check/index.js b/.github/actions/breaking-pr-check/index.js new file mode 100644 index 00000000000..f8e590c2f19 --- /dev/null +++ b/.github/actions/breaking-pr-check/index.js @@ -0,0 +1,66 @@ +const core = require('@actions/core'); +const github = require('@actions/github'); + +function raiseError(message) { + throw new Error(message); +} + +async function getPullRequest() { + const client = github.getOctokit(process.env.GITHUB_TOKEN); + + const pr = github.context.payload.pull_request; + if (!pr) { + throw new Error( + "This action can only be invoked in `pull_request_target` or `pull_request` events. Otherwise the pull request can't be inferred.", + ); + } + + const owner = pr.base.user.login; + const repo = pr.base.repo.name; + + const { data } = await client.rest.pulls.get({ + owner, + repo, + pull_number: pr.number, + }); + + return data; +} + +function checkTitle(title) { + if (/^[a-z]+(\([a-z-]+\))?!: /.test(title)) { + raiseError( + `Do not use exclamation mark ('!') to indicate breaking change in the PR Title.`, + ); + } +} + +function checkDescription(body, labels) { + if (!labels.some(label => label.name === 'breaking change')) { + return; + } + const [firstLine, secondLine] = body.split(/\r?\n/); + + if (!firstLine || !/^BREAKING CHANGE:/.test(firstLine)) { + raiseError( + `Breaking change PR body should start with "BREAKING CHANGE:". See https://typescript-eslint.io/maintenance/releases#2-merging-breaking-changes.`, + ); + } + if (!secondLine) { + raiseError( + `The description of breaking change is missing. See https://typescript-eslint.io/maintenance/releases#2-merging-breaking-changes.`, + ); + } +} + +async function run() { + const pullRequest = await getPullRequest(); + try { + checkTitle(pullRequest.title); + checkDescription(pullRequest.body, pullRequest.labels); + } catch (e) { + core.setFailed(e.message); + } +} + +run(); diff --git a/.github/workflows/semantic-breaking-change-pr.yml b/.github/workflows/semantic-breaking-change-pr.yml new file mode 100644 index 00000000000..dcae58270de --- /dev/null +++ b/.github/workflows/semantic-breaking-change-pr.yml @@ -0,0 +1,21 @@ +name: Semantic Breaking Change PR + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + - labeled + - unlabeled + +jobs: + main: + name: Validate Breaking Change PR + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare-install + - uses: ./.github/actions/breaking-pr-check + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}