mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
deploy_build.sh has two entry points — setup_kcd2_dice_solver.sh calls it
directly, and the post-commit hook starts a unit that runs it — and nothing
serialised them.
When they overlap, the second run's `vite build --emptyOutDir` truncates
dist.staging while the first run's rsync is still reading it. rsync exits 24,
`set -e` aborts before assert_mount_live, and dist/ is left holding a fresh
index.html that references assets which never arrived, with the previous
bundle's files already deleted by --delete-after.
Nothing downstream notices, which is what makes it worth fixing rather than
tolerating: `try_files {path} /index.html` answers the missing chunks with the
SPA HTML, so the browser is handed HTML where it expects JavaScript and no 404
reaches any log (verified against the live site: /assets/does-not-exist.js
returns 200 text/html). The hook's `|| true` then swallows the unit failure, so
the commit succeeds and the site is quietly broken.
Reproduced with a throttled rsync: rc=24, live left with index.html and two of
six chunks. With flock, two concurrent runs each publish a complete bundle.
Also tightened assert_mount_live, which announced success in three cases where
it had verified nothing: docker absent, container not running, and a failed
`docker exec` — the last because docker reports OCI runtime errors on stdout,
so `2>/dev/null` does not suppress them and the guard's non-empty test passed
on a 184-byte error string. It now requires an actual inode and says plainly
when the mount was not verified.
Found by the working-diff review these same changes had skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
132 lines
5.4 KiB
Bash
Executable File
132 lines
5.4 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# deploy_build.sh — build the static bundle that dice.kuhy.duckdns.org serves.
|
|
#
|
|
# The single build path, used by both setup_kcd2_dice_solver.sh and the
|
|
# post-commit auto-rebuild, so the live site can never be produced two
|
|
# different ways.
|
|
#
|
|
# Why it does not just run `vite build`:
|
|
#
|
|
# The serving container bind-mounts dist/ at /srv. A bind mount follows the
|
|
# INODE, so anything that replaces the directory (rm -rf dist && mv new dist)
|
|
# silently detaches the mount until the container is restarted — the site then
|
|
# serves the old files forever with no error anywhere. And a plain `vite build`
|
|
# empties dist/ before writing, so the site 404s for the length of the build.
|
|
#
|
|
# Building into dist.staging/ and rsyncing into dist/ avoids both, but only
|
|
# with --delete-after --delay-updates: plain --delete removes stale files
|
|
# BEFORE writing the new ones, so a client that fetched index.html moments
|
|
# earlier can 404 on the very asset it is asking for.
|
|
#
|
|
# Preserving the inode is necessary but not sufficient, so the publish is
|
|
# followed by a check that the container is still mounted on this directory —
|
|
# see assert_mount_live for why that cannot be done by watching the inode here.
|
|
#
|
|
# Usage:
|
|
# deploy_build.sh Build and publish into dist/.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
|
readonly SCRIPT_DIR
|
|
readonly STAGING="${SCRIPT_DIR}/dist.staging"
|
|
readonly LIVE="${SCRIPT_DIR}/dist"
|
|
# Must match setup_kcd2_dice_solver.sh's DICE_CONTAINER.
|
|
readonly CONTAINER="kcd2-dice"
|
|
|
|
die() {
|
|
echo "deploy_build: $1" >&2
|
|
exit 1
|
|
}
|
|
|
|
# nvm-installed toolchains are not on PATH for systemd/git-hook invocations.
|
|
ensure_toolchain() {
|
|
if [[ -s ${NVM_DIR:-$HOME/.nvm}/nvm.sh ]]; then
|
|
# shellcheck disable=SC1091 # sourced at runtime, path varies by machine
|
|
source "${NVM_DIR:-$HOME/.nvm}/nvm.sh"
|
|
fi
|
|
command -v node >/dev/null || die "node not found."
|
|
command -v pnpm >/dev/null || die "pnpm not found."
|
|
command -v rsync >/dev/null || die "rsync not found."
|
|
}
|
|
|
|
# Confirm the serving container is still mounted on THIS directory.
|
|
#
|
|
# Watching dist/'s own inode across the rsync cannot detect a detached mount and
|
|
# never could: rsync into an existing directory never changes it, and if dist/
|
|
# had already been removed (it is gitignored, so `git clean -xdff` removes it)
|
|
# the mkdir above recreates it at a fresh inode before anything is measured.
|
|
# Either way the check passes while the container serves the pre-deletion
|
|
# snapshot forever, with no error anywhere. The only thing that actually
|
|
# answers the question is what the CONTAINER sees.
|
|
assert_mount_live() {
|
|
local running host_inode container_inode
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
echo "deploy_build: docker not found — mount NOT verified." >&2
|
|
return 0
|
|
fi
|
|
running="$(docker ps --format '{{.Names}}' 2>/dev/null || true)"
|
|
if ! grep -qx "$CONTAINER" <<<"$running"; then
|
|
echo "deploy_build: ${CONTAINER} not running — mount NOT verified." >&2
|
|
return 0
|
|
fi
|
|
|
|
host_inode="$(stat -c %i "$LIVE")"
|
|
# `docker exec` reports OCI runtime errors on STDOUT, so 2>/dev/null does not
|
|
# suppress them and a failure arrives as a long string rather than as empty
|
|
# output. Require an actual inode number instead of merely non-empty.
|
|
container_inode="$(docker exec "$CONTAINER" stat -c %i /srv 2>/dev/null || true)"
|
|
if [[ ! $container_inode =~ ^[0-9]+$ ]]; then
|
|
echo "deploy_build: could not read /srv inode from ${CONTAINER} — mount NOT verified." >&2
|
|
return 0
|
|
fi
|
|
if [[ $host_inode != "$container_inode" ]]; then
|
|
echo "deploy_build: dist/ was replaced — ${CONTAINER} is serving a detached copy." >&2
|
|
echo "deploy_build: restarting ${CONTAINER} to re-attach the bind mount…" >&2
|
|
docker restart "$CONTAINER" >/dev/null
|
|
container_inode="$(docker exec "$CONTAINER" stat -c %i /srv 2>/dev/null || true)"
|
|
[[ $host_inode == "$container_inode" ]] ||
|
|
die "${CONTAINER} still not mounted on ${LIVE} after a restart."
|
|
echo "deploy_build: ${CONTAINER} re-attached." >&2
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
# Both entry points run this same script — setup_kcd2_dice_solver.sh calls it
|
|
# directly, and the post-commit hook starts a unit that runs it — so one lock
|
|
# closes the race between them. Without it, a second run's `vite --emptyOutDir`
|
|
# truncates dist.staging while the first run's rsync is still reading it:
|
|
# rsync exits 24, `set -e` stops the script before assert_mount_live, and dist/
|
|
# is left holding a fresh index.html that references assets which never
|
|
# arrived. Nothing downstream notices — `try_files` serves index.html for the
|
|
# missing chunks, so the browser is handed HTML where it expects JavaScript and
|
|
# no 404 reaches any log.
|
|
#
|
|
# Wait rather than fail: a rebuild that arrives during a setup run still needs
|
|
# to happen, and the unit has no start timeout.
|
|
exec 9>"${SCRIPT_DIR}/.deploy.lock"
|
|
flock -w 600 9 || die "another deploy_build is still running after 10 minutes."
|
|
|
|
ensure_toolchain
|
|
cd "$SCRIPT_DIR"
|
|
|
|
echo "Installing dependencies…"
|
|
pnpm install --frozen-lockfile
|
|
|
|
echo "Building into dist.staging…"
|
|
pnpm exec tsc -b
|
|
pnpm exec vite build --outDir dist.staging --emptyOutDir
|
|
|
|
# Fail before touching the live directory, never after.
|
|
[[ -f "${STAGING}/index.html" ]] || die "build produced no index.html."
|
|
|
|
mkdir -p "$LIVE"
|
|
rsync -a --delete-after --delay-updates "${STAGING}/" "${LIVE}/"
|
|
assert_mount_live
|
|
|
|
echo "Published $(find "$LIVE" -type f | wc -l) files to ${LIVE} (inode $(stat -c %i "$LIVE"))."
|
|
}
|
|
|
|
main "$@"
|