Skip to content

Commit a5ea4b5

Browse files
dmealingclaude
andcommitted
ci: gate migrate-ts real-Postgres on every push, not after publish
The migrate-ts PG suites ran only on the v* tag push. Tags are pushed AFTER `bun publish`, so red arrived strictly after four immutable registries were already updated — which is why the lane sat red for eight consecutive releases (v0.20.11 through v0.21.1, repaired in #280) with nobody acting on it. The same suites `describe.skip` silently without MIGRATE_TS_PG_URL, so local runs, PR runs, and even conformance.yml's own migrate-ts step all looked green while the real-engine half executed nowhere except post-publish. ADR-0015 makes migrate-ts the project's only migrate engine, so this is the only real-engine gate on migration correctness. local-ci.yml's ts-slow lane now arms the suite from its EXISTING Postgres sidecar — no new container, no hosted minutes (the job takes ~50s; the "EXPENSIVE" label on integration-tests.yml belongs to the 5-port Testcontainers matrix, not this). It is ordered before the docker integration step so a container-readiness flake there cannot mask the migrate verdict. A sentinel test makes the silent skip loud where it matters: a lane that intends real PG sets MIGRATE_TS_PG_EXPECT=1 beside the URL, and the sentinel fails if the URL ever stops being set. A workflow-level `test -n` check could not do this — it inspects the workflow's env, not what the test process reads, so a rename inside the tests is precisely the drift it would miss. The skip stays the default for contributors without Postgres and for the deliberately DB-free lanes. The tag job is kept as the cold-environment release backstop (the self-hosted runner is warm), with its comment rewritten so red there reads as a broken release already live, not as noise. RELEASING.md gains a wait-for-local-ci-green step before publish — the last gate that can precede the irreversible one. Residual, stated plainly: red is now post-merge rather than pre-merge; a publish cut minutes after a merge can still beat the verdict; a cold-only failure still surfaces first on the tag; and if the self-hosted runner is down nothing goes red at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTGT5ksntpcJDZVJ5VyXHS
1 parent c499d50 commit a5ea4b5

6 files changed

Lines changed: 75 additions & 3 deletions

File tree

.github/workflows/conformance.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ jobs:
142142
cd ../integration-tests && bun test test/validation-conformance.test.ts
143143
# migrate-ts — the sole cross-port schema-migration engine (ADR-0015).
144144
# Its unit + integrity suites need no DB; the PG integration tests
145-
# (gated on MIGRATE_TS_PG_URL) self-skip here and run in integration-tests.yml.
145+
# (gated on MIGRATE_TS_PG_URL) self-skip here and run in local-ci's ts-slow
146+
# lane (every push to main) + integration-tests.yml (the v* tag backstop).
146147
cd ../migrate-ts && bun test
147148
# Doc-template + CLI suites: byte-identity template gate, embedded-template
148149
# gate, neutrality / collision guards, and the docs golden corpus +

.github/workflows/integration-tests.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,14 @@ jobs:
116116
# migrate-ts PG integration tests — the apply / lifecycle / rollback +
117117
# introspection suites that exercise REAL Postgres behavior (advisory locks,
118118
# multi-tenant ledger, down-migrations) that pg-mem cannot fake. They
119-
# `describe.skip` unless MIGRATE_TS_PG_URL is set, so no workflow ran them
120-
# until now. A `services: postgres` container supplies the URL.
119+
# `describe.skip` unless MIGRATE_TS_PG_URL is set; a `services: postgres`
120+
# container supplies the URL.
121+
#
122+
# The PRIMARY gate for these suites is local-ci.yml's ts-slow lane, on every push
123+
# to main. This job is the cold-environment RELEASE BACKSTOP on the v* tag. Tags
124+
# are pushed AFTER publish (docs/RELEASING.md), so red HERE means a broken release
125+
# is already live on four immutable registries — treat it as an incident, never as
126+
# noise. That inversion is exactly how this lane sat red for eight releases.
121127
migrate-ts-pg:
122128
runs-on: ubuntu-latest
123129
services:
@@ -150,4 +156,7 @@ jobs:
150156
- name: Run migrate-ts suite against real Postgres
151157
env:
152158
MIGRATE_TS_PG_URL: postgres://migrate:migrate@localhost:5432/migrate_test
159+
# Arms the in-suite sentinel: if the URL above ever stops being set, the
160+
# suite FAILS instead of silently skipping and reporting a green release gate.
161+
MIGRATE_TS_PG_EXPECT: '1'
153162
run: cd server/typescript/packages/migrate-ts && bun test

.github/workflows/local-ci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,16 @@ jobs:
140140
persist-credentials: false
141141
- env:
142142
METAOBJECTS_TEST_PG_URL: postgres://metaobjects:metaobjects@localhost:${{ job.services.postgres.ports['5432'] }}/metaobjects_test
143+
# Arms migrate-ts's real-Postgres suites (apply / lifecycle / rollback /
144+
# introspection) on every push to main. They previously ran ONLY on the v* tag
145+
# push — strictly AFTER the immutable four-registry publish — and so sat red for
146+
# eight straight releases (v0.20.11 … v0.21.1) with nobody looking. Reuses this
147+
# job's existing sidecar; the suite is proven to coexist in one database in a
148+
# serial run (the hosted tag job runs it against a single migrate_test DB).
149+
MIGRATE_TS_PG_URL: postgres://metaobjects:metaobjects@localhost:${{ job.services.postgres.ports['5432'] }}/metaobjects_test
150+
# Makes the in-suite sentinel FAIL if the URL above ever rots away (renamed
151+
# variable, dropped sidecar) rather than describe.skip-ing in silence.
152+
MIGRATE_TS_PG_EXPECT: '1'
143153
run: scripts/ci-local.sh --only ts-slow --strict-toolchains
144154

145155
# Java FAST lane — java + kotlin conformance only (the quick correctness signal).

docs/RELEASING.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,17 @@ A red run here has caught real cross-port divergence (view-DDL identifier quotin
198198
strategy mismatches) that the unit suites missed.
199199

200200
### 3. Promote to `latest`
201+
202+
**Before `bun publish`: confirm the `local-ci` run for the release commit is green.**
203+
Its `ts-slow` lane now carries the real-Postgres migrate gate. Publishing is irreversible on
204+
all four registries, and the `v*` tag is pushed *after* `bun publish` — so the tag-triggered
205+
`integration-tests` run can never be the pre-publish gate. This is the last gate that can
206+
precede the irreversible step.
207+
208+
```bash
209+
gh run list --workflow local-ci.yml --limit 1 --json headSha,conclusion
210+
```
211+
201212
```bash
202213
# bump the candidate set to the final <version>
203214
rm bun.lock && bun install

scripts/ci-local.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ gate_ts_build_typecheck() { bun_install && bun run --filter '*' build && bun run
146146
# integration tests, but the typecheck already runs in ts-fast, so don't repeat it.
147147
gate_ts_build() { bun_install && bun run --filter '*' build; }
148148

149+
# migrate-ts real-Postgres suites — apply / lifecycle / rollback / introspection against a
150+
# real engine. ADR-0015 makes migrate-ts the project's ONLY migrate engine, so this is the
151+
# ONLY real-engine gate on migration correctness. The CI ts-slow job supplies
152+
# MIGRATE_TS_PG_URL (its Postgres sidecar) plus MIGRATE_TS_PG_EXPECT=1, which arms an
153+
# in-suite sentinel that fails loudly if that URL ever stops being set. Without those env
154+
# vars (a local run with no Postgres) the PG describes self-skip exactly as before.
155+
gate_migrate_ts_pg() { bun_install && ( cd server/typescript/packages/migrate-ts && bun test ); }
156+
149157
# ── conformance.yml — per-port conformance corpora (exact CI commands) ────────
150158
gate_conf_ts() {
151159
bun_install || return 1
@@ -345,6 +353,9 @@ else
345353
# runs (umbrella `ts` / local full), its build already produced it — only build
346354
# here when ts-slow runs in isolation (the CI ts-slow job).
347355
if want_any ts ts-slow && ! want_any ts ts-fast; then step_if bun "ts build (for integration)" gate_ts_build; fi
356+
# Ordered BEFORE the docker integration step so a container-readiness flake there
357+
# can never prevent the migrate verdict from being produced.
358+
want_any ts ts-slow && step_if bun "migrate-ts real-PG suite" gate_migrate_ts_pg
348359
want_any ts ts-slow && run_integration_for ts ts
349360
want_any java java-slow && run_integration_for java java kotlin
350361
want python && run_integration_for python python
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Loud-skip guard for the real-Postgres suites in this package.
3+
*
4+
* Every one of them (`apply-pg`, `lifecycle-pg`, `postgres-roundtrip`,
5+
* `postgres-lenient-inet`, `pg-adopt-view-239`, …) `describe.skip`s when
6+
* `MIGRATE_TS_PG_URL` is unset. That is correct for a contributor with no local
7+
* Postgres — but in CI the same silence let the lane rot RED for eight consecutive
8+
* releases (v0.20.11 … v0.21.1) while every other lane stayed green, because the only
9+
* workflow that set the URL ran on the `v*` tag push, i.e. strictly AFTER the
10+
* irreversible four-registry publish.
11+
*
12+
* A lane that INTENDS to run these suites sets `MIGRATE_TS_PG_EXPECT=1` alongside the
13+
* URL. This test then fails loudly if the URL plumbing ever rots — a renamed variable,
14+
* a dropped sidecar, an unpublished port — instead of the suites quietly skipping and
15+
* the lane reporting success over zero real-engine coverage.
16+
*
17+
* A workflow-level `test -n "$MIGRATE_TS_PG_URL"` step cannot do this job: it checks the
18+
* workflow's environment, not what the test process actually reads, so a rename inside
19+
* the tests is exactly the drift it would miss. The URL-set-but-Postgres-broken arm needs
20+
* no sentinel — those suites already fail loudly on connect.
21+
*/
22+
import { describe, expect, test } from "bun:test";
23+
24+
describe("real-PG gate sentinel", () => {
25+
test("MIGRATE_TS_PG_URL is set when the lane declares MIGRATE_TS_PG_EXPECT=1", () => {
26+
if (process.env["MIGRATE_TS_PG_EXPECT"] === "1") {
27+
expect(process.env["MIGRATE_TS_PG_URL"]).toBeTruthy();
28+
}
29+
});
30+
});

0 commit comments

Comments
 (0)