Skip to content

Commit 7c0c742

Browse files
authored
test(repo): fix live e2e reboot flake and cut cancellations (#7016)
## TL;DR fixes the live e2e ssl reboot flake and stops develop pushes from cancelling a running live suite - `ssl-enforcement update` reboots the database and `get` can report it applied before the reboot ends, so the reboot landed in the next file and killed `migration up` with `57P01`. the test now waits for a new postmaster after the toggle and the restore, and `queryLiveDb` no longer crashes when a restart drops an idle client - merge queue bursts land develop pushes closer together than one live run, so each push cancelled the running suite and turned its commit red. pushes now share a lane that never cancels a running suite ## ref - spotted in: [migration up](https://github.com/supabase/cli/actions/runs/37284888772/job/111681321268) [cancelled run](https://github.com/supabase/cli/actions/runs/37296027587)
1 parent c81007b commit 7c0c742

5 files changed

Lines changed: 79 additions & 12 deletions

File tree

‎.github/MAINTAINERS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ from **Issues → Labels** if it is missing.
8787

8888
[`Live E2E`](./workflows/live-e2e.yml) exercises managed staging after every push
8989
to `develop`, daily at 06:23 UTC, and on manual dispatch. New `develop` pushes
90-
cancel superseded push runs; nightly and manual runs execute independently.
90+
replace only a queued push run; nightly and manual runs execute independently.
9191
Nightly runs do not depend on a new beta version: they also detect staging
9292
changes between CLI releases.
9393

‎.github/workflows/live-e2e-suite.yml‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@ on:
1212
required: false
1313
type: string
1414
default: live-e2e
15-
cancel_in_progress:
16-
description: Cancel an older run in the same group
17-
required: false
18-
type: boolean
19-
default: false
2015
secrets:
2116
SUPABASE_E2E_CLI_LIVE_STAGING_ACCESS_TOKEN:
2217
required: true
@@ -33,7 +28,7 @@ jobs:
3328
timeout-minutes: 30
3429
concurrency:
3530
group: ${{ inputs.concurrency_group }}
36-
cancel-in-progress: ${{ inputs.cancel_in_progress }}
31+
cancel-in-progress: false
3732
env:
3833
SUPABASE_LIVE_API_URL: https://api.supabase.green
3934
SUPABASE_LIVE_PROJECT_NAME: supabase-cli-live

‎.github/workflows/live-e2e.yml‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ jobs:
1717
uses: ./.github/workflows/live-e2e-suite.yml
1818
with:
1919
ref: ${{ github.sha }}
20-
# Pushes to develop share a branch group and cancel superseded pushes.
20+
# Pushes to develop share a branch group that never cancels a running suite.
2121
# Manual, scheduled, and stable-gate runs use independent groups because
2222
# they each own separate temporary projects and may run concurrently.
2323
concurrency_group: live-e2e-${{ github.event_name == 'push' && github.ref || github.run_id }}
24-
cancel_in_progress: ${{ github.event_name == 'push' }}
2524
secrets:
2625
SUPABASE_E2E_CLI_LIVE_STAGING_ACCESS_TOKEN: ${{ secrets.SUPABASE_E2E_CLI_LIVE_STAGING_ACCESS_TOKEN }}

‎apps/cli/src/commands/ssl-enforcement/update/update.live.test.ts‎

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { expect } from "vitest";
44
import {
55
experimentalProjectLiveFlags,
66
type LiveFixtures,
7+
queryLiveDb,
78
requireLiveSuccess,
89
test,
910
throwWithCleanup,
@@ -15,10 +16,11 @@ type LiveRun = Awaited<ReturnType<LiveFixtures["cli"]>>;
1516
// Bound the polled gets and the restore so one hung subprocess cannot exhaust
1617
// the live testTimeout and leave the shared project with a flipped posture.
1718
const POLL_ATTEMPT_EXIT_TIMEOUT_MS = 20_000;
19+
const DB_PROBE_TIMEOUT_MS = 10_000;
1820
const RESTORE_EXIT_TIMEOUT_MS = 60_000;
1921
const PROOF_INTERVAL_MS = 2_000;
2022
const PROOF_TIMEOUT_MS = 60_000;
21-
// Fits the worst case (bounded capture/toggle + two proofs + restore) with headroom.
23+
// Fits the worst case (bounded capture/toggle + proofs and reboot waits + restore) with headroom.
2224
const LIVE_TIMEOUT_MS = 600_000;
2325

2426
const SslEnforcementPosture = Schema.Struct({
@@ -108,6 +110,54 @@ function expectApplied(
108110
);
109111
}
110112

113+
function postmasterStart(dbUrl: string, label: string) {
114+
return Effect.tryPromise({
115+
try: () =>
116+
queryLiveDb<{ started: string }>(
117+
dbUrl,
118+
"select pg_postmaster_start_time()::text as started",
119+
undefined,
120+
DB_PROBE_TIMEOUT_MS,
121+
),
122+
catch: (error) =>
123+
new SslEnforcementLiveError({
124+
message: `${label}: ${error instanceof Error ? error.message : String(error)}`,
125+
cause: error,
126+
}),
127+
}).pipe(
128+
Effect.timeoutOrElse({
129+
duration: DB_PROBE_TIMEOUT_MS,
130+
orElse: () =>
131+
Effect.fail(
132+
new SslEnforcementLiveError({
133+
message: `${label}: no answer within ${DB_PROBE_TIMEOUT_MS}ms`,
134+
}),
135+
),
136+
}),
137+
Effect.flatMap(([row]) =>
138+
row === undefined
139+
? Effect.fail(
140+
new SslEnforcementLiveError({ message: `${label}: no postmaster start time` }),
141+
)
142+
: Effect.succeed(row.started),
143+
),
144+
);
145+
}
146+
147+
// Each posture change reboots the database, possibly after get already reports it applied.
148+
function awaitReboot(dbUrl: string, previous: string, label: string) {
149+
return postmasterStart(dbUrl, label).pipe(
150+
Effect.filterOrFail(
151+
(started) => started !== previous,
152+
() =>
153+
new SslEnforcementLiveError({
154+
message: `${label}: the postmaster started at ${previous} is still running`,
155+
}),
156+
),
157+
Effect.retry(proofSchedule),
158+
);
159+
}
160+
111161
// Not wired to the test `signal`: an interrupt SIGKILLs an in-flight restore
112162
// mid-request (the run's scope release kills the process group), so letting the
113163
// bounded restore run out is strictly safer.
@@ -125,6 +175,10 @@ test(
125175
RESTORE_EXIT_TIMEOUT_MS,
126176
);
127177
const posture = captured.currentConfig.database;
178+
const initialStart = yield* postmasterStart(
179+
project.dbUrl,
180+
"postmaster start capture for ssl-enforcement update",
181+
);
128182

129183
const toggle = Effect.gen(function* () {
130184
const updated = yield* cliEffect(
@@ -146,7 +200,13 @@ test(
146200
!posture,
147201
"ssl-enforcement get proof for ssl-enforcement update",
148202
);
203+
return yield* awaitReboot(
204+
project.dbUrl,
205+
initialStart,
206+
"database reboot after ssl-enforcement update",
207+
);
149208
});
209+
const toggleExit = yield* Effect.exit(toggle);
150210

151211
const restore = Effect.gen(function* () {
152212
const restored = yield* cliEffect(
@@ -160,10 +220,16 @@ test(
160220
posture,
161221
"ssl-enforcement get proof of the restored posture for ssl-enforcement update",
162222
);
223+
if (Exit.isSuccess(toggleExit)) {
224+
yield* awaitReboot(
225+
project.dbUrl,
226+
toggleExit.value,
227+
"database reboot after the ssl-enforcement update restore",
228+
);
229+
}
163230
});
164231

165232
// The restore runs whatever the toggle did; neither failure hides the other.
166-
const toggleExit = yield* Effect.exit(toggle);
167233
const restoreExit = yield* Effect.exit(restore);
168234
return {
169235
toggleError: Exit.isFailure(toggleExit) ? Cause.squash(toggleExit.cause) : undefined,

‎apps/cli/tests/helpers/live.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,16 @@ export async function queryLiveDb<T extends Record<string, unknown>>(
278278
dbUrl: string,
279279
query: string,
280280
values?: ReadonlyArray<unknown>,
281+
connectionTimeoutMillis?: number,
281282
): Promise<T[]> {
282283
// Verified TLS against the Supabase CA, so the query works whatever the project's SSL enforcement.
283-
const client = new pg.Client({ connectionString: dbUrl, ssl: { ca: rootCaBundle() } });
284+
const client = new pg.Client({
285+
connectionString: dbUrl,
286+
ssl: { ca: rootCaBundle() },
287+
connectionTimeoutMillis,
288+
});
289+
// A restart that drops an idle or closing client surfaces only as an `error` event.
290+
client.on("error", () => {});
284291
await client.connect();
285292
try {
286293
const result = await client.query(query, values === undefined ? undefined : [...values]);

0 commit comments

Comments
 (0)