diff --git a/.github/workflows/deploy-site-task.yml b/.github/workflows/deploy-site-task.yml index 5964693..f0ca4a9 100644 --- a/.github/workflows/deploy-site-task.yml +++ b/.github/workflows/deploy-site-task.yml @@ -77,12 +77,12 @@ jobs: # One update for the job, because each one is a network round trip that can fail on its # own. REQUIRE_BROTLI later makes a missing brotli fatal, so this keeps the build from - # failing, and git-restore-mtime is what the next step runs. + # failing. - name: Install build tools step run: | set -Eeuo pipefail sudo apt-get update - sudo apt-get install --yes --no-install-recommends brotli git-restore-mtime + sudo apt-get install --yes --no-install-recommends brotli # Git stores no mtimes, so a checkout stamps every file with the moment it was written. # The deploy uploads with --link-dest against the previous release, and a file only links @@ -100,12 +100,42 @@ jobs: # began with, so a media file that acquires a bad one stays present, correctly named and # unreadable, through every later release. The live check is what notices that, by # requesting images and failing on the 403. - # `git restore-mtime`, the subcommand form, because the package installs into git's - # exec-path at /usr/lib/git-core rather than onto PATH, so the bare name does not resolve. + # The action rather than the Ubuntu package, because the package is git-tools v2022.12 and + # that release shells out to `git whatchanged`, which current git refuses to run without + # `--i-still-use-this` - a flag there is no way to pass through. It failed twelve times in + # one step, restored nothing, and exited 0, so every release since #65 was a full copy + # while CI reported success. Upstream replaced whatchanged with `git log` in v2025.08, and + # this action vendors exactly that version. - name: Restore file mtimes step + uses: chetan/git-restore-mtime-action@d186aca54f8760da4dec55313195e51ed3ebb0b3 # v2.3 + with: + args: static + + # The step above is the second tool to claim it restored these and not have done it, so + # the outcome is asserted rather than the tool trusted. This is the check whose absence + # let #65 ship broken: `git restore-mtime` printed "1,052 files to be processed" and then + # processed none of them, which reads exactly like success. + # + # The discriminator is that a restored file cannot be newer than the commit it was dated + # from, so no file under static/ may be newer than HEAD's own commit time. A checkout + # necessarily happens after the commit it checks out, so an unrestored tree is always + # newer than that bound and a restored one never is. It calibrates itself from the + # repository, so no measured constant goes stale as content moves. + # + # Counting distinct mtime days looks like the obvious check and is wrong: git restores + # the LAST COMMIT time, and static/ arrived in a bulk import, so a correctly restored + # tree here has exactly one distinct day. That version was written first and rejected + # only because it was run against a real restored clone before being trusted. + - name: Assert mtimes were restored step run: | set -Eeuo pipefail - git restore-mtime static + bound=$(git log -1 --format=%ct) + newest=$(find static -type f -printf '%T@\n' | sort -n | tail -1 | cut -d. -f1) + echo "static/ newest mtime $newest, HEAD committed $bound, margin $((bound - newest))s" + if [ "$newest" -gt "$bound" ]; then + echo "::error::mtime restore did nothing: static/ carries files newer than HEAD's commit, so they still hold their checkout time and --link-dest will link nothing." + exit 1 + fi # The pin lives in the action, so the deploy and validation cannot install different generators. - name: Install Hugo step