Skip to content

Commit

Permalink
feat: allow backport to remote repository
Browse files Browse the repository at this point in the history
  • Loading branch information
tasso94 committed Dec 19, 2023
1 parent 930286d commit 671709e
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 30 deletions.
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ inputs:
Note that the pull request's headref is excluded automatically.
Can be used in addition to backport labels.
By default, only backport labels are used to specify the target branches.
target_owner:
description: ""
target_repo:
description: ""

outputs:
created_pull_numbers:
Expand Down
59 changes: 45 additions & 14 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class Backport {
this.git = git;
}
run() {
var _a, _b, _c, _d, _e, _f;
var _a, _b, _c, _d, _e, _f, _g, _h;
return __awaiter(this, void 0, void 0, function* () {
try {
const payload = this.github.getPayload();
Expand Down Expand Up @@ -162,12 +162,20 @@ class Backport {
!label.match(this.config.labels.pattern)));
}
console.log(`Will copy labels matching ${this.config.copy_labels_pattern}. Found matching labels: ${labelsToCopy}`);
let pwd = this.config.pwd;
if (this.config.target_repo && this.config.target_owner) {
yield this.git.clone(this.config.pwd, this.config.target_owner, this.config.target_repo);
// Change PWD to cloned target repo.
pwd = this.config.pwd + `/${this.config.target_repo}`;
yield this.git.remoteAdd(pwd, "source", owner, repo);
yield this.git.fetch(mainpr.head.ref, pwd, 0, "source");
}
const successByTarget = new Map();
const createdPullRequestNumbers = new Array();
for (const target of target_branches) {
console.log(`Backporting to target branch '${target}...'`);
try {
yield this.git.fetch(target, this.config.pwd, 1);
yield this.git.fetch(target, pwd, 1);
}
catch (error) {
if (error instanceof git_1.GitRefNotFoundError) {
Expand All @@ -190,7 +198,7 @@ class Backport {
const branchname = `backport-${pull_number}-to-${target}`;
console.log(`Start backport to ${branchname}`);
try {
yield this.git.checkout(branchname, `origin/${target}`, this.config.pwd);
yield this.git.checkout(branchname, `origin/${target}`, pwd);
}
catch (error) {
const message = this.composeMessageForCheckoutFailure(target, branchname, commitShasToCherryPick);
Expand All @@ -205,7 +213,7 @@ class Backport {
continue;
}
try {
yield this.git.cherryPick(commitShasToCherryPick, this.config.pwd);
yield this.git.cherryPick(commitShasToCherryPick, pwd);
}
catch (error) {
const message = this.composeMessageForCherryPickFailure(target, branchname, commitShasToCherryPick);
Expand All @@ -220,7 +228,7 @@ class Backport {
continue;
}
console.info(`Push branch to origin`);
const pushExitCode = yield this.git.push(branchname, this.config.pwd);
const pushExitCode = yield this.git.push(branchname, pwd);
if (pushExitCode != 0) {
const message = this.composeMessageForGitPushFailure(target, pushExitCode);
console.error(message);
Expand All @@ -236,8 +244,8 @@ class Backport {
console.info(`Create PR for ${branchname}`);
const { title, body } = this.composePRContent(target, mainpr);
const new_pr_response = yield this.github.createPR({
owner,
repo,
owner: (_e = this.config.target_owner) !== null && _e !== void 0 ? _e : owner,
repo: (_f = this.config.target_repo) !== null && _f !== void 0 ? _f : repo,
title,
body,
head: branchname,
Expand All @@ -258,7 +266,7 @@ class Backport {
}
const new_pr = new_pr_response.data;
if (this.config.copy_milestone == true) {
const milestone = (_e = mainpr.milestone) === null || _e === void 0 ? void 0 : _e.number;
const milestone = (_g = mainpr.milestone) === null || _g === void 0 ? void 0 : _g.number;
if (milestone) {
console.info("Setting milestone to " + milestone);
const set_milestone_response = yield this.github.setMilestone(new_pr.number, milestone);
Expand All @@ -278,7 +286,7 @@ class Backport {
}
}
if (this.config.copy_requested_reviewers == true) {
const reviewers = (_f = mainpr.requested_reviewers) === null || _f === void 0 ? void 0 : _f.map((reviewer) => reviewer.login);
const reviewers = (_h = mainpr.requested_reviewers) === null || _h === void 0 ? void 0 : _h.map((reviewer) => reviewer.login);
if ((reviewers === null || reviewers === void 0 ? void 0 : reviewers.length) > 0) {
console.info("Setting reviewers " + reviewers);
const reviewRequest = Object.assign(Object.assign({}, this.github.getRepo()), { pull_number: new_pr.number, reviewers: reviewers });
Expand Down Expand Up @@ -384,8 +392,9 @@ class Backport {
(see action log for full response)`;
}
composeMessageForSuccess(pr_number, target) {
const repo = this.config.target_owner && this.config.target_repo ? `${this.config.target_owner}/${this.config.target_repo}` : '';
return (0, dedent_1.default) `Successfully created backport PR for \`${target}\`:
- #${pr_number}`;
- ${repo}#${pr_number}`;
}
createOutput(successByTarget, createdPullRequestNumbers) {
const anyTargetFailed = Array.from(successByTarget.values()).includes(false);
Expand Down Expand Up @@ -463,8 +472,9 @@ class GitRefNotFoundError extends Error {
}
exports.GitRefNotFoundError = GitRefNotFoundError;
class Git {
constructor(execa) {
constructor(execa, token) {
this.execa = execa;
this.token = token;
}
git(command, args, pwd) {
var _a;
Expand All @@ -488,12 +498,13 @@ class Git {
* @param ref the sha, branchname, etc to fetch
* @param pwd the root of the git repository
* @param depth the number of commits to fetch
* @param remote
* @throws GitRefNotFoundError when ref not found
* @throws Error for any other non-zero exit code
*/
fetch(ref, pwd, depth) {
fetch(ref, pwd, depth, remote = "origin") {
return __awaiter(this, void 0, void 0, function* () {
const { exitCode } = yield this.git("fetch", [`--depth=${depth}`, "origin", ref], pwd);
const { exitCode } = yield this.git("fetch", depth > 0 ? [`--depth=${depth}`, remote, ref] : [remote, ref], pwd);
if (exitCode === 128) {
throw new GitRefNotFoundError(`Expected to fetch '${ref}', but couldn't find it`, ref);
}
Expand All @@ -502,6 +513,22 @@ class Git {
}
});
}
clone(pwd, owner, repo) {
return __awaiter(this, void 0, void 0, function* () {
const { exitCode } = yield this.git("clone", [`https://x-access-token:${this.token}@github.com/${owner}/${repo}.git`], pwd);
if (exitCode !== 0) {
throw new Error(`'git clone ${owner}/${repo}' failed with exit code ${exitCode}`);
}
});
}
remoteAdd(pwd, source, owner, repo) {
return __awaiter(this, void 0, void 0, function* () {
const { exitCode } = yield this.git("remote", ["add", source, `https://x-access-token:${this.token}@github.com/${owner}/${repo}.git`], pwd);
if (exitCode !== 0) {
throw new Error(`'git remote add ${owner}/${repo}' failed with exit code ${exitCode}`);
}
});
}
findCommitsInRange(range, pwd) {
return __awaiter(this, void 0, void 0, function* () {
const { exitCode, stdout } = yield this.git("log", ['--pretty=format:"%H"', "--reverse", range], pwd);
Expand Down Expand Up @@ -939,6 +966,8 @@ function run() {
const copy_milestone = core.getInput("copy_milestone");
const copy_requested_reviewers = core.getInput("copy_requested_reviewers");
const experimental = JSON.parse(core.getInput("experimental"));
const target_owner = core.getInput("target_owner");
const target_repo = core.getInput("target_repo");
if (merge_commits != "fail" && merge_commits != "skip") {
const message = `Expected input 'merge_commits' to be either 'fail' or 'skip', but was '${merge_commits}'`;
console.error(message);
Expand All @@ -953,7 +982,7 @@ function run() {
}
}
const github = new github_1.Github(token);
const git = new git_1.Git(execa_1.execa);
const git = new git_1.Git(execa_1.execa, token);
const config = {
pwd,
labels: { pattern: pattern === "" ? undefined : new RegExp(pattern) },
Expand All @@ -965,6 +994,8 @@ function run() {
copy_milestone: copy_milestone === "true",
copy_requested_reviewers: copy_requested_reviewers === "true",
experimental: Object.assign(Object.assign({}, backport_1.experimentalDefaults), experimental),
target_repo: target_repo === "" ? undefined : target_repo,
target_owner: target_owner === "" ? undefined : target_owner
};
const backport = new backport_1.Backport(github, config, git);
return backport.run();
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 22 additions & 8 deletions src/backport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export type Config = {
copy_milestone: boolean;
copy_assignees: boolean;
copy_requested_reviewers: boolean;
experimental: Experimental;
experimental: Experimental,
target_repo?: string,
target_owner?: string;
};

type Experimental = {
Expand Down Expand Up @@ -209,13 +211,24 @@ export class Backport {
`Will copy labels matching ${this.config.copy_labels_pattern}. Found matching labels: ${labelsToCopy}`,
);

let pwd = this.config.pwd;
if (this.config.target_repo && this.config.target_owner) {
await this.git.clone(this.config.pwd, this.config.target_owner, this.config.target_repo);

// Change PWD to cloned target repo.
pwd = this.config.pwd + `/${this.config.target_repo}`;

await this.git.remoteAdd(pwd, "source", owner, repo);
await this.git.fetch(mainpr.head.ref, pwd, 0, "source");
}

const successByTarget = new Map<string, boolean>();
const createdPullRequestNumbers = new Array<number>();
for (const target of target_branches) {
console.log(`Backporting to target branch '${target}...'`);

try {
await this.git.fetch(target, this.config.pwd, 1);
await this.git.fetch(target, pwd, 1);
} catch (error) {
if (error instanceof GitRefNotFoundError) {
const message = this.composeMessageForFetchTargetFailure(error.ref);
Expand All @@ -241,7 +254,7 @@ export class Backport {
await this.git.checkout(
branchname,
`origin/${target}`,
this.config.pwd,
pwd,
);
} catch (error) {
const message = this.composeMessageForCheckoutFailure(
Expand All @@ -261,7 +274,7 @@ export class Backport {
}

try {
await this.git.cherryPick(commitShasToCherryPick, this.config.pwd);
await this.git.cherryPick(commitShasToCherryPick, pwd);
} catch (error) {
const message = this.composeMessageForCherryPickFailure(
target,
Expand All @@ -280,7 +293,7 @@ export class Backport {
}

console.info(`Push branch to origin`);
const pushExitCode = await this.git.push(branchname, this.config.pwd);
const pushExitCode = await this.git.push(branchname, pwd);
if (pushExitCode != 0) {
const message = this.composeMessageForGitPushFailure(
target,
Expand All @@ -300,8 +313,8 @@ export class Backport {
console.info(`Create PR for ${branchname}`);
const { title, body } = this.composePRContent(target, mainpr);
const new_pr_response = await this.github.createPR({
owner,
repo,
owner: this.config.target_owner ?? owner,
repo: this.config.target_repo ?? repo,
title,
body,
head: branchname,
Expand Down Expand Up @@ -511,8 +524,9 @@ export class Backport {
}

private composeMessageForSuccess(pr_number: number, target: string) {
const repo = this.config.target_owner && this.config.target_repo ? `${this.config.target_owner}/${this.config.target_repo}` : '';
return dedent`Successfully created backport PR for \`${target}\`:
- #${pr_number}`;
- ${repo}#${pr_number}`;
}

private createOutput(
Expand Down
33 changes: 30 additions & 3 deletions src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class GitRefNotFoundError extends Error {
}

export class Git {
constructor(private execa: Execa) {}
constructor(private execa: Execa, private token: string) {}

private async git(command: string, args: string[], pwd: string) {
console.log(`git ${command} ${args.join(" ")}`);
Expand All @@ -31,13 +31,14 @@ export class Git {
* @param ref the sha, branchname, etc to fetch
* @param pwd the root of the git repository
* @param depth the number of commits to fetch
* @param remote
* @throws GitRefNotFoundError when ref not found
* @throws Error for any other non-zero exit code
*/
public async fetch(ref: string, pwd: string, depth: number) {
public async fetch(ref: string, pwd: string, depth: number, remote: string = "origin") {
const { exitCode } = await this.git(
"fetch",
[`--depth=${depth}`, "origin", ref],
depth > 0 ? [`--depth=${depth}`, remote, ref] : [remote, ref],
pwd,
);
if (exitCode === 128) {
Expand All @@ -52,6 +53,32 @@ export class Git {
}
}

public async clone(pwd: string, owner: string, repo:string) {
const { exitCode } = await this.git(
"clone",
[`https://x-access-token:${this.token}@github.com/${owner}/${repo}.git`],
pwd,
);
if (exitCode !== 0) {
throw new Error(
`'git clone ${owner}/${repo}' failed with exit code ${exitCode}`,
);
}
}

public async remoteAdd(pwd: string, source: string, owner: string, repo:string) {
const { exitCode } = await this.git(
"remote",
["add", source, `https://x-access-token:${this.token}@github.com/${owner}/${repo}.git`],
pwd,
);
if (exitCode !== 0) {
throw new Error(
`'git remote add ${owner}/${repo}' failed with exit code ${exitCode}`,
);
}
}

public async findCommitsInRange(
range: string,
pwd: string,
Expand Down
6 changes: 5 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ async function run(): Promise<void> {
const copy_milestone = core.getInput("copy_milestone");
const copy_requested_reviewers = core.getInput("copy_requested_reviewers");
const experimental = JSON.parse(core.getInput("experimental"));
const target_owner = core.getInput("target_owner");
const target_repo = core.getInput("target_repo");

if (merge_commits != "fail" && merge_commits != "skip") {
const message = `Expected input 'merge_commits' to be either 'fail' or 'skip', but was '${merge_commits}'`;
Expand All @@ -40,7 +42,7 @@ async function run(): Promise<void> {
}

const github = new Github(token);
const git = new Git(execa);
const git = new Git(execa, token);
const config: Config = {
pwd,
labels: { pattern: pattern === "" ? undefined : new RegExp(pattern) },
Expand All @@ -53,6 +55,8 @@ async function run(): Promise<void> {
copy_milestone: copy_milestone === "true",
copy_requested_reviewers: copy_requested_reviewers === "true",
experimental: { ...experimentalDefaults, ...experimental },
target_repo: target_repo === "" ? undefined : target_repo,
target_owner: target_owner === "" ? undefined : target_owner
};
const backport = new Backport(github, config, git);

Expand Down
2 changes: 1 addition & 1 deletion src/test/git.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Git, GitRefNotFoundError } from "../git";
import { execa } from "execa";

const git = new Git(execa);
const git = new Git(execa, "");
let response = { exitCode: 0, stdout: "" };

jest.mock("execa", () => ({
Expand Down

0 comments on commit 671709e

Please sign in to comment.