From 29c8f99ee7acd10dbfeb514fcfbf5728f00670e2 Mon Sep 17 00:00:00 2001 From: Gauthier Petetin Date: Wed, 4 Oct 2023 06:55:59 +0200 Subject: [PATCH 1/4] feat(action): new action to indicate release version where bug was detected with a label The new bug report template makes it mandatory to indicate what release version the bug was detected in. We want to exploit that information by extracting the version number from issue body and indicate it by adding corresponding regression prod label on the issue. In addition to it, we also want to add a label to indicate when the bug has been reported by someone external to the MetaMask organisation. We will be able to leverage these two new labels to improve our bug triage process and our metrics collection. --- .../add-regression-prod-label-to-issue.ts | 431 ++++++++++++++++++ .../workflows/add-regression-prod-label.yml | 31 ++ package.json | 1 + 3 files changed, 463 insertions(+) create mode 100644 .github/scripts/add-regression-prod-label-to-issue.ts create mode 100644 .github/workflows/add-regression-prod-label.yml diff --git a/.github/scripts/add-regression-prod-label-to-issue.ts b/.github/scripts/add-regression-prod-label-to-issue.ts new file mode 100644 index 00000000000..497a11582f6 --- /dev/null +++ b/.github/scripts/add-regression-prod-label-to-issue.ts @@ -0,0 +1,431 @@ +import * as core from '@actions/core'; +import { context, getOctokit } from '@actions/github'; +import { GitHub } from '@actions/github/lib/utils'; + +// A labelable object can be a pull request or an issue +interface Labelable { + id: string; + number: number; + repoOwner: string; + repoName: string; + body: string; + author: string; + labels: { + id: string; + name: string; + }[]; +} + +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. + // We can't use "GITHUB_TOKEN" here, as its permissions don't allow neither to create new labels + // nor to retrieve the list of organisations a user belongs to. + // In our case, we may want to create "regression-prod-x.y.z" label when it doesn't already exist. + // We may also want to retrieve the list of organisations a user belongs to. + // As a consequence, we need to create our own "REGRESSION_PROD_LABEL_TOKEN" with "repo" and "read:org" permissions. + // Such a token allows both to create new labels and fetch user's list of organisations. + const personalAccessToken = process.env.REGRESSION_PROD_LABEL_TOKEN; + if (!personalAccessToken) { + core.setFailed('REGRESSION_PROD_LABEL_TOKEN not found'); + process.exit(1); + } + + // Retrieve pull request info from context + const issueRepoOwner = context.repo.owner; + const issueRepoName = context.repo.repo; + const issueNumber = context.payload.issue?.number; + if (!issueNumber) { + core.setFailed('Issue number not found'); + process.exit(1); + } + + // Initialise octokit, required to call Github GraphQL API + const octokit: InstanceType = getOctokit(personalAccessToken, { + previews: ['bane'], // The "bane" preview is required for adding, updating, creating and deleting labels. + }); + + // Retrieve issue + const issue: Labelable = await retrieveIssue( + octokit, + issueRepoOwner, + issueRepoName, + issueNumber, + ); + + // Retrieve issue's author list of organisations + const orgs: string[] = await retrieveUserOrgs(octokit, issue?.author); + + // Add external contributor label to the issue if author is not part of the MetaMask organisation + if (!orgs.includes('MetaMask')) { + // Craft external contributor label to add + const externalContributorLabelName = `external-contributor`; + const externalContributorLabelColor = 'B60205'; // red + const externalContributorLabelDescription = `Issue or PR created by user outside MetaMask organisation`; + + // Add external contributor label to the issue + await addLabelToLabelable( + octokit, + issue, + externalContributorLabelName, + externalContributorLabelColor, + externalContributorLabelDescription, + ); + } + + // Extract release version from issue body (is existing) + const releaseVersion = extractReleaseVersionFromIssueBody(issue.body); + + // Add regression prod label to the issue if release version was found is issue body + if (releaseVersion) { + // Craft regression prod label to add + const regressionProdLabelName = `regression-prod-${releaseVersion}`; + const regressionProdLabelColor = '5319E7'; // violet + const regressionProdLabelDescription = `Regression bug that was found in production in release ${releaseVersion}`; + + let regressionProdLabelFound: boolean = false; + const regressionProdLabelsToBeRemoved: { + id: string; + name: string; + }[] = []; + + // Loop over issue's labels, to see if regression labels are either missing, or to be removed + issue?.labels?.forEach((label) => { + if (label?.name === regressionProdLabelName) { + regressionProdLabelFound = true; + } else if (label?.name?.startsWith('regression-prod-')) { + regressionProdLabelsToBeRemoved.push(label); + } + }); + + // Add regression prod label to the issue if missing + if (regressionProdLabelFound) { + console.log( + `Issue ${issue?.number} already has ${regressionProdLabelName} label.`, + ); + } else { + console.log( + `Add ${regressionProdLabelName} label to issue ${issue?.number}.`, + ); + await addLabelToLabelable( + octokit, + issue, + regressionProdLabelName, + regressionProdLabelColor, + regressionProdLabelDescription, + ); + } + + // Remove other regression prod label from the issue + await Promise.all( + regressionProdLabelsToBeRemoved.map((label) => { + removeLabelFromLabelable(octokit, issue, label?.id); + }), + ); + } else { + console.log( + `No release version was found in body of issue ${issue?.number}.`, + ); + } +} + +// This helper function checks if issue's body has a bug report format. +function extractReleaseVersionFromIssueBody( + issueBody: string, +): string | undefined { + // Remove newline characters + const cleanedIssueBody = issueBody.replace(/\r?\n/g, ' '); + + // Extract version from the cleaned issue body + const regex = /### Version\s+((.*?)(?= |$))/; + const versionMatch = cleanedIssueBody.match(regex); + const version = versionMatch?.[1]; + + // Check if version is in the format x.y.z + if (version && !/^(\d+\.)?(\d+\.)?(\*|\d+)$/.test(version)) { + throw new Error('Version is not in the format x.y.z'); + } + + return version; +} + +// This function retrieves the repo +async function retrieveRepo( + octokit: InstanceType, + repoOwner: string, + repoName: string, +): Promise { + const retrieveRepoQuery = ` + query RetrieveRepo($repoOwner: String!, $repoName: String!) { + repository(owner: $repoOwner, name: $repoName) { + id + } + } +`; + + const retrieveRepoResult: { + repository: { + id: string; + }; + } = await octokit.graphql(retrieveRepoQuery, { + repoOwner, + repoName, + }); + + const repoId = retrieveRepoResult?.repository?.id; + + return repoId; +} + +// This function retrieves the label on a specific repo +async function retrieveLabel( + octokit: InstanceType, + repoOwner: string, + repoName: string, + labelName: string, +): Promise { + const retrieveLabelQuery = ` + query RetrieveLabel($repoOwner: String!, $repoName: String!, $labelName: String!) { + repository(owner: $repoOwner, name: $repoName) { + label(name: $labelName) { + id + } + } + } + `; + + const retrieveLabelResult: { + repository: { + label: { + id: string; + }; + }; + } = await octokit.graphql(retrieveLabelQuery, { + repoOwner, + repoName, + labelName, + }); + + const labelId = retrieveLabelResult?.repository?.label?.id; + + return labelId; +} + +// This function creates the label on a specific repo +async function createLabel( + octokit: InstanceType, + repoId: string, + labelName: string, + labelColor: string, + labelDescription: string, +): Promise { + const createLabelMutation = ` + mutation CreateLabel($repoId: ID!, $labelName: String!, $labelColor: String!, $labelDescription: String) { + createLabel(input: {repositoryId: $repoId, name: $labelName, color: $labelColor, description: $labelDescription}) { + label { + id + } + } + } + `; + + const createLabelResult: { + createLabel: { + label: { + id: string; + }; + }; + } = await octokit.graphql(createLabelMutation, { + repoId, + labelName, + labelColor, + labelDescription, + }); + + const labelId = createLabelResult?.createLabel?.label?.id; + + return labelId; +} + +// This function creates or retrieves the label on a specific repo +async function createOrRetrieveLabel( + octokit: InstanceType, + repoOwner: string, + repoName: string, + labelName: string, + labelColor: string, + labelDescription: string, +): Promise { + // Check if label already exists on the repo + let labelId = await retrieveLabel(octokit, repoOwner, repoName, labelName); + + // If label doesn't exist on the repo, create it + if (!labelId) { + // Retrieve PR's repo + const repoId = await retrieveRepo(octokit, repoOwner, repoName); + + // Create label on repo + labelId = await createLabel( + octokit, + repoId, + labelName, + labelColor, + labelDescription, + ); + } + + return labelId; +} + +// This function retrieves the issue on a specific repo +async function retrieveIssue( + octokit: InstanceType, + repoOwner: string, + repoName: string, + issueNumber: number, +): Promise { + const retrieveIssueQuery = ` + query GetIssue($repoOwner: String!, $repoName: String!, $issueNumber: Int!) { + repository(owner: $repoOwner, name: $repoName) { + issue(number: $issueNumber) { + id + body + author { + login + } + labels(first: 100) { + nodes { + id + name + } + } + } + } + } + `; + + const retrieveIssueResult: { + repository: { + issue: { + id: string; + body: string; + author: { + login: string; + }; + labels: { + nodes: { + id: string; + name: string; + }[]; + }; + }; + }; + } = await octokit.graphql(retrieveIssueQuery, { + repoOwner, + repoName, + issueNumber, + }); + + const issue: Labelable = { + id: retrieveIssueResult?.repository?.issue?.id, + number: issueNumber, + repoOwner: repoOwner, + repoName: repoName, + body: retrieveIssueResult?.repository?.issue?.body, + author: retrieveIssueResult?.repository?.issue?.author?.login, + labels: retrieveIssueResult?.repository?.issue?.labels?.nodes, + }; + + return issue; +} + +// This function adds label to a labelable object (i.e. a pull request or an issue) +async function addLabelToLabelable( + octokit: InstanceType, + labelable: Labelable, + labelName: string, + labelColor: string, + labelDescription: string, +): Promise { + // Retrieve label from the labelable's repo, or create label if required + const labelId = await createOrRetrieveLabel( + octokit, + labelable?.repoOwner, + labelable?.repoName, + labelName, + labelColor, + labelDescription, + ); + + const addLabelsToLabelableMutation = ` + mutation AddLabelsToLabelable($labelableId: ID!, $labelIds: [ID!]!) { + addLabelsToLabelable(input: {labelableId: $labelableId, labelIds: $labelIds}) { + clientMutationId + } + } + `; + + await octokit.graphql(addLabelsToLabelableMutation, { + labelableId: labelable?.id, + labelIds: [labelId], + }); +} + +// This function removes a label from a labelable object (i.e. a pull request or an issue) +async function removeLabelFromLabelable( + octokit: InstanceType, + labelable: Labelable, + labelId: string, +): Promise { + const removeLabelsFromLabelableMutation = ` + mutation RemoveLabelsFromLabelable($labelableId: ID!, $labelIds: [ID!]!) { + removeLabelsFromLabelable(input: {labelableId: $labelableId, labelIds: $labelIds}) { + clientMutationId + } + } + `; + + await octokit.graphql(removeLabelsFromLabelableMutation, { + labelableId: labelable?.id, + labelIds: [labelId], + }); +} + +// This function retrieves the list of organizations a specific user belongs to +async function retrieveUserOrgs( + octokit: InstanceType, + username: string, +): Promise { + const userOrgsQuery = ` + query UserOrgs($login: String!) { + user(login: $login) { + organizations(first: 100) { + nodes { + login + } + } + } + } + `; + + const retrieveUserOrgsResult: { + user: { + organizations: { + nodes: { + login: string; + }[]; + }; + }; + } = await octokit.graphql(userOrgsQuery, { login: username }); + + // Extract the organization logins from the result + const orgs = retrieveUserOrgsResult.user.organizations.nodes.map( + (node: { login: string }) => node.login, + ); + + return orgs; +} diff --git a/.github/workflows/add-regression-prod-label.yml b/.github/workflows/add-regression-prod-label.yml new file mode 100644 index 00000000000..eb1022cf026 --- /dev/null +++ b/.github/workflows/add-regression-prod-label.yml @@ -0,0 +1,31 @@ +name: Add regression prod label to issue, in case it is a production bug + +on: + issues: + types: + - opened + - edited + +jobs: + add-regression-prod-label: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 1 # This retrieves only the latest commit. + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + cache: yarn + + - name: Install dependencies + run: yarn --immutable + + - name: Add regression prod label to issue + id: add-regression-prod-label-to-issue + env: + REGRESSION_PROD_LABEL_TOKEN: ${{ secrets.REGRESSION_PROD_LABEL_TOKEN }} + run: npm run add-regression-prod-label-to-issue \ No newline at end of file diff --git a/package.json b/package.json index 787d5497145..227e9b83d05 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "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", "close-release-bug-report-issue": "ts-node ./.github/scripts/close-release-bug-report-issue.ts", + "add-regression-prod-label-to-issue": "ts-node ./.github/scripts/add-regression-prod-label-to-issue.ts", "patch:tx": "./scripts/patch-transaction-controller.sh", "storybook-generate": "sb-rn-get-stories", "storybook-watch": "sb-rn-watcher" From b7b9ea5ba95c5b1c3c02f64fa955417e63a69866 Mon Sep 17 00:00:00 2001 From: Gauthier Petetin Date: Thu, 5 Oct 2023 08:11:41 +0200 Subject: [PATCH 2/4] fix(action): check if issue validates issue template --- .../add-regression-prod-label-to-issue.ts | 257 +++++++++++++----- 1 file changed, 194 insertions(+), 63 deletions(-) diff --git a/.github/scripts/add-regression-prod-label-to-issue.ts b/.github/scripts/add-regression-prod-label-to-issue.ts index 497a11582f6..f46a33ddb2d 100644 --- a/.github/scripts/add-regression-prod-label-to-issue.ts +++ b/.github/scripts/add-regression-prod-label-to-issue.ts @@ -16,6 +16,47 @@ interface Labelable { }[]; } +// An enum, to categorise issues, based on template it matches +enum IssueType { + GeneralIssue, + BugReport, + None, +} + +// Titles of our two issues templates ('general-issue.yml' and 'bug-report.yml' issue) +const generalIssueTemplateTitles = [ + '### What is this about?', + '### Scenario', + '### Design', + '### Technical Details', + '### Threat Modeling Framework', + '### Acceptance Criteria', + '### References', +]; +const bugReportTemplateTitles = [ + '### Describe the bug', + '### Expected behavior', + '### Screenshots', + '### Steps to reproduce', + '### Error messages or log output', + '### Version', + '### Build type', + '### Device', + '### Operating system', + '### Additional context', + '### Severity', +]; + +// External contributor label +const externalContributorLabelName = `external-contributor`; +const externalContributorLabelColor = 'B60205'; // red +const externalContributorLabelDescription = `Issue or PR created by user outside MetaMask organisation`; + +// Craft invalid issue template label +const invalidIssueTemplateLabelName = `INVALID-ISSUE-TEMPLATE`; +const invalidIssueTemplateLabelColor = 'B60205'; // red +const invalidIssueTemplateLabelDescription = `Issue's body doesn't match any issue template.`; + main().catch((error: Error): void => { console.error(error); process.exit(1); @@ -57,79 +98,65 @@ async function main(): Promise { issueNumber, ); - // Retrieve issue's author list of organisations - const orgs: string[] = await retrieveUserOrgs(octokit, issue?.author); - - // Add external contributor label to the issue if author is not part of the MetaMask organisation - if (!orgs.includes('MetaMask')) { - // Craft external contributor label to add - const externalContributorLabelName = `external-contributor`; - const externalContributorLabelColor = 'B60205'; // red - const externalContributorLabelDescription = `Issue or PR created by user outside MetaMask organisation`; - - // Add external contributor label to the issue - await addLabelToLabelable( - octokit, - issue, - externalContributorLabelName, - externalContributorLabelColor, - externalContributorLabelDescription, - ); - } + // Add external contributor label to the issue, in case author is not part of the MetaMask organisation + await addExternalContributorLabel(octokit, issue); - // Extract release version from issue body (is existing) - const releaseVersion = extractReleaseVersionFromIssueBody(issue.body); + // Check if issue's body matches one of the two issues templates ('general-issue.yml' or 'bug-report.yml') + const issueType: IssueType = extractIssueTypeFromIssueBody(issue.body); - // Add regression prod label to the issue if release version was found is issue body - if (releaseVersion) { - // Craft regression prod label to add - const regressionProdLabelName = `regression-prod-${releaseVersion}`; - const regressionProdLabelColor = '5319E7'; // violet - const regressionProdLabelDescription = `Regression bug that was found in production in release ${releaseVersion}`; + if (issueType === IssueType.GeneralIssue) { + console.log("Issue matches 'general-issue.yml' template."); + await removeInvalidIssueTemplateLabelIfPresent(octokit, issue); + } else if (issueType === IssueType.BugReport) { + console.log("Issue matches 'bug-report.yml' template."); + await removeInvalidIssueTemplateLabelIfPresent(octokit, issue); - let regressionProdLabelFound: boolean = false; - const regressionProdLabelsToBeRemoved: { - id: string; - name: string; - }[] = []; - - // Loop over issue's labels, to see if regression labels are either missing, or to be removed - issue?.labels?.forEach((label) => { - if (label?.name === regressionProdLabelName) { - regressionProdLabelFound = true; - } else if (label?.name?.startsWith('regression-prod-')) { - regressionProdLabelsToBeRemoved.push(label); - } - }); + // Extract release version from issue body (is existing) + const releaseVersion = extractReleaseVersionFromIssueBody(issue.body); - // Add regression prod label to the issue if missing - if (regressionProdLabelFound) { - console.log( - `Issue ${issue?.number} already has ${regressionProdLabelName} label.`, - ); + // Add regression prod label to the issue if release version was found is issue body + if (releaseVersion) { + await addRegressionProdLabel(octokit, releaseVersion, issue); } else { console.log( - `Add ${regressionProdLabelName} label to issue ${issue?.number}.`, - ); - await addLabelToLabelable( - octokit, - issue, - regressionProdLabelName, - regressionProdLabelColor, - regressionProdLabelDescription, + `No release version was found in body of issue ${issue?.number}.`, ); } + } else { + const errorMessage = + "Issue body does not match any of expected templates ('general-issue.yml' or 'bug-report.yml')."; + console.log(errorMessage); - // Remove other regression prod label from the issue - await Promise.all( - regressionProdLabelsToBeRemoved.map((label) => { - removeLabelFromLabelable(octokit, issue, label?.id); - }), - ); + // Add invalid issue template label to the issue, in case issue doesn't match any template + await addInvalidIssueTemplateLabel(octokit, issue); + + // Github action shall fail in case issue doesn't match any template + throw new Error(errorMessage); + } +} + +// This helper function checks if issue's body matches one of the two issues templates ('general-issue.yml' or 'bug-report.yml'). +function extractIssueTypeFromIssueBody(issueBody: string): IssueType { + let missingGeneralIssueTitle: boolean = false; + for (const title of generalIssueTemplateTitles) { + if (!issueBody.includes(title)) { + missingGeneralIssueTitle = true; + } + } + + let missingBugReportTitle: boolean = false; + for (const title of bugReportTemplateTitles) { + if (!issueBody.includes(title)) { + missingBugReportTitle = true; + } + } + + if (!missingGeneralIssueTitle) { + return IssueType.GeneralIssue; + } else if (!missingBugReportTitle) { + return IssueType.BugReport; } else { - console.log( - `No release version was found in body of issue ${issue?.number}.`, - ); + return IssueType.None; } } @@ -153,6 +180,110 @@ function extractReleaseVersionFromIssueBody( return version; } +// This function adds the "external-contributor" label to the issue, in case author is not part of the MetaMask organisation +async function addExternalContributorLabel( + octokit: InstanceType, + issue: Labelable, +): Promise { + // Retrieve issue's author list of organisations + const orgs: string[] = await retrieveUserOrgs(octokit, issue?.author); + + // If author is not part of the MetaMask organisation + if (!orgs.includes('MetaMask')) { + // Add external contributor label to the issue + await addLabelToLabelable( + octokit, + issue, + externalContributorLabelName, + externalContributorLabelColor, + externalContributorLabelDescription, + ); + } +} + +// This function adds the correct "regression-prod-x.y.z" label to the issue, and removes other ones +async function addRegressionProdLabel( + octokit: InstanceType, + releaseVersion: string, + issue: Labelable, +): Promise { + // Craft regression prod label to add + const regressionProdLabelName = `regression-prod-${releaseVersion}`; + const regressionProdLabelColor = '5319E7'; // violet + const regressionProdLabelDescription = `Regression bug that was found in production in release ${releaseVersion}`; + + let regressionProdLabelFound: boolean = false; + const regressionProdLabelsToBeRemoved: { + id: string; + name: string; + }[] = []; + + // Loop over issue's labels, to see if regression labels are either missing, or to be removed + issue?.labels?.forEach((label) => { + if (label?.name === regressionProdLabelName) { + regressionProdLabelFound = true; + } else if (label?.name?.startsWith('regression-prod-')) { + regressionProdLabelsToBeRemoved.push(label); + } + }); + + // Add regression prod label to the issue if missing + if (regressionProdLabelFound) { + console.log( + `Issue ${issue?.number} already has ${regressionProdLabelName} label.`, + ); + } else { + console.log( + `Add ${regressionProdLabelName} label to issue ${issue?.number}.`, + ); + await addLabelToLabelable( + octokit, + issue, + regressionProdLabelName, + regressionProdLabelColor, + regressionProdLabelDescription, + ); + } + + // Remove other regression prod label from the issue + await Promise.all( + regressionProdLabelsToBeRemoved.map((label) => { + removeLabelFromLabelable(octokit, issue, label?.id); + }), + ); +} + +// This function adds the "INVALID-ISSUE-TEMPLATE" label to the issue +async function addInvalidIssueTemplateLabel( + octokit: InstanceType, + issue: Labelable, +): Promise { + // Add label to issue + await addLabelToLabelable( + octokit, + issue, + invalidIssueTemplateLabelName, + invalidIssueTemplateLabelColor, + invalidIssueTemplateLabelDescription, + ); +} + +// This function removes the "INVALID-ISSUE-TEMPLATE" label from the issue, in case it's present +async function removeInvalidIssueTemplateLabelIfPresent( + octokit: InstanceType, + issue: Labelable, +): Promise { + // Check if label is present on issue + const label = issue?.labels?.find( + (label) => label.name === invalidIssueTemplateLabelName, + ); + + if (label?.id) { + // Remove label from issue + await removeLabelFromLabelable(octokit, issue, label.id); + } +} + // This function retrieves the repo async function retrieveRepo( octokit: InstanceType, From 0048d47156af6e0986feefff79402ea8d02abbb0 Mon Sep 17 00:00:00 2001 From: Gauthier Petetin Date: Thu, 5 Oct 2023 08:50:56 +0200 Subject: [PATCH 3/4] fix(action): renaming files to better describe what github action aims to achieve The github action was initially only meant to add regression-prod labels while the scope is nox extended, which is why it's worth renaming a few things to reflect it. --- ...sue.ts => check-issue-template-and-add-labels.ts} | 6 +++--- ...l.yml => check-issue-template-and-add-labels.yml} | 12 ++++++------ package.json | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) rename .github/scripts/{add-regression-prod-label-to-issue.ts => check-issue-template-and-add-labels.ts} (98%) rename .github/workflows/{add-regression-prod-label.yml => check-issue-template-and-add-labels.yml} (61%) diff --git a/.github/scripts/add-regression-prod-label-to-issue.ts b/.github/scripts/check-issue-template-and-add-labels.ts similarity index 98% rename from .github/scripts/add-regression-prod-label-to-issue.ts rename to .github/scripts/check-issue-template-and-add-labels.ts index f46a33ddb2d..3a5ee43d1b9 100644 --- a/.github/scripts/add-regression-prod-label-to-issue.ts +++ b/.github/scripts/check-issue-template-and-add-labels.ts @@ -68,11 +68,11 @@ async function main(): Promise { // nor to retrieve the list of organisations a user belongs to. // In our case, we may want to create "regression-prod-x.y.z" label when it doesn't already exist. // We may also want to retrieve the list of organisations a user belongs to. - // As a consequence, we need to create our own "REGRESSION_PROD_LABEL_TOKEN" with "repo" and "read:org" permissions. + // As a consequence, we need to create our own "LABEL_TOKEN" with "repo" and "read:org" permissions. // Such a token allows both to create new labels and fetch user's list of organisations. - const personalAccessToken = process.env.REGRESSION_PROD_LABEL_TOKEN; + const personalAccessToken = process.env.LABEL_TOKEN; if (!personalAccessToken) { - core.setFailed('REGRESSION_PROD_LABEL_TOKEN not found'); + core.setFailed('LABEL_TOKEN not found'); process.exit(1); } diff --git a/.github/workflows/add-regression-prod-label.yml b/.github/workflows/check-issue-template-and-add-labels.yml similarity index 61% rename from .github/workflows/add-regression-prod-label.yml rename to .github/workflows/check-issue-template-and-add-labels.yml index eb1022cf026..11baaeafbc6 100644 --- a/.github/workflows/add-regression-prod-label.yml +++ b/.github/workflows/check-issue-template-and-add-labels.yml @@ -1,4 +1,4 @@ -name: Add regression prod label to issue, in case it is a production bug +name: Check issue template and add labels on: issues: @@ -23,9 +23,9 @@ jobs: - name: Install dependencies run: yarn --immutable - - - name: Add regression prod label to issue - id: add-regression-prod-label-to-issue + + - name: Check issue template and add labels + id: check-issue-template-and-add-labels env: - REGRESSION_PROD_LABEL_TOKEN: ${{ secrets.REGRESSION_PROD_LABEL_TOKEN }} - run: npm run add-regression-prod-label-to-issue \ No newline at end of file + LABEL_TOKEN: ${{ secrets.LABEL_TOKEN }} + run: npm run check-issue-template-and-add-labels diff --git a/package.json b/package.json index 227e9b83d05..343d8268a37 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "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", "close-release-bug-report-issue": "ts-node ./.github/scripts/close-release-bug-report-issue.ts", - "add-regression-prod-label-to-issue": "ts-node ./.github/scripts/add-regression-prod-label-to-issue.ts", + "check-issue-template-and-add-labels": "ts-node ./.github/scripts/check-issue-template-and-add-labels.ts", "patch:tx": "./scripts/patch-transaction-controller.sh", "storybook-generate": "sb-rn-get-stories", "storybook-watch": "sb-rn-watcher" From 3b6e5a1225a1ab7fdd41994475d61e66ccb88e29 Mon Sep 17 00:00:00 2001 From: Gauthier Petetin Date: Wed, 11 Oct 2023 17:23:10 -0300 Subject: [PATCH 4/4] fix(action): update implementation to support users with private profiles Nicholas Ellul raised that the script would not be able to retrieve list of organisations for users who have their GitHub set to private mode. As a consequence, these users would appear as external contributors while they are not. This commit introduces a different way to check if users belong to the MetaMask organisation, which allows to support even users with private profiles. --- .../check-issue-template-and-add-labels.ts | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/.github/scripts/check-issue-template-and-add-labels.ts b/.github/scripts/check-issue-template-and-add-labels.ts index 3a5ee43d1b9..f53d343371d 100644 --- a/.github/scripts/check-issue-template-and-add-labels.ts +++ b/.github/scripts/check-issue-template-and-add-labels.ts @@ -185,11 +185,8 @@ async function addExternalContributorLabel( octokit: InstanceType, issue: Labelable, ): Promise { - // Retrieve issue's author list of organisations - const orgs: string[] = await retrieveUserOrgs(octokit, issue?.author); - // If author is not part of the MetaMask organisation - if (!orgs.includes('MetaMask')) { + if (!(await userBelongsToMetaMaskOrg(octokit, issue?.author))) { // Add external contributor label to the issue await addLabelToLabelable( octokit, @@ -526,37 +523,28 @@ async function removeLabelFromLabelable( }); } -// This function retrieves the list of organizations a specific user belongs to -async function retrieveUserOrgs( +// This function checks if user belongs to MetaMask organization on Github +async function userBelongsToMetaMaskOrg( octokit: InstanceType, username: string, -): Promise { - const userOrgsQuery = ` - query UserOrgs($login: String!) { +): Promise { + const userBelongsToMetaMaskOrgQuery = ` + query UserBelongsToMetaMaskOrg($login: String!) { user(login: $login) { - organizations(first: 100) { - nodes { - login - } + organization(login: "MetaMask") { + id } } } `; - const retrieveUserOrgsResult: { + const userBelongsToMetaMaskOrgResult: { user: { - organizations: { - nodes: { - login: string; - }[]; + organization: { + id: string; }; }; - } = await octokit.graphql(userOrgsQuery, { login: username }); - - // Extract the organization logins from the result - const orgs = retrieveUserOrgsResult.user.organizations.nodes.map( - (node: { login: string }) => node.login, - ); + } = await octokit.graphql(userBelongsToMetaMaskOrgQuery, { login: username }); - return orgs; + return Boolean(userBelongsToMetaMaskOrgResult?.user?.organization?.id); }