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

feat: allow backporting to remote repository #405

Merged
merged 6 commits into from
May 3, 2024
Merged
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
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 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if this doesn't need to be used externally, better make it private

return !!this.downstreamRepo;
}

getRemote(): "downstream" | "origin" {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, could be private

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
Loading