Skip to content
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
17 changes: 15 additions & 2 deletions src/automerge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { CheckRun, CommitStatus, IssueEvent, MergeMethod, PullRequest, Repo

export const AUTOMERGE_LABELS = new Set(["automerge", "tag: automerge"]);
export const AUTOMERGE_GRACE_PERIOD_MS = 10_000;
export const MERGE_ATTEMPT_LIMIT = 3;
export const MERGEABILITY_POLL_INTERVAL_MS = 1_000;
export const MERGEABILITY_POLL_LIMIT = 10;

Expand Down Expand Up @@ -67,6 +68,7 @@ export async function evaluatePullRequest(
let pullRequest = await github.pullRequest(number);
const labelObservedAt = now();
if (pullRequest.state !== "open" || pullRequest.draft || !hasAutomergeLabel(pullRequest)) return false;
let mergeAttempts = 0;
let mergeabilityPolls = 0;

for (;;) {
Expand Down Expand Up @@ -112,7 +114,18 @@ export async function evaluatePullRequest(
return false;
}
const merged = await github.merge(number, pullRequest.head.sha, method);
console.log(merged ? `Merged ${repository}#${number}` : `GitHub blocked merge of ${repository}#${number}`);
return merged;
if (merged) {
console.log(`Merged ${repository}#${number}`);
return true;
}
mergeAttempts += 1;
if (mergeAttempts >= MERGE_ATTEMPT_LIMIT) {
console.log(`GitHub blocked merge of ${repository}#${number}`);
return false;
}
console.log(`Retrying merge of ${repository}#${number}`);
await waitFor(MERGEABILITY_POLL_INTERVAL_MS);
pullRequest = await github.pullRequest(number);
if (pullRequest.state !== "open" || pullRequest.draft || !hasAutomergeLabel(pullRequest)) return false;
}
}
40 changes: 37 additions & 3 deletions tests/automerge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
gracePeriodRemaining,
hasAutomergeLabel,
mergeMethod,
MERGE_ATTEMPT_LIMIT,
MERGEABILITY_POLL_INTERVAL_MS,
MERGEABILITY_POLL_LIMIT,
type AutomergeGitHub,
Expand Down Expand Up @@ -201,12 +202,45 @@ test("asks GitHub to merge after all actions finish", async () => {
expect(github.merges).toEqual([{ number: 12, sha: "abc123", method: "squash" }]);
});

test("leaves required-condition enforcement to GitHub", async () => {
test("retries when another pull request merges first", async () => {
const github = new FakeGitHub();
const mergeResults = [false, true];
const waits: number[] = [];
let requests = 0;
github.pullRequest = async () => {
requests += 1;
return { ...github.pull, mergeable: requests === 2 ? null : true };
};
github.merge = async (number, sha, method) => {
github.merges.push({ number, sha, method });
return mergeResults.shift() || false;
};

expect(
await evaluatePullRequest(github, 12, "owner/repository", {
wait: async (milliseconds) => {
waits.push(milliseconds);
},
}),
).toBe(true);
expect(waits).toEqual([MERGEABILITY_POLL_INTERVAL_MS, MERGEABILITY_POLL_INTERVAL_MS]);
expect(github.merges).toHaveLength(2);
});

test("stops retrying when GitHub continues to block the merge", async () => {
const github = new FakeGitHub();
github.mergeResult = false;
const waits: number[] = [];

expect(await evaluatePullRequest(github, 12, "owner/repository")).toBe(false);
expect(github.merges).toHaveLength(1);
expect(
await evaluatePullRequest(github, 12, "owner/repository", {
wait: async (milliseconds) => {
waits.push(milliseconds);
},
}),
).toBe(false);
expect(waits).toEqual(Array(MERGE_ATTEMPT_LIMIT - 1).fill(MERGEABILITY_POLL_INTERVAL_MS));
expect(github.merges).toHaveLength(MERGE_ATTEMPT_LIMIT);
});

test.each([
Expand Down