forked from heavy-duty/ceremony
Merge pull request #277 from cndgrr/build/251-vendored-manifest
feat(guards): the tag declares its vendorable set
This commit is contained in:
commit
017c571438
6 changed files with 535 additions and 5 deletions
234
.github/scripts/vendored-check.sh
vendored
Executable file
234
.github/scripts/vendored-check.sh
vendored
Executable file
|
|
@ -0,0 +1,234 @@
|
|||
#!/usr/bin/env bash
|
||||
# The vendored-manifest self-guard (issue #251; #248's near-miss). Consumers
|
||||
# mirror the agent-facing doc set declared by docs/VENDORED.txt, and
|
||||
# actions/docs-sync already enforces "manifest ∪ .ceremony/, nothing else"
|
||||
# on the CONSUMER side. Nothing enforced the other end: a new doctrine file
|
||||
# could land at ceremony's root and nobody add it to the manifest, and the
|
||||
# miss is SILENT — docs-sync asserts byte-identity for the files the
|
||||
# manifest names, so a doc it omits is never checked and every consumer
|
||||
# drifts doctrine-blind with green guards. #248 (RELEASES.md) nearly shipped
|
||||
# that way, caught only by a hand-written `grep -Fx RELEASES.md` row in
|
||||
# test/docs-sync.test.sh — the hardcoded list this guard abolishes, one
|
||||
# layer down. That row is deleted; this script carries its intent.
|
||||
#
|
||||
# Two directions, two mechanisms, because only one of them can be a scan
|
||||
# (#251 D2):
|
||||
#
|
||||
# * MANIFEST → TREE is a scan: every entry resolves to a regular,
|
||||
# non-empty, tracked file at the declared path — no symlink (PR #43:
|
||||
# a symlink read as doctrine while staying invisible), no directory,
|
||||
# no `../` escape.
|
||||
# * TREE → MANIFEST cannot scan, because nothing in the tree answers
|
||||
# "which files are vendorable" — the manifest is the only
|
||||
# machine-readable notion of it. So it gets a CLOSED-WORLD RULE
|
||||
# instead: every `*.md` at the repository ROOT is either in the
|
||||
# manifest or in the exemption list below. Adding a root doc then
|
||||
# forces a one-line decision — vendor it or exempt it — and the
|
||||
# refusal names the file and both fixes.
|
||||
#
|
||||
# The rule is ROOT-LEVEL `*.md` ONLY. It does not walk docs/, actions/ or
|
||||
# drills/: those hold no agent-facing doctrine, and a recursive version
|
||||
# would grow the exemption list past the length at which a reviewer still
|
||||
# reads it — which is the failure this rule is shaped against.
|
||||
#
|
||||
# The exemption list lives HERE, in the script. CONTRIBUTING.md's
|
||||
# vendored-set sentence is documentation, never an input: two declarations
|
||||
# of the same set is the drift the manifest exists to prevent.
|
||||
#
|
||||
# Usage: vendored-check.sh [tree-dir] (default: the repo root — the CI
|
||||
# step; tests point it at fixture trees)
|
||||
set -euo pipefail
|
||||
|
||||
MANIFEST="docs/VENDORED.txt"
|
||||
|
||||
tree="${1:-.}"
|
||||
|
||||
# The root docs that are deliberately ceremony-only. Each carries the reason
|
||||
# it is not vendored, because the reason is what lets the next reviewer
|
||||
# judge the next addition. Prints the reason and returns 0 when exempt.
|
||||
exempt_reason() {
|
||||
case "$1" in
|
||||
README.md)
|
||||
echo "ceremony's own front page — a consumer's router is AGENTS.md, not this repo's README"
|
||||
;;
|
||||
CONTRIBUTING.md)
|
||||
echo "repo-specific facts (this repo's roster, scopes and conventions); every governed repo writes its own"
|
||||
;;
|
||||
CHANGELOG.md)
|
||||
echo "ceremony's own release history; a consumer keeps its own"
|
||||
;;
|
||||
FLEET.md)
|
||||
echo "the operator's fleet map — about running the fleet, not about how a governed repo works"
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
die() {
|
||||
printf 'vendored-check: %s\n' "$@" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
manifest_file="$tree/$MANIFEST"
|
||||
[ -f "$manifest_file" ] || die \
|
||||
"no $MANIFEST under $tree — the manifest is the sole declaration of the" \
|
||||
" vendored doc set, and this guard has nothing to guard without it."
|
||||
|
||||
# Blank lines are skipped, exactly as actions/docs-sync reads it: the guard
|
||||
# and the tool must accept the same file, or one of them is the bug.
|
||||
mapfile -t manifest < <(grep -v '^[[:space:]]*$' "$manifest_file" || true)
|
||||
[ "${#manifest[@]}" -gt 0 ] || die \
|
||||
"$MANIFEST is empty — an empty doctrine set is a ceremony bug, not a repo" \
|
||||
" with no rules."
|
||||
|
||||
# Whether the tracked-file assertion can bind: only when the tree IS a git
|
||||
# work tree root. Fixture trees are plain directories, and asserting
|
||||
# tracked-ness against an enclosing repository would be asserting about the
|
||||
# wrong tree.
|
||||
#
|
||||
# When it cannot bind, SAY SO. This guard's whole argument is that a silent
|
||||
# miss is worse than a loud one, and a guard that quietly stops asserting one
|
||||
# of its four properties is exactly that shape — so the skip is announced on
|
||||
# every run, green or red, rather than inferred from the absence of a
|
||||
# refusal (#251 round 1).
|
||||
tracked_check=no
|
||||
tracked_note="tracked-ness NOT asserted: $tree is not a git work tree root, so
|
||||
'is this file in the tag's tree' cannot be answered about THIS tree. The
|
||||
other three manifest assertions (regular file, non-empty, no
|
||||
symlink/dir/escape) still bind."
|
||||
if command -v git >/dev/null 2>&1; then
|
||||
toplevel="$(git -C "$tree" rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [ -n "$toplevel" ] && [ "$toplevel" = "$(cd "$tree" && pwd -P)" ]; then
|
||||
tracked_check=yes
|
||||
tracked_note=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Every refusal is collected and reported together, one multi-line string
|
||||
# per offending file: a guard that stops at the first problem makes a
|
||||
# builder pay one CI round per file.
|
||||
problems=()
|
||||
|
||||
# --- manifest → tree ---------------------------------------------------------
|
||||
|
||||
for entry in "${manifest[@]}"; do
|
||||
case "$entry" in
|
||||
/* | *..*)
|
||||
problems+=("$(
|
||||
printf '%s\n' \
|
||||
"$MANIFEST names '$entry' — an absolute path or a '..' escape. The" \
|
||||
" mirror writes only inside a consumer's .ceremony/, so a path that" \
|
||||
" leaves it is never vendorable." \
|
||||
" Fix: name the path relative to the repository root, with no '..'."
|
||||
)")
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
path="$tree/$entry"
|
||||
|
||||
# -L before -f: `[ -f ]` follows the link, so a symlink to a real file
|
||||
# would otherwise pass as a regular one.
|
||||
if [ -L "$path" ]; then
|
||||
problems+=("$(
|
||||
printf '%s\n' \
|
||||
"$MANIFEST names '$entry', which is a SYMLINK. A symlink vendors as" \
|
||||
" doctrine while its content lives somewhere the mirror never checks" \
|
||||
" (PR #43's round: it read as doctrine and stayed invisible)." \
|
||||
" Fix: make '$entry' a regular file, or drop the entry from $MANIFEST."
|
||||
)")
|
||||
continue
|
||||
fi
|
||||
if [ -d "$path" ]; then
|
||||
problems+=("$(
|
||||
printf '%s\n' \
|
||||
"$MANIFEST names '$entry', which is a DIRECTORY. The manifest declares" \
|
||||
" files, one per line — a directory entry vendors nothing." \
|
||||
" Fix: name each file under '$entry' on its own line, or drop the entry."
|
||||
)")
|
||||
continue
|
||||
fi
|
||||
if [ ! -f "$path" ]; then
|
||||
problems+=("$(
|
||||
printf '%s\n' \
|
||||
"$MANIFEST names '$entry' but the tree has no such file. Every consumer" \
|
||||
" mirroring this ref would fail on it." \
|
||||
" Fix: add '$entry' to the tree, or remove it from $MANIFEST."
|
||||
)")
|
||||
continue
|
||||
fi
|
||||
if [ ! -s "$path" ]; then
|
||||
problems+=("$(
|
||||
printf '%s\n' \
|
||||
"$MANIFEST names '$entry', which is EMPTY. An empty file vendors as" \
|
||||
" doctrine that says nothing, and is read as doctrine anyway." \
|
||||
" Fix: write '$entry', or remove it from $MANIFEST."
|
||||
)")
|
||||
continue
|
||||
fi
|
||||
if [ "$tracked_check" = yes ] &&
|
||||
! git -C "$tree" ls-files --error-unmatch -- "$entry" >/dev/null 2>&1; then
|
||||
problems+=("$(
|
||||
printf '%s\n' \
|
||||
"$MANIFEST names '$entry', which is not TRACKED. A file absent from the" \
|
||||
" tag's tree cannot be fetched by a consumer syncing at that tag," \
|
||||
" however present it is on this machine." \
|
||||
" Fix: git add '$entry', or remove it from $MANIFEST."
|
||||
)")
|
||||
continue
|
||||
fi
|
||||
done
|
||||
|
||||
# --- tree → manifest: the closed world over root `*.md` ----------------------
|
||||
|
||||
in_manifest() {
|
||||
local p
|
||||
for p in "${manifest[@]}"; do
|
||||
[ "$p" = "$1" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
shopt -s nullglob
|
||||
exempted=()
|
||||
vendored=()
|
||||
for path in "$tree"/*.md; do
|
||||
doc="${path##*/}"
|
||||
if in_manifest "$doc"; then
|
||||
vendored+=("$doc")
|
||||
continue
|
||||
fi
|
||||
if reason="$(exempt_reason "$doc")"; then
|
||||
exempted+=("$doc — $reason")
|
||||
continue
|
||||
fi
|
||||
problems+=("$(
|
||||
printf '%s\n' \
|
||||
"'$doc' is a root doc in NEITHER list. Every root *.md is either vendored" \
|
||||
" doctrine — mirrored into every governed repo at .ceremony/ — or" \
|
||||
" deliberately ceremony-only, and nothing in the tree says which, so the" \
|
||||
" decision has to be written down. Fix, one of:" \
|
||||
" * add '$doc' to $MANIFEST, if it is agent-facing doctrine that every" \
|
||||
" governed repo must carry;" \
|
||||
" * add '$doc' to the exemption list in" \
|
||||
" .github/scripts/vendored-check.sh, with the reason it stays" \
|
||||
" ceremony-only."
|
||||
)")
|
||||
done
|
||||
shopt -u nullglob
|
||||
|
||||
if [ "${#problems[@]}" -gt 0 ]; then
|
||||
{
|
||||
printf 'vendored-check: %d problem(s) — docs/VENDORED.txt and the tree disagree.\n\n' \
|
||||
"${#problems[@]}"
|
||||
printf '%s\n\n' "${problems[@]}"
|
||||
[ -z "$tracked_note" ] || printf 'vendored-check: %s\n' "$tracked_note"
|
||||
} >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'vendored-check: %d manifest entries resolve; %d root docs vendored, %d exempt.\n' \
|
||||
"${#manifest[@]}" "${#vendored[@]}" "${#exempted[@]}"
|
||||
[ -z "$tracked_note" ] || printf 'vendored-check: %s\n' "$tracked_note"
|
||||
[ "${#vendored[@]}" -eq 0 ] || printf ' vendored: %s\n' "${vendored[@]}"
|
||||
[ "${#exempted[@]}" -eq 0 ] || printf ' exempt: %s\n' "${exempted[@]}"
|
||||
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
|
|
@ -35,6 +35,11 @@ jobs:
|
|||
# Five stale markers survived the tags that shipped their machinery
|
||||
# (#221); #238 makes the release candidate reject that drift.
|
||||
run: bash .github/scripts/marker-check.sh
|
||||
- name: Vendored manifest
|
||||
# The manifest rules (issue #251; #248's near-miss): a doctrine file
|
||||
# at the root that nobody added to docs/VENDORED.txt is invisible to
|
||||
# every consumer's docs-sync, so it fails CI here instead.
|
||||
run: bash .github/scripts/vendored-check.sh
|
||||
- name: Tests
|
||||
env:
|
||||
# The npm-backed version_write case may skip locally when npm is
|
||||
|
|
|
|||
14
changelog.d/251.md
Normal file
14
changelog.d/251.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
### Added
|
||||
|
||||
- CI now refuses a root `*.md` declared in neither `docs/VENDORED.txt` nor the
|
||||
guard's short exemption list, so a new doctrine file can no longer reach a
|
||||
tag undeclared and stay invisible to every consumer's `docs-sync` (#251).
|
||||
- The same guard reads the manifest the other way: every entry must resolve to
|
||||
a regular, non-empty, tracked file — no symlink, no directory, no `../`
|
||||
escape (#251).
|
||||
|
||||
### Changed
|
||||
|
||||
- Consumer guidance: re-vendor tooling reads the pin's `docs/VENDORED.txt`,
|
||||
never a hardcoded list, so a new doctrine file propagates at the next
|
||||
ordinary pin bump with zero list edits (#251).
|
||||
|
|
@ -598,6 +598,38 @@ It is **unreleased** (#248) until that tag exists: consumers add
|
|||
`.ceremony/RELEASES.md` only with the ordinary pin bump and re-sync, never by
|
||||
copying it ahead of their pinned doctrine set.
|
||||
|
||||
### Read the manifest, never a copy of it
|
||||
|
||||
Anything on the consumer's side that needs to know *which* documents are
|
||||
vendored — a re-vendor script, a `docs-sync` equivalent, the task list of a
|
||||
conversion issue — reads **the pin's `docs/VENDORED.txt`** and never names
|
||||
the files itself. The manifest is available at the pinned ref from `0.1.0`
|
||||
and later — it shipped with `actions/docs-sync` itself (ceremony#19), in the
|
||||
same commit, and that tool has read it rather than a list since — and it is
|
||||
one path per line, relative to ceremony's root, blank lines ignored:
|
||||
|
||||
```sh
|
||||
# the vendored doc set at the ref this repo is pinned to
|
||||
curl -fsSL "https://raw.githubusercontent.com/heavy-duty/ceremony/<pinned-tag>/docs/VENDORED.txt"
|
||||
```
|
||||
|
||||
That is the whole benefit: a doctrine file added in ceremony — `RELEASES.md`
|
||||
was the last, ceremony#248 — reaches every consumer at its next **ordinary
|
||||
pin bump**, with **zero list edits** anywhere. A hardcoded list propagates
|
||||
nothing, and its staleness is silent rather than red: `docs-sync --check`
|
||||
asserts byte-identity for the files the list names and says nothing at all
|
||||
about one it omits, so a consumer keeps a green guard while governing
|
||||
itself with doctrine it no longer has.
|
||||
|
||||
What makes reading the manifest *sufficient* — rather than merely better
|
||||
than a copy — is that ceremony's CI now refuses a root doctrine file that is
|
||||
declared in neither the manifest nor a short in-script exemption list
|
||||
(`.github/scripts/vendored-check.sh`), so the manifest at a tag is the
|
||||
complete set as of that tag. That guarantee is **unreleased** (#251) until
|
||||
the first tag carrying it exists; the manifest is worth reading at every
|
||||
earlier pin regardless, since it is what `actions/docs-sync` has always
|
||||
mirrored.
|
||||
|
||||
The consumer's ci.yml gains the guard alongside the others:
|
||||
|
||||
```yaml
|
||||
|
|
|
|||
|
|
@ -18,11 +18,13 @@ SCRIPT="$ROOT/actions/docs-sync/docs-sync.sh"
|
|||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# RELEASES.md's consumer-availability promise is true only when the real
|
||||
# manifest carries it (#248's review round). The fixture cases below prove
|
||||
# manifest-driven behavior; this row binds that behavior to the promised file.
|
||||
check "real manifest includes the release doctrine" 0 "RELEASES.md" \
|
||||
grep -Fx RELEASES.md "$ROOT/docs/VENDORED.txt"
|
||||
# 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.
|
||||
|
||||
# --- fixture builders --------------------------------------------------------
|
||||
|
||||
|
|
|
|||
243
test/vendored.test.sh
Normal file
243
test/vendored.test.sh
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
#!/usr/bin/env bash
|
||||
# Contract tests for .github/scripts/vendored-check.sh (issue #251) — the
|
||||
# self-guard that makes docs/VENDORED.txt authoritative over ceremony's OWN
|
||||
# tree. Driven against constructed fixture trees plus the real one; the CI
|
||||
# step runs the same script against the real tree.
|
||||
#
|
||||
# The fixture doc set is deliberately NOT ceremony's real six: a guard that
|
||||
# hardcodes the vendored list instead of reading the manifest fails these
|
||||
# rows, which is the failure the whole issue is about.
|
||||
#
|
||||
# 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"
|
||||
|
||||
CHECK="$ROOT/.github/scripts/vendored-check.sh"
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# --- fixture builders --------------------------------------------------------
|
||||
|
||||
# tree <name> <manifest-entry...> — a fixture tree carrying only the manifest.
|
||||
tree() {
|
||||
local dir="$TMP/$1"
|
||||
shift
|
||||
rm -rf "$dir"
|
||||
mkdir -p "$dir/docs"
|
||||
printf '%s\n' "$@" >"$dir/docs/VENDORED.txt"
|
||||
}
|
||||
|
||||
# doc <tree> <path> [content] — a regular file in a fixture tree.
|
||||
doc() {
|
||||
local path="$TMP/$1/$2"
|
||||
mkdir -p "$(dirname "$path")"
|
||||
printf '%s\n' "${3:-# a doc}" >"$path"
|
||||
}
|
||||
|
||||
# real_copy <name> — the real tree reduced to what the guard reads: the
|
||||
# manifest, every path the manifest names, and every root *.md. Cases mutate
|
||||
# the copy, so the guard's verdict on ceremony's actual doc set is proven
|
||||
# without touching the working tree.
|
||||
real_copy() {
|
||||
local dir="$TMP/$1" entry
|
||||
rm -rf "$dir"
|
||||
mkdir -p "$dir/docs"
|
||||
cp "$ROOT/docs/VENDORED.txt" "$dir/docs/VENDORED.txt"
|
||||
cp "$ROOT"/*.md "$dir/"
|
||||
while IFS= read -r entry; do
|
||||
[ -n "$entry" ] || continue
|
||||
mkdir -p "$dir/$(dirname "$entry")"
|
||||
cp "$ROOT/$entry" "$dir/$entry"
|
||||
done <"$ROOT/docs/VENDORED.txt"
|
||||
}
|
||||
|
||||
run_check() {
|
||||
bash "$CHECK" "$TMP/$1"
|
||||
}
|
||||
|
||||
# --- the happy tree ----------------------------------------------------------
|
||||
|
||||
# One manifest entry lives in a subdirectory: the manifest is PATHS, not
|
||||
# filenames (docs-sync's fixtures prove the same), and the closed-world rule
|
||||
# over the root must not regress that to root-only.
|
||||
tree ok AGENTS.md RULES.md guide/DEEP.md
|
||||
doc ok AGENTS.md
|
||||
doc ok RULES.md
|
||||
doc ok guide/DEEP.md
|
||||
check "a tree whose root docs are all declared passes" 0 "3 manifest entries resolve" \
|
||||
run_check ok
|
||||
|
||||
tree blanks AGENTS.md '' RULES.md
|
||||
doc blanks AGENTS.md
|
||||
doc blanks RULES.md
|
||||
check "blank manifest lines are skipped, as docs-sync skips them" 0 "2 manifest entries" \
|
||||
run_check blanks
|
||||
|
||||
# --- the closed world: a root doc in neither list ----------------------------
|
||||
|
||||
# The #248 near-miss, replayed as a test: a new doctrine file lands at the
|
||||
# root and nobody adds it to the manifest.
|
||||
tree newdoc AGENTS.md
|
||||
doc newdoc AGENTS.md
|
||||
doc newdoc NEWDOC.md
|
||||
check "a root doc in neither list reds" 1 "'NEWDOC.md' is a root doc in NEITHER list" \
|
||||
run_check newdoc
|
||||
check "...and the refusal names the manifest fix" 1 "add 'NEWDOC.md' to docs/VENDORED.txt" \
|
||||
run_check newdoc
|
||||
check "...and the refusal names the exemption fix" 1 "add 'NEWDOC.md' to the exemption list" \
|
||||
run_check newdoc
|
||||
|
||||
# The decision the guard forces, taken each way: vendor it…
|
||||
tree newdoc-vendored AGENTS.md NEWDOC.md
|
||||
doc newdoc-vendored AGENTS.md
|
||||
doc newdoc-vendored NEWDOC.md
|
||||
check "a root doc added to the manifest passes" 0 "2 manifest entries" \
|
||||
run_check newdoc-vendored
|
||||
|
||||
# …or exempt it. The exemption list is in the script and carries a reason;
|
||||
# README.md is one of the four ceremony-only root docs it names.
|
||||
tree exempted AGENTS.md
|
||||
doc exempted AGENTS.md
|
||||
doc exempted README.md
|
||||
check "a root doc on the exemption list passes, with its reason" 0 "exempt: README.md" \
|
||||
run_check exempted
|
||||
|
||||
# The exemption list is NEVER read from prose. CONTRIBUTING.md's vendored-set
|
||||
# sentence is documentation; two declarations of the same set is the drift
|
||||
# the manifest exists to prevent (#251 D2's second must-fail).
|
||||
tree prose AGENTS.md
|
||||
doc prose AGENTS.md
|
||||
doc prose EXTRA.md
|
||||
doc prose CONTRIBUTING.md "The vendored set is AGENTS.md and EXTRA.md."
|
||||
check "a doc declared only in prose still reds" 1 "'EXTRA.md' is a root doc in NEITHER list" \
|
||||
run_check prose
|
||||
|
||||
# --- the rule is ROOT-level only ---------------------------------------------
|
||||
|
||||
# A guard that walked the tree would need an exemption list long enough that
|
||||
# nobody reads it — the exact failure the root-only rule is shaped against.
|
||||
# So an undeclared *.md under docs/, actions/ or drills/ must stay GREEN.
|
||||
tree subdirs AGENTS.md
|
||||
doc subdirs AGENTS.md
|
||||
doc subdirs docs/CONSUMERS.md
|
||||
doc subdirs actions/thing/README.md
|
||||
doc subdirs drills/2026-07-01.md
|
||||
check "undeclared *.md below the root stays green (no recursion)" 0 "1 manifest entries" \
|
||||
run_check subdirs
|
||||
|
||||
# --- manifest → tree: the scan -----------------------------------------------
|
||||
|
||||
tree missing AGENTS.md GONE.md
|
||||
doc missing AGENTS.md
|
||||
check "a manifest entry with no file reds, naming it" 1 "names 'GONE.md' but the tree has no such file" \
|
||||
run_check missing
|
||||
|
||||
tree symlinked AGENTS.md LINK.md
|
||||
doc symlinked AGENTS.md
|
||||
ln -s AGENTS.md "$TMP/symlinked/LINK.md"
|
||||
check "a manifest entry pointing at a symlink reds" 1 "names 'LINK.md', which is a SYMLINK" \
|
||||
run_check symlinked
|
||||
|
||||
tree dir-entry AGENTS.md guide
|
||||
doc dir-entry AGENTS.md
|
||||
mkdir -p "$TMP/dir-entry/guide"
|
||||
check "a manifest entry pointing at a directory reds" 1 "names 'guide', which is a DIRECTORY" \
|
||||
run_check dir-entry
|
||||
|
||||
tree empty-entry AGENTS.md HOLLOW.md
|
||||
doc empty-entry AGENTS.md
|
||||
: >"$TMP/empty-entry/HOLLOW.md"
|
||||
check "a manifest entry pointing at an empty file reds" 1 "names 'HOLLOW.md', which is EMPTY" \
|
||||
run_check empty-entry
|
||||
|
||||
# The escape case exists as a docs-sync fixture; ceremony's own manifest must
|
||||
# not be the one place it goes unchecked.
|
||||
tree escape AGENTS.md ../outside.md
|
||||
doc escape AGENTS.md
|
||||
check "a manifest entry escaping with ../ reds" 1 "names '../outside.md'" \
|
||||
run_check escape
|
||||
|
||||
tree absolute AGENTS.md /etc/hosts
|
||||
doc absolute AGENTS.md
|
||||
check "an absolute manifest entry reds" 1 "names '/etc/hosts'" \
|
||||
run_check absolute
|
||||
|
||||
# --- the manifest itself -----------------------------------------------------
|
||||
|
||||
rm -rf "$TMP/no-manifest"
|
||||
mkdir -p "$TMP/no-manifest"
|
||||
check "a tree with no manifest reds" 1 "no docs/VENDORED.txt under" \
|
||||
run_check no-manifest
|
||||
|
||||
rm -rf "$TMP/empty-manifest"
|
||||
mkdir -p "$TMP/empty-manifest/docs"
|
||||
: >"$TMP/empty-manifest/docs/VENDORED.txt"
|
||||
check "an empty manifest reds" 1 "is empty" run_check empty-manifest
|
||||
|
||||
# --- tracked-ness ------------------------------------------------------------
|
||||
|
||||
# A file present on this machine but absent from the tag's tree cannot be
|
||||
# fetched by a consumer syncing at that tag. The assertion binds only where
|
||||
# it can: when the tree IS a git work tree root.
|
||||
tree tracked AGENTS.md RULES.md
|
||||
doc tracked AGENTS.md
|
||||
doc tracked RULES.md
|
||||
git init -q "$TMP/tracked"
|
||||
git -C "$TMP/tracked" add docs/VENDORED.txt AGENTS.md RULES.md
|
||||
check "a git tree whose manifest entries are all tracked passes" 0 "2 manifest entries" \
|
||||
run_check tracked
|
||||
|
||||
tree untracked AGENTS.md RULES.md
|
||||
doc untracked AGENTS.md
|
||||
doc untracked RULES.md
|
||||
git init -q "$TMP/untracked"
|
||||
git -C "$TMP/untracked" add docs/VENDORED.txt AGENTS.md
|
||||
check "a git tree with an untracked manifest entry reds" 1 "names 'RULES.md', which is not TRACKED" \
|
||||
run_check untracked
|
||||
|
||||
# ...and where it cannot bind, the skip ANNOUNCES ITSELF rather than being
|
||||
# inferred from the absence of a refusal (#251 round 1). A guard that quietly
|
||||
# stops asserting one of its four properties is the silent miss this whole
|
||||
# script argues against, so the degradation is visible on both output paths.
|
||||
check "a non-git tree says tracked-ness was not asserted" 0 "tracked-ness NOT asserted" \
|
||||
run_check ok
|
||||
check "...and says it on the red path too, beside the refusals" 1 "tracked-ness NOT asserted" \
|
||||
run_check newdoc
|
||||
|
||||
# The converse, so the note is not simply always printed: where the tree IS a
|
||||
# git work tree root the assertion bound, and nothing is announced.
|
||||
no_skip_note() { ! run_check tracked 2>&1 | grep -qF "tracked-ness NOT asserted"; }
|
||||
check "a git work tree root announces no skip — the assertion bound" 0 "" \
|
||||
no_skip_note
|
||||
|
||||
# --- the real tree -----------------------------------------------------------
|
||||
|
||||
check "this tree, unmodified, is green" 0 "manifest entries resolve" bash "$CHECK" "$ROOT"
|
||||
|
||||
# The #248 near-miss on the REAL doc set: a scratch root doc nobody declared.
|
||||
real_copy scratch
|
||||
doc scratch SCRATCHDOC.md
|
||||
check "a scratch root doc on the real tree reds, naming it" 1 "'SCRATCHDOC.md' is a root doc in NEITHER list" \
|
||||
run_check scratch
|
||||
|
||||
# RELEASES.md stays listed — the regression criterion #248's review round
|
||||
# bought, now asserted BY THE GUARD rather than by a hardcoded `grep -Fx` row
|
||||
# in test/docs-sync.test.sh (#251 D1, D4). The closed world holds in both
|
||||
# directions: dropping it from the manifest alone reds…
|
||||
real_copy releases-dropped
|
||||
grep -v '^RELEASES\.md$' "$ROOT/docs/VENDORED.txt" >"$TMP/releases-dropped/docs/VENDORED.txt"
|
||||
check "dropping RELEASES.md from the manifest alone reds" 1 "'RELEASES.md' is a root doc in NEITHER list" \
|
||||
run_check releases-dropped
|
||||
|
||||
# …and it is green only when the file leaves the root in the same breath.
|
||||
real_copy releases-gone
|
||||
grep -v '^RELEASES\.md$' "$ROOT/docs/VENDORED.txt" >"$TMP/releases-gone/docs/VENDORED.txt"
|
||||
rm -f "$TMP/releases-gone/RELEASES.md"
|
||||
check "dropping RELEASES.md from the manifest AND the root is green" 0 "manifest entries resolve" \
|
||||
run_check releases-gone
|
||||
|
||||
summary
|
||||
Loading…
Reference in a new issue