review-verdict.yml's classify/post step never executes — job goes green, review-verdict/h10 is never posted automatically #751

Closed
opened 2026-08-06 14:36:21 +02:00 by timothy · 3 comments
Owner

Found while running the #748 scratch-base probe. Not caused by #748, and not caused by the 1.27.1 upgrade — it predates both.

The defect

review-verdict.yml's step "Classify the PR and post the review-verdict status" never executes, and the job reports success anyway.

In the job log the runner emits:

Unable to interpolate expression 'format('set -euo pipefail\n\nCONTEXT="review-verdict/h10"\n# The description this job writes when it repairs ...
::group::Run Classify the PR and post the review-verdict status
  <env block>
::endgroup::

…and then jumps straight to the post-checkout cleanup. The step markers confirm it:

⭐ Run Main Checkout the PR's BASE ref
⭐ Run Post Checkout the PR's BASE ref
  ✅  Success - Post Checkout the PR's BASE ref
🏁  Job succeeded

There is no ⭐ Run Main Classify the PR and post the review-verdict status, no stdout from the script, and no success/failure marker for it. The step is silently dropped and the job goes green.

How long

Present in run 1832 (2026-08-04T21:46Z), which is before the Gitea 1.25.4 → 1.27.1 upgrade, and in every review-verdict run I sampled since (1844, 1849, 1852, 1856). So this is not an upgrade regression. Not bisected further back.

Consequences

  1. review-verdict/h10 is never posted automatically. The only thing that has been setting it is a human/agent running scripts/post-review-verdict.sh. On PR #749 the context was simply absent after a fully green CI run until I posted it by hand.
  2. Both exemption classes are dead. Renovate-authored and docs-only PRs are supposed to be auto-passed by this workflow. They are not — they get no h10 at all, so they cannot satisfy the required check. Worth checking whether recent dependency PRs have been silently unmergeable.
  3. The failure is silent and green, which is the worst shape: the job's own context (Review verdict / Set review-verdict status) reports success, so nothing surfaces. docs/ci-cd.md deliberately keeps that context out of branch protection precisely so a workflow cannot satisfy the gate by merely running — that design decision is what stopped this from being much worse.

Why it blocks #748

#748 wants to declare permissions: on this workflow so the owner default can move to Restricted. The whole point of the scratch-base probe is to prove the declared scope still permits POST /statuses/{sha}. That is unprovable while the step that would POST never runs. #748 should stay blocked until this is fixed.

Likely cause (unconfirmed)

The run: body is a very large shell script containing ${{-adjacent text, backticks and \n sequences; the runner is rewriting it through format(...) and failing to interpolate. Suspect something in the script body is being parsed as an expression. A minimal repro would be to bisect the script comment block out of the run: step.

Done-when

  • Root cause identified (what in the run: body defeats interpolation)
  • The step executes and posts review-verdict/h10 on a scratch-base probe PR (probe-named context, never the real one)
  • A dropped/non-executed step fails the job instead of going green — the silent-green half is the real defect
  • Confirm whether Renovate/docs-only PRs have been unmergeable, and unblock any that were
  • Adversarial review passed
Found while running the #748 scratch-base probe. **Not** caused by #748, and **not** caused by the 1.27.1 upgrade — it predates both. ## The defect `review-verdict.yml`'s step **"Classify the PR and post the review-verdict status" never executes**, and the job reports `success` anyway. In the job log the runner emits: ``` Unable to interpolate expression 'format('set -euo pipefail\n\nCONTEXT="review-verdict/h10"\n# The description this job writes when it repairs ... ::group::Run Classify the PR and post the review-verdict status <env block> ::endgroup:: ``` …and then jumps straight to the post-checkout cleanup. The step markers confirm it: ``` ⭐ Run Main Checkout the PR's BASE ref ⭐ Run Post Checkout the PR's BASE ref ✅ Success - Post Checkout the PR's BASE ref 🏁 Job succeeded ``` There is **no** `⭐ Run Main Classify the PR and post the review-verdict status`, no stdout from the script, and no success/failure marker for it. The step is silently dropped and the job goes green. ## How long Present in run **1832 (2026-08-04T21:46Z)**, which is before the Gitea 1.25.4 → 1.27.1 upgrade, and in every review-verdict run I sampled since (1844, 1849, 1852, 1856). So this is **not** an upgrade regression. Not bisected further back. ## Consequences 1. **`review-verdict/h10` is never posted automatically.** The only thing that has been setting it is a human/agent running `scripts/post-review-verdict.sh`. On PR #749 the context was simply **absent** after a fully green CI run until I posted it by hand. 2. **Both exemption classes are dead.** Renovate-authored and docs-only PRs are supposed to be auto-passed by this workflow. They are not — they get no `h10` at all, so they cannot satisfy the required check. Worth checking whether recent dependency PRs have been silently unmergeable. 3. **The failure is silent and green**, which is the worst shape: the job's own context (`Review verdict / Set review-verdict status`) reports success, so nothing surfaces. `docs/ci-cd.md` deliberately keeps that context out of branch protection precisely so a workflow cannot satisfy the gate by merely running — that design decision is what stopped this from being much worse. ## Why it blocks #748 #748 wants to declare `permissions:` on this workflow so the owner default can move to Restricted. The whole point of the scratch-base probe is to prove the declared scope still permits `POST /statuses/{sha}`. **That is unprovable while the step that would POST never runs.** #748 should stay blocked until this is fixed. ## Likely cause (unconfirmed) The `run:` body is a very large shell script containing `${{`-adjacent text, backticks and `\n` sequences; the runner is rewriting it through `format(...)` and failing to interpolate. Suspect something in the script body is being parsed as an expression. A minimal repro would be to bisect the script comment block out of the `run:` step. ## Done-when - [x] Root cause identified (what in the `run:` body defeats interpolation) - [x] The step executes and posts `review-verdict/h10` on a scratch-base probe PR (probe-named context, never the real one) - [x] **A dropped/non-executed step fails the job instead of going green** — the silent-green half is the real defect - [x] Confirm whether Renovate/docs-only PRs have been unmergeable, and unblock any that were - [x] Adversarial review passed
timothy added the ci-cdpriority: highsecurity labels 2026-08-06 14:36:21 +02:00
timothy added the in-progress label 2026-08-06 20:14:12 +02:00
Author
Owner

Claiming this (Claude Code session, 2026-08-06).

Root cause found — it is line 413 of the workflow itself, inside a shell comment.

The run: body contains exactly one ${{ }} sequence, and it is not meant as an expression:

# not work here, also measured: with `concurrency: {group: …-${{ pr number }},

The runner does not know that is a shell comment. It scans the whole run: scalar for ${{, finds that one, and therefore rewrites the entire 675-line body into a single format(...) call so it can splice the expression result back in — which is exactly the Unable to interpolate expression 'format('set -euo pipefail\n\nCONTEXT="review-verdict/h10"... in the log. pr number is not a valid expression, the interpolation of the whole scalar fails, and the step is dropped. One stray token in prose takes the entire step with it, because the format() rewrite is all-or-nothing.

It dates the outage precisely. git log -S / git blame put that line in 8f6d4f443 (fix(706,707,711): fence the review-verdict write on the timeline retarget count, 2026-08-03) — the commit that added the count_retargets note explaining why a concurrency group does not work here. So the window opens 2026-08-03, which is consistent with the report (present in run 1832 on 2026-08-04, predates the 1.27.1 upgrade) and closes the "not bisected further back" gap.

Grimly apt: the workflow was disabled by the prose documenting a fix, not by the fix.

Plan — the third Done-when box (silent green) is the real defect, so the fix is at the boundary, not the site:

  1. Neutralise the prose so no ${{ can appear in a run: body.
  2. A static guard so this class cannot recur: assert no ${{ occurs inside any run: scalar in .gitea/workflows/*.yml. This is the boundary fix — the specific line is just the first instance.
  3. A dropped-step guard so a non-executed step fails the job instead of going green, measured on a scratch-base probe with a probe-named context (never the real one).
  4. Audit which Renovate/docs-only PRs were unmergeable in the 2026-08-03 → now window.

Not bundling #742 (the other open issue touching this file). #751 is an urgent surgical restore of a dead required gate; #742 changes the provenance model of the same 857-line security-critical file. Landing them together would enlarge the review surface on a gate that is currently non-functional. #742 goes after this merges.

Claiming this (Claude Code session, 2026-08-06). **Root cause found — it is line 413 of the workflow itself, inside a shell comment.** The `run:` body contains exactly one `${{ }}` sequence, and it is not meant as an expression: ``` # not work here, also measured: with `concurrency: {group: …-${{ pr number }}, ``` The runner does not know that is a shell comment. It scans the whole `run:` scalar for `${{`, finds that one, and therefore rewrites the **entire 675-line body** into a single `format(...)` call so it can splice the expression result back in — which is exactly the `Unable to interpolate expression 'format('set -euo pipefail\n\nCONTEXT="review-verdict/h10"...` in the log. `pr number` is not a valid expression, the interpolation of the whole scalar fails, and the step is dropped. One stray token in prose takes the entire step with it, because the format() rewrite is all-or-nothing. **It dates the outage precisely.** `git log -S` / `git blame` put that line in **8f6d4f443** (`fix(706,707,711): fence the review-verdict write on the timeline retarget count`, 2026-08-03) — the commit that added the `count_retargets` note explaining why a concurrency group does not work here. So the window opens 2026-08-03, which is consistent with the report (present in run 1832 on 2026-08-04, predates the 1.27.1 upgrade) and closes the "not bisected further back" gap. Grimly apt: the workflow was disabled by the prose documenting a fix, not by the fix. **Plan** — the third Done-when box (silent green) is the real defect, so the fix is at the boundary, not the site: 1. Neutralise the prose so no `${{` can appear in a `run:` body. 2. A **static guard** so this class cannot recur: assert no `${{` occurs inside any `run:` scalar in `.gitea/workflows/*.yml`. This is the boundary fix — the specific line is just the first instance. 3. A **dropped-step guard** so a non-executed step fails the job instead of going green, measured on a scratch-base probe with a probe-named context (never the real one). 4. Audit which Renovate/docs-only PRs were unmergeable in the 2026-08-03 → now window. **Not bundling #742** (the other open issue touching this file). #751 is an urgent surgical restore of a dead required gate; #742 changes the provenance model of the same 857-line security-critical file. Landing them together would enlarge the review surface on a gate that is currently non-functional. #742 goes after this merges.
Author
Owner

Probe evidence (live, on this instance)

Both halves verified with a scratch-base probe pair per the documented procedure in docs/ci-cd.md. Probe context probe751/h10 throughout — never the real one, since a status forged on a scratch head is inherited by any later PR carrying the same head sha (#663).

Negative control — PR #753, run 1863: a dropped step is now RED

Unable to interpolate expression 'format('set -euo pipefail\n\n# NEVER WRITE AN EXPRESSION DELIMITER…
(no `⭐ Run Main Classify the PR and post the review-verdict status`)
❌  Failure - Main Assert the classifier actually executed
🏁  Job failed

This also answers the question the original report could not: a later step DOES still run after the runner drops an earlier one. The classify step was previously the job's last step, so there was never a subsequent step to observe. The guard therefore works, and the ~700-line body does not need to move into scripts/.

Positive — PR #752, run 1866: the step executes AND posts

Retarget fence: 0 retarget event(s) observed before classifying (trusted=yes).
Changed files (1, complete=yes):
Decision: state=success — docs-only change (no code, no protected path)
Posted probe751/h10=success on f47b5d2.
The classify step executed (start marker present at /tmp/h10-classifier-started-1866-1).
🏁  Job succeeded

The probe found a SECOND defect, and it is why box 2 needed two rounds

Round 1 of the positive probe ran the classify step successfully and still posted nothing:

Retarget fence: 0 retarget event(s) observed before classifying (trusted=no).
Decision: state=success — docs-only change (no code, no protected path)
::error::Could not establish a trusted retarget count for PR #752 (before=no, after=no) … Posting nothing

A page past the end of GET /issues/{n}/timeline is the JSON value null, not [] — four bytes, measured at Gitea 1.27.1. count_retargets gated on type == "array", so it read the real terminator as unreadable, never reached a validated empty page, and rt_ok was never yes for any pull request. The #706 fence therefore withheld every exemption success. Fixing the interpolation alone would not have restored the exemptions — the same user-visible outcome as this issue, by an unrelated route.

The instance is not even consistent between endpoints: /issues/{n}/comments returns [] when empty. Both shapes now terminate the walk, and the regression test is parameterised over both.

Two reasons it read as deliberate rather than broken:

  • It had never run. The fence shipped in 8f6d4f443 — the same commit whose prose comment stopped the step executing. Merging a guard and first executing it are different events.
  • The test double asserted the wrong shape while claiming to be measured. Its comment read "Real shapes, measured on this instance and deliberately mirrored" and it printed [] past the end. Correcting the double and restoring the old gate turns 18 tests red — all of them had been green for the wrong reason.

Box 4 — blast radius audit

Window 2026-08-03 21:41 (bad commit) → now, all PRs:

PR Class Outcome
#739 docs-only exempt AFFECTED — merged 08-05 with zero commit statuses on its head; got in only via admin force-merge
#738 docs-only exempt closed unmerged (superseded by #739), never needed a bypass
#717, #718 exempt (renovate) unaffected by timing only — merged minutes before the bad commit
#723/725/737/741/745/749 non-exempt all carry a hand-posted Review-verdict: with creator=timothy — normal path, no separate hole

Nothing is stuck right now (no open PRs besides the probes). But #743 disabled admin force-merge on 08-06 00:58, so the escape that let #739 through is gone: the next docs-only or Renovate-manifest PR would have been permanently unmergeable with no bypass.

Also fixed, both found while editing this code

  • ci.verdict-write-retarget-fence stated this as a narrow residual ("a timeline over the 20-page cap can never be exempted — rt_ok stays no on every run") when the behaviour was universal. Corrected in place, since a stale claim that reads as checked talks the next reader out of verifying.
  • The workflow cited ci.paged-endpoint-completeness, a key that has never existed as a record anywhere. Repointed at the record that actually owns this walk.
  • Start marker keyed on run id + attempt: RUNNER_TEMP is /tmp on this runner, not a private per-job dir, so a stale marker could have made the guard pass on a run whose step was dropped.

Independent adversarial review (cross-family + a second cold lens) is running now. PR to follow once it is clean — not before.

## Probe evidence (live, on this instance) Both halves verified with a scratch-base probe pair per the documented procedure in `docs/ci-cd.md`. Probe context `probe751/h10` throughout — never the real one, since a status forged on a scratch head is inherited by any later PR carrying the same head sha (#663). ### Negative control — PR #753, run 1863: a dropped step is now RED ``` Unable to interpolate expression 'format('set -euo pipefail\n\n# NEVER WRITE AN EXPRESSION DELIMITER… (no `⭐ Run Main Classify the PR and post the review-verdict status`) ❌ Failure - Main Assert the classifier actually executed 🏁 Job failed ``` This also answers the question the original report could not: **a later step DOES still run after the runner drops an earlier one.** The classify step was previously the job's last step, so there was never a subsequent step to observe. The guard therefore works, and the ~700-line body does **not** need to move into `scripts/`. ### Positive — PR #752, run 1866: the step executes AND posts ``` Retarget fence: 0 retarget event(s) observed before classifying (trusted=yes). Changed files (1, complete=yes): Decision: state=success — docs-only change (no code, no protected path) Posted probe751/h10=success on f47b5d2. The classify step executed (start marker present at /tmp/h10-classifier-started-1866-1). 🏁 Job succeeded ``` ## The probe found a SECOND defect, and it is why box 2 needed two rounds Round 1 of the positive probe ran the classify step successfully and **still posted nothing**: ``` Retarget fence: 0 retarget event(s) observed before classifying (trusted=no). Decision: state=success — docs-only change (no code, no protected path) ::error::Could not establish a trusted retarget count for PR #752 (before=no, after=no) … Posting nothing ``` A page past the end of `GET /issues/{n}/timeline` is the JSON value `null`, not `[]` — four bytes, measured at Gitea 1.27.1. `count_retargets` gated on `type == "array"`, so it read the real terminator as *unreadable*, never reached a validated empty page, and `rt_ok` was never `yes` **for any pull request**. The #706 fence therefore withheld **every** exemption `success`. Fixing the interpolation alone would not have restored the exemptions — the same user-visible outcome as this issue, by an unrelated route. The instance is not even consistent between endpoints: `/issues/{n}/comments` returns `[]` when empty. Both shapes now terminate the walk, and the regression test is parameterised over both. Two reasons it read as deliberate rather than broken: - **It had never run.** The fence shipped in 8f6d4f443 — the same commit whose prose comment stopped the step executing. Merging a guard and first executing it are different events. - **The test double asserted the wrong shape while claiming to be measured.** Its comment read "Real shapes, measured on this instance and deliberately mirrored" and it printed `[]` past the end. Correcting the double and restoring the old gate turns **18 tests red** — all of them had been green for the wrong reason. ## Box 4 — blast radius audit Window 2026-08-03 21:41 (bad commit) → now, all PRs: | PR | Class | Outcome | |---|---|---| | #739 docs-only | exempt | **AFFECTED** — merged 08-05 with **zero** commit statuses on its head; got in only via admin force-merge | | #738 docs-only | exempt | closed unmerged (superseded by #739), never needed a bypass | | #717, #718 | exempt (renovate) | unaffected **by timing only** — merged minutes before the bad commit | | #723/725/737/741/745/749 | non-exempt | all carry a hand-posted `Review-verdict:` with `creator=timothy` — normal path, no separate hole | **Nothing is stuck right now** (no open PRs besides the probes). But #743 disabled admin force-merge on 08-06 00:58, so the escape that let #739 through is gone: the next docs-only or Renovate-manifest PR would have been permanently unmergeable with no bypass. ## Also fixed, both found while editing this code - `ci.verdict-write-retarget-fence` stated this as a narrow residual ("a timeline over the 20-page cap can never be exempted — `rt_ok` stays `no` on every run") when the behaviour was **universal**. Corrected in place, since a stale claim that reads as checked talks the next reader out of verifying. - The workflow cited `ci.paged-endpoint-completeness`, **a key that has never existed as a record anywhere**. Repointed at the record that actually owns this walk. - Start marker keyed on run id + attempt: `RUNNER_TEMP` is `/tmp` on this runner, not a private per-job dir, so a stale marker could have made the guard **pass** on a run whose step was dropped. Independent adversarial review (cross-family + a second cold lens) is running now. PR to follow once it is clean — not before.
Author
Owner

Closing record

Outcome: Fixed and merged in #764 (9a5d34e88). Three independent defects, each alone enough to stop review-verdict/h10 being posted:

  1. A ${{ }} sequence in a shell comment in the classify step. The runner scans the whole run: scalar for the expression opener and rewrites the entire body into one format(...) call; pr number does not parse, so it drops the step and concludes the job green. Introduced by 8f6d4f443 (2026-08-03) — the #706 note explaining why a concurrency group does not work, quoting a concurrency: snippet as illustration. The prose documenting a fix disabled the fix.
  2. The retarget fence never trusted its count. A page past the end of /issues/{n}/timeline is JSON null, not []; count_retargets gated on type == "array", so rt_ok was never yes for any PR and every exemption success was withheld. Found by running the probe — fixing (1) alone would not have restored the exemptions.
  3. The same nil-slice shape on /commits/{sha}/status, which made read_existing_verdict take its exit 1 path and post nothing. Found by cold review of the fix for (2).

Root cause: For (1), a run: body is not shell when the runner reads it — a comment is not inert, and the format(...) rewrite is all-or-nothing. For (2) and (3), a nil Go slice serialises to null, so every list-shaped field on this API is suspect. Four endpoints, three shapes: timeline → bare null; commits/{sha}/status → {"statuses": null}; comments and pulls/{n}/files → []; statuses/{sha} → []. Only a per-endpoint measurement settles it.

Decisions/conventions changed: added ci.workflow-run-body-no-expressions; amended ci.verdict-write-retarget-fence (its stated residual described the universal failure as a narrow over-cap edge case) and ci.exemption-provenance (PROTECTED now covers CLAUDE.md/AGENTS.md; the statuses: null tolerance). The workflow cited ci.paged-endpoint-completeness, a key that has never existed as a record — repointed.

Reusable knowledge:

  • A green job with no step output never ran the step. Check for ⭐ Run Main <step>, and check DURATION: the broken job succeeded in 6s, the fixed one takes 14–17s because it actually classifies and posts.
  • A guard that has never executed has told you nothing. Both (1) and (2) shipped in the same commit, so the fence had never once run in production. Merging a guard and first executing it are different events.
  • A fidelity claim in a test double is an assertion, and it decays. The fence stub printed [] past the end while its comment read "Real shapes, measured on this instance and deliberately mirrored". Correcting it and restoring the old gate reddens most of the fence suite — every one of those tests had been green for the wrong reason.
  • Dont write a guard against a constant your own repo has already measured as wrong. Round 3 shipped a truncation check against limit=100 that was dead code, because Gitea clamps to MAX_RESPONSE_ITEMS = 50 — documented in scripts/pr-changed-files.sh, two test files and ci.script-tests-job before the guard was written. Ask the server (read page 2) instead of hardcoding a cap.
  • total_count on the combined status endpoint is per-PAGE, not per-commit, so length-vs-total cannot detect truncation. Three guards were written for that one hole; the first two were no-ops.
  • A stray governance exemption: DOCS_ONLY matched CLAUDE.md/AGENTS.md, so the documents defining the H10 rule were docs-only-auto-exemptible. Dead while the step was dropped; restoring the exemptions re-opened it.

Verification: 460 tests; 29 mutations across six rounds, including two controls that must not red (an inert top-level YAML comment, a legitimate step deletion). Live scratch-base probe pair with a negative control, six rounds, the last on the merged executable body (byte-identical — only comments changed after it): positive posts the docs-only exemption with trusted=yes; negative drops the step and the new guard fails the job with the context absent, which is what established that a later step still runs after a drop. Post-merge, end-to-end: #761 and #762 — Renovate PRs that carried no h10 and could not merge — were re-triggered by a body edit and both now read h10 = success | Exempt: authored by the renovate bot account… | creator=null. Five cold review rounds, alternating model families; none found a path to a green h10 on an unreviewed head, and every round found a defect beside the fix.

Deferred: #756 — docker-build.ymls test/migrations are also required contexts and there a dropped step is fail-OPEN (green having done no work), strictly worse than here; this PR guarded the file where the failure mode is safe, because that is where the live bug was. #763 — /statuses/{sha} is read twice unpaged while limit clamps to 50, mitigated here conservatively (unread or unreadable page 2 → assume raced → repair to pending, which is sticky per-sha) pending real paging.

Docs updated: docs/ci-cd.md (Review-verdict gate), docs/decisions/records/ci/workflow-run-body-no-expressions.md (new), docs/decisions/records/ci/verdict-write-retarget-fence.md, docs/decisions/records/ci/exemption-provenance.md, docs/decisions/README.md (regenerated).

## Closing record **Outcome:** Fixed and merged in #764 (`9a5d34e88`). Three independent defects, each alone enough to stop `review-verdict/h10` being posted: 1. A `${{ }}` sequence in a **shell comment** in the classify step. The runner scans the whole `run:` scalar for the expression opener and rewrites the entire body into one `format(...)` call; `pr number` does not parse, so it **drops the step and concludes the job green**. Introduced by 8f6d4f443 (2026-08-03) — the #706 note explaining why a concurrency group does not work, quoting a `concurrency:` snippet as illustration. The prose documenting a fix disabled the fix. 2. **The retarget fence never trusted its count.** A page past the end of `/issues/{n}/timeline` is JSON `null`, not `[]`; `count_retargets` gated on `type == "array"`, so `rt_ok` was never `yes` for *any* PR and **every** exemption `success` was withheld. Found by running the probe — fixing (1) alone would not have restored the exemptions. 3. **The same nil-slice shape on `/commits/{sha}/status`**, which made `read_existing_verdict` take its `exit 1` path and post nothing. Found by cold review of the fix for (2). **Root cause:** For (1), a `run:` body is not shell when the runner reads it — a comment is not inert, and the `format(...)` rewrite is all-or-nothing. For (2) and (3), **a nil Go slice serialises to `null`**, so every list-shaped field on this API is suspect. Four endpoints, three shapes: `timeline` → bare `null`; `commits/{sha}/status` → `{"statuses": null}`; `comments` and `pulls/{n}/files` → `[]`; `statuses/{sha}` → `[]`. Only a per-endpoint measurement settles it. **Decisions/conventions changed:** added `ci.workflow-run-body-no-expressions`; amended `ci.verdict-write-retarget-fence` (its stated residual described the universal failure as a narrow over-cap edge case) and `ci.exemption-provenance` (PROTECTED now covers `CLAUDE.md`/`AGENTS.md`; the `statuses: null` tolerance). The workflow cited `ci.paged-endpoint-completeness`, **a key that has never existed as a record** — repointed. **Reusable knowledge:** - **A green job with no step output never ran the step.** Check for `⭐ Run Main <step>`, and check DURATION: the broken job succeeded in **6s**, the fixed one takes 14–17s because it actually classifies and posts. - **A guard that has never executed has told you nothing.** Both (1) and (2) shipped in the same commit, so the fence had never once run in production. Merging a guard and first executing it are different events. - **A fidelity claim in a test double is an assertion, and it decays.** The fence stub printed `[]` past the end while its comment read "Real shapes, measured on this instance and deliberately mirrored". Correcting it and restoring the old gate reddens most of the fence suite — every one of those tests had been green for the wrong reason. - **Dont write a guard against a constant your own repo has already measured as wrong.** Round 3 shipped a truncation check against `limit=100` that was dead code, because Gitea clamps to `MAX_RESPONSE_ITEMS` = 50 — documented in `scripts/pr-changed-files.sh`, two test files and `ci.script-tests-job` before the guard was written. Ask the server (read page 2) instead of hardcoding a cap. - **`total_count` on the combined status endpoint is per-PAGE, not per-commit**, so length-vs-total cannot detect truncation. Three guards were written for that one hole; the first two were no-ops. - **A stray governance exemption:** `DOCS_ONLY` matched `CLAUDE.md`/`AGENTS.md`, so the documents defining the H10 rule were docs-only-auto-exemptible. Dead while the step was dropped; restoring the exemptions re-opened it. **Verification:** 460 tests; **29 mutations across six rounds**, including two controls that must *not* red (an inert top-level YAML comment, a legitimate step deletion). Live scratch-base probe pair with a negative control, six rounds, the last on the merged executable body (byte-identical — only comments changed after it): positive posts the docs-only exemption with `trusted=yes`; negative drops the step and the new guard fails the job with the context absent, which is what established that a later step still runs after a drop. **Post-merge, end-to-end:** #761 and #762 — Renovate PRs that carried no `h10` and could not merge — were re-triggered by a body edit and both now read `h10 = success | Exempt: authored by the renovate bot account… | creator=null`. Five cold review rounds, alternating model families; none found a path to a green `h10` on an unreviewed head, and every round found a defect beside the fix. **Deferred:** #756 — `docker-build.yml`s `test`/`migrations` are also required contexts and there a dropped step is **fail-OPEN** (green having done no work), strictly worse than here; this PR guarded the file where the failure mode is safe, because that is where the live bug was. #763 — `/statuses/{sha}` is read twice unpaged while `limit` clamps to 50, mitigated here conservatively (unread or unreadable page 2 → assume raced → repair to `pending`, which is sticky per-sha) pending real paging. **Docs updated:** `docs/ci-cd.md` (Review-verdict gate), `docs/decisions/records/ci/workflow-run-body-no-expressions.md` (new), `docs/decisions/records/ci/verdict-write-retarget-fence.md`, `docs/decisions/records/ci/exemption-provenance.md`, `docs/decisions/README.md` (regenerated).
timothy removed the in-progress label 2026-08-10 21:35:43 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: timothy/ersatztv#751