pediatric-ai-scribe-v3/scripts/build-image.sh
Daniel 8d0dc968b3 feat: a deploy you can repeat, and prove afterwards
Reproducibility means two things here: the same commit builds the same image,
and the running container can be asked which commit it is.

  - Base images are pinned by digest, not by tag. A tag moves; two builds of one
    commit could otherwise differ. These are manifest-list digests, so buildx
    still picks the right architecture.

  - scripts/build-image.sh also writes ped-ai-local:<revision>, an immutable
    name a deploy can refer to instead of chasing :latest. Its summary goes to
    stderr so stdout stays the Compose invocation.

  - Compose takes the image from PED_AI_IMAGE, so a deploy runs a specific
    revision-tagged image while a local build still uses the local tag.

  - scripts/deploy.sh pins that image in the file Compose interpolates from,
    waits for health, then asks /api/build which revision is actually serving
    and rolls back to the previous image if it does not match. Healthy is not
    the same as running what you asked for. The rollback path was exercised.

  - The entrypoint applies migrations before the app starts, so code and schema
    arrive together. node-pg-migrate takes an advisory lock; losing it is not an
    error, it waits and looks again, so a rolling restart does not fail. A real
    migration failure stops the container rather than serving on a schema that
    does not match the build. RUN_MIGRATIONS=false opts out.

  - The Forgejo workflow builds through that same script, tags by full revision,
    and has an opt-in deploy job. It refuses to run if the deploy directory has
    uncommitted work rather than resetting over it.

The running image was labelled revision=unknown, and /api/build said "unknown",
because `docker compose up --build` never passes GIT_REVISION. That is exactly
the hole this closes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-11 00:41:11 +02:00

39 lines
1.4 KiB
Bash
Executable file

#!/bin/sh
# Build only; this never starts services.
#
# The revision is validated and baked into the image (BUILD_ID + the OCI
# revision label), so the result can always be traced back to a commit and
# /api/build can report it at runtime. Plain `docker compose build` does not set
# GIT_REVISION and produces an image labelled "unknown" — which is why this is
# the supported way to build.
#
# The image is tagged twice: ped-ai-local:<revision> is immutable and is what a
# deploy should name, ped-ai-local:latest is the convenience pointer.
set -eu
cd "$(dirname "$0")/.."
if [ -e .git ]; then
GIT_REVISION=$(env -i PATH="$PATH" HOME="${HOME:-}" git rev-parse --verify 'HEAD^{commit}')
printf '%s\n' "$GIT_REVISION" | grep -Eq '^[0-9a-f]{40}$' || {
echo 'Expected a full lowercase Git SHA' >&2
exit 1
}
else
GIT_REVISION=unknown
echo 'Unversioned development build: revision unknown' >&2
fi
export GIT_REVISION
# A local build always writes the local tag, even when .env pins PED_AI_IMAGE to
# a deployed registry image — otherwise building here would quietly overwrite
# the tag a deploy is pinned to.
PED_AI_IMAGE=ped-ai-local:latest
export PED_AI_IMAGE
docker compose build "$@" pediatric-scribe
if [ "$GIT_REVISION" != unknown ]; then
docker tag ped-ai-local:latest "ped-ai-local:$GIT_REVISION"
echo "Built ped-ai-local:$GIT_REVISION (also tagged :latest)" >&2
else
echo 'Built ped-ai-local:latest with no recorded revision' >&2
fi