From 8df3bc9c1b0001b3ebc2773cb9913b13c117b534 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Mon, 8 May 2023 12:51:02 +0400 Subject: [PATCH] Label PRs based on the labels of the associated issue (#17603) * Implement CI to copy issue labels over to PRs * wip * wip * wip * wip * wip * wip * wip * wip * wip * clean up * clean up --- .github/scripts/label-prs.ts | 173 +++++++++++++++++++++++++ .github/workflows/label-prs.yml | 32 +++++ .husky/pre-push | 4 + .validate-branch-namerc.js | 11 ++ package.json | 8 +- yarn.lock | 221 ++++++++++++++++++++++++++++++++ 6 files changed, 448 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/label-prs.ts create mode 100644 .github/workflows/label-prs.yml create mode 100755 .husky/pre-push create mode 100644 .validate-branch-namerc.js diff --git a/.github/scripts/label-prs.ts b/.github/scripts/label-prs.ts new file mode 100644 index 000000000..ef4e790db --- /dev/null +++ b/.github/scripts/label-prs.ts @@ -0,0 +1,173 @@ +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 { + const token = process.env.GITHUB_TOKEN; + + if (!token) { + core.setFailed('GITHUB_TOKEN not found'); + process.exit(1); + } + + const octokit = getOctokit(token); + + const headRef = context.payload.pull_request?.head.ref || ''; + + let issueNumber = await getIssueNumberFromPullRequestBody(); + if (issueNumber === "") { + bailIfIsBranchNameInvalid(headRef); + bailIfIsNotFeatureBranch(headRef); + issueNumber = getIssueNumberFromBranchName(headRef); + } + + await updateLabels(octokit, issueNumber); +} + +async function getIssueNumberFromPullRequestBody(): Promise { + console.log("Checking if the PR's body references an issue..."); + + let ISSUE_LINK_IN_PR_DESCRIPTION_REGEX = + /(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s#\d+/gi; + + const prBody = await getPullRequestBody(); + + let matches = prBody.match(ISSUE_LINK_IN_PR_DESCRIPTION_REGEX); + if (!matches || matches?.length === 0) { + console.log( + 'No direct link can be drawn between the PR and an issue from the PR body because no issue number was referenced.', + ); + return ""; + } + + if (matches?.length > 1) { + console.log( + 'No direct link can be drawn between the PR and an issue from the PR body because more than one issue number was referenced.', + ); + return ""; + } + + const ISSUE_NUMBER_REGEX = /\d+/; + const issueNumber = matches[0].match(ISSUE_NUMBER_REGEX)?.[0] || ''; + + console.log(`Found issue number ${issueNumber} in PR body.`); + + return issueNumber; +} + +async function getPullRequestBody(): Promise { + if (context.eventName !== 'pull_request') { + console.log('This action should only run on pull_request events.'); + process.exit(1); + } + + const prBody = context.payload.pull_request?.body || ''; + return prBody; +} + +function bailIfIsBranchNameInvalid(branchName: string): void { + const BRANCH_REGEX = + /^(main|develop|(ci|chore|docs|feat|feature|fix|perf|refactor|revert|style)\/\d*(?:[-](?![-])\w*)*|Version-v\d+\.\d+\.\d+)$/; + const isValidBranchName = new RegExp(BRANCH_REGEX).test(branchName); + + if (!isValidBranchName) { + console.log('This branch name does not follow the convention.'); + console.log( + 'Here are some example branch names that are accepted: "fix/123-description", "feat/123-longer-description", "feature/123", "main", "develop", "Version-v10.24.2".', + ); + console.log( + 'No issue could be linked to this PR, so no labels were copied', + ); + + process.exit(0); + } +} + +function bailIfIsNotFeatureBranch(branchName: string): void { + if ( + branchName === 'main' || + branchName === 'develop' || + branchName.startsWith('Version-v') + ) { + console.log(`${branchName} is not a feature branch.`); + console.log( + 'No issue could be linked to this PR, so no labels were copied', + ); + process.exit(0); + } +} + +async function updateLabels(octokit: InstanceType, issueNumber: string): Promise { + interface ILabel { + name: string; + }; + + const owner = context.repo.owner; + const repo = context.repo.repo; + + const issue = await octokit.rest.issues.get({ + owner: owner, + repo: repo, + issue_number: Number(issueNumber), + }); + + const getNameFromLabel = (label: ILabel): string => label.name + + const issueLabels = issue.data.labels.map(label => getNameFromLabel(label as ILabel)); + + const prNumber = context.payload.number; + + const pr = await octokit.rest.issues.get({ + owner: owner, + repo: repo, + issue_number: prNumber, + }); + + const startingPRLabels = pr.data.labels.map(label => getNameFromLabel(label as ILabel)); + + const dedupedFinalPRLabels = [ + ...new Set([...startingPRLabels, ...issueLabels]), + ]; + + const hasIssueAdditionalLabels = !sortedArrayEqual( + startingPRLabels, + dedupedFinalPRLabels, + ); + if (hasIssueAdditionalLabels) { + await octokit.rest.issues.update({ + owner, + repo, + issue_number: prNumber, + labels: dedupedFinalPRLabels, + }); + } +} + +function getIssueNumberFromBranchName(branchName: string): string { + console.log('Checking if the branch name references an issue...'); + + let issueNumber: string; + if (branchName.split('/').length > 1) { + issueNumber = branchName.split('/')[1].split('-')[0]; + } else { + issueNumber = branchName.split('-')[0]; + } + + console.log(`Found issue number ${issueNumber} in branch name.`); + + return issueNumber; +} + +function sortedArrayEqual(array1: string[], array2: string[]): boolean { + const lengthsAreEqual = array1.length === array2.length; + const everyElementMatchesByIndex = array1.every( + (value: string, index: number): boolean => value === array2[index], + ); + + return lengthsAreEqual && everyElementMatchesByIndex; +} diff --git a/.github/workflows/label-prs.yml b/.github/workflows/label-prs.yml new file mode 100644 index 000000000..f915a881a --- /dev/null +++ b/.github/workflows/label-prs.yml @@ -0,0 +1,32 @@ +name: Label PR + +on: + pull_request: + types: [assigned, opened, edited, synchronize, reopened] + +jobs: + label-pr: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '16' + + - name: Install Yarn + run: npm install -g yarn + + - name: Install dependencies + run: yarn + + - name: Run PR labelling script + run: npm run label-prs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 000000000..bbf5399ef --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +yarn validate-branch-name \ No newline at end of file diff --git a/.validate-branch-namerc.js b/.validate-branch-namerc.js new file mode 100644 index 000000000..8685701d1 --- /dev/null +++ b/.validate-branch-namerc.js @@ -0,0 +1,11 @@ +const BRANCH_REGEX = + /^(main|develop|(ci|chore|docs|feat|feature|fix|perf|refactor|revert|style)\/\d*(?:[-](?![-])\w*)*|Version-v\d+\.\d+\.\d+)$/; + +const ERROR_MSG = + 'This branch name does not follow our conventions.' + + '\n' + + 'Rename it with "git branch -m "' + + '\n' + + 'Here are some example branch names that are accepted: "fix/123-description", "feat/123-longer-description", "feature/123", "main", "develop", "Version-v10.24.2".'; + +module.exports = { pattern: BRANCH_REGEX, errorMsg: ERROR_MSG }; diff --git a/package.json b/package.json index afaf7c54a..c33842858 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,9 @@ "test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn storybook:build && npx http-server storybook-build --port 6006 \" \"wait-on tcp:6006 && yarn test-storybook --maxWorkers=2\"", "githooks:install": "husky install", "fitness-functions": "ts-node development/fitness-functions/index.ts", - "generate-beta-commit": "node ./development/generate-beta-commit.js" + "generate-beta-commit": "node ./development/generate-beta-commit.js", + "validate-branch-name": "validate-branch-name", + "label-prs": "ts-node ./.github/scripts/label-prs.ts" }, "resolutions": { "analytics-node/axios": "^0.21.2", @@ -211,6 +213,8 @@ "request@^2.85.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch" }, "dependencies": { + "@actions/core": "^1.10.0", + "@actions/github": "^5.1.1", "@babel/runtime": "^7.5.5", "@download/blockies": "^1.0.3", "@ensdomains/content-hash": "^2.5.6", @@ -317,6 +321,7 @@ "fuse.js": "^3.2.0", "globalthis": "^1.0.1", "human-standard-token-abi": "^2.0.0", + "husky": "^8.0.3", "immer": "^9.0.6", "is-retry-allowed": "^2.2.0", "jest-junit": "^14.0.1", @@ -360,6 +365,7 @@ "unicode-confusables": "^0.1.1", "uuid": "^8.3.2", "valid-url": "^1.0.9", + "validate-branch-name": "^1.3.0", "web3-stream-provider": "^4.0.0", "zxcvbn": "^4.4.2" }, diff --git a/yarn.lock b/yarn.lock index 39656d679..69484f285 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,37 @@ __metadata: version: 6 cacheKey: 8 +"@actions/core@npm:^1.10.0": + version: 1.10.0 + resolution: "@actions/core@npm:1.10.0" + dependencies: + "@actions/http-client": ^2.0.1 + uuid: ^8.3.2 + checksum: 0a75621e007ab20d887434cdd165f0b9036f14c22252a2faed33543d8b9d04ec95d823e69ca636a25245574e4585d73e1e9e47a845339553c664f9f2c9614669 + languageName: node + linkType: hard + +"@actions/github@npm:^5.1.1": + version: 5.1.1 + resolution: "@actions/github@npm:5.1.1" + dependencies: + "@actions/http-client": ^2.0.1 + "@octokit/core": ^3.6.0 + "@octokit/plugin-paginate-rest": ^2.17.0 + "@octokit/plugin-rest-endpoint-methods": ^5.13.0 + checksum: 2210bd7f8e1e8b407b7df74a259523dc4c63f4ad3a6bfcc0d7867b6e9c3499bd3e25d7de7a9a1bbd0de3be441a8832d5c0b5c0cff3036cd477378c0ec5502434 + languageName: node + linkType: hard + +"@actions/http-client@npm:^2.0.1": + version: 2.1.0 + resolution: "@actions/http-client@npm:2.1.0" + dependencies: + tunnel: ^0.0.6 + checksum: 25a72a952cc95fb4b3ab086da73a5754dd0957c206637cace69be2e16f018cc1b3d3c40d3bcf89ffd8a5929d5e8445594b498b50db306a50ad7536023f8e3800 + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.1.0": version: 2.2.0 resolution: "@ampproject/remapping@npm:2.2.0" @@ -4692,6 +4723,116 @@ __metadata: languageName: node linkType: hard +"@octokit/auth-token@npm:^2.4.4": + version: 2.5.0 + resolution: "@octokit/auth-token@npm:2.5.0" + dependencies: + "@octokit/types": ^6.0.3 + checksum: 45949296c09abcd6beb4c3f69d45b0c1f265f9581d2a9683cf4d1800c4cf8259c2f58d58e44c16c20bffb85a0282a176c0d51f4af300e428b863f27b910e6297 + languageName: node + linkType: hard + +"@octokit/core@npm:^3.6.0": + version: 3.6.0 + resolution: "@octokit/core@npm:3.6.0" + dependencies: + "@octokit/auth-token": ^2.4.4 + "@octokit/graphql": ^4.5.8 + "@octokit/request": ^5.6.3 + "@octokit/request-error": ^2.0.5 + "@octokit/types": ^6.0.3 + before-after-hook: ^2.2.0 + universal-user-agent: ^6.0.0 + checksum: f81160129037bd8555d47db60cd5381637b7e3602ad70735a7bdf8f3d250c7b7114a666bb12ef7a8746a326a5d72ed30a1b8f8a5a170007f7285c8e217bef1f0 + languageName: node + linkType: hard + +"@octokit/endpoint@npm:^6.0.1": + version: 6.0.12 + resolution: "@octokit/endpoint@npm:6.0.12" + dependencies: + "@octokit/types": ^6.0.3 + is-plain-object: ^5.0.0 + universal-user-agent: ^6.0.0 + checksum: b48b29940af11c4b9bca41cf56809754bb8385d4e3a6122671799d27f0238ba575b3fde86d2d30a84f4dbbc14430940de821e56ecc6a9a92d47fc2b29a31479d + languageName: node + linkType: hard + +"@octokit/graphql@npm:^4.5.8": + version: 4.8.0 + resolution: "@octokit/graphql@npm:4.8.0" + dependencies: + "@octokit/request": ^5.6.0 + "@octokit/types": ^6.0.3 + universal-user-agent: ^6.0.0 + checksum: f68afe53f63900d4a16a0a733f2f500df2695b731f8ed32edb728d50edead7f5011437f71d069c2d2f6d656227703d0c832a3c8af58ecf82bd5dcc051f2d2d74 + languageName: node + linkType: hard + +"@octokit/openapi-types@npm:^12.11.0": + version: 12.11.0 + resolution: "@octokit/openapi-types@npm:12.11.0" + checksum: 8a7d4bd6288cc4085cabe0ca9af2b87c875c303af932cb138aa1b2290eb69d32407759ac23707bb02776466e671244a902e9857896903443a69aff4b6b2b0e3b + languageName: node + linkType: hard + +"@octokit/plugin-paginate-rest@npm:^2.17.0": + version: 2.21.3 + resolution: "@octokit/plugin-paginate-rest@npm:2.21.3" + dependencies: + "@octokit/types": ^6.40.0 + peerDependencies: + "@octokit/core": ">=2" + checksum: acf31de2ba4021bceec7ff49c5b0e25309fc3c009d407f153f928ddf436ab66cd4217344138378d5523f5fb233896e1db58c9c7b3ffd9612a66d760bc5d319ed + languageName: node + linkType: hard + +"@octokit/plugin-rest-endpoint-methods@npm:^5.13.0": + version: 5.16.2 + resolution: "@octokit/plugin-rest-endpoint-methods@npm:5.16.2" + dependencies: + "@octokit/types": ^6.39.0 + deprecation: ^2.3.1 + peerDependencies: + "@octokit/core": ">=3" + checksum: 30fcc50c335d1093f03573d9fa3a4b7d027fc98b215c43e07e82ee8dabfa0af0cf1b963feb542312ae32d897a2f68dc671577206f30850215517bebedc5a2c73 + languageName: node + linkType: hard + +"@octokit/request-error@npm:^2.0.5, @octokit/request-error@npm:^2.1.0": + version: 2.1.0 + resolution: "@octokit/request-error@npm:2.1.0" + dependencies: + "@octokit/types": ^6.0.3 + deprecation: ^2.0.0 + once: ^1.4.0 + checksum: baec2b5700498be01b4d958f9472cb776b3f3b0ea52924323a07e7a88572e24cac2cdf7eb04a0614031ba346043558b47bea2d346e98f0e8385b4261f138ef18 + languageName: node + linkType: hard + +"@octokit/request@npm:^5.6.0, @octokit/request@npm:^5.6.3": + version: 5.6.3 + resolution: "@octokit/request@npm:5.6.3" + dependencies: + "@octokit/endpoint": ^6.0.1 + "@octokit/request-error": ^2.1.0 + "@octokit/types": ^6.16.1 + is-plain-object: ^5.0.0 + node-fetch: ^2.6.7 + universal-user-agent: ^6.0.0 + checksum: c0b4542eb4baaf880d673c758d3e0b5c4a625a4ae30abf40df5548b35f1ff540edaac74625192b1aff42a79ac661e774da4ab7d5505f1cb4ef81239b1e8510c5 + languageName: node + linkType: hard + +"@octokit/types@npm:^6.0.3, @octokit/types@npm:^6.16.1, @octokit/types@npm:^6.39.0, @octokit/types@npm:^6.40.0": + version: 6.41.0 + resolution: "@octokit/types@npm:6.41.0" + dependencies: + "@octokit/openapi-types": ^12.11.0 + checksum: fd6f75e0b19b90d1a3d244d2b0c323ed8f2f05e474a281f60a321986683548ef2e0ec2b3a946aa9405d6092e055344455f69f58957c60f58368c8bdda5b7d2ab + languageName: node + linkType: hard + "@oozcitak/dom@npm:1.15.10": version: 1.15.10 resolution: "@oozcitak/dom@npm:1.15.10" @@ -9913,6 +10054,13 @@ __metadata: languageName: node linkType: hard +"babel-plugin-add-module-exports@npm:^0.2.1": + version: 0.2.1 + resolution: "babel-plugin-add-module-exports@npm:0.2.1" + checksum: 0d40e7b970161a10960fbbd0492565ae2f3f4872b4da089f8f5bb874cfac4e38e088e4d96bc33cef22a8963774abe84622feeebbbe049ad95c4ee33c907b277d + languageName: node + linkType: hard + "babel-plugin-add-react-displayname@npm:^0.0.5": version: 0.0.5 resolution: "babel-plugin-add-react-displayname@npm:0.0.5" @@ -10351,6 +10499,13 @@ __metadata: languageName: node linkType: hard +"before-after-hook@npm:^2.2.0": + version: 2.2.3 + resolution: "before-after-hook@npm:2.2.3" + checksum: a1a2430976d9bdab4cd89cb50d27fa86b19e2b41812bf1315923b0cba03371ebca99449809226425dd3bcef20e010db61abdaff549278e111d6480034bebae87 + languageName: node + linkType: hard + "better-opn@npm:^2.1.1": version: 2.1.1 resolution: "better-opn@npm:2.1.1" @@ -13247,6 +13402,17 @@ __metadata: languageName: node linkType: hard +"current-git-branch@npm:^1.1.0": + version: 1.1.0 + resolution: "current-git-branch@npm:1.1.0" + dependencies: + babel-plugin-add-module-exports: ^0.2.1 + execa: ^0.6.1 + is-git-repository: ^1.0.0 + checksum: 57042d5c9fc608a951e81310da1caa3bdf917d0fede06996bfd7eaab5bdee7d29ced3440c3c73e5d90e503e689e2084e1906b1a17723e35684b1579d1bb5a54f + languageName: node + linkType: hard + "cwd@npm:^0.10.0": version: 0.10.0 resolution: "cwd@npm:0.10.0" @@ -13774,6 +13940,13 @@ __metadata: languageName: node linkType: hard +"deprecation@npm:^2.0.0, deprecation@npm:^2.3.1": + version: 2.3.1 + resolution: "deprecation@npm:2.3.1" + checksum: f56a05e182c2c195071385455956b0c4106fe14e36245b00c689ceef8e8ab639235176a96977ba7c74afb173317fac2e0ec6ec7a1c6d1e6eaa401c586c714132 + languageName: node + linkType: hard + "deps-regex@npm:^0.1.4": version: 0.1.4 resolution: "deps-regex@npm:0.1.4" @@ -16173,6 +16346,21 @@ __metadata: languageName: node linkType: hard +"execa@npm:^0.6.1": + version: 0.6.3 + resolution: "execa@npm:0.6.3" + dependencies: + cross-spawn: ^5.0.1 + get-stream: ^3.0.0 + is-stream: ^1.1.0 + npm-run-path: ^2.0.0 + p-finally: ^1.0.0 + signal-exit: ^3.0.0 + strip-eof: ^1.0.0 + checksum: 2c66177731273a7c0a4c031af81b486b67ec1eeeb8f353ebc68e0cfe7f63aca9ebc1e6fe03ba10f130f2bd179c0ac69b35668fe2bfc1ceb68fbf5291d0783457 + languageName: node + linkType: hard + "execa@npm:^0.7.0": version: 0.7.0 resolution: "execa@npm:0.7.0" @@ -20036,6 +20224,16 @@ __metadata: languageName: node linkType: hard +"is-git-repository@npm:^1.0.0": + version: 1.1.1 + resolution: "is-git-repository@npm:1.1.1" + dependencies: + execa: ^0.6.1 + path-is-absolute: ^1.0.1 + checksum: 2873d41da9ae5771a9118bdd743f32fa868301c57e8e4d8e255d4e14c04267112294a29f2824531fa554696042d8d87185811cff2de2a06381cff7d61d9ac22d + languageName: node + linkType: hard + "is-glob@npm:^2.0.0, is-glob@npm:^2.0.1": version: 2.0.1 resolution: "is-glob@npm:2.0.1" @@ -23951,6 +24149,8 @@ __metadata: version: 0.0.0-use.local resolution: "metamask-crx@workspace:." dependencies: + "@actions/core": ^1.10.0 + "@actions/github": ^5.1.1 "@babel/code-frame": ^7.12.13 "@babel/core": ^7.12.1 "@babel/eslint-parser": ^7.13.14 @@ -24280,6 +24480,7 @@ __metadata: unicode-confusables: ^0.1.1 uuid: ^8.3.2 valid-url: ^1.0.9 + validate-branch-name: ^1.3.0 vinyl: ^2.2.1 vinyl-buffer: ^1.0.1 vinyl-source-stream: ^2.0.0 @@ -33597,6 +33798,13 @@ __metadata: languageName: node linkType: hard +"universal-user-agent@npm:^6.0.0": + version: 6.0.0 + resolution: "universal-user-agent@npm:6.0.0" + checksum: 5092bbc80dd0d583cef0b62c17df0043193b74f425112ea6c1f69bc5eda21eeec7a08d8c4f793a277eb2202ffe9b44bec852fa3faff971234cd209874d1b79ef + languageName: node + linkType: hard + "universalify@npm:^0.1.0, universalify@npm:^0.1.2": version: 0.1.2 resolution: "universalify@npm:0.1.2" @@ -33956,6 +34164,19 @@ __metadata: languageName: node linkType: hard +"validate-branch-name@npm:^1.3.0": + version: 1.3.0 + resolution: "validate-branch-name@npm:1.3.0" + dependencies: + commander: ^8.3.0 + cosmiconfig: ^7.0.1 + current-git-branch: ^1.1.0 + bin: + validate-branch-name: cli.js + checksum: be82c1e39bfe0519fa02f01670b5ef928903a19289249559c9148c0fd20df356f373f2490fbfd54d018868a6348cc2ceb82fb67d5f5c48c15ecb48735b9b87fb + languageName: node + linkType: hard + "validate-npm-package-license@npm:^3.0.1": version: 3.0.4 resolution: "validate-npm-package-license@npm:3.0.4"