diff --git a/Dockerfile b/Dockerfile index e2c6878a..4b380cb7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/scripts/deploy-homeserver.sh b/scripts/deploy-homeserver.sh index 550b34fc..80b4fd8d 100644 --- a/scripts/deploy-homeserver.sh +++ b/scripts/deploy-homeserver.sh @@ -8,7 +8,8 @@ # (The Forgejo-era invocation `ssh homeserver "bash -s -- " < 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 -> 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) ------------------------------------------------ diff --git a/tests/unit/deployScript.unit.spec.ts b/tests/unit/deployScript.unit.spec.ts index 489a168e..51861cc0 100644 --- a/tests/unit/deployScript.unit.spec.ts +++ b/tests/unit/deployScript.unit.spec.ts @@ -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') + }) })