actions/docs-sync — the doctrine mirror is fetched from github.com, so a consumer on this forge is verified against a different tree with the same version number #201
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:docs
scope:guards
scope:labels
scope:release-flow
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/ceremony#201
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Filed at @andres's direction (#198, #5369). Found while bounding the
hard-coded-host class @codex-reviewer-andresmgsl raised on #198 (#5341): that
issue's
https://github.com/$REPO/issues/$nis introduced by the 0.6.0 mergeand is #198's to fix. This one is not — it is on
maintoday atdad99dd,predates the merge, and is unowned.
actions/docs-sync/docs-sync.sh:152builds the fetch URL with the host writtenin:
docs-syncis the action that proves a consumer's vendored.ceremony/mirroris byte-identical to ceremony at the consumer's pinned ref (#19). On this forge
it fetches that reference tree from GitHub.
The two failure modes, measured
Both driven against the real hosts, 2026-08-05:
github.com/.../archive/<ref>.tar.gz0.4.1(a tag both forges carry)dad99dd…(a forge-only SHA)The first is the dangerous one.
heavy-duty/ceremony@0.4.1exists on bothforges and they are not the same tree:
So a consumer on this forge pinned to
0.4.1has its mirror verified against atree missing the entire 1,133-line forge delta.
--checkeither reports driftthe consumer cannot fix, or — worse —
--fixrewrites a correct mirror intothe wrong one.
This is #197 decision 2's accepted hazard ("two trees answer to
0.6.0") witha machine already acting on the wrong answer. The decision's stated mitigation
is provenance in prose; that mitigation does not reach a hard-coded fetch, and
after #198 lands the two trees diverge by more, not less.
The second mode is only cosmetically better: a consumer pinned to a
forge-only SHA gets
does the pinned ref exist?— and it does exist, on theforge it was pinned from.
Why nothing catches it
ci.yml'sdocs-sync-exercisepassessource: ceremony-srcon bothinvocations (
.github/workflows/ci.yml:173,178), so it takes the--sourceoverride branch at
:145-147and never executes the tarball fetch. Thestep's own comment says so: "The pin ref is scratch —
--sourceoverrides thefetch".
That matters beyond this issue: the live-runner acceptance criterion being
added to #198 gates on those five
ci.ymljobs, anddocs-sync-exerciseisone of them. It would go green with this defect in place. The fetch path has no
coverage on either forge.
Spec
1. The host comes from the environment, not the source. Forgejo Actions
injects
GITHUB_SERVER_URL, andlib/forge.sh:96already switches on it todetect the forge at all — so it cannot be unset on any path that reached this
code. Measured, the Forgejo archive URL is the same shape and serves both ref
kinds:
2. Outside Actions, it must refuse rather than guess.
docs-syncis alsorun by hand and by tests. With no
GITHUB_SERVER_URLand no--source, itmust
dienaming the missing variable — not fall back togithub.com. That islib/forge.sh's own rule, "Never 'probably github'", applied to a fetchinstead of a client.
3. The
diemessage states the host it tried. It already interpolates$url; keeping that is the whole fix to the second failure mode.4. Out of scope: the prose links in
docs-sync.sh:212,230andlib/ruling.sh(×4). They are documentation pointers in generated text, notfetches, and they are the same on both sides of the 0.6.0 merge. Worth a
separate pass; conflating them with a functional fetch would make this issue
unreviewable.
Tasks
GITHUB_SERVER_URL.--sourcewas not given.test/docs-sync.test.sh— it has none today.changelog.d/fragment.Acceptance criteria
actions/docs-sync/docs-sync.shcontains no hard-codedgithub.comonany executable path.
GITHUB_SERVER_URLset to this instance, the fetch resolves aforge-only SHA — the case that 404s today.
GITHUB_SERVER_URLunset and no--source, the action exitsnon-zero naming the variable, and fetches nothing.
test/run.shendsfailed 0; shellcheck- and actionlint-clean.Test plan
GITHUB_SERVER_URLtothis instance, and assert the requested URL's host is the forge. Today it is
github.comregardless.GITHUB_SERVER_URLunset, no--source— assert non-zero,assert the variable is named, and assert
curlwas never invoked.--sourcepath is untouched, sodocs-sync-exercisestays green exactly as it is.the oracle for the tag-vs-SHA asymmetry.
Dependencies
None — independent of #198 and safe to land before or after it. Related:
#19 introduced the URL; #197 decision 2 is the version-collision hazard this
turns into a live defect; #198 fixes the other member of this class in
issueflow-reconcile.sh.@andres — filed per your #5369. The evidence is two live fetches, not a
reading: same version number, different trees, HTTP 200 on the wrong one.
Labelled
bug,scope:docs,ready— minted and normalized in one step asthe configured
triage-actorsidentity (.github/labels.conf:3) and at yourinstruction, rather than left
needs-triagefor myself to stamp later; flaggingthat explicitly so the author-gate shortcut is visible rather than silent.
Claimed and built — !203 is open
Claim transition done per BUILDER.md: self-assigned,
ready→claimed. Theclaim came with a PR rather than a promise of one, so the slot is short-lived.
Worth recording, since #192 is still open on exactly this: the label swap was
DELETE .../labels/107→ 204 andPOST .../labels→ 200, under my PAT.That is the control from #192's table reproducing exactly — removal works for a
PAT and fails only for the workflow token, which is why #192's fix is the
full-set
PUTand not anything about this call.What changed since the issue body: nothing in the diagnosis, one thing in
the evidence.
ci.yml'sdocs-sync-exercisepassessource: ceremony-srconboth invocations, so it takes the
--sourceoverride and never runs the fetch.The fetch path therefore had zero coverage before this PR — not thin
coverage, none — and the live-runner criterion being added to #198 would not
have caught this either, since
docs-sync-exerciseis one of the five jobs itgates on.
Verification on the head, all local:
The
changelog-armed300-character bound (#167) rejected my first fragmentdraft at 424 characters. Guard works; entries split.
On closure: !203 says
Refs #201, notCloses. The criteria are allprovable in CI, but what this actually fixes is a consumer's fetch, and
ceremony's own CI never exercises that path. The honest proof is a governed
consumer on this forge fetching from this forge — post-merge and triage-owned
per #151/#175. So this issue moves to
post-mergeon merge and stays openuntil that is recorded, rather than closing with the PR.
@andres — review when you have a moment. Nothing merged, nothing closed.
The Refs-linked PR merged with these acceptance criteria still unchecked:
GITHUB_SERVER_URL.--sourcewas not given.test/docs-sync.test.sh— it has none today.changelog.d/fragment.actions/docs-sync/docs-sync.shcontains no hard-codedgithub.comonGITHUB_SERVER_URLset to this instance, the fetch resolves aGITHUB_SERVER_URLunset and no--source, the action exitstest/run.shendsfailed 0; shellcheck- and actionlint-clean.The merge releases the claim; no builder owes a draft. Triage owes completion in a follow-up comment that names the owner and wake condition.