From 0c2519397debe157698e1f50d5f0a8fab165c643 Mon Sep 17 00:00:00 2001 From: Gauthier Petetin Date: Fri, 28 Jul 2023 15:15:18 -0300 Subject: [PATCH] feat: github action to check if PR has requested labels before being merged (#19984) * feat(action): check if pr includes requested labels * fix(action): add missing QA label * fix(action): check if no label prevents merging * fix(action): remove QA label check * fix(action): add missing reopened condition * fix(action): increase list of labels which prevent merges --- .../scripts/check-pr-has-required-labels.ts | 97 +++++++++++++++++++ .github/workflows/check-pr-labels.yml | 38 ++++++++ .github/workflows/do-not-merge.yml | 17 ---- package.json | 3 +- 4 files changed, 137 insertions(+), 18 deletions(-) create mode 100644 .github/scripts/check-pr-has-required-labels.ts create mode 100644 .github/workflows/check-pr-labels.yml delete mode 100644 .github/workflows/do-not-merge.yml diff --git a/.github/scripts/check-pr-has-required-labels.ts b/.github/scripts/check-pr-has-required-labels.ts new file mode 100644 index 000000000..7e9ec3573 --- /dev/null +++ b/.github/scripts/check-pr-has-required-labels.ts @@ -0,0 +1,97 @@ +import * as core from '@actions/core'; +import { context, getOctokit } from '@actions/github'; +import { GitHub } from '@actions/github/lib/utils'; + +main().catch((error: Error): void => { + console.error(error); + process.exit(1); +}); + +async function main(): Promise { + // "GITHUB_TOKEN" is an automatically generated, repository-specific access token provided by GitHub Actions. + const githubToken = process.env.GITHUB_TOKEN; + if (!githubToken) { + core.setFailed('GITHUB_TOKEN not found'); + process.exit(1); + } + + // Initialise octokit, required to call Github GraphQL API + const octokit: InstanceType = getOctokit(githubToken); + + // Retrieve pull request info from context + const prRepoOwner = context.repo.owner; + const prRepoName = context.repo.repo; + const prNumber = context.payload.pull_request?.number; + if (!prNumber) { + core.setFailed('Pull request number not found'); + process.exit(1); + } + + // Retrieve pull request labels + const prLabels = await retrievePullRequestLabels(octokit, prRepoOwner, prRepoName, prNumber); + + const preventMergeLabels = ["needs-qa", "QA'd but questions", "issues-found", "need-ux-ds-review", "blocked", "stale", "DO-NOT-MERGE"]; + + let hasTeamLabel = false; + + // Check pull request has at least required QA label and team label + for (const label of prLabels) { + if (label.startsWith("team-") || label === "external-contributor") { + console.log(`PR contains a team label as expected: ${label}`); + hasTeamLabel = true; + } + if (preventMergeLabels.includes(label)) { + throw new Error(`PR cannot be merged because it still contains this label: ${label}`); + } + if (hasTeamLabel) { + return; + } + } + + // Otherwise, throw an arror to prevent from merging + let errorMessage = ''; + if (!hasTeamLabel) { + errorMessage += 'No team labels found on the PR. '; + } + errorMessage += 'Please add the required label(s) before merging the PR.'; + throw new Error(errorMessage); + +} + +// This function retrieves the pull request on a specific repo +async function retrievePullRequestLabels(octokit: InstanceType, repoOwner: string, repoName: string, prNumber: number): Promise { + + const retrievePullRequestLabelsQuery = ` + query RetrievePullRequestLabels($repoOwner: String!, $repoName: String!, $prNumber: Int!) { + repository(owner: $repoOwner, name: $repoName) { + pullRequest(number: $prNumber) { + labels(first: 100) { + nodes { + name + } + } + } + } + } + `; + + const retrievePullRequestLabelsResult: { + repository: { + pullRequest: { + labels: { + nodes: { + name: string; + }[]; + } + }; + }; + } = await octokit.graphql(retrievePullRequestLabelsQuery, { + repoOwner, + repoName, + prNumber, + }); + + const pullRequestLabels = retrievePullRequestLabelsResult?.repository?.pullRequest?.labels?.nodes?.map(labelObject => labelObject?.name); + + return pullRequestLabels || []; +} diff --git a/.github/workflows/check-pr-labels.yml b/.github/workflows/check-pr-labels.yml new file mode 100644 index 000000000..47de292ca --- /dev/null +++ b/.github/workflows/check-pr-labels.yml @@ -0,0 +1,38 @@ +name: "Check PR has required labels" +on: + pull_request: + branches: + - main + types: + - opened + - reopened + - synchronize + - labeled + - unlabeled + +jobs: + check-pr-labels: + runs-on: ubuntu-latest + permissions: + pull-requests: read + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 # This is needed to checkout all branches + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + cache: yarn + + - name: Install dependencies + run: yarn --immutable + + - name: Check PR has required labels + id: check-pr-has-required-labels + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npm run check-pr-has-required-labels diff --git a/.github/workflows/do-not-merge.yml b/.github/workflows/do-not-merge.yml deleted file mode 100644 index 1315a282b..000000000 --- a/.github/workflows/do-not-merge.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Fails the pull request if it has the "DO-NOT-MERGE" label - -name: Check "DO-NOT-MERGE" label - -on: - pull_request: - types: [opened, reopened, labeled, unlabeled, synchronize] - -jobs: - do-not-merge: - runs-on: ubuntu-latest - if: ${{ contains(github.event.pull_request.labels.*.name, 'DO-NOT-MERGE') }} - steps: - - name: 'Check for label "DO-NOT-MERGE"' - run: | - echo 'This check fails PRs with the "DO-NOT-MERGE" label to block merging' - exit 1 diff --git a/package.json b/package.json index 364305541..ca3fada5f 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,8 @@ "fitness-functions": "ts-node development/fitness-functions/index.ts", "generate-beta-commit": "node ./development/generate-beta-commit.js", "validate-branch-name": "validate-branch-name", - "add-release-label-to-pr-and-linked-issues": "ts-node ./.github/scripts/add-release-label-to-pr-and-linked-issues.ts" + "add-release-label-to-pr-and-linked-issues": "ts-node ./.github/scripts/add-release-label-to-pr-and-linked-issues.ts", + "check-pr-has-required-labels": "ts-node ./.github/scripts/check-pr-has-required-labels.ts" }, "resolutions": { "simple-update-notifier@^1.0.0": "^2.0.0",