chore(ci): pr-evidence FAIL output tells you to push (body edit does not re-run the gate) (#5944)

ci.yml ignores the 'edited' event, so adding the Evidence block to the PR body after a
push does not re-run check:pr-evidence — you need another commit. The FAIL report now
says so, at the exact place someone sees the red check. + 5 unit tests (classification +
hint-on-fail / no-hint-on-pass). Decided against a separate edited-triggered workflow:
pr-evidence is not a required check (no ruleset gates it; release PRs merge UNSTABLE, not
BLOCKED), so the gap is cosmetic and the generate-release skill already puts Evidence in
the body before the first push.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-02 14:53:58 -03:00
committed by GitHub
parent 0d6add19dc
commit 16ccd5f586
2 changed files with 65 additions and 1 deletions

View File

@@ -231,7 +231,16 @@ if (isMain) {
} else if (result === "pass") {
reportLines.push("Result: PASS", "", reason);
} else {
reportLines.push("Result: FAIL", "", reason);
reportLines.push(
"Result: FAIL",
"",
reason,
"",
"> Editing the PR body to add the evidence does NOT re-run this gate — `ci.yml` " +
"does not listen to the `edited` event. Add the `## Evidence` block, then **push a " +
"commit** (or re-run this job) to re-validate. For releases, put the Evidence block in " +
"the body BEFORE the first push (see the generate-release skill, Phase 0)."
);
}
const report = buildReport(reportLines);

View File

@@ -0,0 +1,55 @@
import test from "node:test";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
const mod = await import("../../scripts/check/check-pr-evidence.mjs");
const { evaluatePrBody } = mod;
const SCRIPT = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../../scripts/check/check-pr-evidence.mjs"
);
function run(body) {
try {
const out = execFileSync("node", [SCRIPT], {
encoding: "utf8",
env: { ...process.env, PR_BODY: body },
});
return { code: 0, out };
} catch (err) {
return { code: err.status ?? 1, out: `${err.stdout || ""}${err.stderr || ""}` };
}
}
test("evaluatePrBody: no outcome claim → pass (no evidence required)", () => {
const r = evaluatePrBody("Adds a helper module.");
assert.equal(r.result, "pass");
assert.match(r.reason, /no evidence required/i);
});
test("evaluatePrBody: outcome claim + evidence block → pass", () => {
const r = evaluatePrBody("Tests pass.\n\n## Evidence\n```\ntests 20 / pass 20 / fail 0\n```");
assert.equal(r.result, "pass");
});
test("evaluatePrBody: outcome claim without evidence → fail", () => {
const r = evaluatePrBody("All 20 tests pass and 0 errors.");
assert.equal(r.result, "fail");
});
test("the FAIL report explains that editing the body does not re-run the gate (push instead)", () => {
const { code, out } = run("All 20 tests pass and 0 errors."); // claim, no evidence
assert.equal(code, 1, "gate fails on a claim with no evidence");
assert.match(out, /Result: FAIL/);
assert.match(out, /does NOT re-run this gate/);
assert.match(out, /push a commit/i);
});
test("the hint does NOT appear when the gate passes", () => {
const { code, out } = run("Tests pass.\n\n## Evidence\n```\ntests 20 / pass 20 / fail 0\n```");
assert.equal(code, 0);
assert.match(out, /Result: PASS/);
assert.doesNotMatch(out, /does NOT re-run this gate/);
});