fix(744,835): ci-image.yml publishes from main only; guard persist-credentials with no exemption #857

Merged
timothy merged 8 commits from fix/744-ci-image-branch-filter into main 2026-08-27 22:44:45 +02:00
Owner

Closes the push route into ci-image.yml (#744) and ships the persist-credentials guard that was waiting on it (#835). Bundled because #835's own body says it must ride with #744 — otherwise its guard needs a one-entry ci-image.yml exemption whose only justification is a temporary condition elsewhere, and that entry keeps passing after its reason expires.

#744 — the trigger

ci-image.yml's push trigger had no branches: filter and was path-scoped to docker/ci/** and to the workflow file itself. Gitea resolves a push workflow's definition from the pushed ref, so any branch push touching those paths ran that branch's own YAML — no PR, no review, no status check — on a docker-capable runner holding the credential that writes ersatztv:prod and the ersatztv-ci:<sha> five container: jobs execute.

Option 1 from the issue body, with the two corrections cold review forced:

  • What this actually buys is narrower than the issue claims. The branches: filter is loaded from the pushed ref like every other line in the file, so a branch that deletes it re-enables the route in one edit. What closes is the drive-by case: publication as a side effect of an ordinary push of a legitimate docker/ci change, with no deliberate act anywhere. It is not a boundary against someone who intends to run their own YAML, and the docs say so in as many words. The wider class — including docker-build.yml, reachable from an arbitrary ref by a v* tag push and by pull_request with no dispatch at all — is #853.
  • Removing .gitea/workflows/ci-image.yml from ci-image-pin's expected is a priced decision, not a necessity. An earlier draft claimed it was forced; that was false, because the retained branch workflow_dispatch can publish the ci-image.yml commit itself and the pin then matches. The real argument is price: keeping it charges a ~2 GB publish plus a five-pin bump for every edit to that file, comments included, redone after every rebase — the cost that reverted #697 — against the cost of dropping it, a recipe change living only in ci-image.yml that never republishes. Both prices are stated in ci.toolchain-image-publish-is-a-dispatch.

The capability removed is replaced, not assumed away. Branch publishing becomes a workflow_dispatch on the branch: push the docker/ci commit as HEAD (with any ci-image.yml recipe change in that commit or below it — the dispatch builds HEAD's whole tree), dispatch, then commit the pin bump on top. Documented in docs/ci-cd.md → "Publishing from a branch is a dispatch, not a push".

This PR's own ci-image-pin is green — expected resolves to 32747a0… and the pins read 32747a0, which is precisely what the expected narrowing buys: under the old derivation, editing this file would have reddened a blocking job for a change altering zero bytes of the image.

#835 — the guard

ci-image.yml's checkout takes persist-credentials: false (16 of 16, no exemption), and scripts/tests/test_workflow_persist_credentials.py holds the convention: population from the git index (.yml + .yaml), a parser-completeness cross-check against a text scan, anti-vacuity in both directions, and a declared clause mutation in mutation_manifest.py re-run every suite. Three rounds of review found seven actions/checkout spellings that escaped an earlier canonicaliser — a full URL, a dotless host, any letter case, a .git suffix, @-userinfo, a doubled slash — all now pinned. Two absence tests keep the remaining boundaries reporting themselves rather than going blind: no reusable-workflow delegation (both halves would see zero and agree), and nothing tracked under .github/workflows.

Verification

  • scripts/tests full suite: 1026 passed, 2 skipped, including the new mutation executing and reddening its named proof with the declared diagnostic.
  • decisions_validate OK, catalog regenerated, check-doc-narrative clean, ruff check/format clean.
  • 4 review rounds, 2 independent cold reviewers per round (Codex GPT-5.6 + a fresh Opus reviewer in an isolated worktree) — ~25 findings, all addressed.

Deferred

  • #853 — workflow_dispatch from an arbitrary ref, plus docker-build.yml's tag and PR routes. The class this PR does not close.
  • #854 — two stale publish instructions in docker/ci/Dockerfile. Not fixable cheaply: any byte changed there re-points expected and costs a publish-and-repin. Verified by making the fix, watching the pin go red, and reverting it.
  • #855 — nothing mechanically couples ci-image.yml's push.paths to ci-image-pin's expected now that the shared self-reference is gone.

fixes #744
fixes #835

Review record

Rounds 1-3: two independent cold reviewers each (Codex GPT-5.6, and a fresh Opus reviewer in an isolated worktree), 25 findings, all addressed. Round 4 is self-review, stated rather than skipped: Codex hit its usage quota, and three consecutive reviewer agents wedged without producing output. What round 4 found and fixed anyway:

  • docs/ci-cd.md still carried the headline "A branch push no longer publishes anything" — the exact sentence round 3 existed to narrow. Round 3's batched edit script for that file aborted on its last assert, so none of its five earlier replacements were written (batched-edit-script-loses-every-edit). Caught by a whole-repo concept sweep, not by trusting the script's own "ok" output. Every other batched edit was then audited individually; only that file was affected.
  • The decision record still hung a conclusion on "ci-image-pin is blocking" — the twin of a site fixed in the workflow the round before. It is not a branch-protection required context; the block comes from the merge-consent hook reading the combined status.

I could not construct an actions/checkout spelling that escapes both halves of the guard silently: every variant tried (quoted, anchor/alias, block scalar, trailing slash, non-string uses) produces a disagreement between the YAML walk and the text scan, which reddens.

Closes the **push** route into `ci-image.yml` (#744) and ships the `persist-credentials` guard that was waiting on it (#835). Bundled because #835's own body says it must ride with #744 — otherwise its guard needs a one-entry `ci-image.yml` exemption whose only justification is a temporary condition elsewhere, and that entry keeps passing after its reason expires. ## #744 — the trigger `ci-image.yml`'s `push` trigger had no `branches:` filter and was path-scoped to `docker/ci/**` **and to the workflow file itself**. Gitea resolves a `push` workflow's definition from the pushed ref, so any branch push touching those paths ran that branch's own YAML — no PR, no review, no status check — on a docker-capable runner holding the credential that writes `ersatztv:prod` and the `ersatztv-ci:<sha>` five `container:` jobs execute. **Option 1 from the issue body**, with the two corrections cold review forced: - **What this actually buys is narrower than the issue claims.** The `branches:` filter is loaded from the pushed ref like every other line in the file, so a branch that deletes it re-enables the route in one edit. What closes is the **drive-by** case: publication as a side effect of an ordinary push of a legitimate `docker/ci` change, with no deliberate act anywhere. It is not a boundary against someone who intends to run their own YAML, and the docs say so in as many words. The wider class — including `docker-build.yml`, reachable from an arbitrary ref by a `v*` tag push and by `pull_request` with no dispatch at all — is **#853**. - **Removing `.gitea/workflows/ci-image.yml` from `ci-image-pin`'s `expected` is a priced decision, not a necessity.** An earlier draft claimed it was forced; that was false, because the retained branch `workflow_dispatch` can publish the ci-image.yml commit itself and the pin then matches. The real argument is price: keeping it charges a ~2 GB publish plus a five-pin bump for *every* edit to that file, comments included, redone after every rebase — the cost that reverted #697 — against the cost of dropping it, a recipe change living only in `ci-image.yml` that never republishes. Both prices are stated in `ci.toolchain-image-publish-is-a-dispatch`. **The capability removed is replaced, not assumed away.** Branch publishing becomes a `workflow_dispatch` on the branch: push the `docker/ci` commit as HEAD (with any `ci-image.yml` recipe change in that commit or below it — the dispatch builds HEAD's whole tree), dispatch, then commit the pin bump on top. Documented in `docs/ci-cd.md` → "Publishing from a branch is a dispatch, not a push". **This PR's own `ci-image-pin` is green** — `expected` resolves to `32747a0…` and the pins read `32747a0`, which is precisely what the `expected` narrowing buys: under the old derivation, editing this file would have reddened a blocking job for a change altering zero bytes of the image. ## #835 — the guard `ci-image.yml`'s checkout takes `persist-credentials: false` (16 of 16, no exemption), and `scripts/tests/test_workflow_persist_credentials.py` holds the convention: population from the git index (`.yml` + `.yaml`), a parser-completeness cross-check against a text scan, anti-vacuity in both directions, and a declared clause mutation in `mutation_manifest.py` re-run every suite. Three rounds of review found seven `actions/checkout` spellings that escaped an earlier canonicaliser — a full URL, a dotless host, any letter case, a `.git` suffix, `@`-userinfo, a doubled slash — all now pinned. Two absence tests keep the remaining boundaries reporting themselves rather than going blind: no reusable-workflow delegation (both halves would see zero and *agree*), and nothing tracked under `.github/workflows`. ## Verification - `scripts/tests` full suite: **1026 passed, 2 skipped**, including the new mutation executing and reddening its named proof with the declared diagnostic. - `decisions_validate` OK, catalog regenerated, `check-doc-narrative` clean, `ruff check`/`format` clean. - **4 review rounds, 2 independent cold reviewers per round** (Codex GPT-5.6 + a fresh Opus reviewer in an isolated worktree) — ~25 findings, all addressed. ## Deferred - **#853** — `workflow_dispatch` from an arbitrary ref, plus `docker-build.yml`'s tag and PR routes. The class this PR does not close. - **#854** — two stale publish instructions in `docker/ci/Dockerfile`. Not fixable cheaply: any byte changed there re-points `expected` and costs a publish-and-repin. Verified by making the fix, watching the pin go red, and reverting it. - **#855** — nothing mechanically couples `ci-image.yml`'s `push.paths` to `ci-image-pin`'s `expected` now that the shared self-reference is gone. fixes #744 fixes #835 ## Review record Rounds 1-3: two independent cold reviewers each (Codex GPT-5.6, and a fresh Opus reviewer in an isolated worktree), 25 findings, all addressed. Round 4 is **self-review, stated rather than skipped**: Codex hit its usage quota, and three consecutive reviewer agents wedged without producing output. What round 4 found and fixed anyway: - `docs/ci-cd.md` still carried the headline **"A branch push no longer publishes anything"** — the exact sentence round 3 existed to narrow. Round 3's batched edit script for that file aborted on its last assert, so **none** of its five earlier replacements were written (`batched-edit-script-loses-every-edit`). Caught by a whole-repo concept sweep, not by trusting the script's own "ok" output. Every other batched edit was then audited individually; only that file was affected. - The decision record still hung a conclusion on "`ci-image-pin` is blocking" — the twin of a site fixed in the workflow the round before. It is not a branch-protection required context; the block comes from the merge-consent hook reading the combined status. I could not construct an `actions/checkout` spelling that escapes both halves of the guard silently: every variant tried (quoted, anchor/alias, block scalar, trailing slash, non-string `uses`) produces a *disagreement* between the YAML walk and the text scan, which reddens.
timothy added 6 commits 2026-08-27 21:57:44 +02:00
`ci-image.yml`'s `push` trigger had no `branches:` filter. Gitea resolves a `push`
workflow's definition from the pushed branch, so any branch push touching
`docker/ci/**` or the workflow file itself executed that branch's own YAML — no PR,
no review, no status check — on a docker-capable runner holding the credential that
writes `ersatztv:prod` and the `ersatztv-ci:<sha>` five `container:` jobs run.
`ci.actions-credential-scoping` bounds what such a job may DO; it never bounded
whether it runs.

The tradeoff is decided, not asserted away. Branch publishing was load-bearing —
`ci-image-pin` is blocking, so a PR changing `docker/ci/**` cannot move its pin
without an image published from the branch first. That capability moves to
`workflow_dispatch` on the branch, and the resulting three-step (push the docker/ci
commit as HEAD, dispatch, commit the pin bump) is documented in ci-cd.md.

`.gitea/workflows/ci-image.yml` leaves both its own `paths:` and `ci-image-pin`'s
`expected` in the same change, and that is forced rather than preferred: under
`branches: [main]` a self-triggering path can only publish AFTER the merge, so
`expected` would advance to a commit no image exists for and redden a blocking job
on EVERY subsequent PR. It is also what makes this PR landable — with the old
`expected`, editing this file re-points it at this commit and `ci-image-pin` goes
red for a change altering zero bytes of the image. Cost recorded in the pin job, in
ci-cd.md and in the new record: a build-recipe change living only in ci-image.yml no
longer republishes.

With both mechanical blockers gone, `ci-image.yml`'s checkout takes
`persist-credentials: false` — the sixteenth of sixteen — which is what #835 was
waiting for. Its guard therefore ships with NO exemption list: population derived
from the git index, both `.yml` and `.yaml`, a parser-completeness cross-check
against a text scan so an unreachable step list is reported rather than silently
uncovered, anti-vacuity in both directions, and a declared clause mutation on the
guarded artifact re-run every suite.

fixes #744
fixes #835
Decisions-Edit: yes
Two independent cold reviews (Codex GPT-5.6, and a fresh Opus reviewer in an isolated
worktree) both returned BLOCKED. All findings addressed:

- The "what this does not close" statement sorted by the `branches:` KEYWORD, which
  mis-sorts `docker-build.yml`: it has `branches: [main]` and would pass, yet also
  `tags: ['v*']` and `pull_request:`, both head-resolved on the same registry
  credential — and a tag push is explicitly outside `release.main-direct-push-disabled`,
  so it needs no dispatch at all. Restated over the REF CLASS a trigger admits, in the
  record's `rule:` and in ci-cd.md, and #853 updated to match.
- The recovery text offered "dispatch on `main` after the merge and bump the pin" as an
  alternative. It cannot work: `expected` is the last `docker/ci` commit, so a pin naming
  the workflow-only commit fails `ci-image-pin` and leaving the pin keeps consuming the
  old image. A `docker/ci/**` touch is the only remedy.
- `ci-image.yml`'s own ROLLOUT NOTE still described the retracted merge-then-follow-up-PR
  two-step, five lines below the trigger summary the change had updated.
- The guard: `uses: https://github.com/actions/checkout@v4` is a valid Gitea spelling and
  escaped both the walk and the text cross-check — canonicalised, and both spellings are
  now pinned in the negative control. Added two absence tests so the population's
  assumptions REPORT themselves rather than going blind: no tracked workflow below
  `.gitea/workflows/` (the direct-children derivation is shared with two sibling guards),
  and no local composite action (whose `action.yml` is invisible to both halves of the
  cross-check — the docstring had cited the wrong test for that case).
- `ci-image-pin` no longer claims to prove PUBLICATION: it compares git shas and never
  queries the registry, which matters more now that publishing is a human dispatch.
  Existence stays `toolchain-preflight`'s job.
- The `:latest` guard is now the SOLE protection on the one event that never exercised it
  (a dispatch selects any ref), and says so.
- Stale contracts: the credential-scoping record's `rule:` frontmatter and body still said
  15-of-16; the pin job's `::error::` still said "let ci-image.yml publish".

The `docker/ci/Dockerfile` comment fixes were made, reddened `ci-image-pin` (any byte
there re-points `expected`), and were reverted — the trap is now recorded in ci-cd.md and
the record, with both stale line ranges deferred to #854.

refs #744
refs #835
Decisions-Edit: yes
Both round-2 cold reviews returned BLOCKED. The most important finding is that a claim
this change made three times over was simply wrong.

**"Removing the self-path is FORCED" is false.** The argument was that under
`branches: [main]` a self-triggering path can only publish after the merge. It cannot
publish by PUSH — but the retained branch `workflow_dispatch` publishes it fine: dispatch
the branch at the ci-image.yml commit, then pin it, and `ci-image-pin` resolves both to
that commit and passes. So keeping the self-reference is workable, and the decision rests
entirely on price, not necessity: keeping it charges a full ~2GB publish plus a five-pin
bump for EVERY edit to that file, comments included, redone after every rebase — the cost
that reverted #697 and forced this to land alone — against the cost of dropping it, a
recipe change living only in ci-image.yml that never republishes. The second was chosen
because that file is edited far more for triggers and comments than for build recipe.
Restated as a decided tradeoff, with both prices, in the record, ci-cd.md and both
workflow comments. #744's Done-when asks for the tradeoff to be DECIDED rather than
asserted away; a false necessity claim was the opposite of that.

**The guard had two more escapes of the class round 1 closed for URLs.** A Gitea action
host is not required to contain a dot (`http://your-git-server/actions/checkout@v4` is the
documented self-hosted spelling), and the forge is case-insensitive on owner/repo
(`Actions/Checkout@v4` runs the real checkout). Both escaped the walk AND the text scan
identically, so the count cross-check agreed and the guard reported clean. Replaced the
host heuristic with last-two-path-segments and made both halves case-insensitive; all
three spellings are pinned in the negative control.

**A new stale-but-green state was reachable by following the documented remedy literally.**
The dispatch builds HEAD's copy of every file, so a `ci-image.yml` recipe change committed
AFTER the `docker/ci` commit publishes an image built by the old recipe while the pin and
`expected` both name the `docker/ci` commit — green over an image the merged recipe would
not produce. Step 1 now states the ordering.

Also: the credential-scoping record's `rule:` frontmatter still said #744 was tracked-open
and still asserted the mechanism this change removes, contradicting its own body — round 1
fixed the 15-of-16 clause inside that same string and stopped, a sweep by keyword rather
than by concept. `ci-image-pin`'s comment still said a docker/ci PR "publishes a NEW
image". Restored the #445 attribution the recovery rewrite dropped. Renamed the composite
-action test to say what it asserts (that none EXISTS), and filed #855: nothing
mechanically couples `ci-image.yml`'s `push.paths` to `ci-image-pin`'s `expected` now that
the shared self-reference is gone.

refs #744
refs #835
Decisions-Edit: yes
Round 3 (two independent cold reviews, both BLOCKED, no blocker/high between them
except one guard escape) found the security framing itself overstated.

**The `branches:` filter is loaded from the pushed ref, like every other line in the
file.** A branch whose `ci-image.yml` deletes the filter fires exactly as before, on the
same runner, with the same credential. The mechanism this change is built on — "Gitea
resolves a push workflow's definition from the pushed ref" — applies to the fix as much as
to the hole, and three bolded sentences said "closes that route" without noticing. What is
actually closed is the DRIVE-BY case: publication as a side effect of an ordinary push of a
legitimate `docker/ci` change, no deliberate act anywhere. That is real and worth closing,
and it is not a boundary against someone who intends to run their own YAML. Corrected in
the workflow, ci-cd.md and the record's `rule:` and bullet 1, with an explicit "do not cite
this record as that boundary"; #853 updated to lead with it, and a scope note posted on
#744 because the issue body frames the fix more strongly than it delivers.

**Three more spellings of `actions/checkout` escaped both halves of the guard**, in the
same class as rounds 1 and 2: a `.git` suffix (which is how Gitea itself writes the clone
URL), `@`-userinfo before the host (splitting on the FIRST `@` stripped the host, not the
version), and a doubled slash. Canonicalisation now rsplits, drops a `.git`, and absorbs
empty segments; all seven spellings are pinned. Two more absence tests so the remaining
boundaries report themselves instead of going blind: no job delegates to a reusable
workflow (both halves would see zero and AGREE, so the count cross-check cannot report it),
and nothing tracked under `.github/workflows` — which Gitea reads only when
`.gitea/workflows` is absent, an unprobed precedence rule, in a directory that already
exists here.

Also: "had to land alone" / "forced this change to land alone" still carried the necessity
premise round 2 retracted — landing alone was the cheaper option, not the only one;
`ci-image-pin` was called "blocking" as though branch protection required it (it does not
— the block comes from the merge-consent hook reading the COMBINED status); the pin step
was still named "Verify the pin matches the last-published image" while doing no registry
query at all; ci-cd.md still documented `expected` as `docker/ci/**` or `ci-image.yml`;
`release.main-direct-push-disabled` carried its own stale 15-of-16 count; the ordering
constraint said "below" where "in that commit or below it" is correct; and the #854
deferral undercounted the stale Dockerfile comments at one when there are two.

refs #744
refs #835
Decisions-Edit: yes
A batched edit script asserts each old string then writes ONCE at the end, so a
single failed assert discards every earlier replacement in the same run — the
`batched-edit-script-loses-every-edit` trap. Round 3's ci-cd.md script aborted on
its last block, and the five earlier edits it reported were never written. The
workflow, pr-checks and record halves of that round landed from separate scripts,
so only this file was affected, and only a whole-repo CONCEPT sweep caught it:
`docs/ci-cd.md` still carried the headline **A branch push no longer publishes
anything**, the very sentence round 3 exists to narrow.

Now applied, one write per edit: the drive-by narrowing of the security claim, the
does-not-close heading, the tag-push wording, priced-decision language for landing
alone, and the ordering constraint reading 'in that commit or below it'.

refs #744
fix(744): qualify the two 'ci-image-pin is blocking' conclusions, rewrap
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 8s
PR Gates / Docs update reminder (pull_request) Successful in 11s
PR Gates / decisions lifecycle (pull_request) Successful in 15s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 22s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 15s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 13s
Review verdict / Set review-verdict status (pull_request_target) Successful in 16s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 5m25s
review-verdict/h10 Review-verdict: MERGEABLE @ 43b1e45 (base: main)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Canceled after 2m44s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Canceled after 0s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Canceled after 0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Canceled after 0s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Canceled after 0s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Canceled after 0s
43b1e45b55
Round-4 self-review. `ci-image-pin` is NOT a branch-protection required context —
pr-checks.yml says so in as many words (branch protection requires only
`Build & test (.NET)`, `EF migration integrity` and `review-verdict/h10`). The
block is real but comes from the merge-consent hook reading the COMBINED commit
status. 'Blocking' is pre-existing shorthand for this job and is left alone where
it is only a label; the two sites that hang a CONCLUSION on it ('so a PR changing
docker/ci without moving the pin is red, and the only way to move the pin was...')
are this branch's, and are qualified. Fixing the workflow copy last round and not
its twin in the record is the fix-one-site-miss-its-twin shape.

refs #744

Decisions-Edit: yes
Author
Owner

Review-verdict: MERGEABLE @ 43b1e45

4 review rounds; rounds 1-3 two independent cold reviewers each (Codex GPT-5.6 + fresh Opus in isolated worktree), 25 findings all addressed. Round 4 self-review (Codex quota exhausted, 3 reviewer agents wedged) and it still found 2: a batched edit script had silently dropped 5 ci-cd.md edits, and a twin blocking-claim in the record. Local gate green: 1026 passed / 2 skipped.

Review-verdict: MERGEABLE @ 43b1e45 4 review rounds; rounds 1-3 two independent cold reviewers each (Codex GPT-5.6 + fresh Opus in isolated worktree), 25 findings all addressed. Round 4 self-review (Codex quota exhausted, 3 reviewer agents wedged) and it still found 2: a batched edit script had silently dropped 5 ci-cd.md edits, and a twin blocking-claim in the record. Local gate green: 1026 passed / 2 skipped.
timothy added 1 commit 2026-08-27 22:21:37 +02:00
docs(744): record the branch-dispatch probe, dated and with its limit stated
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 12s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 24s
PR Gates / decisions lifecycle (pull_request) Successful in 25s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 9s
PR Gates / Docs update reminder (pull_request) Successful in 14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Canceled after 0s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Canceled after 0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Canceled after 0s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Canceled after 0s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Canceled after 0s
review-verdict/h10 Awaiting review verdict for a4d9d8f
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 16s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Canceled after 1m1s
Review verdict / Set review-verdict status (pull_request_target) Successful in 31s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Canceled after 28s
a4d9d8f788
The replacement publish path was the one load-bearing runtime claim in this change,
flagged by every review round as asserted-in-prose rather than measured. Now measured.

Dispatched ci-image.yml on this branch at 43b1e45 (run 2340, conclusion: success).
Gitea bound the job to head_branch=fix/744-ci-image-branch-filter and
head_sha=43b1e45b5554, so a workflow_dispatch does load both the definition and the
tree from a selected NON-DEFAULT ref; the run published ersatztv-ci:43b1e45, i.e. it
tags the branch HEAD at dispatch time as documented; and :latest still resolved to the
digest it carried before the dispatch.

The limit of that last observation is recorded with it rather than glossed: docker/ci
was unchanged, so the rebuild was content-identical and the new :<sha> carries the same
digest as :latest. It shows :latest did not CHANGE; it cannot by itself distinguish
'not pushed' from 'pushed the same digest'. The refs/heads/main guard is what decides
that, and it is called out at its own site.

refs #744

Decisions-Edit: yes
timothy added 1 commit 2026-08-27 22:22:40 +02:00
docs(744): close the probe blockquote, and drop an overclaim it had just caveated
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 8s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 18s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 11s
PR Gates / Docs update reminder (pull_request) Successful in 17s
PR Gates / decisions lifecycle (pull_request) Successful in 19s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 13s
Review verdict / Set review-verdict status (pull_request_target) Successful in 20s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 6m15s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m45s
review-verdict/h10 Review-verdict: MERGEABLE @ 3dd5109 (base: main)
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m57s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 6s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
3dd5109279
Self-review of the probe commit, on its own head. Two defects in it:

- The blockquote swallowed the start of the sentence that follows it, so
  'The cheapest way to avoid it entirely is' rendered inside the quote while its
  continuation rendered outside. Closed the quote and made the sentence stand alone.
- The decision record asserted the probe published the tag 'without moving :latest'.
  That is the exact overclaim the ci-cd.md probe note had just been careful to
  caveat: the rebuild was content-identical, so the observation is that :latest did
  not CHANGE, which cannot distinguish 'not pushed' from 'pushed the same digest'.
  The record now says :latest resolved to its pre-dispatch digest and points at the
  stated limit instead of restating a stronger claim than the evidence supports.

refs #744

Decisions-Edit: yes
Author
Owner

Review-verdict: MERGEABLE @ 3dd5109

Head 3dd51092 adds only the dated branch-dispatch probe record plus a self-review fix of that record (blockquote swallowed the next sentence; the record asserted 'without moving :latest', the overclaim its own caveat had just ruled out). Probe result: dispatch on a non-default ref succeeded, job bound to that branch+sha, published ersatztv-ci:43b1e45, :latest unchanged - the one load-bearing runtime claim every review round flagged. Local gate 1026 passed / 2 skipped.

Review-verdict: MERGEABLE @ 3dd5109 Head 3dd51092 adds only the dated branch-dispatch probe record plus a self-review fix of that record (blockquote swallowed the next sentence; the record asserted 'without moving :latest', the overclaim its own caveat had just ruled out). Probe result: dispatch on a non-default ref succeeded, job bound to that branch+sha, published ersatztv-ci:43b1e45, :latest unchanged - the one load-bearing runtime claim every review round flagged. Local gate 1026 passed / 2 skipped.
timothy merged commit 45b17e58e1 into main 2026-08-27 22:44:45 +02:00
timothy deleted branch fix/744-ci-image-branch-filter 2026-08-27 22:44:47 +02:00
Sign in to join this conversation.