docs: deploy.sh is the deploy path, and it was documented nowhere
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 47s
Forgejo Docker Build / Root app tests (push) Successful in 48s
Forgejo Android APK / Build signed APK (push) Successful in 2m22s
Forgejo Docker Build / Build Docker image (push) Successful in 11s
Forgejo Docker Build / Deploy to the host (push) Failing after 2s

scripts/deploy.sh landed on 2026-09-11 as "a deploy you can repeat, and prove
afterwards". It moves the PED_AI_IMAGE pin, waits for health, reads /api/build
to find out which revision is actually serving, and rolls back when that is not
the revision asked for. Its own comment says it: "Healthy is not the same as
running what was asked for."

No doc mentioned it. Both deployment.md and DEVELOPMENT.md instead showed
`build-image.sh` then `docker compose up -d --no-build` then `curl /api/health`
— a sequence that moves no pin and proves no revision. Following it deployed
nothing four times in one session, eventually reverting the app by 31 commits
and removing a feature, which was then reported as a bug.

Both docs now lead with deploy.sh and say why `up` by hand is not a deploy.

Also replaced yesterday's build-image.sh warning, which told the operator to
`sed` the pin themselves. That reimplemented, badly, one of the three things
deploy.sh already does — and it was written while auditing these very files
without noticing the script was there. It now prints the deploy.sh line to run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
Daniel 2026-09-12 06:04:13 +02:00
parent 739286b53b
commit c9b6d6b3c9
3 changed files with 59 additions and 42 deletions

View file

@ -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 <image-ref> [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

View file

@ -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.

View file

@ -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