@@ -101,6 +101,26 @@ jobs:
101101 run : |
102102 mkdir -p .video-cache/dev .video-cache/prod
103103
104+ # Per-chapter source hash. A chapter is rebuilt when this hash differs
105+ # from the .hash sidecar stored alongside the cached video. Inputs:
106+ # the chapter's own scene/thumb sources plus shared files that affect
107+ # every chapter (scene_utils, imgs, manim.cfg). The cache restore-keys
108+ # fallback can resurrect stale videos, so existence-only checks are
109+ # not sufficient — see commit 496d908 for the bug this guards against.
110+ # NOTE: keep this function in sync with the copy in "Build chapters".
111+ compute_chapter_hash() {
112+ local chapter="$1"
113+ {
114+ find "$chapter" -maxdepth 1 -type f \( -name 'Scene*.py' -o -name 'Thumb.py' \) -print0 \
115+ | sort -z | xargs -0 sha256sum
116+ find scene_utils -type f -name '*.py' -print0 \
117+ | sort -z | xargs -0 sha256sum
118+ find imgs -type f -print0 \
119+ | sort -z | xargs -0 sha256sum
120+ sha256sum manim.cfg
121+ } | sha256sum | awk '{print $1}'
122+ }
123+
104124 # Determine build mode
105125 if [ "${{ inputs.prod }}" = "true" ]; then
106126 CACHE_DIR=".video-cache/prod"
@@ -116,14 +136,26 @@ jobs:
116136 echo "rebuild=${{ inputs.chapters }}" >> "$GITHUB_OUTPUT"
117137 fi
118138 else
119- # On push, rebuild dev chapters that have cache misses
139+ # On push, rebuild chapters whose source hash doesn't match the cached hash
120140 REBUILD=""
121141 for N in 0 1 2 3 4 5 6 7 8 9; do
122142 CHAPTER="Chapter${N}"
123- if [ ! -f "${CACHE_DIR}/${CHAPTER}_480p15.mp4" ] || [ ! -f "${CACHE_DIR}/${CHAPTER}.png" ]; then
124- if [ -d "$CHAPTER" ]; then
125- REBUILD="${REBUILD:+${REBUILD},}${N}"
126- fi
143+ [ -d "$CHAPTER" ] || continue
144+
145+ VIDEO="${CACHE_DIR}/${CHAPTER}_480p15.mp4"
146+ THUMB="${CACHE_DIR}/${CHAPTER}.png"
147+ HASHFILE="${CACHE_DIR}/${CHAPTER}.hash"
148+
149+ CURRENT_HASH=$(compute_chapter_hash "$CHAPTER")
150+
151+ if [ ! -f "$VIDEO" ] || [ ! -f "$THUMB" ] || [ ! -f "$HASHFILE" ]; then
152+ echo "${CHAPTER}: rebuild (missing artifact)"
153+ REBUILD="${REBUILD:+${REBUILD},}${N}"
154+ elif [ "$(cat "$HASHFILE")" != "$CURRENT_HASH" ]; then
155+ echo "${CHAPTER}: rebuild (source changed: $(cat "$HASHFILE") -> $CURRENT_HASH)"
156+ REBUILD="${REBUILD:+${REBUILD},}${N}"
157+ else
158+ echo "${CHAPTER}: cache hit"
127159 fi
128160 done
129161 echo "rebuild=${REBUILD}" >> "$GITHUB_OUTPUT"
@@ -133,6 +165,20 @@ jobs:
133165 env :
134166 ELEVEN_API_KEY : ${{ secrets.ELEVEN_API_KEY }}
135167 run : |
168+ # NOTE: keep this function in sync with the copy in "Determine chapters to build".
169+ compute_chapter_hash() {
170+ local chapter="$1"
171+ {
172+ find "$chapter" -maxdepth 1 -type f \( -name 'Scene*.py' -o -name 'Thumb.py' \) -print0 \
173+ | sort -z | xargs -0 sha256sum
174+ find scene_utils -type f -name '*.py' -print0 \
175+ | sort -z | xargs -0 sha256sum
176+ find imgs -type f -print0 \
177+ | sort -z | xargs -0 sha256sum
178+ sha256sum manim.cfg
179+ } | sha256sum | awk '{print $1}'
180+ }
181+
136182 # Determine target cache directory
137183 if [ "${{ inputs.prod }}" = "true" ]; then
138184 CACHE_DIR=".video-cache/prod"
@@ -174,6 +220,7 @@ jobs:
174220 # Cache the results
175221 cp "docs/${CHAPTER}_480p15.mp4" "${CACHE_DIR}/${CHAPTER}_480p15.mp4"
176222 cp "docs/${CHAPTER}.png" "${CACHE_DIR}/${CHAPTER}.png"
223+ compute_chapter_hash "$CHAPTER" > "${CACHE_DIR}/${CHAPTER}.hash"
177224
178225 echo "${CHAPTER} complete"
179226 done
0 commit comments