mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-24 19:10:22 +01:00
feat(action): github action to automatically add label "release-x.y.z" when PRs get merged (#19061)
* feat(action): github action to add release label when PR gets merged * feat(action): make sure the action only runs for PRs merged in main branch * fix(action): update labels default color * fix(action): add check on release label format * fix(action): type function explicitely * feat(action): add possibility to extract next release version number from artifact * fix(action): rename next rc cut number into next semver version * feat(action): add a github action to create release branch * fix(action): default branch is develop * fix(action): specify name of workflow used to create release branch * fix(action): handle case where artifact doesn't exist * fix(action): create branch but not the PR * feat(action): fetch next semver version from release branches name or from package.json * fix(action): remove unused Create Release Branch action * fix(action): release branch format was not correct * feat(action): take tags into account when calculating next version number * feat(action): add the possibility to force next semver version * fix(action): update comments * fix(action): adopt kebak-case instead of snake_case * fix(action): rename PERSONAL_ACCESS_TOKEN into RELEASE_LABEL_TOKEN * fix(action): yarn installation not required * fix(action): yarn install shall be immutable * fix(action): make the script compatible with ShellCheck * fix(script): exit script earlier if condition is met * fix(action): use closingIssuesReferences instead of timeline events * fix(action): add execute permissions to script * fix(action): remove duplicate comment
This commit is contained in:
parent
bce053f7ef
commit
3bbfe87e9e
328
.github/scripts/add-release-label-to-pr-and-linked-issues.ts
vendored
Normal file
328
.github/scripts/add-release-label-to-pr-and-linked-issues.ts
vendored
Normal file
@ -0,0 +1,328 @@
|
||||
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;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
main().catch((error: Error): void => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
async function main(): Promise<void> {
|
||||
// "GITHUB_TOKEN" is an automatically generated, repository-specific access token provided by GitHub Actions.
|
||||
// We can't use "GITHUB_TOKEN" here, as its permissions are scoped to the repository where the action is running.
|
||||
// "GITHUB_TOKEN" does not have access to other repositories, even when they belong to the same organization.
|
||||
// As we want to update linked issues which are not necessarily located in the same repository,
|
||||
// we need to create our own "RELEASE_LABEL_TOKEN" with "repo" permissions.
|
||||
// Such a token allows to access other repositories of the MetaMask organisation.
|
||||
const personalAccessToken = process.env.RELEASE_LABEL_TOKEN;
|
||||
if (!personalAccessToken) {
|
||||
core.setFailed('RELEASE_LABEL_TOKEN not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const nextReleaseVersionNumber = process.env.NEXT_SEMVER_VERSION;
|
||||
if (!nextReleaseVersionNumber) {
|
||||
// NEXT_SEMVER_VERSION is automatically deduced as minor version bump on top of the latest version
|
||||
// found, either in repo's list of branches, or in repo's list of tags or in repo's "package.json" version.
|
||||
// For edge cases (e.g. major version bumps, etc.), where the value can not be deduced automatically,
|
||||
// NEXT_SEMVER_VERSION can be defined manually set by defining FORCE_NEXT_SEMVER_VERSION variable in
|
||||
// section "Secrets and variables">"Actions">"Variables">"New repository variable" in the settings of this repo.
|
||||
// Example value: 6.5.0
|
||||
core.setFailed('NEXT_SEMVER_VERSION not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!isValidVersionFormat(nextReleaseVersionNumber)) {
|
||||
core.setFailed(`NEXT_SEMVER_VERSION (${nextReleaseVersionNumber}) is not a valid version format. The expected format is "x.y.z", where "x", "y" and "z" are numbers.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Release label indicates the next release version number
|
||||
// Example release label: "release-6.5.0"
|
||||
const releaseLabelName = `release-${nextReleaseVersionNumber}`;
|
||||
const releaseLabelColor = "ededed";
|
||||
const releaseLabelDescription = `Issue or pull request that will be included in release ${nextReleaseVersionNumber}`;
|
||||
|
||||
// Initialise octokit, required to call Github GraphQL API
|
||||
const octokit: InstanceType<typeof GitHub> = getOctokit(
|
||||
personalAccessToken,
|
||||
{
|
||||
previews: ["bane"], // The "bane" preview is required for adding, updating, creating and deleting labels.
|
||||
},
|
||||
);
|
||||
|
||||
// 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
|
||||
const pullRequest: Labelable = await retrievePullRequest(octokit, prRepoOwner, prRepoName, prNumber);
|
||||
|
||||
// Add the release label to the pull request
|
||||
await addLabelToLabelable(octokit, pullRequest, releaseLabelName, releaseLabelColor, releaseLabelDescription);
|
||||
|
||||
// Retrieve linked issues for the pull request
|
||||
const linkedIssues: Labelable[] = await retrieveLinkedIssues(octokit, prRepoOwner, prRepoName, prNumber);
|
||||
|
||||
// Add the release label to the linked issues
|
||||
for (const linkedIssue of linkedIssues) {
|
||||
await addLabelToLabelable(octokit, linkedIssue, releaseLabelName, releaseLabelColor, releaseLabelDescription);
|
||||
}
|
||||
}
|
||||
|
||||
// This helper function checks if version has the correct format: "x.y.z" where "x", "y" and "z" are numbers.
|
||||
function isValidVersionFormat(str: string): boolean {
|
||||
const regex = /^\d+\.\d+\.\d+$/;
|
||||
return regex.test(str);
|
||||
}
|
||||
|
||||
// This function retrieves the repo
|
||||
async function retrieveRepo(octokit: InstanceType<typeof GitHub>, repoOwner: string, repoName: string): Promise<string> {
|
||||
|
||||
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<typeof GitHub>, repoOwner: string, repoName: string, labelName: string): Promise<string> {
|
||||
|
||||
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<typeof GitHub>, repoId: string, labelName: string, labelColor: string, labelDescription: string): Promise<string> {
|
||||
|
||||
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<typeof GitHub>, repoOwner: string, repoName: string, labelName: string, labelColor: string, labelDescription: string): Promise<string> {
|
||||
|
||||
// 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 pull request on a specific repo
|
||||
async function retrievePullRequest(octokit: InstanceType<typeof GitHub>, repoOwner: string, repoName: string, prNumber: number): Promise<Labelable> {
|
||||
|
||||
const retrievePullRequestQuery = `
|
||||
query GetPullRequest($repoOwner: String!, $repoName: String!, $prNumber: Int!) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
pullRequest(number: $prNumber) {
|
||||
id
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const retrievePullRequestResult: {
|
||||
repository: {
|
||||
pullRequest: {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
};
|
||||
};
|
||||
} = await octokit.graphql(retrievePullRequestQuery, {
|
||||
repoOwner,
|
||||
repoName,
|
||||
prNumber,
|
||||
});
|
||||
|
||||
const pullRequest: Labelable = {
|
||||
id: retrievePullRequestResult?.repository?.pullRequest?.id,
|
||||
number: prNumber,
|
||||
repoOwner: repoOwner,
|
||||
repoName: repoName,
|
||||
createdAt: retrievePullRequestResult?.repository?.pullRequest?.createdAt,
|
||||
}
|
||||
|
||||
return pullRequest;
|
||||
}
|
||||
|
||||
|
||||
// This function retrieves the list of linked issues for a pull request
|
||||
async function retrieveLinkedIssues(octokit: InstanceType<typeof GitHub>, repoOwner: string, repoName: string, prNumber: number): Promise<Labelable[]> {
|
||||
|
||||
// We assume there won't be more than 100 linked issues
|
||||
const retrieveLinkedIssuesQuery = `
|
||||
query ($repoOwner: String!, $repoName: String!, $prNumber: Int!) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
pullRequest(number: $prNumber) {
|
||||
closingIssuesReferences(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
number
|
||||
createdAt
|
||||
repository {
|
||||
name
|
||||
owner {
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const retrieveLinkedIssuesResult: {
|
||||
repository: {
|
||||
pullRequest: {
|
||||
closingIssuesReferences: {
|
||||
nodes: Array<{
|
||||
id: string;
|
||||
number: number;
|
||||
createdAt: string;
|
||||
repository: {
|
||||
name: string;
|
||||
owner: {
|
||||
login: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
} = await octokit.graphql(retrieveLinkedIssuesQuery, {
|
||||
repoOwner,
|
||||
repoName,
|
||||
prNumber
|
||||
});
|
||||
|
||||
const linkedIssues = retrieveLinkedIssuesResult?.repository?.pullRequest?.closingIssuesReferences?.nodes?.map((issue: {
|
||||
id: string;
|
||||
number: number;
|
||||
createdAt: string;
|
||||
repository: {
|
||||
name: string;
|
||||
owner: {
|
||||
login: string;
|
||||
};
|
||||
};
|
||||
}) => {
|
||||
return {
|
||||
id: issue?.id,
|
||||
number: issue?.number,
|
||||
repoOwner: issue?.repository?.owner?.login,
|
||||
repoName: issue?.repository?.name,
|
||||
createdAt: issue?.createdAt
|
||||
};
|
||||
}) || [];
|
||||
|
||||
return linkedIssues;
|
||||
}
|
||||
|
||||
// This function adds label to a labelable object (i.e. a pull request or an issue)
|
||||
async function addLabelToLabelable(octokit: InstanceType<typeof GitHub>, labelable: Labelable, labelName: string, labelColor: string, labelDescription: string): Promise<void> {
|
||||
|
||||
// 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],
|
||||
});
|
||||
|
||||
}
|
42
.github/workflows/add-release-label.yml
vendored
Normal file
42
.github/workflows/add-release-label.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
name: Add release label to PR and linked issues when PR gets merged
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
add-release-label:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true
|
||||
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: '16'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn --immutable
|
||||
|
||||
- name: Set execute permissions for script
|
||||
run: chmod +x ./development/get-next-semver-version.sh
|
||||
|
||||
- name: Get the next semver version
|
||||
id: get-next-semver-version
|
||||
env:
|
||||
FORCE_NEXT_SEMVER_VERSION: ${{ vars.FORCE_NEXT_SEMVER_VERSION }}
|
||||
run: ./development/get-next-semver-version.sh $FORCE_NEXT_SEMVER_VERSION
|
||||
|
||||
- name: Add release label to PR and linked issues
|
||||
id: add-release-label-to-pr-and-linked-issues
|
||||
env:
|
||||
RELEASE_LABEL_TOKEN: ${{ secrets.RELEASE_LABEL_TOKEN }}
|
||||
NEXT_SEMVER_VERSION: ${{ env.NEXT_SEMVER_VERSION }}
|
||||
run: npm run add-release-label-to-pr-and-linked-issues
|
27
development/get-next-semver-version.sh
Normal file
27
development/get-next-semver-version.sh
Normal file
@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
FORCE_NEXT_SEMVER_VERSION=$1
|
||||
|
||||
# If FORCE_NEXT_SEMVER_VERSION is defined and not empty, use its value and skip the next operations
|
||||
if [ -n "$FORCE_NEXT_SEMVER_VERSION" ]
|
||||
then
|
||||
echo "NEXT_SEMVER_VERSION=${FORCE_NEXT_SEMVER_VERSION}" >> "$GITHUB_ENV"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get the highest version from release branches
|
||||
VERSION_BRANCHES=$(git branch -r | grep -o 'release/[0-9]*\.[0-9]*\.[0-9]*' | grep -o '[0-9]*\.[0-9]*\.[0-9]*' | sort --version-sort | tail -n 1)
|
||||
|
||||
# Get the highest version from tags
|
||||
VERSION_TAGS=$(git tag | grep -o 'v[0-9]*\.[0-9]*\.[0-9]*' | grep -o '[0-9]*\.[0-9]*\.[0-9]*' | sort --version-sort | tail -n 1)
|
||||
|
||||
# Get the version from package.json
|
||||
VERSION_PACKAGE=$(node -p "require('./package.json').version")
|
||||
|
||||
# Compare versions and keep the highest one
|
||||
HIGHEST_VERSION=$(printf "%s\n%s\n%s" "$VERSION_BRANCHES" "$VERSION_TAGS" "$VERSION_PACKAGE" | sort --version-sort | tail -n 1)
|
||||
|
||||
# Increment the minor version of the highest version found
|
||||
NEXT_VERSION=$(echo "$HIGHEST_VERSION" | awk -F. -v OFS=. '{$2++; print}')
|
||||
|
||||
echo "NEXT_SEMVER_VERSION=${NEXT_VERSION}" >> "$GITHUB_ENV"
|
@ -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",
|
||||
"label-prs": "ts-node ./.github/scripts/label-prs.ts"
|
||||
"label-prs": "ts-node ./.github/scripts/label-prs.ts",
|
||||
"add-release-label-to-pr-and-linked-issues": "ts-node ./.github/scripts/add-release-label-to-pr-and-linked-issues.ts"
|
||||
},
|
||||
"resolutions": {
|
||||
"@babel/core": "patch:@babel/core@npm%3A7.21.5#./.yarn/patches/@babel-core-npm-7.21.5-c72c337956.patch",
|
||||
|
Loading…
Reference in New Issue
Block a user