Skip to content

Commit

Permalink
Add forward-merger code (#175)
Browse files Browse the repository at this point in the history
  • Loading branch information
AyodeAwe authored Jan 31, 2024
1 parent 7dec54c commit 2483504
Show file tree
Hide file tree
Showing 7 changed files with 456 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type OpsBotConfigFeatureNames = {
label_checker: boolean;
release_drafter: boolean;
recently_updated: boolean;
forward_merger: boolean;
};

/**
Expand All @@ -49,6 +50,7 @@ export const DefaultOpsBotConfig: OpsBotConfig = {
release_drafter: false,
recently_updated: false,
recently_updated_threshold: 5,
forward_merger: false,
};

/**
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import { initBranchChecker } from "./plugins/BranchChecker";
import { initLabelChecker } from "./plugins/LabelChecker";
import { initRecentlyUpdated } from "./plugins/RecentlyUpdated";
import { initReleaseDrafter } from "./plugins/ReleaseDrafter";
import { initForwardMerger } from "./plugins/ForwardMerger";

export = (app: Probot) => {
initBranchChecker(app);
initLabelChecker(app);
initReleaseDrafter(app);
initAutoMerger(app);
initRecentlyUpdated(app);
initForwardMerger(app);
};
125 changes: 125 additions & 0 deletions src/plugins/ForwardMerger/forward_merger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright (c) 2024, NVIDIA CORPORATION.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { OpsBotPlugin } from "../../plugin";
import { PayloadRepository } from "../../types";
import { isVersionedBranch, getVersionFromBranch } from "../../shared";
import { basename } from "path";
import { Context } from "probot";

export class ForwardMerger extends OpsBotPlugin {
context: Context;
currentBranch: string;
repo: PayloadRepository;

constructor(
context: Context,
private payload: Context<"push">["payload"]
) {
super("forward_merger", context);
this.context = context;
this.currentBranch = basename(this.payload.ref);
this.repo = payload.repository;
}

async mergeForward(): Promise<void> {
if (await this.pluginIsDisabled()) return;

if (!isVersionedBranch(this.currentBranch)) {
this.logger.info("Will not merge forward on non-versioned branch");
return;
}

const branches = await this.getBranches();
const sortedBranches = this.sortBranches(branches);
const nextBranch = this.getNextBranch(sortedBranches);

if (!nextBranch) return;

const { data: pr } = await this.context.octokit.pulls.create({
owner: this.repo.owner.login,
repo: this.repo.name,
title: "Forward-merge " + this.currentBranch + " into " + nextBranch,
head: this.currentBranch,
base: nextBranch,
maintainer_can_modify: false,
body: `Forward-merge triggered by push to ${this.currentBranch} that creates a PR to keep ${nextBranch} up-to-date. If this PR is unable to be immediately merged due to conflicts, it will remain open for the team to manually merge. See [forward-merger docs](https://docs.rapids.ai/maintainers/forward-merger/) for more info.`,
});

try {
this.logger.info("Merging PR");
await this.context.octokit.pulls.merge({
owner: this.repo.owner.login,
repo: this.repo.name,
pull_number: pr.number,
sha: pr.head.sha,
});
} catch (error) {
await this.issueComment(
pr.number,
"**FAILURE** - Unable to forward-merge due to an error, **manual** merge is necessary. Do not use the `Resolve conflicts` option in this PR, follow these instructions https://docs.rapids.ai/maintainers/forward-merger/ \n **IMPORTANT**: When merging this PR, do not use the [auto-merger](https://docs.rapids.ai/resources/auto-merger/) (i.e. the `/merge` comment). Instead, an admin must manually merge by changing the merging strategy to `Create a Merge Commit`. Otherwise, history will be lost and the branches become incompatible."
);
return;
}

await this.issueComment(
pr.number,
"**SUCCESS** - forward-merge complete."
);
}

async getBranches(): Promise<string[]> {
const branches = await this.context.octokit.paginate(
this.context.octokit.repos.listBranches,
{
owner: this.repo.owner.login,
repo: this.repo.name,
}
);
return branches
.filter((branch) => isVersionedBranch(branch.name))
.map((branch) => branch.name);
}

sortBranches(branches: string[]): string[] {
return branches.sort((a, b) => {
const [yearA, monthA] = getVersionFromBranch(a).split(".").map(Number);
const [yearB, monthB] = getVersionFromBranch(b).split(".").map(Number);
if (yearA !== yearB) {
return yearA - yearB;
} else {
return monthA - monthB;
}
});
}

getNextBranch(sortedBranches: string[]): string | undefined {
const currentBranchIndex = sortedBranches.findIndex(
(branch) => branch === this.currentBranch
);
const nextBranch = sortedBranches[currentBranchIndex + 1];
return nextBranch;
}

async issueComment(id, comment): Promise<void> {
await this.context.octokit.issues.createComment({
owner: this.repo.owner.login,
repo: this.repo.name,
issue_number: id,
body: comment,
});
}
}
24 changes: 24 additions & 0 deletions src/plugins/ForwardMerger/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2024, NVIDIA CORPORATION.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Probot } from "probot";
import { ForwardMerger } from "./forward_merger";

export const initForwardMerger = (app: Probot) => {
app.on(["push"], async (context) => {
await new ForwardMerger(context, context.payload).mergeForward();
});
};
4 changes: 4 additions & 0 deletions test/fixtures/contexts/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import {
mockPaginate,
mockPullsGet,
mockUpdateRelease,
mockCreatePR,
mockListBranches,
} from "../../mocks";
import type { EmitterWebhookEventName } from "@octokit/webhooks/dist-types/types";

Expand All @@ -57,6 +59,7 @@ export const makeContext = (payload, name: EmitterWebhookEventName) => {
list: mockListPulls,
listReviews: mockListReviews,
merge: mockMerge,
create: mockCreatePR,
},
repos: {
createCommitStatus: mockCreateCommitStatus,
Expand All @@ -66,6 +69,7 @@ export const makeContext = (payload, name: EmitterWebhookEventName) => {
getReleaseByTag: mockGetReleaseByTag,
updateRelease: mockUpdateRelease,
compareCommitsWithBasehead: mockCompareCommitsWithBasehead,
listBranches: mockListBranches,
},
users: {
getByUsername: mockGetByUsername,
Expand Down
Loading

0 comments on commit 2483504

Please sign in to comment.