Repository navigation
ci(framework-build-base): tag every published digest immutably, refuse a silent re-point - #31784
Conversation
…e a silent re-point Every digest the publish job pushes, and the digest already published under a requested sha, now carries its own tag SHA-DIGEST12 that is never deleted or re-pointed, so a GHCR cleanup of untagged versions or a rebuild of the same pin never takes a pinned digest away. A rebuild over a published tag is refused unless replace_published is also true. Re-anchors the dispatch-gates self-test row for the moved no-check-families declaration (line 32 to 44). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSrcejjpWHd8D1fpmZ
…om a held tag A write whose read-back differs now says the tag was written and the digest is not yet held, instead of claiming nothing changed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSrcejjpWHd8D1fpmZ
…ipts/ path The replace refusal spelled the cloud repository's script as a scripts/ path, which check-self-test-wired reads as an invocation of a file in this tree. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSrcejjpWHd8D1fpmZ
|
domain:devx#2 · zhuangjianguo · session_01QsR2jTo666XXx9e1Gm21C6 Contract-review-claim: session_01QsR2jTo666XXx9e1Gm21C6 · head dd539bd · 2026-11-09T04:14Z Rules face ( |
|
domain:devx#2 · zhuangjianguo · session_01QsR2jTo666XXx9e1Gm21C6 Contract reviewServed-tier: Inputs read: card #31756 (body, triage 6731150162, claim 6731438753, os-dev-report 6731764376), PR #31784 (body, 2-file list, net diff ① Derived judgments
② Semver levelNo changeset, and ③ Boundary flagsDev deviations (os-dev-report 6731764376), each weighed:
④ Design judgment
Scenario A, GHCR cleanup of untagged versions. Fresh path: Scenario B, Re-point rule. Both tag steps call Header and summary: "never deleted or re-pointed" stands in the paragraph directly after "A tag is written once" with the reason (card's done-when); the Findings:
Verdict basis: no must-fix. The should-fix earns a delta review of its fix; the two nits end with this round. Implemented-by: VERDICT: PASS |
…lished A failed tag read (registry, token or network) used to read as unpublished, so the job rebuilt and moved the sha tag past the replace guard and before the backfill. Now only buildx's not-found answer means unpublished; any other failure stops the job naming the ref and the error. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSrcejjpWHd8D1fpmZ
|
domain:devx#2 · zhuangjianguo · session_01QsR2jTo666XXx9e1Gm21C6 Contract-review-claim: session_01QsR2jTo666XXx9e1Gm21C6 · head 65fc33f · 2026-11-09T04:41Z Delta review of the should-fix in 6732088839, by the same reviewer. |
|
domain:devx#2 · zhuangjianguo · session_01QsR2jTo666XXx9e1Gm21C6 Contract reviewServed-tier: Delta round of record 6732088839 (head Inputs read: card #31756 (body and every comment, nothing new since the os-dev-report 6731764376), PR #31784 (updated body, 2-file list, net diff What the head holds: fix commit ① Derived judgments
② Semver levelUnchanged from 6732088839: no package published, ③ Boundary flagsThe dev raised no new deviation and no open question for the fix. The PR body's new proof cases ( ④ Design judgment
The wording. buildx Other 404s and other denials. A missing package takes the same 404 path and reads as unpublished, which is right: the push then creates it and there is nothing to protect. A private package or a token without pull scope does not answer 404 on GHCR: an authorization failure is wrapped as Shell. Likelihood of a false fail-closed. The grep target is text owned by containerd (the Findings:
The should-fix of 6732088839 is closed by the tree: a transient registry, token or network error at Implemented-by: VERDICT: PASS |
|
domain:devx#2 · zhuangjianguo · session_01QsR2jTo666XXx9e1Gm21C6 维护者速读
席位核对 · ACCEPT · 2026-11-09T04:42Z · head
|
Requested by Steedos · project thread
Fixes #31756
Clause-②: no
Metadata-protocol: none
维护者速读
SHA-DIGEST12(提交号加 digest 前 12 位),永不删除、永不改指向;已发布但没有这个标签的旧 digest,下次被请求时自动补上;rebuild: true覆盖已发布标签时直接拒绝,除非同时给replace_published: true。.github/workflows/framework-build-base.yml的Is this commit already packaged步骤(新的拒绝分支),然后两个打标签步骤(kept、digest_tag,脚本逐字相同,含--prefer-index=false)。workflow_dispatch已发布的 pin,应看到 Summary 打印 digest 标签,且匿名可读$IMAGE:SHA-DIGEST12。What changes
.github/workflows/framework-build-base.yml,publishjob only:Tag the pushed digest under its own namewrites$IMAGE:$SHA-${DIGEST:7:12}onto the pushed digest withdocker buildx imagetools create --prefer-index=false, then reads the tag back and refuses (::error::, exit 1) unless it holds exactly that digest. A digest that does not match^sha256:[0-9a-f]{64}$is refused before anything is written. DIGEST and SHA reach the script throughenv:only.Keep the published digest served under its own tagruns whenever theSHAtag already exists, with the same script against the existing digest. On the skip path it still builds nothing; a pin published before this change gains its digest tag the next time it is requested. On the replace path it runs before the build, so the old digest is held by its own tag before theSHAtag moves.workflow_dispatchinputreplace_published(default false). InIs this commit already packaged,rebuild: trueover a published tag withoutreplace_published: trueprints::error::naming the existing digest and the downstream step (in the cloud repository,framework-base-image.sh write, then commit.objectstack-base-image) and exits 1.repository_dispatchcarries neither input, so it is unaffected; the header says so.Is this commit already packagedused to read every failedimagetools inspectas "not published", so a registry, token or network error rebuilt the commit and moved itsSHAtag past both guards, before the backfill. The tag is now absent only when buildx answersERROR: REF: not found(containerd's resolver wraps a 404 for the tag asREF:pluserrdefs.ErrNotFound, the textnot found; the step comment cites both sources). Any other failure prints::error::naming the ref and the error and exits 1; a read that names no digest fails the same way.scripts/pm/dispatch-gates.self-test.mjs: the census rowframework-build-base.yml:32 no-check-familiesre-anchored to:44(the declaration moved down 12 header lines). The declaration's own reason now also names the digest-tag writes, so it stays true.Tests
No test file is added: the publish path cannot run from a PR (the
verifydry run pushes nothing), so the proof is a local run of the workflow's ownrun:bodies, extracted verbatim from the YAML at head, against a fakedockerthat keeps a registry in a text file (echoinstead of a registry write). The harness first asserts the two tag steps run byte-identical scripts.Eight branches at 65fc33f, each with its expected exit
IMAGE is
ghcr.io/objectstack-ai/framework-build-base; SHA is forty 1s; long digests are shortened here with..., the run printed them whole.Red on main, and one mutation leg
Is this commit already packagedscript, same fake registry with SHA at D1,REBUILD=true: exit 0, outputsskip=false. The build then pushes and moves the SHA tag, and D1 is left untagged. On this branch the same input is therefusecase above, exit 1.transientcase exits 0.existingreads the failed inspect as unpublished (skip=false), the build moves the SHA tag from D1 to D2, and D1 is left untagged. At 65fc33f the same case exits 1 with the registry unchanged.--prefer-index=false: with the flag removed from a temp copy (2 lines changed, 0 left carrying it), the fakedockermimics buildx's default for a lone image manifest, wrapping it in a new index with its own digest. Thefreshcase then goes red:::error::wrote IMAGE:SHA-aaaaaaaaaaaa, but it reads back as sha256:f895effb..., not sha256:aaaaaaaaaaaa...0001, so sha256:aaaaaaaaaaaa...0001 is not yet held by a tag of its own.(exit 1). The buildx behaviour it mimics is read from buildx'sutil/imagetools/create.go(combine: a single non-index source is carbon-copied only whenpreferIndexis false) and the flag's help text incommands/imagetools/create.go(default true). The publish job pushes a single-platform image withprovenance: falseandsbom: false, so its digest names a plain image manifest.Acceptance notes
imagetools create --tag ... IMAGE@DIGEST. Without--prefer-index=false, buildx wraps a lone image manifest in a new index, so the tag would hold a different digest and the pinned one would stay untagged. The flag is added, and the read-back makes any such drift loud.replace_publishedpath, before the build, so a pin rebuilt on purpose never leaves its old digest untagged, even when a later step fails.Note the build wall, so the build-wall reading still measures the build only.Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 46 commands. All 46 exit 0 (check:pm-dispatch-gates: 2205 fast-tier cases pass, 328.5 s).--ranreconciliation: 46 derived, 46 run, 0 NOT-MEASURED, 0 UNRUN.node scripts/check-self-test-wired.mjs --self-testwent red on this diff's first spelling of the refusal (it named cloud's script as ascripts/path, which the live-corpus battery reads as an invocation of a missing file in this tree). It is green on the respelled text.verify(the PR's dry run) is CI's to report on this head.workflow_dispatchon an already-published pin (it should backfill the digest tag), then an anonymous manifest read of$IMAGE:SHA-DIGEST12. The seat asks the maintainer first, because it writes to the registry.Generated by Claude Code