The first successful galactus deploy came up all-green while the web tier was
running a build from two commits earlier. The registry held web:latest from
3ff56e6; the host still had a web:latest cached from 4ee7ec7; the deploy
reported success and served the old one. The API was only current because it
had been pulled by hand during earlier debugging.
Two independent failures, both fixed here.
1. Images are not pulled. The deploy action's `pull: true` does not reliably
refresh an already-cached moving tag on a standalone endpoint. Added a
Pull images step (deploy/scripts/pull-images.mjs) that pulls each image
through Portainer's Docker API with registry credentials and fails the
deploy if a pull fails — note the endpoint answers 200 even when the pull
errored, so the stream body has to be inspected, not just the status.
2. The drift check could not see it. Both the verify step and the web footer
compared APP_VERSION, but on a branch build BOTH tiers report "master", so
equality proved nothing. They now compare gitSha, which is the only field
that differs between two builds of the same branch. api and web come from
one matrix run, so a difference can only mean an image was not replaced.
This needed a /version on the web tier too — previously its build identity
was only readable by scraping window.__APP_BUILD__ out of the HTML.
pull-images.mjs builds the X-Registry-Auth header as URL-safe base64 WITH
padding: Node's "base64url" omits the padding and Portainer's Go decoder
rejects it with "Illegal base64 data at input byte N".
Verified against galactus: pulls both images, and exits non-zero on a
nonexistent tag.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first deploy attempt (run 705) died on "Input required and not supplied:
token", which names the action's input rather than the secret that was unset —
the repo had only REGISTRY_USERNAME and REGISTRY_PASSWORD, so every deploy
secret was missing on both workflows. That is also why the endpoint_id /
pull_image input-name bug had gone unnoticed: neither workflow had ever got
far enough to use them.
Both workflows now check their required secrets up front and fail listing the
ones that are empty. The scope=full-only secrets are only required when the
dispatch is actually scope=full.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes the gap between "what tag did I deploy" and "what is actually running",
and gives the schema a history that can be reasoned about across releases.
Migrations
- Baseline the existing schema as 0000_init (migrate diff --from-empty). The
schema had only ever been applied with `prisma db push`, so no history
existed and schema state was disconnected from app version. Existing
databases must be baselined once with `migrate resolve --applied 0000_init`;
the workflows print this remedy on P3005.
- Run `prisma migrate deploy` as a deploy STEP, not the container CMD — as a
CMD, N replicas would race each other applying the same migration.
Version reporting
- GET /version on the API reports the APP_VERSION / GIT_SHA / BUILD_DATE that
build.yml already baked into both images but nothing ever read.
- The web footer shows the web build and flags an api/web mismatch. The two
cannot drift at build time (one matrix run) but can at deploy time.
- Both deploy workflows now fail if the running API does not report the tag
that was dispatched — a stack naming a tag is not proof of what is running.
- scripts/set-version.mjs stamps every package.json, which had all sat at
0.1.0 while real releases shipped as v1.x.
Pre-migrate backup
- deploy/scripts/pre-migrate-backup.mjs dumps the database from INSIDE the
still-running old API container over Portainer's Docker API, so the file
lands in the volume the Operaciones restore screen reads. A dump taken on
the CI runner would be unreachable by the only restore path we have.
Verifies the artefact with `gzip -t` before letting the migration proceed.
galactus
- deploy/galactus/*.compose.yml: standalone-Docker ports of the Swarm stacks.
Plain compose silently ignores `deploy:`, so restart_policy becomes
`restart: unless-stopped` — without it nothing returns after a host reboot.
- .gitea/workflows/deploy-galactus.yml drives endpoint 3 with its own secrets.
Fixes
- deploy.yml passed `endpoint_id` and `pull_image` to
cssnr/portainer-stack-deploy-action, which has no such inputs (they are
`endpoint` and `pull`). The endpoint was silently never set.
docs/DEPLOY_AND_MIGRATIONS.md documents expand/contract as the rule for schema
changes: Prisma has no down-migrations, so a code rollback never rolls the
schema back, and restoring the replication master from a dump diverges every
replica.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>