Merge pull request #269 from andriujoseba/build/237-doors-unchanged
docs: define doors-unchanged drill evidence
This commit is contained in:
commit
1b84d27691
4 changed files with 231 additions and 0 deletions
15
.github/scripts/release-path.sh
vendored
Executable file
15
.github/scripts/release-path.sh
vendored
Executable file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# The release doors' executable path (discussion #217; issue #237). The
|
||||||
|
# 0.5.0 record had to explain why lib/ruling.sh changed without changing a
|
||||||
|
# door. Keep this list executable so a doors-unchanged record measures the
|
||||||
|
# workflow and only the scripts it actually runs, while the test catches a
|
||||||
|
# new or removed dependency before a record can silently omit it.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
printf '%s\n' \
|
||||||
|
.github/workflows/release.yml \
|
||||||
|
bin/ \
|
||||||
|
lib/version.sh \
|
||||||
|
lib/decide.sh \
|
||||||
|
lib/facts.sh \
|
||||||
|
lib/changelog.sh
|
||||||
5
changelog.d/237.md
Normal file
5
changelog.d/237.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Define the doors-unchanged drill record and an executable release-path list,
|
||||||
|
so a release may reuse live evidence only when its door bytes are unchanged
|
||||||
|
since the last rehearsed tag. (#237)
|
||||||
|
|
@ -66,6 +66,46 @@ record is the only thing that survives the drill, and 0.2.0's record shipped
|
||||||
its first draft asserting a cleanup that had not happened (#135) — false
|
its first draft asserting a cleanup that had not happened (#135) — false
|
||||||
evidence in the one file whose job is to be evidence.
|
evidence in the one file whose job is to be evidence.
|
||||||
|
|
||||||
|
A record has one of three shapes. A **rehearsal** records the disposable-repo
|
||||||
|
run above. **Doors unchanged** records the mechanically checked claim below
|
||||||
|
when a new rehearsal would execute the same bytes as the last one. **WAIVED**
|
||||||
|
records a maintainer's judgement under the standing paragraph below. If the
|
||||||
|
doors-unchanged conditions do not all hold, the release owes a rehearsal or a
|
||||||
|
waiver; the narrower shape is never a substitute for either.
|
||||||
|
|
||||||
|
## Doors unchanged
|
||||||
|
|
||||||
|
The builder may assert that no disposable-repo rehearsal is owed only when
|
||||||
|
all three conditions below hold at the candidate head. The release PR's panel
|
||||||
|
verifies the claim like any other evidence, and if any reviewer rules a full
|
||||||
|
drill owed, that verdict wins.
|
||||||
|
|
||||||
|
1. `git diff <last-rehearsed-tag>..HEAD -- <release-path>` contains no change
|
||||||
|
except the `CEREMONY_SELF_REF` pin line in
|
||||||
|
`.github/workflows/release.yml`.
|
||||||
|
2. The release path is exactly the output of
|
||||||
|
`.github/scripts/release-path.sh`: `.github/workflows/release.yml`, `bin/`,
|
||||||
|
`lib/version.sh`, `lib/decide.sh`, `lib/facts.sh`, and
|
||||||
|
`lib/changelog.sh`. The script is the record author's copy-paste source;
|
||||||
|
its contract test keeps this inline list and the workflow's direct and
|
||||||
|
transitive dependencies in agreement.
|
||||||
|
3. The last rehearsed tag's own record is a full rehearsal, its release is
|
||||||
|
published, and `main` was re-armed to `-dev` after it.
|
||||||
|
|
||||||
|
The baseline is the last **rehearsed** tag, never merely the previous tag. A
|
||||||
|
previous-tag baseline could chain one doors-unchanged assertion from another
|
||||||
|
while the doors drift a small diff at a time; the last-rehearsed anchor makes
|
||||||
|
any accumulated release-path change force a new rehearsal.
|
||||||
|
|
||||||
|
The record carries all three measurements as observed at its candidate head,
|
||||||
|
never copied from an earlier record. `drills/0.4.1.md` and
|
||||||
|
`drills/0.5.0.md` are the worked examples; the latter's amendment from a
|
||||||
|
predicted empty `lib/` diff to the observed `lib/ruling.sh` delta is why each
|
||||||
|
candidate is measured afresh (#233). Re-running its stricter baseline now is
|
||||||
|
also the path-enumeration proof: `git diff 0.4.0 0.5.0 -- <release-path>` is
|
||||||
|
only the `CEREMONY_SELF_REF` pin, while adding `lib/ruling.sh` makes the diff
|
||||||
|
non-empty even though neither release door reads that file (#217, #237).
|
||||||
|
|
||||||
`actions/drill-recorded` refuses any bare-version tree whose record is
|
`actions/drill-recorded` refuses any bare-version tree whose record is
|
||||||
missing or blank. A waived drill is still a record: the file says WAIVED and
|
missing or blank. A waived drill is still a record: the file says WAIVED and
|
||||||
why — a maintainer's call, visible and reviewable in the release PR's diff,
|
why — a maintainer's call, visible and reviewable in the release PR's diff,
|
||||||
|
|
|
||||||
171
test/release-path.test.sh
Executable file
171
test/release-path.test.sh
Executable file
|
|
@ -0,0 +1,171 @@
|
||||||
|
#!/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" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
printf '#!/usr/bin/env bash\n# shellcheck source=lib/version.sh\n. "%s"\n' \
|
||||||
|
"\$(cd \"\$(dirname \"\${BASH_SOURCE[0]}\")\" && pwd)/version.sh" \
|
||||||
|
>"$tree/lib/facts.sh"
|
||||||
|
printf '#!/usr/bin/env bash\n' >"$tree/lib/version.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' \
|
||||||
|
bash "$PATH_SCRIPT"
|
||||||
|
check "real workflow and transitive dependencies match the manifest" 0 "" \
|
||||||
|
path_check "$ROOT"
|
||||||
|
|
||||||
|
# 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/changelog.sh$| lib/changelog.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"
|
||||||
|
: >"$tree/lib/facts.sh"
|
||||||
|
check "removing facts' version source fails as a stale path" 1 \
|
||||||
|
"stale path: lib/version.sh" path_check "$tree"
|
||||||
|
|
||||||
|
summary
|
||||||
Loading…
Reference in a new issue