diff --git a/scripts/mac-mini/build-app.sh b/scripts/mac-mini/build-app.sh index 120da1474..e3bb69593 100755 --- a/scripts/mac-mini/build-app.sh +++ b/scripts/mac-mini/build-app.sh @@ -113,6 +113,55 @@ build_base_images() { echo "" } +# Returns 0 (true) if the sveltekit-base:local image is older than the most +# recent commit touching any path that would actually change its contents. +# +# Why this exists: per-app web Dockerfiles do `FROM sveltekit-base:local` and +# do NOT re-copy packages/shared-* — those packages are baked into the base +# image. So a change to e.g. packages/shared-utils only reaches the live web +# app if the base image is also rebuilt. We learned this the hard way on +# 2026-04-08 when a CSP fix in shared-utils sat unused in production for an +# hour because every `build-app.sh mana-web` reused the cached base layer. +# +# Detection: compare the base image's Created timestamp against the last git +# commit that touched packages/, the base Dockerfile, or pnpm-lock.yaml. If +# any such commit is newer than the image, the image is stale. +is_base_image_stale() { + # No image at all → definitely needs building + if ! $DOCKER image inspect sveltekit-base:local >/dev/null 2>&1; then + return 0 + fi + + local image_iso image_epoch latest_commit_epoch + image_iso=$($DOCKER inspect -f '{{.Created}}' sveltekit-base:local 2>/dev/null) + + # Docker on the Mac Mini emits RFC3339 with the *local* TZ offset, e.g. + # "2026-04-08T18:30:12.871278257+02:00". On Linux it's typically a Z + # suffix. We strip everything from char 19 onward (fractional + offset), + # leaving "2026-04-08T18:30:12", and parse it as LOCAL time — which is + # what the original timestamp meant on this host. The GNU fallback can + # handle the full string directly because GNU date understands offsets. + image_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "${image_iso:0:19}" +%s 2>/dev/null \ + || date -d "$image_iso" +%s 2>/dev/null \ + || echo 0) + + if [ "$image_epoch" = "0" ]; then + # Couldn't parse — be conservative and force a rebuild + return 0 + fi + + # %ct is the committer date as Unix epoch — directly comparable. + latest_commit_epoch=$(git -C "$PROJECT_ROOT" log -1 --format=%ct -- \ + packages/ \ + docker/Dockerfile.sveltekit-base \ + pnpm-lock.yaml 2>/dev/null || echo 0) + + if [ "$latest_commit_epoch" -gt "$image_epoch" ]; then + return 0 # stale + fi + return 1 # fresh +} + build_services() { local services=("$@") @@ -120,9 +169,18 @@ build_services() { for svc in "${services[@]}"; do case "$svc" in *-web) - if ! $DOCKER image inspect sveltekit-base:local >/dev/null 2>&1; then - echo "=== Building sveltekit-base (first time) ===" + if is_base_image_stale; then + if ! $DOCKER image inspect sveltekit-base:local >/dev/null 2>&1; then + echo "=== Building sveltekit-base (first time) ===" + else + local last_commit + last_commit=$(git -C "$PROJECT_ROOT" log -1 --oneline -- \ + packages/ docker/Dockerfile.sveltekit-base pnpm-lock.yaml 2>/dev/null || echo "?") + echo "=== Rebuilding sveltekit-base (stale: newer commit touches packages/) ===" + echo " Triggering commit: $last_commit" + fi $DOCKER build -f "$PROJECT_ROOT/docker/Dockerfile.sveltekit-base" -t sveltekit-base:local "$PROJECT_ROOT" + echo "" fi break ;;