fix(deploy): gera o importmap com a condition react-server e falha em stale run

OPS99 passou a rodar 'pnpm exec payload generate:importmap' no builder do
Dockerfile sem --conditions=react-server; o server-only resolve para o
módulo que lança e TODO build Docker falha — desde a OPS99 (27/08 19:05
UTC) nenhum deploy real aconteceu e a prod ficou no commit 4d6533c04.
Agora o builder chama 'pnpm generate:importmap' (o script dono das flags,
mesmo usado no predev).

O guard de stale run (main avançou durante o verify) fazia 'exit 0' e o
job reportava sucesso sem deployar (run 34675436246); agora falha com
fatal, sem falso verde.

Testes: unit guarda a linha do Dockerfile (script canônico) e os dois
pontos do guard de stale; build standalone de main validado localmente.
This commit is contained in:
Francisco Solla 2026-09-12 11:32:33 -03:00
parent 60b4e71dc1
commit e0c61d9075
3 changed files with 24 additions and 6 deletions

View file

@ -48,7 +48,7 @@ RUN --mount=type=secret,id=database_url,env=DATABASE_URL \
S3_SECRET_ACCESS_KEY=build-dummy \
NEXT_OUTPUT_STANDALONE=1 \
NODE_OPTIONS="--no-deprecation --max-old-space-size=8000" \
pnpm exec payload generate:importmap && \
pnpm generate:importmap && \
pnpm exec next build
# Production image, copy all the files and run next

View file

@ -8,7 +8,8 @@
# (The Forgejo-era invocation `ssh homeserver "bash -s -- <sha>" < script`
# is gone — no SSH hop, no workstation involvement.)
#
# Flow: HEAD guard (only the current main HEAD deploys) -> flock
# Flow: HEAD guard (only the current main HEAD deploys; a stale run FAILS the
# job — never a false green with prod left on the old image) -> flock
# serialization -> workspace fetch at <sha> -> docker login (local registry)
# -> build of the MIGRATOR stage (it never runs `next build`, so it builds
# even against the old schema) -> push of the migrator (registry-qualified
@ -47,8 +48,7 @@ fatal() {
main_head="$(git ls-remote "$TEQO_REPO_URL" refs/heads/main | awk '{print $1}')"
if [ "$main_head" != "$SHA" ]; then
say "stale run: main is $main_head, job deploys $SHA — skipping"
exit 0
fatal "stale run: main is $main_head, job deploys $SHA — refusing to deploy an outdated SHA"
fi
exec 9>"$DEPLOY_LOCK"
@ -56,8 +56,7 @@ flock -w 3600 9 || fatal "another deploy holds $DEPLOY_LOCK"
main_head="$(git ls-remote "$TEQO_REPO_URL" refs/heads/main | awk '{print $1}')"
if [ "$main_head" != "$SHA" ]; then
say "stale run after lock: main is $main_head, job deploys $SHA — skipping"
exit 0
fatal "stale run after lock: main is $main_head, job deploys $SHA — refusing to deploy an outdated SHA"
fi
# --- idempotency (OPS65) ------------------------------------------------

View file

@ -22,6 +22,13 @@ describe('scripts/deploy-homeserver.sh (OPS53 deploy pipeline)', () => {
expect(script).toContain('stale run')
})
it('fails the run when main advanced during verify — no silent false success', () => {
// The skip used to `exit 0`, so GitHub reported a green deploy that never
// happened while prod stayed on an older image (2026-09-12 incident).
expect(script).not.toMatch(/say "stale run/)
expect(script.match(/fatal "stale run/g) ?? []).toHaveLength(2)
})
it('skips idempotently when the running container already runs the SHA (OPS65)', () => {
expect(script).toContain('already deployed')
expect(script).toContain('docker inspect')
@ -103,4 +110,16 @@ describe('scripts/deploy-homeserver.sh (OPS53 deploy pipeline)', () => {
const builderStage = dockerfile.slice(dockerfile.indexOf('AS builder'))
expect(builderStage).toContain('next build')
})
it('Dockerfile: generates the importMap via the canonical script (react-server condition)', () => {
// OPS99 added the generator to the builder, but the bare
// `pnpm exec payload generate:importmap` misses `--conditions=react-server`;
// `server-only` then throws and every Docker build failed (2026-09-12
// incident — no deploy landed after OPS99). The package script owns the
// flags.
const dockerfile = readFileSync(join(repoRoot, 'Dockerfile'), 'utf8')
const builderStage = dockerfile.slice(dockerfile.indexOf('AS builder'))
expect(builderStage).toContain('pnpm generate:importmap')
expect(builderStage).not.toContain('pnpm exec payload generate:importmap')
})
})