mesh/delivery-group group feat/341-a-merge-check-judges-the-merge rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds gitea, mesh-delivery → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
The merge tool's pending bypassed the merge check's one writer from the tools' bundle (issue 456); it is now served beside it. A verdict no build judges names the announced base head, so the merge tool no longer refuses it for ever. A late verdict keeps its account, a trunk is read again in each head's turn so a fresh verdict is not put out of date, and a delivered member counts when a rebased member's group is composed again (novox/hq ADR 0323).
176 lines
12 KiB
TypeScript
176 lines
12 KiB
TypeScript
// **A merge check's verdict holds for the trunk commit it judged** (novox/hq ADR 0323, issue 341): both statuses name
|
|
// the head and the trunk commit; a trunk's move puts the merge check of every open pull request into that trunk — and
|
|
// of none into another — back to pending with the reason, and runs nothing; a verdict that lands judged against a trunk
|
|
// that has moved since is said out of date.
|
|
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { checkOnItsWay, lateVerdictComment, outOfDateStatuses, outOfDateWords, trunkNamed, verdictAtTrunk, weighTrunks } from "../dist/trunk.js";
|
|
import { GateWriter } from "../dist/gate.js";
|
|
import { statusesFor, commentFor } from "../dist/pulls.js";
|
|
|
|
const HEAD = "dfb1415aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
const TRUNK = "a2c2c35bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
|
const MOVED = "d28ca95ccccccccccccccccccccccccccccccccc";
|
|
|
|
test("both statuses say the head and the trunk commit the verdict judged, first, whatever the summary's length", () => {
|
|
const c = { owner: "novox", repo: "mesh-host", number: 66, commit: HEAD, verdict: "pass", summary: "x", id: "build-1",
|
|
trunk: TRUNK.slice(0, 12), beside: { "mesh-catalog": "0123456789ab", "mesh-lab": "ba9876543210" },
|
|
gate: { verdict: "pass", summary: "4 of 4 compose ".repeat(20) },
|
|
"repo-check": { verdict: "fail", summary: "its merge-check.sh failed: internal/store/store.go:145:2: Declared redeclared" } };
|
|
const [gate, repo] = statusesFor(c);
|
|
for (const s of [gate, repo]) {
|
|
assert.ok(s.description.startsWith("head dfb1415aaaaa merged into trunk a2c2c35bbbbb — "), s.description);
|
|
assert.ok(s.description.length <= 140, `${s.description.length} characters`);
|
|
assert.equal(trunkNamed(s.description), "a2c2c35bbbbb");
|
|
}
|
|
assert.equal(gate.state, "success");
|
|
assert.equal(repo.state, "failure");
|
|
const comment = commentFor(c)!;
|
|
assert.match(comment, /Judged: head `dfb1415aaaaa` merged into trunk `a2c2c35bbbbb`; read beside it: mesh-catalog `0123456789ab`, mesh-lab `ba9876543210`/);
|
|
|
|
// A verdict of a build seat from before the rule names no trunk, and is said as before.
|
|
const [old] = statusesFor({ ...c, trunk: undefined, gate: { verdict: "pass", summary: "every machine composes" } });
|
|
assert.equal(old.description, "pass: every machine composes");
|
|
assert.equal(trunkNamed(old.description), undefined);
|
|
});
|
|
|
|
test("a trunk's move puts an open head's merge check out of date, naming both commits, unless a check is on its way", () => {
|
|
const verdict = (trunk: string, state = "success") => ({
|
|
"mesh/merge-gate": { state, description: `head ${HEAD.slice(0, 12)} merged into trunk ${trunk.slice(0, 12)} — pass: x` },
|
|
"mesh/repo-check": { state, description: `head ${HEAD.slice(0, 12)} merged into trunk ${trunk.slice(0, 12)} — pass: y` } });
|
|
const put = outOfDateStatuses(verdict(TRUNK), MOVED);
|
|
assert.deepEqual(put.map((s) => [s.context, s.state]), [["mesh/merge-gate", "pending"], ["mesh/repo-check", "pending"]]);
|
|
assert.equal(put[0].description, "out of date: judged against trunk a2c2c35bbbbb, the trunk is now d28ca95ccccc; " +
|
|
"judged again on a recheck or when it is merged");
|
|
assert.equal(put[0].description, outOfDateWords(TRUNK, MOVED));
|
|
assert.equal(trunkNamed(put[0].description), "a2c2c35bbbbb", "an out-of-date status still names what it judged");
|
|
// A failure is out of date too: it judged another merge.
|
|
assert.equal(outOfDateStatuses(verdict(TRUNK, "failure"), MOVED).length, 2);
|
|
// Already judged against the trunk's head; a check on its way or already out of date; no check yet: left alone.
|
|
assert.deepEqual(outOfDateStatuses(verdict(MOVED), MOVED), []);
|
|
assert.deepEqual(outOfDateStatuses({ "mesh/merge-gate": { state: "pending", description: "building on ace since 14:02" } }, MOVED), []);
|
|
assert.deepEqual(outOfDateStatuses({ "mesh/merge-gate": { state: "pending", description: put[0].description } }, MOVED), []);
|
|
assert.deepEqual(outOfDateStatuses({}, MOVED), []);
|
|
// A head with no repository check of its own gets no pending one a verdict would never set.
|
|
assert.deepEqual(outOfDateStatuses({ "mesh/merge-gate": verdict(TRUNK)["mesh/merge-gate"] }, MOVED).map((s) => s.context),
|
|
["mesh/merge-gate"]);
|
|
// A verdict naming no trunk, of a build seat from before the rule, judged an earlier trunk.
|
|
assert.match(outOfDateStatuses({ "mesh/merge-gate": { state: "success", description: "pass: x" } }, MOVED)[0].description,
|
|
/judged against a trunk it does not name, the trunk is now d28ca95ccccc/);
|
|
|
|
assert.ok(checkOnItsWay({ state: "pending", description: "waiting in the build queue since 14:00" }));
|
|
assert.ok(checkOnItsWay({ state: "pending", description: "checking again: a person asked" }));
|
|
assert.ok(!checkOnItsWay({ state: "pending", description: put[0].description }));
|
|
assert.deepEqual(verdictAtTrunk(verdict(MOVED), MOVED), { current: true, onItsWay: false, judged: "d28ca95ccccc" });
|
|
assert.equal(verdictAtTrunk(verdict(TRUNK), MOVED).current, false);
|
|
});
|
|
|
|
/** A forge holding open pull requests into two branches, each head's statuses, and what was written. */
|
|
function aForge(pulls: { number: number; base: string; head: string }[], heads: Record<string, string>) {
|
|
const statuses = new Map<string, Record<string, { state: string; description?: string }>>();
|
|
const writes: string[] = [];
|
|
const verdict = (trunk: string) => ({
|
|
"mesh/merge-gate": { state: "success", description: `head x merged into trunk ${trunk.slice(0, 12)} — pass` },
|
|
"mesh/repo-check": { state: "success", description: `head x merged into trunk ${trunk.slice(0, 12)} — pass` } });
|
|
for (const p of pulls) statuses.set(p.head, verdict(heads[p.base]));
|
|
const forge = {
|
|
listOpenPulls: async () => pulls.map((p) => ({ number: p.number, title: "t", state: "open", merged: false, base: p.base,
|
|
head: "feat", head_sha: p.head, html_url: `http://forge/pulls/${p.number}` })),
|
|
branchHead: async (_o: string, _r: string, branch: string) => heads[branch],
|
|
setCommitStatus: async (_o: string, _r: string, sha: string, s: { context: string; state: string; description: string }) => {
|
|
writes.push(`${sha} ${s.context} ${s.state}`);
|
|
statuses.set(sha, { ...statuses.get(sha), [s.context]: { state: s.state, description: s.description } });
|
|
},
|
|
gateOf: async () => undefined,
|
|
gateHistory: async () => [],
|
|
headChecks: async (_o: string, _r: string, sha: string) => statuses.get(sha) ?? {},
|
|
};
|
|
return { forge, writes, statuses };
|
|
}
|
|
|
|
test("a push to a trunk puts every open pull request into it out of date — twenty-five of them — and none into another", async () => {
|
|
const heads: Record<string, string> = { main: TRUNK, release: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" };
|
|
const pulls = Array.from({ length: 25 }, (_, i) => ({ number: i + 1, base: "main", head: `${String(i).padStart(2, "0")}`.repeat(20) }));
|
|
pulls.push({ number: 99, base: "release", head: "9".repeat(40) });
|
|
const { forge, writes, statuses } = aForge(pulls, heads);
|
|
const gates = new GateWriter(forge);
|
|
const trunks: Record<string, string> = {};
|
|
|
|
// First sight: each trunk is taken as it is, and nothing is put out of date.
|
|
assert.equal(await weighTrunks(forge, gates, trunks, "novox", "mesh-host"), true);
|
|
assert.deepEqual(writes, []);
|
|
assert.equal(trunks["novox/mesh-host:main"], TRUNK);
|
|
// Nothing moved: nothing read past the trunks, nothing written.
|
|
assert.equal(await weighTrunks(forge, gates, trunks, "novox", "mesh-host"), false);
|
|
|
|
heads.main = MOVED;
|
|
assert.equal(await weighTrunks(forge, gates, trunks, "novox", "mesh-host"), true);
|
|
assert.equal(writes.length, 50, "both statuses of each of the 25 pull requests into main");
|
|
for (const p of pulls.filter((p) => p.base === "main")) {
|
|
const s = statuses.get(p.head)!;
|
|
assert.equal(s["mesh/merge-gate"].state, "pending");
|
|
assert.match(s["mesh/merge-gate"].description!, /judged against trunk a2c2c35bbbbb, the trunk is now d28ca95ccccc/);
|
|
assert.equal(s["mesh/repo-check"].state, "pending");
|
|
}
|
|
assert.equal(statuses.get("9".repeat(40))!["mesh/merge-gate"].state, "success", "a pull request into another branch is left");
|
|
assert.equal(trunks["novox/mesh-host:main"], MOVED);
|
|
|
|
// Weighed again, nothing more: the move was taken once, and the out-of-date statuses are left as they are.
|
|
writes.length = 0;
|
|
heads.main = "f".repeat(40);
|
|
await weighTrunks(forge, gates, trunks, "novox", "mesh-host");
|
|
assert.deepEqual(writes, [], "a check already out of date is not written again");
|
|
});
|
|
|
|
test("a trunk that cannot be read is weighed again on the next pass, never recorded half", async () => {
|
|
const heads: Record<string, string> = { main: TRUNK };
|
|
const { forge } = aForge([{ number: 1, base: "main", head: "1".repeat(40) }], heads);
|
|
const trunks: Record<string, string> = { "novox/mesh-host:main": TRUNK };
|
|
heads.main = MOVED;
|
|
const failing = { ...forge, setCommitStatus: async () => { throw new Error("the forge is away"); } };
|
|
assert.equal(await weighTrunks(failing, new GateWriter(failing), trunks, "novox", "mesh-host"), false);
|
|
assert.equal(trunks["novox/mesh-host:main"], TRUNK, "not recorded: the next pass weighs it again");
|
|
});
|
|
|
|
test("the trunk's move takes its turn after a verdict in flight, and weighs what the forge holds then", async () => {
|
|
const heads: Record<string, string> = { main: TRUNK };
|
|
const { forge, statuses } = aForge([{ number: 1, base: "main", head: HEAD }], heads);
|
|
const gates = new GateWriter(forge);
|
|
// A verdict judged against the trunk's new head, in flight when the move is weighed: it lands first, and is left.
|
|
let release!: () => void;
|
|
const held = new Promise<void>((r) => (release = r));
|
|
const verdictLanded = gates.verdict("novox", "mesh-host", HEAD, async () => {
|
|
await held;
|
|
statuses.set(HEAD, { "mesh/merge-gate": { state: "success", description: `head x merged into trunk ${MOVED.slice(0, 12)} — pass` } });
|
|
});
|
|
const moved = gates.outOfDate("novox", "mesh-host", HEAD, (checks) => outOfDateStatuses(checks, MOVED));
|
|
release();
|
|
await verdictLanded;
|
|
assert.deepEqual(await moved, [], "a verdict judged against the trunk's head is not put out of date");
|
|
assert.equal(statuses.get(HEAD)!["mesh/merge-gate"].state, "success");
|
|
});
|
|
|
|
test("a verdict judged against a trunk newer than the pass's first read is not put out of date", async () => {
|
|
const NEWER = "ffffffffffffffffffffffffffffffffffffffff";
|
|
const heads: Record<string, string> = { main: TRUNK };
|
|
const { forge, statuses } = aForge([{ number: 1, base: "main", head: HEAD }], heads);
|
|
const trunks: Record<string, string> = { "novox/mesh-host:main": TRUNK };
|
|
// The pass reads main at MOVED; by the head's turn the trunk moved again, and the head's check answered for it.
|
|
let reads = 0;
|
|
const moving = { ...forge, branchHead: async () => (reads++ === 0 ? MOVED : NEWER) };
|
|
statuses.set(HEAD, { "mesh/merge-gate": { state: "success", description: `head x merged into trunk ${NEWER.slice(0, 12)} — pass` } });
|
|
await weighTrunks(moving, new GateWriter(moving), trunks, "novox", "mesh-host");
|
|
assert.equal(statuses.get(HEAD)!["mesh/merge-gate"].state, "success", "a fresh verdict was put out of date");
|
|
});
|
|
|
|
test("a verdict that landed out of date keeps its account on the pull request", () => {
|
|
const words = outOfDateWords(TRUNK, MOVED);
|
|
const own = lateVerdictComment({ commit: HEAD, trunk: TRUNK.slice(0, 12), verdict: "fail" }, "**Merge check** at `dfb1415a`\n\nthe report", words);
|
|
assert.match(own, /the report/);
|
|
assert.match(own, /\*\*Out of date when it landed\*\*: out of date: judged against trunk a2c2c35bbbbb/);
|
|
const bare = lateVerdictComment({ commit: HEAD, trunk: TRUNK.slice(0, 12), verdict: "pass" }, null, words);
|
|
assert.match(bare, /head dfb1415aaaaa merged into trunk a2c2c35bbbbb, pass/);
|
|
});
|