docs/UPSTREAM-SYNC.md + .upstream-ref + the delta-inventory guard — the recurring sync, written from having just done one (#200) #208

Merged
andres merged 7 commits from build/200-upstream-sync-doc into main 2026-08-05 15:50:35 +00:00
2 changed files with 50 additions and 20 deletions
Showing only changes of commit a48cc719a4 - Show all commits

View file

@ -32,6 +32,10 @@
so a composite `action.yml` or a `.yaml` workflow is seen without anyone so a composite `action.yml` or a `.yaml` workflow is seen without anyone
remembering to add a glob (#200). remembering to add a glob (#200).
- Discovery is git's, not the filesystem's: `ls-files`, so the tarballs `ci.yml`
extracts into the checkout and any developer cache are not parsed as source
(#200).
- It refuses when the recorded commit is missing, absent from the object store, - It refuses when the recorded commit is missing, absent from the object store,
or not an ancestor — three distinct refusals, none of them a skip. `ci.yml` or not an ancestor — three distinct refusals, none of them a skip. `ci.yml`
fetches that exact object so the test reads local evidence without CI fetches that exact object so the test reads local evidence without CI

View file

@ -122,7 +122,6 @@ scan_root() { printf '%s' "${SCAN_ROOT:-$ROOT}"; }
# rest. Excluding is safer than including because a new file type arrives # rest. Excluding is safer than including because a new file type arrives
# scanned rather than invisible. # scanned rather than invisible.
# #
# .git/ not source
# test/ the harness stubs and asserts these tokens by design # test/ the harness stubs and asserts these tokens by design
# changelog.d/ prose fragments # changelog.d/ prose fragments
# *.md prose. `drills/` stays in the INVENTORY because its records # *.md prose. `drills/` stays in the INVENTORY because its records
@ -130,14 +129,21 @@ scan_root() { printf '%s' "${SCAN_ROOT:-$ROOT}"; }
# selector verb in prose is not a decision, and scanning prose # selector verb in prose is not a decision, and scanning prose
# for decisions is the mistake this guard's own comment # for decisions is the mistake this guard's own comment
# handling exists to avoid. # handling exists to avoid.
#
# DISCOVERY IS GIT'S, NOT THE FILESYSTEM'S. An earlier head said "tracked" and
# used `find`, which walks the working directory and knows nothing about the
# index. That is not pedantry: `ci.yml` extracts shellcheck and actionlint
# tarballs INTO the checkout before the suite runs, and any developer cache
# sits there too. @codex-reviewer-andresmgsl reproduced a false red with one
# untracked file. `git ls-files -z` makes "tracked" executable rather than
# prose.
scanned_paths() { scanned_paths() {
local root; root="$(scan_root)" local root; root="$(scan_root)"
find "$root" -type f \ git -C "$root" ls-files -z 2>/dev/null \
-not -path "$root/.git/*" \ | tr '\0' '\n' \
-not -path "$root/test/*" \ | grep -vE '^(test/|changelog\.d/)' \
-not -path "$root/changelog.d/*" \ | grep -vE '\.md$' \
-not -name '*.md' \ | sort
2>/dev/null | sed "s|^$root/||" | sort
} }
forge_specific_files() { forge_specific_files() {
@ -203,15 +209,20 @@ check "every forge-deciding file is named in the inventory" 0 "" no_unlisted
TMP="$(mktemp -d)" TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT trap 'rm -rf "$TMP"' EXIT
fixture_tree() { # -> a tree the scan walks, with no forge-deciding files # A tiny GIT repository, because discovery is git's now: a fixture that is only
# a directory would be invisible to `ls-files` and every must-fail below would
# pass vacuously.
fixture_tree() { # -> a tracked tree with no forge-deciding files
local t="$TMP/tree" local t="$TMP/tree"
rm -rf "$t" rm -rf "$t"
mkdir -p "$t/lib" "$t/actions/x" "$t/bin" "$t/.github/scripts" \ mkdir -p "$t/lib" "$t/.github/workflows"
"$t/.github/workflows" "$t/drills"
printf '#!/usr/bin/env bash\necho hello\n' >"$t/lib/plain.sh" printf '#!/usr/bin/env bash\necho hello\n' >"$t/lib/plain.sh"
printf 'name: ci\non: [push]\n' >"$t/.github/workflows/plain.yml" printf 'name: ci\non: [push]\n' >"$t/.github/workflows/plain.yml"
git -C "$t" init -q 2>/dev/null
git -C "$t" add -A 2>/dev/null
printf '%s' "$t" printf '%s' "$t"
} }
track() { git -C "$root" add -A 2>/dev/null; }
root="$(fixture_tree)" root="$(fixture_tree)"
clean_tree_passes() { SCAN_ROOT="$root" no_unlisted; } clean_tree_passes() { SCAN_ROOT="$root" no_unlisted; }
@ -221,10 +232,10 @@ check "the guard is green on a tree with no forge decisions" 0 "" clean_tree_pas
# shellcheck disable=SC2016 # fixture CONTENT: the literal text a scanned file would hold # shellcheck disable=SC2016 # fixture CONTENT: the literal text a scanned file would hold
printf '%s\n' '#!/usr/bin/env bash' 'case "$(forge_detect)" in forgejo) : ;; esac' \ printf '%s\n' '#!/usr/bin/env bash' 'case "$(forge_detect)" in forgejo) : ;; esac' \
>"$root/lib/scattered.sh" >"$root/lib/scattered.sh"
scattered_shell() { SCAN_ROOT="$root" no_unlisted; } track; scattered_shell() { SCAN_ROOT="$root" no_unlisted; }
check "a new SHELL file deciding the forge fails the real guard" 1 "" scattered_shell check "a new SHELL file deciding the forge fails the real guard" 1 "" scattered_shell
check "...and the failure names the file" 1 "lib/scattered.sh" scattered_shell check "...and the failure names the file" 1 "lib/scattered.sh" scattered_shell
rm -f "$root/lib/scattered.sh" rm -f "$root/lib/scattered.sh"; track
# MUST FAIL on a NON-SHELL surface too, so coverage cannot regress to the old # MUST FAIL on a NON-SHELL surface too, so coverage cannot regress to the old
# shell-only glob: a workflow deciding on the server URL. # shell-only glob: a workflow deciding on the server URL.
@ -232,10 +243,10 @@ rm -f "$root/lib/scattered.sh"
printf '%s\n' 'name: x' 'on: [push]' 'jobs:' ' j:' ' steps:' \ printf '%s\n' 'name: x' 'on: [push]' 'jobs:' ' j:' ' steps:' \
' - run: [ "$GITHUB_SERVER_URL" = https://github.com ] || exit 0' \ ' - run: [ "$GITHUB_SERVER_URL" = https://github.com ] || exit 0' \
>"$root/.github/workflows/scattered.yml" >"$root/.github/workflows/scattered.yml"
scattered_workflow() { SCAN_ROOT="$root" no_unlisted; } track; scattered_workflow() { SCAN_ROOT="$root" no_unlisted; }
check "a new WORKFLOW deciding the forge fails it too" 1 "" scattered_workflow check "a new WORKFLOW deciding the forge fails it too" 1 "" scattered_workflow
check "...naming that file" 1 ".github/workflows/scattered.yml" scattered_workflow check "...naming that file" 1 ".github/workflows/scattered.yml" scattered_workflow
rm -f "$root/.github/workflows/scattered.yml" rm -f "$root/.github/workflows/scattered.yml"; track
# @codex-reviewer-andresmgsl's two reproductions, verbatim as fixtures. Both # @codex-reviewer-andresmgsl's two reproductions, verbatim as fixtures. Both
# passed the hand-picked-glob version 21/21, which is why discovery is derived # passed the hand-picked-glob version 21/21, which is why discovery is derived
@ -244,10 +255,10 @@ mkdir -p "$root/actions/unlisted-forge-decision"
printf '%s\n' 'name: x' 'runs:' ' using: composite' ' steps:' \ printf '%s\n' 'name: x' 'runs:' ' using: composite' ' steps:' \
' - shell: bash' ' env:' ' CEREMONY_FORGE_CLIENT: gh' \ ' - shell: bash' ' env:' ' CEREMONY_FORGE_CLIENT: gh' \
' run: true' >"$root/actions/unlisted-forge-decision/action.yml" ' run: true' >"$root/actions/unlisted-forge-decision/action.yml"
composite_action_seen() { SCAN_ROOT="$root" no_unlisted; } track; composite_action_seen() { SCAN_ROOT="$root" no_unlisted; }
check "a forge declaration in actions/*/action.yml fails the guard" 1 \ check "a forge declaration in actions/*/action.yml fails the guard" 1 \
"actions/unlisted-forge-decision/action.yml" composite_action_seen "actions/unlisted-forge-decision/action.yml" composite_action_seen
rm -rf "$root/actions/unlisted-forge-decision" rm -rf "$root/actions/unlisted-forge-decision"; track
# ...and a workflow written .yaml rather than .yml — `*.yml` was never a # ...and a workflow written .yaml rather than .yml — `*.yml` was never a
# complete workflow surface. # complete workflow surface.
@ -255,22 +266,37 @@ rm -rf "$root/actions/unlisted-forge-decision"
printf '%s\n' 'name: x' 'on: [push]' 'jobs:' ' j:' \ printf '%s\n' 'name: x' 'on: [push]' 'jobs:' ' j:' \
" if: github.server_url == 'https://github.com'" ' steps: []' \ " if: github.server_url == 'https://github.com'" ' steps: []' \
>"$root/.github/workflows/unlisted-forge-decision.yaml" >"$root/.github/workflows/unlisted-forge-decision.yaml"
yaml_workflow_seen() { SCAN_ROOT="$root" no_unlisted; } track; yaml_workflow_seen() { SCAN_ROOT="$root" no_unlisted; }
check "...and one in a .yaml workflow does too" 1 \ check "...and one in a .yaml workflow does too" 1 \
".github/workflows/unlisted-forge-decision.yaml" yaml_workflow_seen ".github/workflows/unlisted-forge-decision.yaml" yaml_workflow_seen
rm -f "$root/.github/workflows/unlisted-forge-decision.yaml" rm -f "$root/.github/workflows/unlisted-forge-decision.yaml"; track
check "...leaving the fixture tree green again" 0 "" clean_tree_passes check "...leaving the fixture tree green again" 0 "" clean_tree_passes
# A declaration is a delta location even in a file that would otherwise read as # A declaration is a delta location even in a file that would otherwise read as
# a shim consumer, so the consumer allow-list cannot hide one. # a shim consumer, so the consumer allow-list cannot hide one.
mkdir -p "$root/lib" mkdir -p "$root/lib"
printf '%s\n' '#!/usr/bin/env bash' 'CEREMONY_FORGE_CLIENT=gh' >"$root/lib/facts.sh" printf '%s\n' '#!/usr/bin/env bash' 'CEREMONY_FORGE_CLIENT=gh' >"$root/lib/facts.sh"
declared_not_exempt() { SCAN_ROOT="$root" no_unlisted; } track; declared_not_exempt() { SCAN_ROOT="$root" no_unlisted; }
check "a shim consumer that DECLARES a client is not exempt" 1 "lib/facts.sh" \ check "a shim consumer that DECLARES a client is not exempt" 1 "lib/facts.sh" \
declared_not_exempt declared_not_exempt
rm -f "$root/lib/facts.sh" rm -f "$root/lib/facts.sh"; track
track
check "...and the tree is green again once it is gone" 0 "" clean_tree_passes check "...and the tree is green again once it is gone" 0 "" clean_tree_passes
# UNTRACKED input is not source. ci.yml extracts shellcheck and actionlint
# tarballs into the checkout before the suite runs, and a developer cache sits
# there too; parsing either is a false red on something outside the repository
# property (@codex-reviewer-andresmgsl, #200 review — reproduced with one file).
printf 'CEREMONY_FORGE_CLIENT=gh\n' >"$root/local-tool-cache.txt"
check "an UNTRACKED marker-bearing file is ignored" 0 "" clean_tree_passes
check "...and is still ignored once it carries a decision" 0 "" clean_tree_passes
track
check "...but the moment it is TRACKED the guard sees it" 1 "local-tool-cache.txt" \
clean_tree_passes
git -C "$root" rm -q --cached local-tool-cache.txt 2>/dev/null
rm -f "$root/local-tool-cache.txt"
check "...and removing it restores green" 0 "" clean_tree_passes
# Path matching, both boundaries. # Path matching, both boundaries.
check "a directory entry does not match a sibling with the same prefix" 1 "" \ check "a directory entry does not match a sibling with the same prefix" 1 "" \
in_inventory drills-old/0.4.1.md in_inventory drills-old/0.4.1.md