PR Gates / CI image pin matches docker/ci (pull_request) Successful in 10s
PR Gates / Docs update reminder (pull_request) Successful in 14s
PR Gates / decisions lifecycle (pull_request) Successful in 17s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m22s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m23s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m34s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m55s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
A session was handed docs/handoffs/chicorytv-issue-queue.md pasted out of /Users/timothy/ersatztv while that tree was 81 commits behind, so it still described the queue protocol #520 retired the day before (read tracker command was ever run against that tree, so every existing "never read its HEAD" guard was irrelevant: a stale checkout serves stale FILES, and docs are what a kickoff depends on. Nothing broke only because selection went through scripts/select-queue.sh. The lore bullet on that tree already prescribed the shape of the fix for its earlier failure modes — "a design flaw, not a discipline failure; a check does not stay true" — so this removes the stale condition instead of adding another check. scripts/refresh-shared-checkout.sh fast-forwards the tree to origin/main and reinstalls web/node_modules when the lockfile moved. It is deliberately timid: it refuses and changes nothing when the tree is not on main, is dirty, is ahead, or is mid-rebase/merge, and it never switches branches, stashes or discards. A NO-OP is a normal outcome. Uses npm ci rather than npm install — the first version used install, which rewrote package-lock.json and left the tree dirty, i.e. the exact state the next run refuses on, so it would have disabled itself after one use. Asserts the tree is clean at exit. refs #541
111 lines
5.6 KiB
Bash
Executable File
111 lines
5.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# H13 (ersatztv#541) — session-end: leave the SHARED checkout at /Users/timothy/ersatztv fresh, so
|
|
# the next session's kickoff paste is current.
|
|
#
|
|
# Why this is a script and not a rule. The standing lore already says the shared tree is not a `main`
|
|
# mirror and its HEAD must never be read as truth — but on 2026-07-21 the trap arrived through a
|
|
# route that rule doesn't cover: a session pasted `docs/handoffs/chicorytv-issue-queue.md` FROM that
|
|
# tree, which was 81 commits behind, and the file still described a queue protocol retired the day
|
|
# before (read tracker #237 — since closed, and now explicitly "do not read for queue state"). No
|
|
# `git log` was consulted, so no discipline check would have fired. A stale checkout silently
|
|
# serves stale FILES, and the only durable fix is to stop it being stale.
|
|
#
|
|
# What it does: fast-forwards the shared tree to origin/main, and reinstalls web/node_modules when
|
|
# the lockfile moved (worktrees copy that directory; a stale copy has broken typecheck before).
|
|
#
|
|
# What it will NOT do — it is deliberately timid, because the tree is shared and another session may
|
|
# be mid-flight in it. It refuses (loudly, exit 0) and leaves the WORKING TREE untouched when it is
|
|
# not on `main`, is dirty, has local commits, or is mid-rebase/merge. It never switches branches,
|
|
# never stashes, never discards. (A refusal reached after the fetch will have advanced
|
|
# refs/remotes/origin/main — remote-tracking refs only, never your files.) A refusal is a normal
|
|
# outcome, not a failure.
|
|
#
|
|
# Run at session end (kickoff session-end protocol). Safe to run at session start too — it is
|
|
# idempotent. Fail-OPEN: a missing/unreachable tree prints a notice and exits 0.
|
|
#
|
|
# ETV_SHARED_CHECKOUT overrides the target directory.
|
|
set -uo pipefail
|
|
|
|
shared="${ETV_SHARED_CHECKOUT:-/Users/timothy/ersatztv}"
|
|
|
|
skip() {
|
|
echo "refresh-shared-checkout: NO-OP — $1"
|
|
echo " (nothing was changed)"
|
|
exit 0
|
|
}
|
|
|
|
[ -d "$shared/.git" ] || skip "no git checkout at $shared"
|
|
|
|
cd "$shared" || skip "cannot cd to $shared"
|
|
|
|
# $shared/.git existing is NOT proof $shared is the repo root: if it is an invalid/emptied .git dir
|
|
# and some ancestor IS a repo, git walks UP and every command below would silently operate on that
|
|
# ancestor instead — the worst possible outcome for a script that fast-forwards and reinstalls.
|
|
toplevel="$(git rev-parse --show-toplevel 2>/dev/null)" || skip "not a git repo: $shared"
|
|
[ "$toplevel" = "$(pwd -P)" ] || skip "$shared is not a repo root (git resolves it to $toplevel)"
|
|
|
|
# mid-operation? never interfere
|
|
git_dir="$(git rev-parse --git-dir 2>/dev/null)" || skip "not a git repo: $shared"
|
|
# NOTE: keep the `|| true` — without it this loop's `&&` tail would abort the script if anyone
|
|
# ever adds `set -e` (a non-match returns 1 as the body's last status).
|
|
for state in rebase-merge rebase-apply MERGE_HEAD CHERRY_PICK_HEAD BISECT_LOG REVERT_HEAD; do
|
|
{ [ -e "$git_dir/$state" ] && skip "a $state is in progress"; } || true
|
|
done
|
|
|
|
branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
|
|
[ "$branch" = "main" ] || skip "on branch '$branch', not main — that is another session's work"
|
|
|
|
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
skip "working tree is dirty (uncommitted changes)"
|
|
fi
|
|
|
|
git fetch origin main --quiet || skip "could not fetch origin/main"
|
|
|
|
ahead="$(git rev-list --count origin/main..HEAD 2>/dev/null)" || skip "cannot compare against origin/main"
|
|
behind="$(git rev-list --count HEAD..origin/main 2>/dev/null)" || skip "cannot compare against origin/main"
|
|
|
|
if [ "$ahead" != "0" ]; then
|
|
skip "$ahead local commit(s) not on origin/main — someone committed here; resolve by hand"
|
|
fi
|
|
|
|
if [ "$behind" = "0" ]; then
|
|
echo "refresh-shared-checkout: already current with origin/main ($(git rev-parse --short HEAD))."
|
|
exit 0
|
|
fi
|
|
|
|
lock_before=""
|
|
[ -f web/package-lock.json ] && lock_before="$(git hash-object web/package-lock.json 2>/dev/null)"
|
|
|
|
if ! git merge --ff-only origin/main --quiet; then
|
|
skip "fast-forward to origin/main failed"
|
|
fi
|
|
|
|
echo "refresh-shared-checkout: fast-forwarded $behind commit(s) to $(git rev-parse --short HEAD)."
|
|
|
|
lock_after=""
|
|
[ -f web/package-lock.json ] && lock_after="$(git hash-object web/package-lock.json 2>/dev/null)"
|
|
|
|
if [ -n "$lock_after" ] && [ "$lock_before" != "$lock_after" ]; then
|
|
echo "refresh-shared-checkout: web/package-lock.json changed — refreshing node_modules..."
|
|
# `npm ci`, not `npm install`: install can REWRITE the lockfile, which would leave this shared
|
|
# tree dirty — and a dirty tree is exactly what the next run (and every worktree-hygiene check)
|
|
# refuses on. ci installs strictly from the lockfile and never writes it.
|
|
# Accepted trade-off: `ci` DELETES node_modules first, so a session copying it at that instant gets
|
|
# a partial copy. Bounded and rare — everything removed is regenerable from the lockfile, no human
|
|
# work can be lost, and this only runs on a merge that actually moved the lockfile.
|
|
if (cd web && npm ci --no-audit --no-fund >/dev/null 2>&1); then
|
|
echo "refresh-shared-checkout: node_modules refreshed."
|
|
else
|
|
echo "refresh-shared-checkout: WARNING — npm ci failed; worktrees copying web/node_modules"
|
|
echo " from here may fail typecheck. Run 'npm ci' in $shared/web by hand."
|
|
fi
|
|
fi
|
|
|
|
# belt and braces: leaving the shared tree dirty is the one outcome that would make this script a
|
|
# net negative, so assert it and say so loudly rather than exiting quietly on a bad state
|
|
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
echo "refresh-shared-checkout: WARNING — the shared tree is DIRTY after refresh:"
|
|
git status --short | sed 's/^/ /'
|
|
echo " Nothing was discarded. Clean it by hand before the next session pastes files from here."
|
|
fi
|