Skip to content

Commit 3393717

Browse files
dovvnloadingclaude
andauthored
Catch the staleness the bundle guard could not see (#391)
The Stop-hook guard rebuilt web_ui/dist/app whenever the bundle was older than web_ui/src. That solves one staleness problem and is silent, by construction, about a second one. If the CHECKOUT is behind the branch work lands on, merged code is not in the working tree at all - so the bundle is perfectly consistent with the source, the mtime walk finds nothing newer, and the guard correctly says nothing while the running app is missing features that already shipped. A canvas went on spawning placeholder nodes on double-click for hours after the commit removing that gesture had merged, because the checkout sat five commits behind origin/main and every local file legitimately matched the bundle built from it. Two additions: - The bundle records the commit it was built from (dist/app/.built-from-commit) and is rebuilt when HEAD no longer matches it. A pull or branch switch that happens to leave mtimes alone is invisible to an mtime walk; a recorded HEAD is not. - When HEAD is behind origin/main the hook says so, naming the branch and the commit count, whether or not it also rebuilt. Reported, never auto-merged - the working tree belongs to the developer, and a Stop hook has no business rewriting it. It reads only already-fetched refs, so it never blocks on the network. Co-authored-by: Claude <noreply@anthropic.com>
1 parent 22f0345 commit 3393717

1 file changed

Lines changed: 80 additions & 18 deletions

File tree

‎tools/rebuild-spa-if-stale.sh‎

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env bash
2-
# Rebuild the SPA bundle whenever it is older than the source it is built
3-
# from.
2+
# Keep web_ui/dist/app honest: rebuild it whenever it no longer reflects the
3+
# checkout, and say so loudly when the CHECKOUT itself is behind.
44
#
55
# WHY THIS EXISTS. The app does not run the source tree. Both the desktop
66
# shell (graphlink_desktop.py) and the FastAPI backend (backend/app.py,
@@ -12,29 +12,49 @@
1212
# work looked like it had never landed.
1313
#
1414
# dist/ is gitignored, so CI cannot catch this - it is a local-state problem
15-
# and needs a local guard. This runs from a Stop hook (.claude/settings.json)
16-
# and rebuilds only when the bundle is actually behind, so a turn that
17-
# touched no frontend source costs one `find`.
15+
# and needs a local guard. This runs from a Stop hook (.claude/settings.json).
1816
#
19-
# It deliberately does NOT gate on git state. The bundle's job is to reflect
20-
# the working tree, which is what the developer is looking at.
17+
# TWO DIFFERENT STALENESS PROBLEMS, and the first version only solved one:
18+
#
19+
# 1. The bundle is older than the source it is built from. Caught by the
20+
# mtime walk below, and rebuilt.
21+
#
22+
# 2. The CHECKOUT is behind the branch the work is landing on. Merged work
23+
# is not in this working tree at all, so the bundle can be perfectly
24+
# consistent with the source and the running app still misses features
25+
# that shipped. The mtime walk is silent here BY CONSTRUCTION - there is
26+
# nothing newer to find - which is exactly how a canvas went on spawning
27+
# placeholder nodes after the commit removing them had already merged.
28+
# Reported below, never auto-merged: the working tree is the developer's,
29+
# and this hook has no business rewriting it.
30+
#
31+
# It deliberately does NOT gate the rebuild on git state. The bundle's job is
32+
# to reflect the working tree, which is what the developer is looking at.
2133

2234
set -u
2335

2436
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
2537
src="$root/web_ui/src"
26-
stamp="$root/web_ui/dist/app/index.html"
38+
dist="$root/web_ui/dist/app"
39+
stamp="$dist/index.html"
40+
# Records the commit the bundle was built from. A pull or a branch switch
41+
# that happens to leave mtimes alone is invisible to the walk below; the
42+
# recorded HEAD is not.
43+
head_stamp="$dist/.built-from-commit"
2744

2845
# Nothing to do outside a checkout that has the frontend.
2946
[ -d "$src" ] || exit 0
3047

3148
emit() { printf '{"systemMessage":%s}\n' "$1"; }
3249

33-
# Inputs that change what the bundle contains. index.html lives under
34-
# src/app/, so it is already covered by the tree walk.
35-
newest="$(
36-
find "$src" -type f -newer "$stamp" -print -quit 2>/dev/null
37-
)"
50+
head_sha=""
51+
if command -v git >/dev/null 2>&1; then
52+
head_sha="$(git -C "$root" rev-parse HEAD 2>/dev/null || true)"
53+
fi
54+
55+
# -- 1. Does the bundle still match the working tree? -----------------------
56+
57+
newest="$(find "$src" -type f -newer "$stamp" -print -quit 2>/dev/null)"
3858
if [ ! -f "$stamp" ]; then
3959
newest="(no bundle yet)"
4060
elif [ -z "$newest" ]; then
@@ -43,11 +63,53 @@ elif [ -z "$newest" ]; then
4363
done
4464
fi
4565

46-
[ -n "$newest" ] || exit 0
66+
# A bundle built from a different commit is stale even when every mtime says
67+
# otherwise - see problem 2 in this file's own header.
68+
if [ -z "$newest" ] && [ -n "$head_sha" ]; then
69+
built_from="$(cat "$head_stamp" 2>/dev/null || true)"
70+
[ "$built_from" = "$head_sha" ] || newest="(built from a different commit)"
71+
fi
4772

48-
if (cd "$root/web_ui" && npm run build >/tmp/graphlink-spa-build.log 2>&1); then
49-
emit '"web_ui/dist/app rebuilt - the desktop app and :8765 now serve the current source."'
50-
else
51-
emit '"web_ui/dist/app is STALE: npm run build failed. See /tmp/graphlink-spa-build.log. The desktop app and :8765 are still serving the previous bundle."'
73+
rebuilt=""
74+
if [ -n "$newest" ]; then
75+
if (cd "$root/web_ui" && npm run build >/tmp/graphlink-spa-build.log 2>&1); then
76+
[ -n "$head_sha" ] && printf '%s' "$head_sha" > "$head_stamp" 2>/dev/null
77+
rebuilt="ok"
78+
else
79+
emit '"web_ui/dist/app is STALE: npm run build failed. See /tmp/graphlink-spa-build.log. The desktop app and :8765 are still serving the previous bundle."'
80+
exit 0
81+
fi
5282
fi
83+
84+
# -- 2. Is the checkout itself behind what has already merged? --------------
85+
#
86+
# Reported, never acted on. Reads only refs already fetched - no network, so
87+
# a Stop hook never blocks on one.
88+
89+
behind=""
90+
if [ -n "$head_sha" ]; then
91+
upstream=""
92+
for candidate in origin/main origin/master; do
93+
if git -C "$root" rev-parse --verify --quiet "$candidate" >/dev/null 2>&1; then
94+
upstream="$candidate"
95+
break
96+
fi
97+
done
98+
if [ -n "$upstream" ]; then
99+
count="$(git -C "$root" rev-list --count "HEAD..$upstream" 2>/dev/null || echo 0)"
100+
[ "$count" -gt 0 ] 2>/dev/null && behind="$count"
101+
fi
102+
fi
103+
104+
if [ -n "$behind" ]; then
105+
branch="$(git -C "$root" rev-parse --abbrev-ref HEAD 2>/dev/null || echo '?')"
106+
if [ -n "$rebuilt" ]; then
107+
emit "\"web_ui/dist/app rebuilt. BUT this checkout ($branch) is $behind commit(s) behind origin/main, so the running app is still missing work that has already merged - the bundle can only ever be as current as the source it is built from. Merge or rebase when your working tree allows it.\""
108+
else
109+
emit "\"web_ui/dist/app matches this source, but the checkout ($branch) is $behind commit(s) behind origin/main - the running app is missing work that has already merged. Merge or rebase when your working tree allows it.\""
110+
fi
111+
exit 0
112+
fi
113+
114+
[ -n "$rebuilt" ] && emit '"web_ui/dist/app rebuilt - the desktop app and :8765 now serve the current source."'
53115
exit 0

0 commit comments

Comments
 (0)