Files
gastownhall__beads/scripts/check-cli-docs-drift.sh
Chris Sells d2f87d50de docs(pipeline): pin the CLI docs to the released bd; repair main's broken CLI reference (#4876)
* docs(pipeline): pin the generated CLI docs to the released bd; repair main's CLI reference

The #4874 squash-merge raced the docs-autofix bot: the bot had pushed a
regeneration from main's source onto the PR branch, so the squash landed a
contradictory mix — the release-aligned hand-written docs plus a CLI
reference re-documenting unreleased commands and flags, with docs.json
referencing four pages (heartbeat, migrate-personal, reclaim, unclaim)
whose files do not exist. That broken nav fails the docsync guard on main
and on every branch cut from it. This restores the intended end state of
#4874 and lands the pin that prevents the race from recurring.

- Regenerate the CLI reference from a bd built from the v1.1.0 tag (CI's
  canonical pure-Go build), removing the unreleased commands/flags and
  fixing the dangling nav entries.
- docs/cli-docs.pin names the release tag; scripts/resolve-docs-bd.sh
  builds and caches a canonical pure-Go bd from that tag, fetching the tag
  first in shallow CI checkouts.
- generate-cli-docs.sh, check-doc-flags.sh, and check-cli-docs-drift.sh
  resolve bd through the pin and ignore a supplied binary while pinned
  (BD_DOCS_IGNORE_PIN=1 bypasses), so the drift gates and the autofix bot
  now enforce docs-match-release instead of docs-match-main.
- tools/docsmint detects the pre-overhaul Docusaurus staging layout that
  bd <= v1.1.0 emits and converts it to the generic form before its normal
  Mintlify transform (tested); removable once the pin reaches a release
  whose emitter writes build/cli-docs.
- check-doc-flags.sh: run without timeout(1) on macOS instead of failing
  Check 4.
- Decision record: engdocs/decisions/2026-07-17-docs-release-pin.md; the
  beads-docs skill §8 updated to match.

* lint: annotate gosec G705 false positive in the docsmint legacy bridge

Same class as the existing nav-splice annotation: a developer tool writing
repo Markdown, not a web-output sink.
2026-07-17 18:03:28 -07:00

233 lines
8.2 KiB
Bash
Executable File

#!/bin/bash
# check-cli-docs-drift.sh — Blame-scoped freshness check for generated CLI docs.
#
# Strict probe first: regenerate the generated doc artifacts and diff against
# the committed copies, exactly like generate-cli-docs.sh --check. When the
# probe fails AND a diff base is known (a PR), the drift is attributed before
# failing anyone:
#
# * regenerate the same artifacts at HEAD and at the merge-base, each in a
# scratch worktree with the canonical pinned build
# (CGO_ENABLED=0 go build -tags gms_pure_go);
# * if the change neither alters the regenerated CLI surface nor touches the
# generated files, the drift was inherited from the base branch: warn and
# pass — contributors only own docs for the code they actually touched;
# * otherwise fail, and write the exact regeneration diff as a patch
# (--patch-out / $DOC_DRIFT_PATCH_OUT) so the fix never depends on the
# contributor's local build environment.
#
# Without a diff base (push to main, releases, plain local runs) the strict
# probe result stands, preserving the historical behavior.
#
# Usage: check-cli-docs-drift.sh [--base <ref>] [--patch-out <file>] [bd-binary]
# --base diff base ref (default: $BD_DOCS_DIFF_BASE, then
# origin/$GITHUB_BASE_REF when set, as in GitHub PR CI)
# --patch-out where to write the regeneration patch on attributable drift
# (default: $DOC_DRIFT_PATCH_OUT when set)
#
# Attribution compares committed states; commit local changes before relying
# on it outside CI.
set -euo pipefail
export LC_ALL=C
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
BASE_REF=""
PATCH_OUT="${DOC_DRIFT_PATCH_OUT:-}"
BD_ARG=""
while [ "$#" -gt 0 ]; do
case "$1" in
--base)
[ "$#" -ge 2 ] || { echo "Error: --base needs a ref" >&2; exit 2; }
BASE_REF="$2"
shift 2
;;
--patch-out)
[ "$#" -ge 2 ] || { echo "Error: --patch-out needs a path" >&2; exit 2; }
PATCH_OUT="$2"
shift 2
;;
-h|--help)
sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
BD_ARG="$1"
shift
;;
esac
done
if [ -z "$BASE_REF" ]; then
BASE_REF="${BD_DOCS_DIFF_BASE:-}"
fi
if [ -z "$BASE_REF" ] && [ -n "${GITHUB_BASE_REF:-}" ]; then
BASE_REF="origin/${GITHUB_BASE_REF}"
fi
# The complete set of generated doc artifacts, as git pathspecs.
GEN_PATHSPECS=(
"docs/CLI_REFERENCE.md"
"docs/cli-reference"
"docs/docs.json"
)
print_fix_help() {
echo ""
echo "To fix in any environment, regenerate with the pipeline's canonical bd"
echo "(built from the release pin in docs/cli-docs.pin when set, else from"
echo "this checkout, CGO_ENABLED=0 -tags gms_pure_go):"
echo " ./scripts/generate-cli-docs.sh"
}
# Check out a commit into a scratch worktree and regenerate every generated
# doc artifact there with the canonical build: the release pin when that
# commit has one, else a pure-Go build of the commit's own source.
regen_worktree() {
local sha="$1" dir="$2"
git -C "$PROJECT_ROOT" worktree add --detach --quiet "$dir" "$sha"
(
cd "$dir"
if [ -x scripts/resolve-docs-bd.sh ] && [ -n "$(scripts/resolve-docs-bd.sh)" ]; then
./scripts/generate-cli-docs.sh >/dev/null
else
CGO_ENABLED=0 go build -tags gms_pure_go -o "$dir/.docs-bd" ./cmd/bd/
./scripts/generate-cli-docs.sh "$dir/.docs-bd" >/dev/null
rm -f "$dir/.docs-bd"
fi
)
}
# Stable content hash of the regenerated artifacts in a worktree, so two
# regenerations can be compared without caring about temp paths.
surface_fingerprint() {
local dir="$1"
(
cd "$dir"
local f d
for f in docs/CLI_REFERENCE.md docs/docs.json; do
if [ -f "$f" ]; then
printf '== %s\n' "$f"
cat "$f"
fi
done
for d in docs/cli-reference; do
if [ -d "$d" ]; then
find "$d" -type f -name '*.md' | sort | while IFS= read -r f; do
printf '== %s\n' "$f"
cat "$f"
done
fi
done
) | sha256sum | awk '{print $1}'
}
# --- Strict probe ------------------------------------------------------------
if "$PROJECT_ROOT/scripts/generate-cli-docs.sh" --check ${BD_ARG:+"$BD_ARG"}; then
exit 0
fi
echo ""
echo "Strict CLI docs freshness probe failed; attributing the drift..."
strict_stands() {
echo "$1"
echo "FAIL: generated CLI docs are out of sync (strict result stands)."
print_fix_help
exit 1
}
if [ -z "$BASE_REF" ]; then
strict_stands "No diff base available (not a PR build)."
fi
if ! command -v go >/dev/null 2>&1; then
strict_stands "Go toolchain unavailable; cannot regenerate for attribution."
fi
if ! git -C "$PROJECT_ROOT" rev-parse --verify --quiet "$BASE_REF" >/dev/null; then
strict_stands "Diff base $BASE_REF is not available locally (shallow clone? use fetch-depth: 0)."
fi
HEAD_SHA="$(git -C "$PROJECT_ROOT" rev-parse HEAD)"
if ! MERGE_BASE="$(git -C "$PROJECT_ROOT" merge-base "$HEAD_SHA" "$BASE_REF")"; then
strict_stands "Cannot compute merge-base with $BASE_REF (shallow clone? use fetch-depth: 0)."
fi
if [ -n "$(git -C "$PROJECT_ROOT" status --porcelain)" ]; then
echo "Note: working tree is dirty; attribution uses committed state only."
fi
SCRATCH="$(mktemp -d)"
W_HEAD="$SCRATCH/head"
W_BASE="$SCRATCH/base"
cleanup() {
git -C "$PROJECT_ROOT" worktree remove --force "$W_HEAD" >/dev/null 2>&1 || true
git -C "$PROJECT_ROOT" worktree remove --force "$W_BASE" >/dev/null 2>&1 || true
rm -rf "$SCRATCH"
}
trap cleanup EXIT
echo "Regenerating docs at HEAD (${HEAD_SHA:0:12}) with the canonical pinned build..."
if ! regen_worktree "$HEAD_SHA" "$W_HEAD"; then
strict_stands "Regeneration at HEAD failed."
fi
if git -C "$W_HEAD" diff --quiet; then
echo "PASS: committed docs are fresh under the canonical pinned build."
echo "(The strict probe failed for a reason that does not affect committed state:"
echo " either the supplied bd binary differs from the canonical pinned build"
echo " [CGO_ENABLED=0 go build -tags gms_pure_go], or uncommitted local edits"
echo " to generated docs made the working tree diverge. Attribution compares"
echo " committed states only - commit local changes before relying on it.)"
exit 0
fi
echo "Regenerating docs at merge-base (${MERGE_BASE:0:12})..."
if ! regen_worktree "$MERGE_BASE" "$W_BASE"; then
strict_stands "Regeneration at the merge-base failed."
fi
PR_TOUCHED="$(git -C "$PROJECT_ROOT" diff --name-only "$MERGE_BASE" "$HEAD_SHA" -- "${GEN_PATHSPECS[@]}")"
FP_HEAD="$(surface_fingerprint "$W_HEAD")"
FP_BASE="$(surface_fingerprint "$W_BASE")"
if [ -z "$PR_TOUCHED" ] && [ "$FP_HEAD" = "$FP_BASE" ]; then
echo ""
echo "WARN: generated docs are stale, but the drift is inherited from the base branch:"
echo "this change neither alters the regenerated CLI surface nor touches generated files."
git -C "$W_HEAD" diff --stat | sed 's/^/ /'
echo "Not failing this PR. The base branch needs a docs regeneration:"
echo " ./scripts/generate-cli-docs.sh"
if [ -n "${GITHUB_ACTIONS:-}" ]; then
echo "::warning::Generated CLI docs are stale on the base branch (inherited drift, not introduced by this PR)."
fi
echo "PASS: no doc drift attributable to this change"
exit 0
fi
echo ""
echo "FAIL: this change affects the generated CLI docs and the committed copies are stale."
if [ "$FP_HEAD" != "$FP_BASE" ]; then
echo " - the regenerated CLI surface differs from the merge-base"
fi
if [ -n "$PR_TOUCHED" ]; then
echo " - generated doc files were modified by this change:"
echo "$PR_TOUCHED" | sed 's/^/ /'
fi
echo ""
git -C "$W_HEAD" diff --stat | sed 's/^/ /'
if [ -n "$PATCH_OUT" ]; then
git -C "$W_HEAD" diff > "$PATCH_OUT"
echo ""
echo "The exact fix (regenerated with CI's canonical build) was written to:"
echo " $PATCH_OUT"
echo "In CI it is uploaded as the 'cli-docs-freshness-patch' artifact;"
echo "download it and run: git apply cli-docs-freshness.patch"
fi
print_fix_help
exit 1