diff --git a/docs/deployment.md b/docs/deployment.md index 1b2c7c60..116bab19 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -41,18 +41,36 @@ cd pediatric-ai-scribe-v3 cp .env.example .env # edit .env — required: APP_URL, JWT_SECRET, DATA_ENCRYPTION_KEY, DB_PASSWORD, an AI provider ./scripts/build-image.sh -sed -i "s|^PED_AI_IMAGE=.*|PED_AI_IMAGE=ped-ai-local:$(git rev-parse HEAD)|" .env -docker compose up -d --no-build -curl -fsS http://127.0.0.1:3552/api/build # must equal `git rev-parse HEAD` +REV=$(git rev-parse HEAD) +scripts/deploy.sh "ped-ai-local:$REV" "$REV" ``` -**Building does not change what `up` starts.** `docker compose` takes the image -from `PED_AI_IMAGE` in `.env`, and `build-image.sh` deliberately does not move -that pin — naming a revision is also how a rollback is done. A pin left from an -earlier deploy therefore starts the older image, and nothing short of -`/api/build` shows it: the build succeeds, `up` reports the container started, -and `/api/health` returns `{ok:true}` from the wrong revision. `build-image.sh` -warns when the pin does not match what it just built. +**Use `scripts/deploy.sh`. Do not run `docker compose up` by hand.** + +Building is not deploying. `docker compose` takes its image from +`PED_AI_IMAGE` in `.env`, and `build-image.sh` does not move that pin — naming a +revision is also how a rollback is done. So a pin left behind by an earlier +deploy starts *that* image, and every signal still reports success: the build +completes, `up` says the container started, and `/api/health` returns +`{ok:true}` from the wrong revision. This has happened: a stale pin silently +reverted the app by 31 commits, removing a feature, and the missing feature was +reported as a new bug. + +`scripts/deploy.sh [expected-revision]` is what closes that gap: + +1. pulls the image if it is not local, and refuses to tear anything down until + it exists; +2. records what is serving now, so there is something to go back to; +3. moves the `PED_AI_IMAGE` pin, so a later plain `docker compose up` brings up + the same image rather than reverting; +4. waits for the container to become healthy; +5. asks `/api/build` which revision is *actually* serving and compares it to the + expected one — catching a stale tag, a cached layer, or a rollback that never + took; +6. rolls back to the previous image if either check fails. + +`build-image.sh` prints the exact `deploy.sh` line to run whenever the pin does +not match the revision it just built. The build uses Node 24 LTS and `npm ci --omit=dev` from the root lockfile. `./scripts/build-image.sh` resolves the full checkout Git commit (including diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 6677618f..59c747dd 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -451,29 +451,28 @@ If a dynamic value enters an HTML string, escape it at the point of insertion. I ## Deployment checks -After deployment, check *which revision* is running, not only that something is: +Deploy with `scripts/deploy.sh`, never with `docker compose up` by hand: + +```bash +./scripts/build-image.sh +REV=$(git rev-parse HEAD) +scripts/deploy.sh "ped-ai-local:$REV" "$REV" +``` + +`deploy.sh` moves the `PED_AI_IMAGE` pin in `.env`, waits for health, then reads +`/api/build` and rolls back if the container came up on a different revision. +Running `up` by hand does none of that: `/api/health` passing proves a container +is up, not that it is the one you built, and a pin left from an earlier deploy +will happily start an older image while everything looks fine. See +[`deployment.md`](deployment.md) for the full sequence. + +To check by hand what is serving: ```bash curl -fsS http://127.0.0.1:3552/api/build # must equal `git rev-parse HEAD` -curl -fsS http://127.0.0.1:3552/api/health docker compose ps pediatric-scribe ``` -`/api/health` passing proves a container is up, not that it is the one you -built. `docker compose up` starts whatever `PED_AI_IMAGE` in `.env` names, and -that pin does not move when you build — so a pin left from an earlier deploy -silently starts the older image, and every signal short of `/api/build` looks -fine. `scripts/build-image.sh` warns when the pin does not match the revision it -just built; the fix is to update the pin: - -```bash -sed -i "s|^PED_AI_IMAGE=.*|PED_AI_IMAGE=ped-ai-local:$(git rev-parse HEAD)|" .env -docker compose up -d --no-build -``` - -The pin is not updated for you, because naming a revision is also how a -deliberate rollback is done. - If the browser still shows old frontend behavior after the revision checks out, force-refresh or check the injected `BUILD_ID` asset query string. diff --git a/scripts/build-image.sh b/scripts/build-image.sh index 1239ba50..d97e7c1b 100755 --- a/scripts/build-image.sh +++ b/scripts/build-image.sh @@ -38,15 +38,16 @@ else echo 'Built ped-ai-local:latest with no recorded revision' >&2 fi -# A deploy runs `docker compose up`, which reads PED_AI_IMAGE from .env — not -# the tag just built. A pin left behind from an earlier deploy therefore starts -# that older image, and `up` reports success either way: the build looks done, -# the health check passes, and the running app is silently an older revision. -# That has already cost a session — it rolled the app back far enough to remove -# a feature, and the missing feature was read as a new bug. +# Building is not deploying. `docker compose up` takes its image from +# PED_AI_IMAGE in .env, which this script does not move, so a pin left behind by +# an earlier deploy starts that older image while every signal reports success: +# the build completes, `up` says the container started, and /api/health returns +# {ok:true} from the wrong revision. # -# Not corrected automatically: the pin is how a deploy names a revision on -# purpose, including a deliberate rollback. Said loudly instead. +# scripts/deploy.sh is the answer to this and has been since it landed: it moves +# the pin, waits for health, then asks /api/build which revision is actually +# serving and rolls back if it is not the one requested. Running `up` by hand +# skips all three. This points at it rather than repeating half of it. if [ "$GIT_REVISION" != unknown ] && [ -f .env ]; then PINNED=$(sed -n 's/^PED_AI_IMAGE=//p' .env | tail -1) case "$PINNED" in @@ -54,15 +55,14 @@ if [ "$GIT_REVISION" != unknown ] && [ -f .env ]; then *"$GIT_REVISION") ;; *) echo >&2 - echo "WARNING: .env pins PED_AI_IMAGE=$PINNED" >&2 - echo " which is NOT the image just built." >&2 - echo " 'docker compose up -d --no-build' will start the pinned image," >&2 - echo " not this build. To deploy what was just built:" >&2 + echo "NOTE: .env still pins PED_AI_IMAGE=$PINNED" >&2 + echo " so 'docker compose up' would start that image, not this build." >&2 echo >&2 - echo " sed -i 's|^PED_AI_IMAGE=.*|PED_AI_IMAGE=ped-ai-local:$GIT_REVISION|' .env" >&2 + echo " Deploy this build with:" >&2 + echo " scripts/deploy.sh ped-ai-local:$GIT_REVISION $GIT_REVISION" >&2 echo >&2 - echo " Then confirm after starting it:" >&2 - echo " curl -fsS http://127.0.0.1:3552/api/build" >&2 + echo " which moves the pin and then verifies /api/build, rolling back" >&2 + echo " if the container comes up on a different revision." >&2 echo >&2 ;; esac