2026-07-22 21:40:49 +00:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# Contract tests for actions/docs-sync (issue #19). Constructed SOURCE trees
|
|
|
|
|
# (a fake ceremony: manifest + docs) and CONSUMER trees (a release.yml
|
fix(docs-sync): the mirror is fetched from the forge in play, never a built-in one (#201)
heavy-duty/ceremony exists on two forges and the same ref names a different
tree on each — this forge's 0.4.1 carries lib/forge*.sh, GitHub's carries none
of it. The fetch URL was hard-coded to github.com, so a consumer's `.ceremony/`
mirror was verified against a tree it never pinned, and the fetch returned
HTTP 200 while doing it: --check reported drift the consumer could not fix,
and --fix would have rewritten a correct mirror into the wrong one.
The host now comes from GITHUB_SERVER_URL, which Actions injects on both
forges and which lib/forge.sh already selects the whole backend on. Unset,
with no --source, is a refusal naming the variable rather than a guess —
the same rule the pin itself has always followed.
The fetch path had no test coverage at all: every existing row passes
--source, which overrides the fetch entirely. It is now driven against a
PATH-stubbed curl that records the URL and serves a tarball, so the real tar
pipeline still runs and which forge a pin resolves against is asserted.
Refs #201
2026-08-05 11:16:23 +00:00
|
|
|
# caller with the pin line), driven offline via --source. The fetch path is
|
|
|
|
|
# driven too, against a PATH-stubbed curl rather than the network (#201) —
|
|
|
|
|
# which forge a pin resolves against is a decision, not plumbing. The fake
|
2026-07-22 21:40:49 +00:00
|
|
|
# source's doc set is deliberately NOT the real five: a script that
|
|
|
|
|
# hardcodes the vendored list instead of reading the manifest fails these
|
|
|
|
|
# rows. set -u, not -e: failing commands are behavior for the harness to
|
|
|
|
|
# inspect.
|
|
|
|
|
set -u
|
|
|
|
|
|
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
|
# shellcheck source=test/harness.sh
|
|
|
|
|
. "$ROOT/test/harness.sh"
|
|
|
|
|
|
|
|
|
|
SCRIPT="$ROOT/actions/docs-sync/docs-sync.sh"
|
|
|
|
|
|
|
|
|
|
TMP="$(mktemp -d)"
|
|
|
|
|
trap 'rm -rf "$TMP"' EXIT
|
|
|
|
|
|
test(guards): the manifest guard drives the whole test plan
One CI step beside the self-ref pin, and test/vendored.test.sh covering
both directions: the manifest -> tree scan (missing, symlink, directory,
empty, ../ escape, absolute, untracked) and the closed-world root rule
(neither list, vendored, exempted, prose is not an input, no recursion
below the root), plus the real tree unmodified and the RELEASES.md
regression both ways.
The one-off `grep -Fx RELEASES.md` row at test/docs-sync.test.sh is
deleted (#251 D4): two spellings of "the manifest is right" is the drift
the manifest exists to prevent. Its intent is now a guard case, which the
next doctrine file inherits for free.
Refs #251
2026-08-04 10:01:39 +00:00
|
|
|
# The real manifest is asserted by test/vendored.test.sh, not here (#251 D4).
|
|
|
|
|
# A `grep -Fx RELEASES.md` row lived at this spot from #248's review round,
|
|
|
|
|
# binding the promise to the one file that had nearly been missed. It was the
|
|
|
|
|
# hardcoded list the manifest exists to abolish, one layer down: two spellings
|
|
|
|
|
# of "the manifest is right" is exactly the drift it prevents. Its intent —
|
|
|
|
|
# every root doctrine file is declared, RELEASES.md included — is now a
|
|
|
|
|
# closed-world guard case, which the next file inherits for free.
|
2026-08-03 18:26:58 +00:00
|
|
|
|
2026-07-22 21:40:49 +00:00
|
|
|
# --- fixture builders --------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
# The main fake ceremony tree: three manifest entries, one in a subdirectory
|
|
|
|
|
# (the manifest is paths, not filenames — the mirror must carry structure).
|
|
|
|
|
SRC="$TMP/src"
|
|
|
|
|
mkdir -p "$SRC/docs" "$SRC/guide"
|
|
|
|
|
printf 'AGENTS.md\nRULES.md\nguide/DEEP.md\n' >"$SRC/docs/VENDORED.txt"
|
|
|
|
|
printf '# router v1\n' >"$SRC/AGENTS.md"
|
|
|
|
|
printf '# rules v1\n' >"$SRC/RULES.md"
|
|
|
|
|
printf '# deep v1\n' >"$SRC/guide/DEEP.md"
|
|
|
|
|
|
|
|
|
|
# The same tree after a manifest removal: RULES.md is no longer vendored
|
|
|
|
|
# (the file itself may even still exist at the source — the MANIFEST is
|
|
|
|
|
# what defines the set).
|
|
|
|
|
SRC_DROPPED="$TMP/src-dropped"
|
|
|
|
|
cp -r "$SRC" "$SRC_DROPPED"
|
|
|
|
|
printf 'AGENTS.md\nguide/DEEP.md\n' >"$SRC_DROPPED/docs/VENDORED.txt"
|
|
|
|
|
|
|
|
|
|
# consumer <name> [pin-ref...] — a consumer tree whose release.yml carries
|
|
|
|
|
# one pin line per ref given (none → a caller with no pin at all).
|
|
|
|
|
consumer() {
|
|
|
|
|
local dir="$TMP/$1" ref
|
|
|
|
|
shift
|
|
|
|
|
rm -rf "$dir"
|
|
|
|
|
mkdir -p "$dir/.github/workflows"
|
|
|
|
|
{
|
|
|
|
|
printf 'name: release\non:\n push:\n branches: [main]\njobs:\n release:\n'
|
|
|
|
|
for ref in "$@"; do
|
|
|
|
|
printf ' uses: heavy-duty/ceremony/.github/workflows/release.yml@%s\n' "$ref"
|
|
|
|
|
done
|
|
|
|
|
} >"$dir/.github/workflows/release.yml"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# in_consumer <name> <args...> — run the script from inside a consumer tree.
|
|
|
|
|
in_consumer() {
|
|
|
|
|
local dir="$1"
|
|
|
|
|
shift
|
|
|
|
|
(cd "$TMP/$dir" && bash "$SCRIPT" "$@")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# --- the pin: never guessed --------------------------------------------------
|
|
|
|
|
|
|
|
|
|
consumer no-pin
|
|
|
|
|
check "pin line absent → refuse, naming the workflow file" 1 \
|
|
|
|
|
".github/workflows/release.yml" in_consumer no-pin --check --source "$SRC"
|
|
|
|
|
check "pin refusal says it never guesses" 1 "never guesses a ref" \
|
|
|
|
|
in_consumer no-pin --check --source "$SRC"
|
|
|
|
|
|
|
|
|
|
consumer two-pins 0.3.0 0.4.0
|
|
|
|
|
check "two pin lines → refuse (ambiguous)" 1 "exactly one" \
|
|
|
|
|
in_consumer two-pins --check --source "$SRC"
|
|
|
|
|
|
|
|
|
|
# Ceremony's own release.yml carries the pin SHAPE inside a header comment;
|
|
|
|
|
# a consumer pasting documentation into a comment must not double its pin.
|
|
|
|
|
consumer commented-pin 0.3.0
|
|
|
|
|
printf ' # docs say: uses: heavy-duty/ceremony/.github/workflows/release.yml@<tag>\n' \
|
|
|
|
|
>>"$TMP/commented-pin/.github/workflows/release.yml"
|
|
|
|
|
check "a commented-out pin line does not count as a second pin" 0 "" \
|
|
|
|
|
in_consumer commented-pin --fix --source "$SRC"
|
|
|
|
|
check "commented pin: the mirror checks clean" 0 "exact mirror" \
|
|
|
|
|
in_consumer commented-pin --check --source "$SRC"
|
|
|
|
|
|
|
|
|
|
rm -rf "$TMP/no-workflow"
|
|
|
|
|
mkdir -p "$TMP/no-workflow"
|
|
|
|
|
check "missing release.yml entirely → refuse, naming it" 1 \
|
|
|
|
|
"no .github/workflows/release.yml" in_consumer no-workflow --check --source "$SRC"
|
|
|
|
|
|
|
|
|
|
# --- the manifest: single source of the set -----------------------------------
|
|
|
|
|
|
|
|
|
|
consumer fresh 0.3.0
|
|
|
|
|
mkdir -p "$TMP/empty-src"
|
|
|
|
|
check "source without a manifest → refuse" 1 "docs/VENDORED.txt" \
|
|
|
|
|
in_consumer fresh --check --source "$TMP/empty-src"
|
|
|
|
|
|
|
|
|
|
mkdir -p "$TMP/blank-src/docs"
|
|
|
|
|
printf '\n \n' >"$TMP/blank-src/docs/VENDORED.txt"
|
|
|
|
|
check "empty manifest → refuse (a ceremony bug, not an empty set)" 1 "empty" \
|
|
|
|
|
in_consumer fresh --check --source "$TMP/blank-src"
|
|
|
|
|
|
|
|
|
|
mkdir -p "$TMP/ghost-src/docs"
|
|
|
|
|
printf 'GHOST.md\n' >"$TMP/ghost-src/docs/VENDORED.txt"
|
|
|
|
|
check "manifest naming a file the source lacks → refuse" 1 "GHOST.md" \
|
|
|
|
|
in_consumer fresh --check --source "$TMP/ghost-src"
|
|
|
|
|
|
|
|
|
|
mkdir -p "$TMP/escape-src/docs"
|
|
|
|
|
printf '../evil.md\n' >"$TMP/escape-src/docs/VENDORED.txt"
|
|
|
|
|
check "manifest path escaping the mirror → refuse" 1 "refusing manifest path" \
|
|
|
|
|
in_consumer fresh --fix --source "$TMP/escape-src"
|
|
|
|
|
|
|
|
|
|
# --- fix: from empty to exact mirror -------------------------------------------
|
|
|
|
|
|
|
|
|
|
check "check before any fix → .ceremony/ missing entirely" 1 \
|
|
|
|
|
"missing entirely" in_consumer fresh --check --source "$SRC"
|
|
|
|
|
|
|
|
|
|
check "--fix from empty writes the manifest set" 0 "added .ceremony/RULES.md" \
|
|
|
|
|
in_consumer fresh --fix --source "$SRC"
|
|
|
|
|
check "vendored file is byte-identical to its source" 0 "" \
|
|
|
|
|
cmp "$SRC/RULES.md" "$TMP/fresh/.ceremony/RULES.md"
|
|
|
|
|
check "a subdirectory manifest path mirrors with its directory" 0 "" \
|
|
|
|
|
cmp "$SRC/guide/DEEP.md" "$TMP/fresh/.ceremony/guide/DEEP.md"
|
|
|
|
|
|
|
|
|
|
check "--fix generated the README" 0 "" test -f "$TMP/fresh/.ceremony/README.md"
|
|
|
|
|
check "README marks the dir machine-managed" 0 "achine-managed" \
|
|
|
|
|
cat "$TMP/fresh/.ceremony/README.md"
|
|
|
|
|
check "README names where the pin lives" 0 ".github/workflows/release.yml" \
|
|
|
|
|
cat "$TMP/fresh/.ceremony/README.md"
|
|
|
|
|
|
|
|
|
|
check "--fix scaffolded the root AGENTS.md stub" 0 ".ceremony/AGENTS.md" \
|
|
|
|
|
cat "$TMP/fresh/AGENTS.md"
|
|
|
|
|
|
|
|
|
|
check "in-sync mirror → check passes" 0 "exact mirror" \
|
|
|
|
|
in_consumer fresh --check --source "$SRC"
|
|
|
|
|
check "--fix is idempotent (second run changes nothing, exits 0)" 0 \
|
|
|
|
|
"nothing to do" in_consumer fresh --fix --source "$SRC"
|
|
|
|
|
|
|
|
|
|
# --- check: every kind of drift fails, naming the offender ---------------------
|
|
|
|
|
|
|
|
|
|
printf 'edited in place\n' >>"$TMP/fresh/.ceremony/RULES.md"
|
|
|
|
|
check "one byte changed in a vendored file → check fails naming it" 1 \
|
|
|
|
|
".ceremony/RULES.md" in_consumer fresh --check --source "$SRC"
|
|
|
|
|
check "drift message teaches the fix" 1 "run docs-sync --fix" \
|
|
|
|
|
in_consumer fresh --check --source "$SRC"
|
|
|
|
|
check "--fix repairs the drift" 0 "updated .ceremony/RULES.md" \
|
|
|
|
|
in_consumer fresh --fix --source "$SRC"
|
|
|
|
|
|
|
|
|
|
rm "$TMP/fresh/.ceremony/RULES.md"
|
|
|
|
|
check "vendored file missing → check fails naming it" 1 \
|
|
|
|
|
".ceremony/RULES.md is missing" in_consumer fresh --check --source "$SRC"
|
|
|
|
|
in_consumer fresh --fix --source "$SRC" >/dev/null
|
|
|
|
|
|
|
|
|
|
printf 'stray\n' >"$TMP/fresh/.ceremony/STRAY.md"
|
|
|
|
|
check "extra file under .ceremony/ → check fails naming it" 1 \
|
|
|
|
|
".ceremony/STRAY.md" in_consumer fresh --check --source "$SRC"
|
|
|
|
|
check "--fix deletes the extra (mirror means mirror)" 0 \
|
|
|
|
|
"deleted .ceremony/STRAY.md" in_consumer fresh --fix --source "$SRC"
|
|
|
|
|
check "the extra is gone from disk" 1 "" test -f "$TMP/fresh/.ceremony/STRAY.md"
|
|
|
|
|
|
|
|
|
|
# A manifest removal at the source: the orphaned vendored copy goes too.
|
|
|
|
|
check "--fix after manifest removal deletes the orphan" 0 \
|
|
|
|
|
"deleted .ceremony/RULES.md" in_consumer fresh --fix --source "$SRC_DROPPED"
|
|
|
|
|
check "post-removal mirror is exact (and counts 2 files)" 0 "(2 files)" \
|
|
|
|
|
in_consumer fresh --check --source "$SRC_DROPPED"
|
|
|
|
|
in_consumer fresh --fix --source "$SRC" >/dev/null
|
|
|
|
|
|
|
|
|
|
# --- the root AGENTS.md stub: created once, never owned -------------------------
|
|
|
|
|
|
|
|
|
|
printf '# my own router, heavily edited\n' >"$TMP/fresh/AGENTS.md"
|
|
|
|
|
check "edited root AGENTS.md → check passes (content is per-repo)" 0 \
|
|
|
|
|
"exact mirror" in_consumer fresh --check --source "$SRC"
|
|
|
|
|
check "--fix never overwrites an existing root AGENTS.md" 0 "nothing to do" \
|
|
|
|
|
in_consumer fresh --fix --source "$SRC"
|
|
|
|
|
check "the edit survived --fix" 0 "my own router" cat "$TMP/fresh/AGENTS.md"
|
|
|
|
|
|
|
|
|
|
rm "$TMP/fresh/AGENTS.md"
|
|
|
|
|
check "root AGENTS.md missing → check fails, teaching --fix" 1 \
|
|
|
|
|
"run docs-sync --fix" in_consumer fresh --check --source "$SRC"
|
|
|
|
|
in_consumer fresh --fix --source "$SRC" >/dev/null
|
|
|
|
|
|
2026-07-22 21:59:37 +00:00
|
|
|
# --- the README is machine-verified, not just machine-written -------------------
|
|
|
|
|
# The marker that says "a hand edit goes red" must itself go red when
|
|
|
|
|
# hand-edited (kimi-bot, PR #43's review round).
|
|
|
|
|
|
|
|
|
|
printf 'hand edit\n' >>"$TMP/fresh/.ceremony/README.md"
|
|
|
|
|
check "hand-edited README → check fails naming it" 1 ".ceremony/README.md" \
|
|
|
|
|
in_consumer fresh --check --source "$SRC"
|
|
|
|
|
check "--fix rewrites the drifted README" 0 "wrote .ceremony/README.md" \
|
|
|
|
|
in_consumer fresh --fix --source "$SRC"
|
|
|
|
|
|
|
|
|
|
rm "$TMP/fresh/.ceremony/README.md"
|
|
|
|
|
check "missing README → check fails naming it" 1 \
|
|
|
|
|
".ceremony/README.md is missing" in_consumer fresh --check --source "$SRC"
|
|
|
|
|
in_consumer fresh --fix --source "$SRC" >/dev/null
|
|
|
|
|
check "README repaired → check green again" 0 "exact mirror" \
|
|
|
|
|
in_consumer fresh --check --source "$SRC"
|
|
|
|
|
|
|
|
|
|
# --- the mirror is plain files: symlinks and friends refused ---------------------
|
|
|
|
|
# PR #43's review round (codex-bot + kimi-bot, independent repros): cp
|
|
|
|
|
# writes THROUGH a committed link, cmp reads through it, and a `find
|
|
|
|
|
# -type f` scan cannot even see it. Both modes refuse; every row with a
|
|
|
|
|
# victim asserts the victim untouched.
|
|
|
|
|
|
|
|
|
|
consumer sneaky 0.3.0
|
|
|
|
|
in_consumer sneaky --fix --source "$SRC" >/dev/null
|
|
|
|
|
printf 'victim v1\n' >"$TMP/sneaky/victim.md"
|
|
|
|
|
|
|
|
|
|
rm "$TMP/sneaky/.ceremony/RULES.md"
|
|
|
|
|
ln -s ../victim.md "$TMP/sneaky/.ceremony/RULES.md"
|
|
|
|
|
check "vendored path as symlink → check refuses naming it" 1 \
|
|
|
|
|
".ceremony/RULES.md" in_consumer sneaky --check --source "$SRC"
|
|
|
|
|
check "vendored path as symlink → fix refuses (never writes through)" 1 \
|
|
|
|
|
"non-regular" in_consumer sneaky --fix --source "$SRC"
|
|
|
|
|
check "the link's target is untouched" 0 "victim v1" cat "$TMP/sneaky/victim.md"
|
|
|
|
|
rm "$TMP/sneaky/.ceremony/RULES.md"
|
|
|
|
|
in_consumer sneaky --fix --source "$SRC" >/dev/null
|
|
|
|
|
|
|
|
|
|
# A stray link is exactly what the -type f extra-file scan was blind to:
|
|
|
|
|
# unlisted doctrine, previously invisible.
|
|
|
|
|
ln -s ../victim.md "$TMP/sneaky/.ceremony/STRAYLINK.md"
|
|
|
|
|
check "stray symlink (invisible to -type f) → check refuses" 1 \
|
|
|
|
|
"STRAYLINK.md" in_consumer sneaky --check --source "$SRC"
|
|
|
|
|
check "stray symlink → fix refuses too (no silent deletion of a link)" 1 \
|
|
|
|
|
"STRAYLINK.md" in_consumer sneaky --fix --source "$SRC"
|
|
|
|
|
rm "$TMP/sneaky/.ceremony/STRAYLINK.md"
|
|
|
|
|
|
|
|
|
|
mkfifo "$TMP/sneaky/.ceremony/PIPE"
|
|
|
|
|
check "a fifo in the mirror → refused, not read" 1 "non-regular" \
|
|
|
|
|
in_consumer sneaky --check --source "$SRC"
|
|
|
|
|
rm "$TMP/sneaky/.ceremony/PIPE"
|
|
|
|
|
|
|
|
|
|
rm -rf "$TMP/sneaky/.ceremony/guide"
|
|
|
|
|
mkdir -p "$TMP/sneaky/elsewhere"
|
|
|
|
|
ln -s ../elsewhere "$TMP/sneaky/.ceremony/guide"
|
|
|
|
|
check "vendored subdirectory as symlink → fix refuses" 1 \
|
|
|
|
|
".ceremony/guide" in_consumer sneaky --fix --source "$SRC"
|
|
|
|
|
check "nothing was written into the linked directory's target" 1 "" \
|
|
|
|
|
test -e "$TMP/sneaky/elsewhere/DEEP.md"
|
|
|
|
|
rm "$TMP/sneaky/.ceremony/guide"
|
|
|
|
|
in_consumer sneaky --fix --source "$SRC" >/dev/null
|
|
|
|
|
check "sneaky consumer repaired → check green" 0 "exact mirror" \
|
|
|
|
|
in_consumer sneaky --check --source "$SRC"
|
|
|
|
|
|
|
|
|
|
consumer linked-mirror 0.3.0
|
|
|
|
|
mkdir -p "$TMP/linked-mirror-target"
|
|
|
|
|
ln -s ../linked-mirror-target "$TMP/linked-mirror/.ceremony"
|
|
|
|
|
check ".ceremony/ itself a symlink → check refuses" 1 "symlink" \
|
|
|
|
|
in_consumer linked-mirror --check --source "$SRC"
|
|
|
|
|
check ".ceremony/ itself a symlink → fix refuses" 1 "symlink" \
|
|
|
|
|
in_consumer linked-mirror --fix --source "$SRC"
|
|
|
|
|
check "the link's target directory stayed empty" 0 "" \
|
|
|
|
|
test -z "$(ls -A "$TMP/linked-mirror-target")"
|
|
|
|
|
|
|
|
|
|
consumer linked-stub 0.3.0
|
|
|
|
|
printf 'stub victim\n' >"$TMP/linked-stub/other.md"
|
|
|
|
|
ln -s other.md "$TMP/linked-stub/AGENTS.md"
|
|
|
|
|
check "root AGENTS.md as symlink → check refuses" 1 \
|
|
|
|
|
"AGENTS.md is a symlink" in_consumer linked-stub --check --source "$SRC"
|
|
|
|
|
check "root AGENTS.md as symlink → fix refuses" 1 \
|
|
|
|
|
"AGENTS.md is a symlink" in_consumer linked-stub --fix --source "$SRC"
|
|
|
|
|
check "the scaffold did not write through the link" 0 "stub victim" \
|
|
|
|
|
cat "$TMP/linked-stub/other.md"
|
|
|
|
|
|
|
|
|
|
# Dangling is the sharpest case: -e is false through a dangling link, so a
|
|
|
|
|
# naive `[ ! -e ] && scaffold` writes the stub through it.
|
|
|
|
|
rm "$TMP/linked-stub/AGENTS.md"
|
|
|
|
|
ln -s does-not-exist.md "$TMP/linked-stub/AGENTS.md"
|
|
|
|
|
check "dangling AGENTS.md symlink → fix refuses (would write through)" 1 \
|
|
|
|
|
"symlink" in_consumer linked-stub --fix --source "$SRC"
|
|
|
|
|
check "nothing appeared at the dangling target" 1 "" \
|
|
|
|
|
test -e "$TMP/linked-stub/does-not-exist.md"
|
|
|
|
|
|
|
|
|
|
rm "$TMP/linked-stub/AGENTS.md"
|
|
|
|
|
mkdir "$TMP/linked-stub/AGENTS.md"
|
|
|
|
|
check "root AGENTS.md as a directory → refused, named" 1 \
|
|
|
|
|
"not a regular file" in_consumer linked-stub --fix --source "$SRC"
|
|
|
|
|
|
2026-07-22 21:40:49 +00:00
|
|
|
# --- the action's wiring: inputs arrive as env vars -----------------------------
|
|
|
|
|
|
|
|
|
|
consumer env-wired 0.3.0
|
|
|
|
|
env_sync() {
|
|
|
|
|
(cd "$TMP/env-wired" && MODE=fix SOURCE="$SRC" bash "$SCRIPT")
|
|
|
|
|
}
|
|
|
|
|
check "env vars drive the script the way action.yml does" 0 \
|
|
|
|
|
"added .ceremony/RULES.md" env_sync
|
|
|
|
|
|
|
|
|
|
env_bad_mode() {
|
|
|
|
|
(cd "$TMP/env-wired" && MODE=frobnicate SOURCE="$SRC" bash "$SCRIPT")
|
|
|
|
|
}
|
|
|
|
|
check "unknown MODE env refused" 1 "unknown mode" env_bad_mode
|
|
|
|
|
check "unknown flag refused" 1 "unknown argument" \
|
|
|
|
|
in_consumer env-wired --check --source "$SRC" --frobnicate
|
|
|
|
|
check "--source without a directory refused" 1 "no such directory" \
|
|
|
|
|
in_consumer env-wired --check --source "$TMP/does-not-exist"
|
|
|
|
|
|
fix(docs-sync): the mirror is fetched from the forge in play, never a built-in one (#201)
heavy-duty/ceremony exists on two forges and the same ref names a different
tree on each — this forge's 0.4.1 carries lib/forge*.sh, GitHub's carries none
of it. The fetch URL was hard-coded to github.com, so a consumer's `.ceremony/`
mirror was verified against a tree it never pinned, and the fetch returned
HTTP 200 while doing it: --check reported drift the consumer could not fix,
and --fix would have rewritten a correct mirror into the wrong one.
The host now comes from GITHUB_SERVER_URL, which Actions injects on both
forges and which lib/forge.sh already selects the whole backend on. Unset,
with no --source, is a refusal naming the variable rather than a guess —
the same rule the pin itself has always followed.
The fetch path had no test coverage at all: every existing row passes
--source, which overrides the fetch entirely. It is now driven against a
PATH-stubbed curl that records the URL and serves a tarball, so the real tar
pipeline still runs and which forge a pin resolves against is asserted.
Refs #201
2026-08-05 11:16:23 +00:00
|
|
|
# --- the fetch path: which forge, and never a guessed one (#201) ---------------
|
|
|
|
|
|
|
|
|
|
# The fetch path had no coverage at all before this: every row above passes
|
|
|
|
|
# --source, which overrides the fetch entirely, so the URL the tool actually
|
|
|
|
|
# builds was asserted nowhere. It is asserted here with a PATH-stubbed curl
|
|
|
|
|
# that records the URL and serves a tarball of the fake source tree — no
|
|
|
|
|
# network, and the real tar pipeline still runs, so --strip-components stays
|
|
|
|
|
# honest. CURL_FAIL makes the stub fail the way a missing ref does.
|
|
|
|
|
FETCHBIN="$TMP/fetchbin"
|
|
|
|
|
mkdir -p "$FETCHBIN"
|
|
|
|
|
cat >"$FETCHBIN/curl" <<'STUB'
|
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
printf '%s\n' "${!#}" >>"$CURL_URL_LOG"
|
|
|
|
|
[ -z "${CURL_FAIL:-}" ] || exit 22
|
|
|
|
|
exec tar -cz -C "$(dirname "$CURL_SRC")" "$(basename "$CURL_SRC")"
|
|
|
|
|
STUB
|
|
|
|
|
chmod +x "$FETCHBIN/curl"
|
|
|
|
|
|
|
|
|
|
export CURL_URL_LOG="$TMP/curl-urls" CURL_SRC="$SRC"
|
|
|
|
|
|
|
|
|
|
consumer fetched 0.4.1
|
|
|
|
|
|
|
|
|
|
# fetch_sync <server-url> <args...> — the fetch path, no --source. Truncates
|
|
|
|
|
# the URL log first so requested_url always answers about this run.
|
|
|
|
|
fetch_sync() {
|
|
|
|
|
local server="$1"
|
|
|
|
|
shift
|
|
|
|
|
: >"$CURL_URL_LOG"
|
|
|
|
|
(cd "$TMP/fetched" && PATH="$FETCHBIN:$PATH" GITHUB_SERVER_URL="$server" \
|
|
|
|
|
bash "$SCRIPT" "$@")
|
|
|
|
|
}
|
|
|
|
|
requested_url() { cat "$CURL_URL_LOG"; }
|
|
|
|
|
|
|
|
|
|
check "the fetch mirrors the pin fetched from the forge in the environment" 0 \
|
|
|
|
|
"added .ceremony/RULES.md" fetch_sync https://forgejo.example.test --fix
|
|
|
|
|
check "...and the URL asked for names that forge, not a built-in one" 0 \
|
|
|
|
|
"https://forgejo.example.test/heavy-duty/ceremony/archive/0.4.1.tar.gz" \
|
|
|
|
|
requested_url
|
|
|
|
|
|
|
|
|
|
# One pin ref, two forges, two trees — the whole reason #201 exists. The same
|
|
|
|
|
# consumer must fetch from whichever forge it is running on.
|
|
|
|
|
fetch_sync https://github.com --fix >/dev/null 2>&1
|
|
|
|
|
check "the same pin on another forge fetches from that forge instead" 0 \
|
|
|
|
|
"https://github.com/heavy-duty/ceremony/archive/0.4.1.tar.gz" requested_url
|
|
|
|
|
|
|
|
|
|
fetch_sync https://forgejo.example.test/ --fix >/dev/null 2>&1
|
|
|
|
|
check "a trailing slash on the server URL does not double the separator" 0 \
|
|
|
|
|
"https://forgejo.example.test/heavy-duty/ceremony/archive/0.4.1.tar.gz" \
|
|
|
|
|
requested_url
|
|
|
|
|
|
|
|
|
|
# Unset is not github.com. A tool that never guesses a ref must not guess a
|
|
|
|
|
# forge either — and it must refuse BEFORE reaching for the network.
|
|
|
|
|
no_server_sync() {
|
|
|
|
|
: >"$CURL_URL_LOG"
|
|
|
|
|
(cd "$TMP/fetched" && PATH="$FETCHBIN:$PATH" \
|
|
|
|
|
env -u GITHUB_SERVER_URL bash "$SCRIPT" --check)
|
|
|
|
|
}
|
|
|
|
|
check "no GITHUB_SERVER_URL and no --source → refuse, naming the variable" 1 \
|
|
|
|
|
"GITHUB_SERVER_URL is unset" no_server_sync
|
|
|
|
|
check "...and the refusal says it never guesses a forge" 1 \
|
|
|
|
|
"never guesses a forge" no_server_sync
|
|
|
|
|
nothing_fetched() { [ ! -s "$CURL_URL_LOG" ]; }
|
|
|
|
|
check "...and nothing was fetched before refusing" 0 "" nothing_fetched
|
|
|
|
|
|
|
|
|
|
# A ref that does not resolve on the forge in play: the message must name the
|
|
|
|
|
# URL actually attempted, so "does the pinned ref exist" is answerable.
|
|
|
|
|
fetch_fail() {
|
|
|
|
|
: >"$CURL_URL_LOG"
|
|
|
|
|
(cd "$TMP/fetched" && PATH="$FETCHBIN:$PATH" CURL_FAIL=1 \
|
|
|
|
|
GITHUB_SERVER_URL=https://forgejo.example.test bash "$SCRIPT" --check)
|
|
|
|
|
}
|
|
|
|
|
check "a failed fetch names the URL it tried" 1 \
|
|
|
|
|
"https://forgejo.example.test/heavy-duty/ceremony/archive/0.4.1.tar.gz" fetch_fail
|
|
|
|
|
check "...and asks about the ref on that forge, not in the abstract" 1 \
|
|
|
|
|
"exist on that forge" fetch_fail
|
|
|
|
|
|
2026-07-22 21:40:49 +00:00
|
|
|
summary
|