On 2026-10-26 13:40 group fix/734-lost-ask merged two of its three members: the forge answered mesh-delivery#73's merge 405 "Please try again later", and in the seconds between, group feat/890-placement merged mesh-delivery#72, so #73 then conflicted. - mesh-delivery-gate: before any member is handed, every member is read for its required statuses, its head still the approved one, and the forge's own mergeable (in conflict, or still being worked out); the forge's "no" is asked again every 5 s for 2 minutes, then the group is held and said urgent with nothing merged. - mesh-delivery-gate: an approved merge waits while another approved merge's pass is under way into a repository it goes into (handed until merged whole, or stopped), whether or not mesh/check gates merges (it waits for a merge, never for a check: ADR 0551). - mesh-delivery-gate: tool delivery_merge_again asks a refused or held merge again on the operator's word. - gitea: the merge asks again after the forge's 405 "Please try again later" (1 s, 2 s, 4 s), and a pull request carries the forge's mergeable, which gitea_commit_statuses answers.
69 lines
3.3 KiB
TypeScript
69 lines
3.3 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { createServer } from "node:http";
|
|
|
|
// **The forge's 405 "Please try again later" on a merge is asked again briefly** (novox/hq issue 1275, 2026-10-26 13:40):
|
|
// mesh-delivery#73's merge was answered so a few seconds after its base moved, and counted as refused at once. The forge
|
|
// says it while it works out whether the pull request merges; the merge is asked again after a short wait, and every other
|
|
// refusal is answered at once. A pull request's own mergeable is carried, so a group can be read whole before it merges.
|
|
|
|
/** A forge whose merge of novox/hq#7 answers each of `answers` in turn (the last for ever), and whose pull request says
|
|
* `mergeable`. */
|
|
async function serve(answers: { status: number; body: string }[], mergeable?: boolean) {
|
|
const { GiteaClient } = await import("../client.ts");
|
|
let merges = 0;
|
|
const server = createServer((req, res) => {
|
|
const url = new URL(req.url ?? "", "http://x");
|
|
if (url.pathname === "/api/v1/repos/novox/hq/pulls/7/merge" && req.method === "POST") {
|
|
const a = answers[Math.min(merges++, answers.length - 1)];
|
|
res.statusCode = a.status;
|
|
return res.end(a.body);
|
|
}
|
|
if (url.pathname === "/api/v1/repos/novox/hq/pulls/7") {
|
|
return res.end(JSON.stringify({ number: 7, state: "open", merged: false, base: { ref: "main" },
|
|
head: { ref: "b", sha: "a".repeat(40) }, html_url: "h/7", ...(mergeable === undefined ? {} : { mergeable }) }));
|
|
}
|
|
res.statusCode = 404;
|
|
res.end("{}");
|
|
});
|
|
await new Promise<void>((r) => server.listen(0, r));
|
|
const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t");
|
|
GiteaClient.mergeRetryAfter = [1, 1, 1];
|
|
return { client, server, merges: () => merges };
|
|
}
|
|
|
|
const later = { status: 405, body: '{"message":"Please try again later"}' };
|
|
const ok = { status: 200, body: "" };
|
|
|
|
test("a merge answered 'try again later' while the forge works out its mergeability, then merged", async (t) => {
|
|
const cases: { name: string; answers: { status: number; body: string }[]; merged: boolean; asked: number }[] = [
|
|
{ name: "merged on the third ask", answers: [later, later, ok], merged: true, asked: 3 },
|
|
{ name: "still 'later' after the last wait: refused, with the forge's words", answers: [later], merged: false, asked: 4 },
|
|
{ name: "a refusal of another kind is answered at once", answers: [{ status: 409, body: '{"message":"head moved"}' }], merged: false, asked: 1 },
|
|
];
|
|
for (const c of cases) {
|
|
await t.test(c.name, async () => {
|
|
const { client, server, merges } = await serve(c.answers);
|
|
try {
|
|
const done = client.mergePullRequest("novox", "hq", 7, "merge", false, "a".repeat(40));
|
|
if (c.merged) await done;
|
|
else await assert.rejects(done, (err: Error) => /40[59]/.test(err.message));
|
|
assert.equal(merges(), c.asked);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
test("a pull request carries the forge's mergeable, and none when the forge did not say", async () => {
|
|
for (const mergeable of [true, false, undefined]) {
|
|
const { client, server } = await serve([ok], mergeable);
|
|
try {
|
|
assert.equal((await client.getPullRequest("novox", "hq", 7)).mergeable, mergeable);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
}
|
|
});
|