ceremony/test/release-path.test.sh
codex-bot-andresmgsl 17b99183ca
Some checks failed
CI / test (pull_request) Failing after 3m49s
CI / release-exercise (pull_request) Successful in 26s
CI / self-guards (pull_request) Successful in 21s
CI / action-exercise (pull_request) Successful in 20s
CI / docs-sync-exercise (pull_request) Successful in 20s
Refs guard / refs-not-closing (pull_request) Successful in 20s
labels / labels (pull_request) Successful in 22s
test: guard release path doctrine
2026-08-25 07:31:39 +00:00

194 lines
7.4 KiB
Bash
Executable file

#!/usr/bin/env bash
# Contract tests for the release-door path manifest (issue #237). The list
# is evidence for skipping a live drill, so drift in either direction must
# fail before a release record can make an incomplete claim.
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=test/harness.sh
. "$ROOT/test/harness.sh"
PATH_SCRIPT="$ROOT/.github/scripts/release-path.sh"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
library_refs() {
local sibling_refs=no
case "$1" in
*/lib/*.sh) sibling_refs=yes ;;
esac
awk -v sibling_refs="$sibling_refs" '
/^[[:space:]]*#/ { next }
{
line = $0
while (match(line, /lib\/[[:alnum:]_.-]+\.sh/)) {
print substr(line, RSTART, RLENGTH)
line = substr(line, RSTART + RLENGTH)
}
# Door libraries source siblings through their own BASH_SOURCE dirname,
# so the executable line ends in /name.sh without a literal lib/ (#237).
if (sibling_refs == "yes" && $0 ~ /^[[:space:]]*(\.|source)[[:space:]]/) {
line = $0
while (match(line, /\/[[:alnum:]_.-]+\.sh/)) {
print "lib" substr(line, RSTART, RLENGTH)
line = substr(line, RSTART + RLENGTH)
}
}
}
' "$1"
}
# derive_path <tree> — print the workflow, bin/ when a bin command sources a
# door library, and the workflow's direct + transitive lib dependencies.
derive_path() {
local tree="$1" workflow
local pending seen=" " lib file refs ref bin_uses_lib=no
workflow="$tree/.github/workflows/release.yml"
printf '%s\n' .github/workflows/release.yml
pending="$(library_refs "$workflow" | sort -u)"
while [ -n "$pending" ]; do
lib="$(printf '%s\n' "$pending" | sed -n '1p')"
pending="$(printf '%s\n' "$pending" | sed '1d')"
case "$seen" in
*" $lib "*) continue ;;
esac
seen="$seen$lib "
printf '%s\n' "$lib"
file="$tree/$lib"
[ -f "$file" ] || continue
refs="$(library_refs "$file" | sort -u)"
if [ -n "$refs" ]; then
pending="$(printf '%s\n%s\n' "$pending" "$refs" | sed '/^$/d' | sort -u)"
fi
done
if [ -d "$tree/bin" ]; then
for file in "$tree"/bin/*; do
[ -f "$file" ] || continue
refs="$(library_refs "$file")"
for ref in $refs; do
case "$seen" in
*" $ref "*) bin_uses_lib=yes ;;
esac
done
done
fi
[ "$bin_uses_lib" = no ] || printf '%s\n' bin/
}
declared_path() {
bash "$1/.github/scripts/release-path.sh"
}
path_check() {
local tree="$1" declared derived missing extra
declared="$(declared_path "$tree" | sort -u)"
derived="$(derive_path "$tree" | sort -u)"
missing="$(comm -13 <(printf '%s\n' "$declared") <(printf '%s\n' "$derived"))"
extra="$(comm -23 <(printf '%s\n' "$declared") <(printf '%s\n' "$derived"))"
if [ -n "$missing" ]; then
printf 'release-path: missing dependency: %s\n' "$missing" >&2
fi
if [ -n "$extra" ]; then
printf 'release-path: stale path: %s\n' "$extra" >&2
fi
[ -z "$missing" ] && [ -z "$extra" ]
}
readme_has_no_path_enumeration() {
local token found=no
for token in bin/ lib/version.sh lib/decide.sh lib/facts.sh lib/changelog.sh; do
if grep -qF "$token" "$ROOT/drills/README.md"; then
printf 'drill doctrine enumerates release path: %s\n' "$token" >&2
found=yes
fi
done
[ "$found" = no ]
}
fixture() {
local name="$1" tree
tree="$TMP/$name"
mkdir -p "$tree/.github/scripts" "$tree/.github/workflows" "$tree/lib" "$tree/bin"
cp "$PATH_SCRIPT" "$tree/.github/scripts/release-path.sh"
printf '#!/usr/bin/env bash\n. "%s"\n' \
"\$ROOT/lib/changelog.sh" >"$tree/bin/assemble"
printf '#!/usr/bin/env bash\n' >"$tree/lib/changelog.sh"
printf '#!/usr/bin/env bash\n' >"$tree/lib/decide.sh"
# facts.sh sources BOTH on this tree: version.sh, and the forge shim #191
# put on the doors' path so a Forgejo consumer can publish (#198). The
# synthetic tree mirrors the real one, or every fixture below reports
# lib/forge.sh stale instead of exercising the case it is about.
printf '#!/usr/bin/env bash\n# shellcheck source=lib/version.sh\n. "%s"\n# shellcheck source=lib/forge.sh\n. "%s"\n' \
"\$(cd \"\$(dirname \"\${BASH_SOURCE[0]}\")\" && pwd)/version.sh" \
"\$(cd \"\$(dirname \"\${BASH_SOURCE[0]}\")\" && pwd)/forge.sh" \
>"$tree/lib/facts.sh"
printf '#!/usr/bin/env bash\n' >"$tree/lib/version.sh"
printf '#!/usr/bin/env bash\n' >"$tree/lib/forge.sh"
printf '%s\n' "$tree"
}
# Exact output is the record author's copy-paste source.
check "manifest prints the specified ordered release path" 0 \
$'.github/workflows/release.yml\nbin/\nlib/version.sh\nlib/decide.sh\nlib/facts.sh\nlib/changelog.sh\nlib/forge.sh' \
bash "$PATH_SCRIPT"
check "real workflow and transitive dependencies match the manifest" 0 "" \
path_check "$ROOT"
check "drill doctrine does not duplicate the executable release path" 0 "" \
readme_has_no_path_enumeration
# A door growing a dependency must name the missing path (#237 D7).
tree="$(fixture missing)"
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\nrun: . "%s"\nrun: . "%s"\n' \
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
"\$CEREMONY_DIR/lib/changelog.sh" "\$CEREMONY_DIR/lib/version.sh" \
"\$CEREMONY_DIR/lib/ruling.sh" \
>"$tree/.github/workflows/release.yml"
printf '#!/usr/bin/env bash\n' >"$tree/lib/ruling.sh"
check "a new workflow library fails with its missing path" 1 \
"missing dependency: lib/ruling.sh" path_check "$tree"
# A library growing a sibling dependency in the production idiom must also
# name the missing path; a literal lib/ marker in a comment is not evidence.
tree="$(fixture missing-transitive)"
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\n' \
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
"\$CEREMONY_DIR/lib/changelog.sh" \
>"$tree/.github/workflows/release.yml"
printf '# shellcheck source=lib/ruling.sh\n. "%s"\n' \
"\$(cd \"\$(dirname \"\${BASH_SOURCE[0]}\")\" && pwd)/ruling.sh" \
>>"$tree/lib/facts.sh"
printf '#!/usr/bin/env bash\n' >"$tree/lib/ruling.sh"
check "a new sibling library fails with its missing path" 1 \
"missing dependency: lib/ruling.sh" path_check "$tree"
# A manifest may not rot into a safe-looking superset.
tree="$(fixture extra)"
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\n' \
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
"\$CEREMONY_DIR/lib/changelog.sh" \
>"$tree/.github/workflows/release.yml"
sed -i 's| lib/forge.sh$| lib/forge.sh \\|' \
"$tree/.github/scripts/release-path.sh"
printf ' lib/ruling.sh\n' >>"$tree/.github/scripts/release-path.sh"
printf '#!/usr/bin/env bash\n' >"$tree/lib/ruling.sh"
check "a path no door reads fails as stale" 1 "stale path: lib/ruling.sh" \
path_check "$tree"
# Transitive sourcing is part of the derivation, not decoration.
tree="$(fixture transitive)"
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\n' \
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
"\$CEREMONY_DIR/lib/changelog.sh" \
>"$tree/.github/workflows/release.yml"
# Only the version source is dropped; the forge source #191 added stays, or
# the fixture reports two stale paths and proves neither of them (#198).
printf '#!/usr/bin/env bash\n# shellcheck source=lib/forge.sh\n. "%s"\n' \
"\$(cd \"\$(dirname \"\${BASH_SOURCE[0]}\")\" && pwd)/forge.sh" \
>"$tree/lib/facts.sh"
check "removing facts' version source fails as a stale path" 1 \
"stale path: lib/version.sh" path_check "$tree"
summary