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
39 lines
1.4 KiB
Bash
Executable file
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
|