Leave a composed member's gate to the group's verdict, reword the mark leftovers, restore main's merge-check.sh (issues 1022, 978)
This commit is contained in:
+3
-6
@@ -17,8 +17,7 @@
|
||||
# toolchain has a C compiler. A requirement of the mesh's own forge the module cache does not hold (a
|
||||
# pinned SDK commit) is taken from its repository's clone beside the check (checks/deps); a module whose
|
||||
# dependencies still cannot be had **fails the check**, by name and why — its tests could not run, and a
|
||||
# test that could not run is a failure, never a note (novox/hq issue 385). The integration tests (`//go:build
|
||||
# integration`) run here, with -tags integration, and only here. A module written in
|
||||
# test that could not run is a failure, never a note (novox/hq issue 385). A module written in
|
||||
# TypeScript is type-checked and tested by
|
||||
# merge-check-typescript.sh, declared above, in the TypeScript toolchain (novox/hq issues 302, 311);
|
||||
# 3. the words (novox/hq ADR 0244): no word the glossary retired for the tools, as copied in
|
||||
@@ -184,10 +183,8 @@ for m in $touched; do
|
||||
fi
|
||||
cgo=0
|
||||
[ -n "$race" ] && cgo=1
|
||||
# The integration tests (a file whose first line is `//go:build integration`: a test that starts a bus, a store or
|
||||
# another process) run here and nowhere else: the unit suite stays under a second (novox/hq playbook 07).
|
||||
(cd "modules/$m" && CGO_ENABLED=$cgo GOPRIVATE=git.novox.be go vet -tags integration ${modfile:+"-modfile=$modfile"} ./... &&
|
||||
CGO_ENABLED=$cgo GOPRIVATE=git.novox.be go test -tags integration ${modfile:+"-modfile=$modfile"} $race -count=1 ./...)
|
||||
(cd "modules/$m" && CGO_ENABLED=$cgo GOPRIVATE=git.novox.be go vet ${modfile:+"-modfile=$modfile"} ./... &&
|
||||
CGO_ENABLED=$cgo GOPRIVATE=git.novox.be go test ${modfile:+"-modfile=$modfile"} $race -count=1 ./...)
|
||||
done
|
||||
if [ -n "$untestable" ]; then
|
||||
echo "the check fails: the tests of$untestable could not run (novox/hq issue 385)"
|
||||
|
||||
@@ -42,7 +42,7 @@ import { commitStatusTool } from "./delivery.js";
|
||||
import { announce, keepWatermark, readWatermark } from "./announce.js";
|
||||
import { mergeTool } from "./merge.js";
|
||||
import { GiteaAdmin, GiteaClient, movedSince } from "./client.js";
|
||||
import { CHECK_CONTEXT, REPO_CHECK_CONTEXT, commentFor, groupGateStatus, headsToAnnounce, statusesFor, type Announced, type Checked,
|
||||
import { CHECK_CONTEXT, REPO_CHECK_CONTEXT, commentFor, composedGate, groupGateStatus, headsToAnnounce, statusesFor, type Announced, type Checked,
|
||||
repoCheckFacts, reposToLook } from "./pulls.js";
|
||||
import { GateWriter, clonedAs, type BuildStarted } from "./gate.js";
|
||||
import { HOOK_PATH, HOOK_PORT, Pace, claimHead, ensureSystemHook, eventsTool, keptSecret, receiver, type ForgeEvent } from "./webhook.js";
|
||||
@@ -397,23 +397,24 @@ async function setVerdict(client: GiteaClient, event: { body: unknown }): Promis
|
||||
const head = await client.branchHead(c.owner, c.repo, trunk).catch(() => undefined);
|
||||
if (head && !sameCommit(c.trunk, head)) {
|
||||
const description = outOfDateWords(c.trunk, head);
|
||||
for (const context of [CHECK_CONTEXT, ...(c["repo-check"] ? [REPO_CHECK_CONTEXT] : [])]) {
|
||||
for (const context of [...(composedGate(c) ? [] : [CHECK_CONTEXT]), ...(c["repo-check"] ? [REPO_CHECK_CONTEXT] : [])]) {
|
||||
await client.setCommitStatus(c.owner, c.repo, c.commit, { state: "pending", context, description,
|
||||
...(target ? { target_url: target } : {}) });
|
||||
}
|
||||
// Its account is kept and shown all the same (ADR 0323 §7): what it judged, and that it is out of date.
|
||||
const said = commentFor(c, await repoCheckFacts(client, c, trunk));
|
||||
if (c.number) await client.addComment(c.owner, c.repo, c.number, lateVerdictComment(c, said, description));
|
||||
if (c.number && !composedGate(c)) await client.addComment(c.owner, c.repo, c.number, lateVerdictComment(c, said, description));
|
||||
console.log(`[gitea] ${c.owner}/${c.repo}#${c.number ?? "?"} at ${c.commit.slice(0, 8)}: merge check ${c.verdict}, ` +
|
||||
`${description}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const facts = await repoCheckFacts(client, c, base ?? c.plan?.base);
|
||||
for (const status of statusesFor(c, facts)) {
|
||||
const gateNow = composedGate(c) ? (await client.headChecks(c.owner, c.repo, c.commit).catch(() => ({} as Record<string, { description?: string }>)))[CHECK_CONTEXT]?.description : undefined;
|
||||
for (const status of statusesFor(c, facts, gateNow)) {
|
||||
await client.setCommitStatus(c.owner, c.repo, c.commit, target ? { ...status, target_url: target } : status);
|
||||
}
|
||||
const comment = commentFor(c, facts);
|
||||
const comment = composedGate(c) ? null : commentFor(c, facts);
|
||||
if (comment && c.number) await client.addComment(c.owner, c.repo, c.number, comment);
|
||||
console.log(`[gitea] ${c.owner}/${c.repo}#${c.number ?? "?"} at ${c.commit.slice(0, 8)}: merge check ${c.verdict}`);
|
||||
}
|
||||
|
||||
+17
-1
@@ -248,8 +248,24 @@ export function groupGateStatus(c: Checked, m: NonNullable<Checked["members"]>[n
|
||||
return { state: stateOf(c.verdict), context: CHECK_CONTEXT, description: clipped(first + said) };
|
||||
}
|
||||
|
||||
/** Whether a verdict's gate layer is the group's composed check's (ADR 0403 §1): no gate was run for the head alone. */
|
||||
export function composedGate(c: Checked): boolean {
|
||||
return c.gate?.verdict === "composed";
|
||||
}
|
||||
|
||||
/** Every status a verdict sets: the gate's, and the repository's own check's when it was said. */
|
||||
export function statusesFor(c: Checked, facts: RepoCheckFacts = {}): CommitStatus[] {
|
||||
export function statusesFor(c: Checked, facts: RepoCheckFacts = {}, gateNow?: string): CommitStatus[] {
|
||||
if (composedGate(c)) {
|
||||
// A member whose gate the group's composed check judges (ADR 0403 §1): only its repository's check is its own to
|
||||
// set. The gate stays pending, saying it waits for the group, unless a composed verdict already stands on it.
|
||||
const out: CommitStatus[] = [];
|
||||
if (!(gateNow ?? "").includes(" composed: ")) {
|
||||
out.push({ state: "pending", context: CHECK_CONTEXT, description: "waits for its group's composed check" });
|
||||
}
|
||||
const repo = c["repo-check"];
|
||||
if (repo) out.push(judgedFirst(c, repoStatus(repo, facts)));
|
||||
return out;
|
||||
}
|
||||
const out = [statusFor(c)];
|
||||
const repo = c["repo-check"];
|
||||
if (repo) out.push(judgedFirst(c, repoStatus(repo, facts)));
|
||||
|
||||
@@ -172,3 +172,23 @@ test("a composed verdict sets every member's gate at its own head, naming its tr
|
||||
assert.equal(groupGateStatus({ ...c, verdict: "fail", summary: "a and b do not compose" }, c.members[0]).state, "failure");
|
||||
assert.equal(groupGateStatus({ ...c, verdict: "error", summary: "could not run" }, c.members[0]).state, "error");
|
||||
});
|
||||
|
||||
// A member's own verdict whose gate layer is the group's composed check's sets its repository check only, and never the
|
||||
// gate as an error: the gate stays pending until the composed verdict lands, in either order (ADR 0403 §1, issue 978).
|
||||
test("a member's own verdict with a composed gate layer leaves the gate to the composed verdict, in either order", async () => {
|
||||
const { statusesFor, composedGate, groupGateStatus, CHECK_CONTEXT, REPO_CHECK_CONTEXT } = await import("../pulls.ts");
|
||||
const c = { owner: "novox", repo: "mesh-catalog", number: 2, commit: "d".repeat(40), id: "build-1", verdict: "pass", summary: "x",
|
||||
gate: { verdict: "composed", summary: "judged by its group" }, "repo-check": { verdict: "pass", summary: "suite passed" } };
|
||||
assert.ok(composedGate(c));
|
||||
// Before the composed verdict: the repository check, and the gate pending, saying it waits.
|
||||
const before = statusesFor(c, {}, undefined);
|
||||
assert.deepEqual(before.map((s) => [s.context, s.state]), [[CHECK_CONTEXT, "pending"], [REPO_CHECK_CONTEXT, "success"]]);
|
||||
assert.match(before[0].description, /waits for its group's composed check/);
|
||||
assert.ok(!before.some((s) => s.state === "error"));
|
||||
// After it: the standing composed gate is left alone.
|
||||
const standing = groupGateStatus({ ...c, group: "feat/x", members: [] }, { owner: "novox", repo: "mesh-catalog", commit: c.commit }).description;
|
||||
const after = statusesFor(c, {}, standing);
|
||||
assert.deepEqual(after.map((s) => [s.context, s.state]), [[REPO_CHECK_CONTEXT, "success"]]);
|
||||
// A verdict with an ordinary gate is as before.
|
||||
assert.equal(composedGate({ ...c, gate: { verdict: "pass", summary: "x" } }), false);
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ func (h *Holder) Work() {
|
||||
}
|
||||
|
||||
// React takes every step the facts allow, after an event that can change what they are: the forge's open pull requests
|
||||
// read when a ready mark may have been made, the steps that need another's word (look), what is owed (Flush), and then
|
||||
// read when a check may have answered, the steps that need another's word (look), what is owed (Flush), and then
|
||||
// the time the next look is wanted at, if any step waits on the clock. Never concurrent with itself.
|
||||
func (h *Holder) React() {
|
||||
h.reactMu.Lock()
|
||||
@@ -70,7 +70,7 @@ func (h *Holder) React() {
|
||||
if h.takeForgeWanted() {
|
||||
if err := h.SeeOpenPulls(); err != nil {
|
||||
h.wantForge()
|
||||
h.again("the forge's open pull requests, read when a ready mark or a check may have changed")
|
||||
h.again("the forge's open pull requests, read when a check may have changed")
|
||||
}
|
||||
}
|
||||
h.look()
|
||||
|
||||
@@ -500,7 +500,7 @@ func Apply(d *Delivery, ev Event, act bool, f Facts) (Transition, error) {
|
||||
}
|
||||
by := f.By
|
||||
if by == "" && (ev == EvMerged || ev == EvDone) && d.MergedAs != "" {
|
||||
// A merge this owner made on a ready mark names it as its author (novox/hq issue 722).
|
||||
// A merge this owner made on the operator's Approve names it as its author (novox/hq issue 722).
|
||||
by = d.Ready.mergedBy()
|
||||
}
|
||||
t := Transition{At: f.Now, From: d.State, To: r.To, Event: ev, Why: why, By: by}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
// The forge's announcer is the only way the mesh hears of a pull request's head, and on 2026-10-09 it missed one: a
|
||||
// pull request opened after its push moved nothing the announcer read, and waited unchecked, unable to merge, with
|
||||
// nothing saying so. This owner asks the forge itself for every open pull request when it starts and when an event says
|
||||
// what the forge shows may have changed — a merge check's status, a new head, a ready mark (events.go), not a poll
|
||||
// what the forge shows may have changed — a merge check's status, a new head (events.go), not a poll
|
||||
// (novox/hq ADR 0406 §1, issue 975) — and once more at a head's bound, and one whose
|
||||
// base requires the merge check — mesh/merge-gate or mesh/repo-check — with neither on its head for UncheckedAfter
|
||||
// since this owner first saw it so is said as a delivery held: a `stalled` line in the state Unannounced, which the
|
||||
|
||||
Reference in New Issue
Block a user