Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wg/rel/only backport to v x.x.x branch #112

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
nodejs 16.18.1
yarn 1.22.4
13 changes: 3 additions & 10 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ inputs:
- body: original PR's body
- mergeCommitSha: SHA of the original PR's merge commit
- number: original PR's number
default: "Backport <%= mergeCommitSha %> from #<%= number %>."
default: "<%- body %> <br> Backport <%= mergeCommitSha %> from #<%= number %>"
github_token:
description: Token for the GitHub API.
required: true
Expand All @@ -28,15 +28,8 @@ inputs:
description: >
The regular expression pattern that PR labels will be tested on to decide whether the PR should be backported and where.
The backport PR's base branch will be extracted from the pattern's required `base` named capturing group.
default: "^backport (?<base>([^ ]+))$"
labels_template:
description: >
Lodash template compiling to a JSON array of labels to add to the backport PR.

The data properties are:
- base: backport PR's base branch
- labels: array containing the original PR's labels, excluding those matching `label_pattern`.
default: "[]"
# default: "^backport (?<base>([^ ]+))$"
default: "^backport (?<base>\d+\.\d+\.x)$"
title_template:
description: >
Lodash template for the backport PR's title.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "backport",
"version": "2.0.3",
"version": "2.0.10",
"license": "MIT",
"main": "dist/index.js",
"files": [
Expand All @@ -10,7 +10,7 @@
"scripts": {
"prebuild": "tsc --build",
"build": "ncc build src/index.ts --minify --target es2021 --v8-cache",
"prettier": "prettier --ignore-path .gitignore \"./**/*.{cjs,js,json,md,ts,yml}\"",
"prettier": "prettier --ignore-path .gitignore \"./**/*.{cjs,js,json,md,ts,yml}\" --write",
"xo": "xo"
},
"dependencies": {
Expand Down
171 changes: 123 additions & 48 deletions src/backport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from "@octokit/webhooks-types";
import ensureError from "ensure-error";
import { compact } from "lodash-es";
import { stripTestPlanFromPRBody } from "./utils.js";

const getBaseBranchFromLabel = (
label: string,
Expand Down Expand Up @@ -76,22 +77,26 @@ const warnIfSquashIsNotTheOnlyAllowedMergeMethod = async ({
};

const backportOnce = async ({
author,
base,
body,
commitSha,
github,
head,
labels,
merged_by,
owner,
repo,
title,
}: Readonly<{
author: string;
base: string;
body: string;
commitSha: string;
github: InstanceType<typeof GitHub>;
head: string;
labels: readonly string[];
merged_by: string;
owner: string;
repo: string;
title: string;
Expand Down Expand Up @@ -120,6 +125,19 @@ const backportOnce = async ({
repo,
title,
});
await github.request(
"POST /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers",
{
owner,
pull_number: number,
repo,
reviewers:
author !== merged_by && merged_by !== ""
? [author, merged_by]
: [author],
team_reviewers: ["release"],
},
);
if (labels.length > 0) {
await github.request(
"PUT /repos/{owner}/{repo}/issues/{issue_number}/labels",
Expand All @@ -141,48 +159,90 @@ const getFailedBackportCommentBody = ({
commitSha,
errorMessage,
head,
prNumber,
runUrl,
}: {
base: string;
commitSha: string;
errorMessage: string;
head: string;
prNumber: number;
runUrl: string;
}) => {
const worktreePath = `.worktrees/backport-${base}`;
return [
`The backport to \`${base}\` failed:`,
"```",
errorMessage,
"```",
"To backport manually, run these commands in your terminal:",
"```bash",
"# Fetch latest updates from GitHub",
"git fetch",
"# Create a new working tree",
`git worktree add ${worktreePath} ${base}`,
"# Navigate to the new working tree",
`cd ${worktreePath}`,
"# Create a new branch",
`git switch --create ${head}`,
"# Cherry-pick the merged commit of this pull request and resolve the conflicts",
`git cherry-pick -x --mainline 1 ${commitSha}`,
"# Push it to GitHub",
`git push --set-upstream origin ${head}`,
"# Go back to the original working tree",
"cd ../..",
"# Delete the working tree",
`git worktree remove ${worktreePath}`,
"```",
`Then, create a pull request where the \`base\` branch is \`${base}\` and the \`compare\`/\`head\` branch is \`${head}\`.`,
].join("\n");
};
return `The backport to \`${base}\` failed at ${runUrl}:
\`\`\`
${errorMessage}
\`\`\`

To backport this PR manually, you can either:

<details>
<summary>Via the sg tool</summary>

Use the \`sg backport\` command to backport your commit to the release branch.

\`\`\`bash
sg backport -r ${base} -p ${prNumber}
\`\`\`
</details>

<details>
<summary>
Via your terminal
</summary>

To backport manually, run these commands in your terminal:

\`\`\`bash
# Fetch latest updates from GitHub
git fetch
# Create a new working tree
git worktree add ${worktreePath} ${base}
# Navigate to the new working tree
cd ${worktreePath}
# Create a new branch
git switch --create ${head}
# Cherry-pick the merged commit of this pull request and resolve the conflicts
git cherry-pick -x --mainline 1 ${commitSha}
# Push it to GitHub
git push --set-upstream origin ${head}
# Go back to the original working tree
cd ../..
# Delete the working tree
git worktree remove ${worktreePath}
\`\`\`

If you encouter conflict, first resolve the conflict and stage all files, then run the commands below:
\`\`\`bash
git cherry-pick --continue
# Push it to GitHub
git push --set-upstream origin ${head}
# Go back to the original working tree
cd ../..
# Delete the working tree
git worktree remove ${worktreePath}
\`\`\`

- [ ] Follow above instructions to backport the commit.
- [ ] Create a pull request where the \`base\` branch is \`${base}\` and the \`compare\`/\`head\` branch is \`${head}\`., [click here to create the pull request](https://github.com/sourcegraph/sourcegraph/compare/${base}...${head}?expand=1).
</details>

Once the pull request has been created, please ensure the following:

- [ ] Make sure to tag \`@sourcegraph/release\` in the pull request description.

- [ ] kindly remove the \`release-blocker\` from this pull request.
`};

const backport = async ({
getBody,
getHead,
getLabels,
getTitle,
labelRegExp,
payload,
runId,
serverUrl,
token,
}: {
getBody: (
Expand All @@ -199,12 +259,6 @@ const backport = async ({
number: number;
}>,
) => string;
getLabels: (
props: Readonly<{
base: string;
labels: readonly string[];
}>,
) => string[];
getTitle: (
props: Readonly<{
base: string;
Expand All @@ -214,6 +268,8 @@ const backport = async ({
) => string;
labelRegExp: RegExp;
payload: PullRequestClosedEvent | PullRequestLabeledEvent;
runId: number;
serverUrl: string;
token: string;
}): Promise<{ [base: string]: number }> => {
const {
Expand All @@ -222,8 +278,10 @@ const backport = async ({
labels: originalLabels,
merge_commit_sha: mergeCommitSha,
merged,
number,
merged_by: originalMergedBy,
number: prNumber,
title: originalTitle,
user: { login: author },
},
repository: {
name: repo,
Expand All @@ -239,7 +297,6 @@ const backport = async ({
}

const baseBranches = getBaseBranches({ labelRegExp, payload });

if (baseBranches.length === 0) {
info("No backports required.");
return {};
Expand All @@ -249,7 +306,7 @@ const backport = async ({

await warnIfSquashIsNotTheOnlyAllowedMergeMethod({ github, owner, repo });

info(`Backporting ${mergeCommitSha} from #${number}.`);
info(`Backporting ${mergeCommitSha} from #${prNumber}.`);

const cloneUrl = new URL(payload.repository.clone_url);
cloneUrl.username = "x-access-token";
Expand All @@ -269,30 +326,32 @@ const backport = async ({
for (const base of baseBranches) {
const body = getBody({
base,
body: originalBody ?? "",
body: originalBody ? stripTestPlanFromPRBody(originalBody) : "",
mergeCommitSha,
number,
});
const head = getHead({ base, number });
const labels = getLabels({
base,
labels: originalLabels
.map(({ name }) => name)
.filter((label) => !labelRegExp.test(label)),
number: prNumber,
});
const title = getTitle({ base, number, title: originalTitle });
const head = getHead({ base, number: prNumber });
const labels = originalLabels
.map((label) => label.name)
.filter((label) => !labelRegExp.test(label));
labels.push("backports", `backported-to-${base}`);

const title = getTitle({ base, number: prNumber, title: originalTitle });
const merged_by = originalMergedBy?.login ?? "";
const runUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${runId}`;
// PRs are handled sequentially to avoid breaking GitHub's log grouping feature.
// eslint-disable-next-line no-await-in-loop
await group(`Backporting to ${base} on ${head}.`, async () => {
try {
const backportPullRequestNumber = await backportOnce({
author,
base,
body,
commitSha: mergeCommitSha,
github,
head,
labels,
merged_by,
owner,
repo,
title,
Expand All @@ -310,8 +369,24 @@ const backport = async ({
commitSha: mergeCommitSha,
errorMessage: error.message,
head,
prNumber,
runUrl,
}),
issue_number: number,
issue_number: prNumber,
owner,
repo,
},
);

await github.request(
"POST /repos/{owner}/{repo}/issues/{issue_number}/labels",
{
issue_number: prNumber,
labels: [
"backports",
"release-blocker",
`failed-backport-to-${base}`,
],
owner,
repo,
},
Expand Down
23 changes: 5 additions & 18 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,12 @@ import { backport } from "./backport.js";

const run = async () => {
try {
const [getBody, getHead, _getLabels, getTitle] = [
const [getBody, getHead, getTitle] = [
"body_template",
"head_template",
"labels_template",
"title_template",
].map((name) => template(getInput(name)));

const getLabels = ({
base,
labels,
}: Readonly<{ base: string; labels: readonly string[] }>): string[] => {
const json = _getLabels({ base, labels });
try {
return JSON.parse(json) as string[];
} catch (_error: unknown) {
const error = ensureError(_error);
throw new Error(`Could not parse labels from invalid JSON: ${json}.`, {
cause: error,
});
}
};

const labelPattern = getInput("label_pattern");
const labelRegExp = new RegExp(labelPattern);

Expand All @@ -39,6 +23,8 @@ const run = async () => {
}

const payload = context.payload as PullRequestEvent;
const runId = context.runId;
const serverUrl = context.serverUrl;

if (payload.action !== "closed" && payload.action !== "labeled") {
throw new Error(
Expand All @@ -49,10 +35,11 @@ const run = async () => {
const createdPullRequestBaseBranchToNumber = await backport({
getBody,
getHead,
getLabels,
getTitle,
labelRegExp,
payload,
runId,
serverUrl,
token,
});
setOutput(
Expand Down
2 changes: 2 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const stripTestPlanFromPRBody = (body: string): string =>
body.replace(/<!--[\s\S]*?-->/g, "");