Skip to content

Commit

Permalink
Merge pull request #405 from tasso94/main
Browse files Browse the repository at this point in the history
feat: allow backporting to remote repository
  • Loading branch information
korthout authored May 3, 2024
2 parents 52886ff + 3c984b3 commit 042b2d4
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 83 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,19 @@ When enabled, the action detects the method used to merge the pull request.

By default, the action always cherry-picks the commits from the pull request.

##### `downstream_repo`

Define if you want to backport to a repository other than where the workflow runs.

By default, the action always backports to the repository in which the workflow runs.

##### `downstream_owner`

Define if you want to backport to another owner than the owner of the repository the workflow runs on.
Only takes effect if the `downstream_repo` property is also defined.

By default, uses the owner of the repository in which the workflow runs.

### `github_token`

Default: `${{ github.token }}`
Expand Down
13 changes: 13 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ inputs:
- For "Merged as a merge commit", the action cherry-picks the commits from the pull request.
By default, the action always cherry-picks the commits from the pull request.
#### `downstream_repo`
Define if you want to backport to a repository other than where the workflow runs.
By default, the action always backports to the repository in which the workflow runs.
#### `downstream_owner`
Define if you want to backport to another owner than the owner of the repository the workflow runs on.
Only takes effect if the `downstream_repo` property is also defined.
By default, uses the owner of the repository in which the workflow runs.
default: >
{
"detect_merge_method": false
Expand Down
131 changes: 91 additions & 40 deletions dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

113 changes: 85 additions & 28 deletions src/backport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ export type Config = {

type Experimental = {
detect_merge_method: boolean;
downstream_repo?: string;
downstream_owner?: string;
};
const experimentalDefaults: Experimental = {
detect_merge_method: false,
downstream_repo: undefined,
downstream_owner: undefined,
};
export { experimentalDefaults };

Expand All @@ -55,25 +59,53 @@ export class Backport {
private config;
private git;

private downstreamRepo;
private downstreamOwner;

constructor(github: GithubApi, config: Config, git: Git) {
this.github = github;
this.config = config;
this.git = git;

this.downstreamRepo = this.config.experimental.downstream_repo ?? undefined;
this.downstreamOwner =
this.config.experimental.downstream_owner ?? undefined;
}

shouldUseDownstreamRepo(): boolean {
return !!this.downstreamRepo;
}

getRemote(): "downstream" | "origin" {
return this.shouldUseDownstreamRepo() ? "downstream" : "origin";
}

async run(): Promise<void> {
try {
const payload = this.github.getPayload();
const owner = this.github.getRepo().owner;
const repo = payload.repository?.name ?? this.github.getRepo().repo;

const workflowOwner = this.github.getRepo().owner;
const owner =
this.shouldUseDownstreamRepo() && this.downstreamOwner // if undefined, use owner of workflow
? this.downstreamOwner
: workflowOwner;

const workflowRepo =
payload.repository?.name ?? this.github.getRepo().repo;
const repo = this.shouldUseDownstreamRepo()
? this.downstreamRepo
: workflowRepo;

if (repo === undefined) throw new Error("No repository defined!");

const pull_number = this.github.getPullNumber();
const mainpr = await this.github.getPullRequest(pull_number);

if (!(await this.github.isMerged(mainpr))) {
const message = "Only merged pull requests can be backported.";
this.github.createComment({
owner,
repo,
owner: workflowOwner,
repo: workflowRepo,
issue_number: pull_number,
body: message,
});
Expand Down Expand Up @@ -172,8 +204,8 @@ export class Backport {
You can either backport this pull request manually, or configure the action to skip merge commits.`;
console.error(message);
this.github.createComment({
owner,
repo,
owner: workflowOwner,
repo: workflowRepo,
issue_number: pull_number,
body: message,
});
Expand Down Expand Up @@ -210,21 +242,25 @@ export class Backport {
`Will copy labels matching ${this.config.copy_labels_pattern}. Found matching labels: ${labelsToCopy}`,
);

if (this.shouldUseDownstreamRepo()) {
await this.git.remoteAdd(this.config.pwd, "downstream", owner, repo);
}

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, this.config.pwd, 1, this.getRemote());
} catch (error) {
if (error instanceof GitRefNotFoundError) {
const message = this.composeMessageForFetchTargetFailure(error.ref);
console.error(message);
successByTarget.set(target, false);
await this.github.createComment({
owner,
repo,
owner: workflowOwner,
repo: workflowRepo,
issue_number: pull_number,
body: message,
});
Expand All @@ -245,7 +281,7 @@ export class Backport {
try {
await this.git.checkout(
branchname,
`origin/${target}`,
`${this.getRemote()}/${target}`,
this.config.pwd,
);
} catch (error) {
Expand All @@ -257,8 +293,8 @@ export class Backport {
console.error(message);
successByTarget.set(target, false);
await this.github.createComment({
owner,
repo,
owner: workflowOwner,
repo: workflowRepo,
issue_number: pull_number,
body: message,
});
Expand All @@ -276,16 +312,20 @@ export class Backport {
console.error(message);
successByTarget.set(target, false);
await this.github.createComment({
owner,
repo,
owner: workflowOwner,
repo: workflowRepo,
issue_number: pull_number,
body: message,
});
continue;
}

console.info(`Push branch to origin`);
const pushExitCode = await this.git.push(branchname, this.config.pwd);
console.info(`Push branch to ${this.getRemote()}`);
const pushExitCode = await this.git.push(
branchname,
this.getRemote(),
this.config.pwd,
);
if (pushExitCode != 0) {
const message = this.composeMessageForGitPushFailure(
target,
Expand All @@ -294,8 +334,8 @@ export class Backport {
console.error(message);
successByTarget.set(target, false);
await this.github.createComment({
owner,
repo,
owner: workflowOwner,
repo: workflowRepo,
issue_number: pull_number,
body: message,
});
Expand All @@ -320,8 +360,8 @@ export class Backport {
const message =
this.composeMessageForCreatePRFailed(new_pr_response);
await this.github.createComment({
owner,
repo,
owner: workflowOwner,
repo: workflowRepo,
issue_number: pull_number,
body: message,
});
Expand Down Expand Up @@ -350,6 +390,10 @@ export class Backport {
const set_assignee_response = await this.github.setAssignees(
new_pr.number,
assignees,
{
owner,
repo,
},
);
if (set_assignee_response.status != 201) {
console.error(JSON.stringify(set_assignee_response));
Expand All @@ -364,7 +408,8 @@ export class Backport {
if (reviewers?.length > 0) {
console.info("Setting reviewers " + reviewers);
const reviewRequest = {
...this.github.getRepo(),
owner,
repo,
pull_number: new_pr.number,
reviewers: reviewers,
};
Expand All @@ -380,19 +425,27 @@ export class Backport {
const label_response = await this.github.labelPR(
new_pr.number,
labelsToCopy,
{
owner,
repo,
},
);
if (label_response.status != 200) {
console.error(JSON.stringify(label_response));
// The PR was still created so let's still comment on the original.
}
}

const message = this.composeMessageForSuccess(new_pr.number, target);
const message = this.composeMessageForSuccess(
new_pr.number,
target,
this.shouldUseDownstreamRepo() ? `${owner}/${repo}` : "",
);
successByTarget.set(target, true);
createdPullRequestNumbers.push(new_pr.number);
await this.github.createComment({
owner,
repo,
owner: workflowOwner,
repo: workflowRepo,
issue_number: pull_number,
body: message,
});
Expand All @@ -401,8 +454,8 @@ export class Backport {
console.error(error.message);
successByTarget.set(target, false);
await this.github.createComment({
owner,
repo,
owner: workflowOwner,
repo: workflowRepo,
issue_number: pull_number,
body: error.message,
});
Expand Down Expand Up @@ -515,9 +568,13 @@ export class Backport {
(see action log for full response)`;
}

private composeMessageForSuccess(pr_number: number, target: string) {
private composeMessageForSuccess(
pr_number: number,
target: string,
downstream: string,
) {
return dedent`Successfully created backport PR for \`${target}\`:
- #${pr_number}`;
- ${downstream}#${pr_number}`;
}

private createOutput(
Expand Down
44 changes: 38 additions & 6 deletions src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,55 @@ 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 the shortname of the repository from where to fetch commits
* @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=${depth}`, remote, ref],
pwd,
);
if (exitCode === 128) {
throw new GitRefNotFoundError(
`Expected to fetch '${ref}', but couldn't find it`,
`Expected to fetch '${ref}' from '${remote}', but couldn't find it`,
ref,
);
} else if (exitCode !== 0) {
throw new Error(
`'git fetch origin ${ref}' failed with exit code ${exitCode}`,
`'git fetch ${remote} ${ref}' failed with exit code ${exitCode}`,
);
}
}

/**
* Adds a new remote Git repository as a shortname.
*
* @param pwd the root of the git repository
* @param shortname the shortname referencing the repository
* @param owner the owner of the GitHub repository
* @param repo the name of the repository
*/
public async remoteAdd(
pwd: string,
shortname: string,
owner: string | undefined,
repo: string | undefined,
) {
const { exitCode } = await this.git(
"remote",
["add", shortname, `https://github.com/${owner}/${repo}.git`],
pwd,
);
if (exitCode !== 0) {
throw new Error(
`'git remote add ${owner}/${repo}' failed with exit code ${exitCode}`,
);
}
}
Expand Down Expand Up @@ -94,10 +126,10 @@ export class Git {
return mergeCommitShas;
}

public async push(branchname: string, pwd: string) {
public async push(branchname: string, remote: string, pwd: string) {
const { exitCode } = await this.git(
"push",
["--set-upstream", "origin", branchname],
["--set-upstream", remote, branchname],
pwd,
);
return exitCode;
Expand Down
Loading

0 comments on commit 042b2d4

Please sign in to comment.