mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 18:00:18 +01:00
3bbfe87e9e
* 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
329 lines
10 KiB
TypeScript
329 lines
10 KiB
TypeScript
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],
|
|
});
|
|
|
|
}
|