From efc57a3af4adfb260f0f61372e681d5b8c1e7099 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 18:35:02 +0200 Subject: [PATCH 01/28] ci: shard cli-e2e, pack CLI e2e shards by estimated wall time, drop redundant Go build Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 18 +----- apps/cli/tests/e2e-sequencer.ts | 75 ++++++++++++++++++----- apps/cli/tests/e2e-sequencer.unit.test.ts | 41 ++++++++++--- 3 files changed, 97 insertions(+), 37 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d88906c01b..259059708f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -381,7 +381,8 @@ jobs: - { suite: cli, shard: 3, shards: 5, name: cli 3/5 } - { suite: cli, shard: 4, shards: 5, name: cli 4/5 } - { suite: cli, shard: 5, shards: 5, name: cli 5/5 } - - { suite: cli-e2e, name: cli-e2e } + - { suite: cli-e2e, shard: 1, shards: 2, name: cli-e2e 1/2 } + - { suite: cli-e2e, shard: 2, shards: 2, name: cli-e2e 2/2 } steps: - name: Checkout uses: useblacksmith/checkout@6fd481652155169ed4d2f25ebaf97464f685175f # v1.0.0-beta @@ -395,19 +396,6 @@ jobs: sticky-disks: "true" docker-images: "true" - - name: Cache Go CLI binary - id: cache-go-binary - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: apps/cli-go/supabase-go - key: go-cli-${{ runner.os }}-${{ hashFiles('apps/cli-go/**/*.go', - 'apps/cli-go/go.mod', 'apps/cli-go/go.sum') }} - - - name: Build Go CLI - if: steps.cache-go-binary.outputs.cache-hit != 'true' - run: go build -o supabase-go . - working-directory: apps/cli-go - # The e2e harness invokes `node apps/cli/dist/supabase.js` with # `SUPABASE_CLI_BINARY_OVERRIDE` pointing at the compiled binary in # `apps/cli/dist/`. Build the CLI explicitly before invoking every @@ -424,7 +412,7 @@ jobs: - name: Run cli-e2e end-to-end tests if: matrix.suite == 'cli-e2e' - run: pnpm exec turbo run test:e2e:run --only --filter=@supabase/cli-e2e + run: pnpm exec turbo run test:e2e:run --only --filter=@supabase/cli-e2e -- --shard=${{ matrix.shard }}/${{ matrix.shards }} env: SUPABASE_GO_BINARY: ${{ github.workspace }}/apps/cli-go/supabase-go diff --git a/apps/cli/tests/e2e-sequencer.ts b/apps/cli/tests/e2e-sequencer.ts index 0ce50c1f43..c45c6ae8db 100644 --- a/apps/cli/tests/e2e-sequencer.ts +++ b/apps/cli/tests/e2e-sequencer.ts @@ -4,7 +4,9 @@ import { BaseSequencer, type TestSpecification } from "vitest/node"; /** Bun `--timings` file of measured e2e file durations, refreshed from CI artifacts. */ const TIMINGS_FILE = "tests/e2e-timings.json"; -const E2E_PROJECTS = new Set(["e2e", "e2e-exclusive"]); +const E2E_PROJECT = "e2e"; +const E2E_EXCLUSIVE_PROJECT = "e2e-exclusive"; +const E2E_PROJECTS = new Set([E2E_PROJECT, E2E_EXCLUSIVE_PROJECT]); /** Duration assumed for every file while no timing has been measured yet. */ export const DEFAULT_DURATION_MS = 30_000; @@ -28,31 +30,70 @@ function compareKeys(a: string, b: string): number { return a < b ? -1 : a > b ? 1 : 0; } +/** An e2e file to shard; exclusive files run one at a time after the concurrent ones. */ +export interface ShardFile { + readonly key: string; + readonly exclusive: boolean; +} + +interface ShardLoad { + readonly longestConcurrent: number; + readonly concurrentTotal: number; + readonly exclusiveTotal: number; +} + +function withFile(load: ShardLoad, duration: number, exclusive: boolean): ShardLoad { + return exclusive + ? { ...load, exclusiveTotal: load.exclusiveTotal + duration } + : { + ...load, + longestConcurrent: Math.max(load.longestConcurrent, duration), + concurrentTotal: load.concurrentTotal + duration, + }; +} + +function estimatedWall(load: ShardLoad, workers: number): number { + return Math.max(load.longestConcurrent, load.concurrentTotal / workers) + load.exclusiveTotal; +} + /** - * Splits keys into `count` shards by measured duration, longest first into the emptiest shard. - * The result depends only on the set of keys, so every shard process derives the same partition. + * Splits files into `count` shards by estimated wall time, longest first into the shard whose + * estimate stays lowest, the less loaded one on ties. Concurrent files share `workers` slots; + * exclusive files add their full duration. + * The result depends only on the set of files, so every shard process derives the same partition. */ export function packShards( - keys: readonly string[], + files: readonly ShardFile[], timings: Readonly>, count: number, + workers: number, ): string[][] { const fallback = fallbackDuration(timings); - const durations = keys - .map((key) => ({ key, duration: timings[key] ?? fallback })) + const durations = files + .map((file) => ({ ...file, duration: timings[file.key] ?? fallback })) .sort((a, b) => b.duration - a.duration || compareKeys(a.key, b.key)); - const shards = Array.from({ length: count }, () => ({ load: 0, keys: new Array() })); - for (const { key, duration } of durations) { - let target = shards[0]; + const empty: ShardLoad = { longestConcurrent: 0, concurrentTotal: 0, exclusiveTotal: 0 }; + const shards = Array.from({ length: count }, () => ({ keys: new Array(), load: empty })); + for (const { key, exclusive, duration } of durations) { + let target: + | { shard: (typeof shards)[number]; load: ShardLoad; wall: number; before: number } + | undefined; for (const shard of shards) { - if (target === undefined || shard.load < target.load) { - target = shard; + const load = withFile(shard.load, duration, exclusive); + const wall = estimatedWall(load, workers); + const before = estimatedWall(shard.load, workers); + if ( + target === undefined || + wall < target.wall || + (wall === target.wall && before < target.before) + ) { + target = { shard, load, wall, before }; } } if (target !== undefined) { - target.load += duration; - target.keys.push(key); + target.shard.load = target.load; + target.shard.keys.push(key); } } return shards.map((shard) => shard.keys); @@ -109,7 +150,13 @@ export class E2eSequencer extends BaseSequencer { const byKey = new Map( files.map((spec) => [relative(root, spec.moduleId).replaceAll("\\", "/"), spec]), ); - const assigned = packShards([...byKey.keys()], timings, shard.count)[shard.index - 1] ?? []; + const shardFiles = [...byKey].map(([key, spec]) => ({ + key, + exclusive: spec.project.name === E2E_EXCLUSIVE_PROJECT, + })); + const workers = + files.find((spec) => spec.project.name === E2E_PROJECT)?.project.config.maxWorkers ?? 1; + const assigned = packShards(shardFiles, timings, shard.count, workers)[shard.index - 1] ?? []; return assigned.flatMap((key) => { const spec = byKey.get(key); return spec === undefined ? [] : [spec]; diff --git a/apps/cli/tests/e2e-sequencer.unit.test.ts b/apps/cli/tests/e2e-sequencer.unit.test.ts index 1826cab568..d90a7e3813 100644 --- a/apps/cli/tests/e2e-sequencer.unit.test.ts +++ b/apps/cli/tests/e2e-sequencer.unit.test.ts @@ -9,11 +9,13 @@ const files = ["src/a.e2e.test.ts", "src/b.e2e.test.ts", "src/c.e2e.test.ts", "s const sortedShards = (shards: readonly (readonly string[])[]) => shards.map((shard) => [...shard].sort()); +const concurrent = (keys: readonly string[]) => keys.map((key) => ({ key, exclusive: false })); + describe("packShards", () => { test.each([1, files.length, files.length + 3])( "partitions every file into exactly one of %i shards", (count) => { - const shards = packShards(files, { "src/a.e2e.test.ts": 100 }, count); + const shards = packShards(concurrent(files), { "src/a.e2e.test.ts": 100 }, count, 1); expect(shards).toHaveLength(count); expect(shards.flat().sort()).toEqual([...files].sort()); @@ -21,7 +23,7 @@ describe("packShards", () => { ); test("leaves trailing shards empty when there are more shards than files", () => { - const shards = packShards(["src/a.e2e.test.ts", "src/b.e2e.test.ts"], {}, 4); + const shards = packShards(concurrent(["src/a.e2e.test.ts", "src/b.e2e.test.ts"]), {}, 4, 1); expect(shards).toEqual([["src/a.e2e.test.ts"], ["src/b.e2e.test.ts"], [], []]); }); @@ -33,8 +35,8 @@ describe("packShards", () => { "src/c.e2e.test.ts": 900, }; - const forward = packShards(files, timings, 2); - const reversed = packShards([...files].reverse(), timings, 2); + const forward = packShards(concurrent(files), timings, 2, 1); + const reversed = packShards(concurrent([...files].reverse()), timings, 2, 1); expect(reversed).toEqual(forward); }); @@ -47,7 +49,7 @@ describe("packShards", () => { "src/d.e2e.test.ts": 10, }; - const shards = packShards(files, timings, 2); + const shards = packShards(concurrent(files), timings, 2, 1); expect(sortedShards(shards)).toEqual([ ["src/b.e2e.test.ts"], @@ -55,6 +57,27 @@ describe("packShards", () => { ]); }); + test("packs concurrent files beside a long one while an exclusive file runs alone", () => { + const timings = { + "src/long.e2e.test.ts": 90, + "src/exclusive.e2e.test.ts": 80, + "src/a.e2e.test.ts": 20, + "src/b.e2e.test.ts": 20, + "src/c.e2e.test.ts": 20, + }; + const shardFiles = Object.keys(timings).map((key) => ({ + key, + exclusive: key === "src/exclusive.e2e.test.ts", + })); + + const shards = packShards(shardFiles, timings, 2, 4); + + expect(sortedShards(shards)).toEqual([ + ["src/a.e2e.test.ts", "src/b.e2e.test.ts", "src/c.e2e.test.ts", "src/long.e2e.test.ts"], + ["src/exclusive.e2e.test.ts"], + ]); + }); + test("weights an unmeasured file at the median of the measured ones", () => { const timings = { "src/a.e2e.test.ts": 100, @@ -63,7 +86,7 @@ describe("packShards", () => { }; // d is unmeasured; at the median (300) it ties with b and follows it into the second shard. - const shards = packShards(files, timings, 2); + const shards = packShards(concurrent(files), timings, 2, 1); expect(sortedShards(shards)).toEqual([ ["src/a.e2e.test.ts", "src/c.e2e.test.ts"], @@ -74,13 +97,15 @@ describe("packShards", () => { test("weights every file at the default duration when nothing has been measured", () => { const allDefault = Object.fromEntries(files.map((file) => [file, DEFAULT_DURATION_MS])); - expect(packShards(files, {}, 3)).toEqual(packShards(files, allDefault, 3)); + expect(packShards(concurrent(files), {}, 3, 1)).toEqual( + packShards(concurrent(files), allDefault, 3, 1), + ); }); test("breaks duration ties by key so equal files spread deterministically", () => { const timings = { "src/c.e2e.test.ts": 50, "src/a.e2e.test.ts": 50, "src/b.e2e.test.ts": 50 }; - const shards = packShards(Object.keys(timings), timings, 3); + const shards = packShards(concurrent(Object.keys(timings)), timings, 3, 1); expect(shards).toEqual([["src/a.e2e.test.ts"], ["src/b.e2e.test.ts"], ["src/c.e2e.test.ts"]]); }); From 07422a72c70b739bc576e7b69cec0d4ee298108f Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 19:20:59 +0200 Subject: [PATCH 02/28] ci: skip unused toolchains and the slow store archive on macOS and Windows test jobs Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 259059708f..1c92ef1de7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -244,8 +244,8 @@ jobs: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} go-cache: "false" tools: node bun pnpm - # Windows unpacks the store archive slower than it downloads the packages. - pnpm-store-cache: ${{ matrix.os != 'windows-latest' }} + # GitHub-hosted runners unpack the store archive slower than they download the packages. + pnpm-store-cache: "false" - name: Run stack integration tests working-directory: packages/stack @@ -317,6 +317,7 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} go-cache: "false" + tools: node bun pnpm # Keyed per suite so each job saves the artifacts it actually prepared. - name: Cache native stack artifacts From 605b9c80f26c3205283b4d57dc0a354a50129687 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 19:43:46 +0200 Subject: [PATCH 03/28] test(stack): create the parallel-isolation stacks concurrently; keep the macOS store archive Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 4 ++-- packages/stack/tests/whole-stack/scenarios.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c92ef1de7..28ca6ce89a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -244,8 +244,8 @@ jobs: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} go-cache: "false" tools: node bun pnpm - # GitHub-hosted runners unpack the store archive slower than they download the packages. - pnpm-store-cache: "false" + # Windows unpacks the store archive slower than it downloads the packages. + pnpm-store-cache: ${{ matrix.os != 'windows-latest' }} - name: Run stack integration tests working-directory: packages/stack diff --git a/packages/stack/tests/whole-stack/scenarios.ts b/packages/stack/tests/whole-stack/scenarios.ts index 9c3ac0ff43..5e4de4674e 100644 --- a/packages/stack/tests/whole-stack/scenarios.ts +++ b/packages/stack/tests/whole-stack/scenarios.ts @@ -401,8 +401,9 @@ export const allEager = (runtime: Runtime) => export const parallel = (runtime: Runtime) => Effect.scoped( Effect.gen(function* () { - const left = yield* wholeStack(runtime); - const right = yield* wholeStack(runtime); + const [left, right] = yield* Effect.all([wholeStack(runtime), wholeStack(runtime)], { + concurrency: "unbounded", + }); yield* Effect.all([assertDefaultPolicy(left), assertDefaultPolicy(right)], { concurrency: "unbounded", }); From 31b3eceaa0a5e7164d32a84147d83aba7d006cb3 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 19:57:29 +0200 Subject: [PATCH 04/28] ci: warm macOS and Windows test caches on protected branches Co-Authored-By: Claude Opus 5.5 --- .github/workflows/fill-sticky-disks.yml | 36 +++++++++++++++++++ .github/workflows/test.yml | 4 +-- packages/stack/tests/whole-stack/scenarios.ts | 5 ++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/.github/workflows/fill-sticky-disks.yml b/.github/workflows/fill-sticky-disks.yml index 2440f388d1..806616ee96 100644 --- a/.github/workflows/fill-sticky-disks.yml +++ b/.github/workflows/fill-sticky-disks.yml @@ -87,3 +87,39 @@ jobs: - name: Stop Docker so its image disk can be saved if: always() run: sudo systemctl stop docker.socket docker.service + + # macOS and Windows test jobs restore actions/cache entries, which pull + # requests and the merge queue can only read from their base branch. + warm-caches: + name: Warm caches (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: [blacksmith-6vcpu-macos-latest, macos-latest, windows-latest] + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # Inputs match the test jobs so the saved keys are the ones they restore. + - name: Setup + uses: ./.github/actions/setup + with: + dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} + go-cache: "false" + tools: node bun pnpm + pnpm-store-cache: ${{ matrix.os != 'windows-latest' }} + + - name: Cache native stack artifacts + if: matrix.os == 'blacksmith-6vcpu-macos-latest' + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/supabase-stack-artifacts + key: stack-artifacts-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('packages/stack/src/Artifacts.ts') }} + + - name: Download native stack artifacts + if: matrix.os == 'blacksmith-6vcpu-macos-latest' + run: pnpm --filter @supabase/stack prepare:test-artifacts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 28ca6ce89a..2c0dd1151d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -319,12 +319,12 @@ jobs: go-cache: "false" tools: node bun pnpm - # Keyed per suite so each job saves the artifacts it actually prepared. + # Fill sticky disks saves every artifact under this key on protected branches. - name: Cache native stack artifacts uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/supabase-stack-artifacts - key: stack-artifacts-${{ runner.os }}-${{ runner.arch }}-${{ matrix.suite }}-${{ hashFiles('packages/stack/src/Artifacts.ts') }} + key: stack-artifacts-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('packages/stack/src/Artifacts.ts') }} - name: Run stack native whole-stack scenarios working-directory: packages/stack diff --git a/packages/stack/tests/whole-stack/scenarios.ts b/packages/stack/tests/whole-stack/scenarios.ts index 5e4de4674e..9c3ac0ff43 100644 --- a/packages/stack/tests/whole-stack/scenarios.ts +++ b/packages/stack/tests/whole-stack/scenarios.ts @@ -401,9 +401,8 @@ export const allEager = (runtime: Runtime) => export const parallel = (runtime: Runtime) => Effect.scoped( Effect.gen(function* () { - const [left, right] = yield* Effect.all([wholeStack(runtime), wholeStack(runtime)], { - concurrency: "unbounded", - }); + const left = yield* wholeStack(runtime); + const right = yield* wholeStack(runtime); yield* Effect.all([assertDefaultPolicy(left), assertDefaultPolicy(right)], { concurrency: "unbounded", }); From acee9d72f15a1cd0042d067765340191809a1650 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 20:10:02 +0200 Subject: [PATCH 05/28] ci: run the Linux stack isolation scenario in its own job Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2c0dd1151d..220ac7dc22 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -448,7 +448,7 @@ jobs: runtime: [native, docker, podman] # Confinement watches the whole temp tree, so it gets a job of its own # instead of sharing one with concurrently running scenarios. - suite: [lifecycle, idle-parallel, confinement] + suite: [lifecycle, idle, isolation, confinement] env: SUPABASE_STACK_TEST_ENGINE: ${{ matrix.runtime == 'podman' && 'podman' || 'docker' }} steps: @@ -471,8 +471,8 @@ jobs: - name: Run stack end-to-end tests run: >- pnpm --filter @supabase/stack test:e2e:run - src/whole-stack.${{ matrix.runtime }}.${{ matrix.suite == 'idle-parallel' && 'idle-parallel' || 'lifecycle' }}.e2e.test.ts - --testNamePattern='${{ matrix.suite == 'confinement' && 'writes stay confined' || matrix.suite == 'lifecycle' && '^(?!.*writes stay confined)' || '.' }}' + src/whole-stack.${{ (matrix.suite == 'idle' || matrix.suite == 'isolation') && 'idle-parallel' || 'lifecycle' }}.e2e.test.ts + --testNamePattern='${{ matrix.suite == 'confinement' && 'writes stay confined' || matrix.suite == 'lifecycle' && '^(?!.*writes stay confined)' || matrix.suite == 'isolation' && 'parallel stack isolation' || '^(?!.*parallel stack isolation)' }}' --sequence.concurrent --passWithNoTests=false From e58f277f7ffae1097379cbdd399e9df1a9ace3e7 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 20:21:23 +0200 Subject: [PATCH 06/28] ci: fix the stack e2e file path Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 220ac7dc22..00237fdbba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -471,7 +471,7 @@ jobs: - name: Run stack end-to-end tests run: >- pnpm --filter @supabase/stack test:e2e:run - src/whole-stack.${{ (matrix.suite == 'idle' || matrix.suite == 'isolation') && 'idle-parallel' || 'lifecycle' }}.e2e.test.ts + src/whole-stack.${{ matrix.runtime }}.${{ (matrix.suite == 'idle' || matrix.suite == 'isolation') && 'idle-parallel' || 'lifecycle' }}.e2e.test.ts --testNamePattern='${{ matrix.suite == 'confinement' && 'writes stay confined' || matrix.suite == 'lifecycle' && '^(?!.*writes stay confined)' || matrix.suite == 'isolation' && 'parallel stack isolation' || '^(?!.*parallel stack isolation)' }}' --sequence.concurrent --passWithNoTests=false From 16aba5eba42fda3a5488b134e14fa139a29bf901 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 20:34:49 +0200 Subject: [PATCH 07/28] Revert "ci: fix the stack e2e file path" This reverts commit e58f277f7ffae1097379cbdd399e9df1a9ace3e7. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 00237fdbba..220ac7dc22 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -471,7 +471,7 @@ jobs: - name: Run stack end-to-end tests run: >- pnpm --filter @supabase/stack test:e2e:run - src/whole-stack.${{ matrix.runtime }}.${{ (matrix.suite == 'idle' || matrix.suite == 'isolation') && 'idle-parallel' || 'lifecycle' }}.e2e.test.ts + src/whole-stack.${{ (matrix.suite == 'idle' || matrix.suite == 'isolation') && 'idle-parallel' || 'lifecycle' }}.e2e.test.ts --testNamePattern='${{ matrix.suite == 'confinement' && 'writes stay confined' || matrix.suite == 'lifecycle' && '^(?!.*writes stay confined)' || matrix.suite == 'isolation' && 'parallel stack isolation' || '^(?!.*parallel stack isolation)' }}' --sequence.concurrent --passWithNoTests=false From bad4e2991f94cbfa47514ce351c0ee3b07670248 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 20:34:49 +0200 Subject: [PATCH 08/28] Revert "ci: run the Linux stack isolation scenario in its own job" This reverts commit acee9d72f15a1cd0042d067765340191809a1650. --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 220ac7dc22..2c0dd1151d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -448,7 +448,7 @@ jobs: runtime: [native, docker, podman] # Confinement watches the whole temp tree, so it gets a job of its own # instead of sharing one with concurrently running scenarios. - suite: [lifecycle, idle, isolation, confinement] + suite: [lifecycle, idle-parallel, confinement] env: SUPABASE_STACK_TEST_ENGINE: ${{ matrix.runtime == 'podman' && 'podman' || 'docker' }} steps: @@ -471,8 +471,8 @@ jobs: - name: Run stack end-to-end tests run: >- pnpm --filter @supabase/stack test:e2e:run - src/whole-stack.${{ (matrix.suite == 'idle' || matrix.suite == 'isolation') && 'idle-parallel' || 'lifecycle' }}.e2e.test.ts - --testNamePattern='${{ matrix.suite == 'confinement' && 'writes stay confined' || matrix.suite == 'lifecycle' && '^(?!.*writes stay confined)' || matrix.suite == 'isolation' && 'parallel stack isolation' || '^(?!.*parallel stack isolation)' }}' + src/whole-stack.${{ matrix.runtime }}.${{ matrix.suite == 'idle-parallel' && 'idle-parallel' || 'lifecycle' }}.e2e.test.ts + --testNamePattern='${{ matrix.suite == 'confinement' && 'writes stay confined' || matrix.suite == 'lifecycle' && '^(?!.*writes stay confined)' || '.' }}' --sequence.concurrent --passWithNoTests=false From 4628281edf1c49f3e0d37a709764e1113ed48511 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 20:48:42 +0200 Subject: [PATCH 09/28] Revert "ci: warm macOS and Windows test caches on protected branches" This reverts commit 31b3eceaa0a5e7164d32a84147d83aba7d006cb3. --- .github/workflows/fill-sticky-disks.yml | 36 ------------------- .github/workflows/test.yml | 4 +-- packages/stack/tests/whole-stack/scenarios.ts | 5 +-- 3 files changed, 5 insertions(+), 40 deletions(-) diff --git a/.github/workflows/fill-sticky-disks.yml b/.github/workflows/fill-sticky-disks.yml index 806616ee96..2440f388d1 100644 --- a/.github/workflows/fill-sticky-disks.yml +++ b/.github/workflows/fill-sticky-disks.yml @@ -87,39 +87,3 @@ jobs: - name: Stop Docker so its image disk can be saved if: always() run: sudo systemctl stop docker.socket docker.service - - # macOS and Windows test jobs restore actions/cache entries, which pull - # requests and the merge queue can only read from their base branch. - warm-caches: - name: Warm caches (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - os: [blacksmith-6vcpu-macos-latest, macos-latest, windows-latest] - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - # Inputs match the test jobs so the saved keys are the ones they restore. - - name: Setup - uses: ./.github/actions/setup - with: - dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} - go-cache: "false" - tools: node bun pnpm - pnpm-store-cache: ${{ matrix.os != 'windows-latest' }} - - - name: Cache native stack artifacts - if: matrix.os == 'blacksmith-6vcpu-macos-latest' - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.cache/supabase-stack-artifacts - key: stack-artifacts-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('packages/stack/src/Artifacts.ts') }} - - - name: Download native stack artifacts - if: matrix.os == 'blacksmith-6vcpu-macos-latest' - run: pnpm --filter @supabase/stack prepare:test-artifacts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2c0dd1151d..28ca6ce89a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -319,12 +319,12 @@ jobs: go-cache: "false" tools: node bun pnpm - # Fill sticky disks saves every artifact under this key on protected branches. + # Keyed per suite so each job saves the artifacts it actually prepared. - name: Cache native stack artifacts uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/supabase-stack-artifacts - key: stack-artifacts-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('packages/stack/src/Artifacts.ts') }} + key: stack-artifacts-${{ runner.os }}-${{ runner.arch }}-${{ matrix.suite }}-${{ hashFiles('packages/stack/src/Artifacts.ts') }} - name: Run stack native whole-stack scenarios working-directory: packages/stack diff --git a/packages/stack/tests/whole-stack/scenarios.ts b/packages/stack/tests/whole-stack/scenarios.ts index 9c3ac0ff43..5e4de4674e 100644 --- a/packages/stack/tests/whole-stack/scenarios.ts +++ b/packages/stack/tests/whole-stack/scenarios.ts @@ -401,8 +401,9 @@ export const allEager = (runtime: Runtime) => export const parallel = (runtime: Runtime) => Effect.scoped( Effect.gen(function* () { - const left = yield* wholeStack(runtime); - const right = yield* wholeStack(runtime); + const [left, right] = yield* Effect.all([wholeStack(runtime), wholeStack(runtime)], { + concurrency: "unbounded", + }); yield* Effect.all([assertDefaultPolicy(left), assertDefaultPolicy(right)], { concurrency: "unbounded", }); From 7e9a00f8f2b999466cbfb0b0be8cd7761cb58c0e Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 20:59:20 +0200 Subject: [PATCH 10/28] test(stack): restore sequential stack creation in the isolation scenario Co-Authored-By: Claude Opus 5.5 --- packages/stack/tests/whole-stack/scenarios.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/stack/tests/whole-stack/scenarios.ts b/packages/stack/tests/whole-stack/scenarios.ts index 5e4de4674e..9c3ac0ff43 100644 --- a/packages/stack/tests/whole-stack/scenarios.ts +++ b/packages/stack/tests/whole-stack/scenarios.ts @@ -401,9 +401,8 @@ export const allEager = (runtime: Runtime) => export const parallel = (runtime: Runtime) => Effect.scoped( Effect.gen(function* () { - const [left, right] = yield* Effect.all([wholeStack(runtime), wholeStack(runtime)], { - concurrency: "unbounded", - }); + const left = yield* wholeStack(runtime); + const right = yield* wholeStack(runtime); yield* Effect.all([assertDefaultPolicy(left), assertDefaultPolicy(right)], { concurrency: "unbounded", }); From eb0efbf712b5fc000b19f148bc38949187f4ac6d Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 21:11:05 +0200 Subject: [PATCH 11/28] test(cli): wait for the final Postgres server in the gen types e2e Co-Authored-By: Claude Opus 5.5 --- apps/cli/src/commands/gen/types/types.e2e.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/cli/src/commands/gen/types/types.e2e.test.ts b/apps/cli/src/commands/gen/types/types.e2e.test.ts index 78997f9979..002cf43717 100644 --- a/apps/cli/src/commands/gen/types/types.e2e.test.ts +++ b/apps/cli/src/commands/gen/types/types.e2e.test.ts @@ -167,6 +167,9 @@ const waitForLocalPostgres = Effect.fnUntraced(function* (containerName: string) "PGPASSWORD=postgres", containerName, "psql", + // The entrypoint's init server listens only on the Unix socket, so TCP reaches the final server. + "-h", + "127.0.0.1", "-U", "postgres", "-d", From 80e80252d077c46fcdde0b097c1e217cb4890e35 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 21:20:48 +0200 Subject: [PATCH 12/28] ci: compare installing every toolchain on macOS native scenarios Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 28ca6ce89a..259059708f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -317,7 +317,6 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} go-cache: "false" - tools: node bun pnpm # Keyed per suite so each job saves the artifacts it actually prepared. - name: Cache native stack artifacts From c69ff757af2f60c38b5d2cceadb3bb04b653ad0b Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 21:32:40 +0200 Subject: [PATCH 13/28] ci: measure cold macOS native scenario caches Co-Authored-By: Claude Opus 5.5 --- .github/actions/setup/action.yml | 11 ++++++++--- .github/workflows/test.yml | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 8e503fe8f6..07a4dd508b 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -32,6 +32,10 @@ inputs: archive restores slower than the registry downloads it, disable this. required: false default: "true" + cache-salt: + description: Temporary measurement salt appended to cache keys. + required: false + default: "" sticky-disks: description: >- Whether to mount dependency caches from Blacksmith sticky disks on @@ -130,6 +134,7 @@ runs: with: version: 2026.9.0 install_args: ${{ inputs.tools }} + cache_key_prefix: mise-v1${{ inputs.cache-salt }} cache_key: ${{ inputs.tools != '' && format('mise-{0}-{1}-{2}-{3}', runner.os, runner.arch, inputs.tools, hashFiles('mise.toml', 'mise.lock', '.node-version', '.bun-version', 'package.json')) || '' }} - name: Resolve pnpm store path @@ -168,9 +173,9 @@ runs: path: | ${{ steps.pnpm-store.outputs.path }}/files ${{ steps.pnpm-store.outputs.path }}/index* - key: pnpm-store-files-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }} + key: pnpm-store-files${{ inputs.cache-salt }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }} restore-keys: | - pnpm-store-files-${{ runner.os }}-${{ runner.arch }}- + pnpm-store-files${{ inputs.cache-salt }}-${{ runner.os }}-${{ runner.arch }}- - name: Resolve pnpm cache path if: inputs.dependency-cache == 'true' @@ -187,7 +192,7 @@ runs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.pnpm-cache.outputs.path }}/lockfile-verified.jsonl - key: pnpm-verify-${{ runner.os }}-${{ inputs.dependency-firewall-token != '' && 'firewall' || 'public' }}-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml') }} + key: pnpm-verify${{ inputs.cache-salt }}-${{ runner.os }}-${{ inputs.dependency-firewall-token != '' && 'firewall' || 'public' }}-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml') }} - name: Resolve Go cache paths if: inputs.dependency-cache == 'true' && inputs.go-cache == 'true' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 259059708f..f4bdc96e0c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -317,13 +317,14 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} go-cache: "false" + cache-salt: -cold${{ github.run_id }}-${{ github.run_attempt }} # Keyed per suite so each job saves the artifacts it actually prepared. - name: Cache native stack artifacts uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/supabase-stack-artifacts - key: stack-artifacts-${{ runner.os }}-${{ runner.arch }}-${{ matrix.suite }}-${{ hashFiles('packages/stack/src/Artifacts.ts') }} + key: stack-artifacts-cold${{ github.run_id }}-${{ github.run_attempt }}-${{ runner.os }}-${{ runner.arch }}-${{ matrix.suite }}-${{ hashFiles('packages/stack/src/Artifacts.ts') }} - name: Run stack native whole-stack scenarios working-directory: packages/stack From f889c3228a0144844a171ce9a7e42800baf491d9 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 21:53:06 +0200 Subject: [PATCH 14/28] Revert "ci: measure cold macOS native scenario caches" This reverts commit c69ff757af2f60c38b5d2cceadb3bb04b653ad0b. --- .github/actions/setup/action.yml | 11 +++-------- .github/workflows/test.yml | 3 +-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 07a4dd508b..8e503fe8f6 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -32,10 +32,6 @@ inputs: archive restores slower than the registry downloads it, disable this. required: false default: "true" - cache-salt: - description: Temporary measurement salt appended to cache keys. - required: false - default: "" sticky-disks: description: >- Whether to mount dependency caches from Blacksmith sticky disks on @@ -134,7 +130,6 @@ runs: with: version: 2026.9.0 install_args: ${{ inputs.tools }} - cache_key_prefix: mise-v1${{ inputs.cache-salt }} cache_key: ${{ inputs.tools != '' && format('mise-{0}-{1}-{2}-{3}', runner.os, runner.arch, inputs.tools, hashFiles('mise.toml', 'mise.lock', '.node-version', '.bun-version', 'package.json')) || '' }} - name: Resolve pnpm store path @@ -173,9 +168,9 @@ runs: path: | ${{ steps.pnpm-store.outputs.path }}/files ${{ steps.pnpm-store.outputs.path }}/index* - key: pnpm-store-files${{ inputs.cache-salt }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }} + key: pnpm-store-files-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }} restore-keys: | - pnpm-store-files${{ inputs.cache-salt }}-${{ runner.os }}-${{ runner.arch }}- + pnpm-store-files-${{ runner.os }}-${{ runner.arch }}- - name: Resolve pnpm cache path if: inputs.dependency-cache == 'true' @@ -192,7 +187,7 @@ runs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.pnpm-cache.outputs.path }}/lockfile-verified.jsonl - key: pnpm-verify${{ inputs.cache-salt }}-${{ runner.os }}-${{ inputs.dependency-firewall-token != '' && 'firewall' || 'public' }}-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml') }} + key: pnpm-verify-${{ runner.os }}-${{ inputs.dependency-firewall-token != '' && 'firewall' || 'public' }}-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml') }} - name: Resolve Go cache paths if: inputs.dependency-cache == 'true' && inputs.go-cache == 'true' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f4bdc96e0c..259059708f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -317,14 +317,13 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} go-cache: "false" - cache-salt: -cold${{ github.run_id }}-${{ github.run_attempt }} # Keyed per suite so each job saves the artifacts it actually prepared. - name: Cache native stack artifacts uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/supabase-stack-artifacts - key: stack-artifacts-cold${{ github.run_id }}-${{ github.run_attempt }}-${{ runner.os }}-${{ runner.arch }}-${{ matrix.suite }}-${{ hashFiles('packages/stack/src/Artifacts.ts') }} + key: stack-artifacts-${{ runner.os }}-${{ runner.arch }}-${{ matrix.suite }}-${{ hashFiles('packages/stack/src/Artifacts.ts') }} - name: Run stack native whole-stack scenarios working-directory: packages/stack From 906f7710d792068b089e0cc5b2331f69fcf2a5d8 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 22:14:46 +0200 Subject: [PATCH 15/28] ci: run the macOS confinement scenario after idle to need one runner fewer Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 259059708f..28fefecc93 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -298,12 +298,12 @@ jobs: # Native artifacts exist for macOS and Linux only, and macOS runners have # no Docker; Linux coverage for both backends comes from test-stack-e2e. os: [blacksmith-6vcpu-macos-latest] - suite: [lifecycle, confinement, idle, isolation] + suite: [lifecycle, idle, isolation] # Concurrent scenarios slow each other on macOS runners, so the - # heaviest ones run alone. + # heaviest ones run alone; the short confinement scenario follows idle + # so a run needs one runner fewer from the small macOS pool. include: - { suite: lifecycle, file: lifecycle, pattern: "^(?!.*writes stay confined)" } - - { suite: confinement, file: lifecycle, pattern: "writes stay confined" } - { suite: idle, file: idle-parallel, pattern: "^(?!.*parallel stack isolation)" } - { suite: isolation, file: idle-parallel, pattern: "parallel stack isolation" } steps: @@ -334,6 +334,15 @@ jobs: --sequence.concurrent --passWithNoTests=false + - name: Run stack native confinement scenario + if: matrix.suite == 'idle' + working-directory: packages/stack + run: >- + pnpm test:e2e:run + src/whole-stack.native.lifecycle.e2e.test.ts + --testNamePattern='writes stay confined' + --passWithNoTests=false + test-summary: if: | always() && From 9b06eb13b7760c3a6aed06ab80ac0c130fb1575b Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 22:47:23 +0200 Subject: [PATCH 16/28] Revert "ci: run the macOS confinement scenario after idle to need one runner fewer" This reverts commit 906f7710d792068b089e0cc5b2331f69fcf2a5d8. --- .github/workflows/test.yml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 28fefecc93..259059708f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -298,12 +298,12 @@ jobs: # Native artifacts exist for macOS and Linux only, and macOS runners have # no Docker; Linux coverage for both backends comes from test-stack-e2e. os: [blacksmith-6vcpu-macos-latest] - suite: [lifecycle, idle, isolation] + suite: [lifecycle, confinement, idle, isolation] # Concurrent scenarios slow each other on macOS runners, so the - # heaviest ones run alone; the short confinement scenario follows idle - # so a run needs one runner fewer from the small macOS pool. + # heaviest ones run alone. include: - { suite: lifecycle, file: lifecycle, pattern: "^(?!.*writes stay confined)" } + - { suite: confinement, file: lifecycle, pattern: "writes stay confined" } - { suite: idle, file: idle-parallel, pattern: "^(?!.*parallel stack isolation)" } - { suite: isolation, file: idle-parallel, pattern: "parallel stack isolation" } steps: @@ -334,15 +334,6 @@ jobs: --sequence.concurrent --passWithNoTests=false - - name: Run stack native confinement scenario - if: matrix.suite == 'idle' - working-directory: packages/stack - run: >- - pnpm test:e2e:run - src/whole-stack.native.lifecycle.e2e.test.ts - --testNamePattern='writes stay confined' - --passWithNoTests=false - test-summary: if: | always() && From 67cf450fb002e33196244a2ee4637648fb9fd458 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 22:57:35 +0200 Subject: [PATCH 17/28] ci: try GitHub-hosted macOS runners for native scenarios Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 259059708f..6e1d63ce3a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -297,7 +297,7 @@ jobs: matrix: # Native artifacts exist for macOS and Linux only, and macOS runners have # no Docker; Linux coverage for both backends comes from test-stack-e2e. - os: [blacksmith-6vcpu-macos-latest] + os: [macos-latest] suite: [lifecycle, confinement, idle, isolation] # Concurrent scenarios slow each other on macOS runners, so the # heaviest ones run alone. From 73a3cbdb173b758631410eeccfbdbda608d16ac1 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 23:25:50 +0200 Subject: [PATCH 18/28] Revert "ci: try GitHub-hosted macOS runners for native scenarios" This reverts commit 67cf450fb002e33196244a2ee4637648fb9fd458. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6e1d63ce3a..259059708f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -297,7 +297,7 @@ jobs: matrix: # Native artifacts exist for macOS and Linux only, and macOS runners have # no Docker; Linux coverage for both backends comes from test-stack-e2e. - os: [macos-latest] + os: [blacksmith-6vcpu-macos-latest] suite: [lifecycle, confinement, idle, isolation] # Concurrent scenarios slow each other on macOS runners, so the # heaviest ones run alone. From 93e7868c5a2b8d92c6f8be4769e37f289c181898 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 23:43:59 +0200 Subject: [PATCH 19/28] ci: run macOS port tests on Blacksmith runners Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 259059708f..f2fe2ca454 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -231,7 +231,7 @@ jobs: matrix: # Linux coverage for these files comes from the integration job, which # runs the whole stack integration project. - os: [macos-latest, windows-latest] + os: [blacksmith-6vcpu-macos-latest, windows-latest] steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 From 8385f052ea48d0b3d55d4ea4ef84af53af504da7 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 23:55:01 +0200 Subject: [PATCH 20/28] test(stack): time parallel isolation phases Co-Authored-By: Claude Opus 5.5 --- packages/stack/tests/whole-stack/scenarios.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/stack/tests/whole-stack/scenarios.ts b/packages/stack/tests/whole-stack/scenarios.ts index 9c3ac0ff43..554100347c 100644 --- a/packages/stack/tests/whole-stack/scenarios.ts +++ b/packages/stack/tests/whole-stack/scenarios.ts @@ -401,6 +401,7 @@ export const allEager = (runtime: Runtime) => export const parallel = (runtime: Runtime) => Effect.scoped( Effect.gen(function* () { + console.log(`PHASE ${runtime} create ${Date.now()}`); const left = yield* wholeStack(runtime); const right = yield* wholeStack(runtime); yield* Effect.all([assertDefaultPolicy(left), assertDefaultPolicy(right)], { @@ -409,10 +410,12 @@ export const parallel = (runtime: Runtime) => yield* Effect.all([clearIdleTimers(left), clearIdleTimers(right)], { concurrency: "unbounded", }); + console.log(`PHASE ${runtime} start ${Date.now()}`); yield* Effect.all([left.stack.composition.start, right.stack.composition.start], { concurrency: "unbounded", }); expect(left.stack.id).not.toBe(right.stack.id); + console.log(`PHASE ${runtime} exercise ${Date.now()}`); const [leftLedger, rightLedger] = yield* Effect.all( [ exerciseStack(left, "parallel-left", "parallel-shared"), @@ -443,6 +446,7 @@ export const parallel = (runtime: Runtime) => expect(new Set([...leftEndpoints, ...rightEndpoints]).size).toBe( leftEndpoints.length + rightEndpoints.length, ); + console.log(`PHASE ${runtime} checks ${Date.now()}`); const leftAnalyticsUrl = (yield* service(left, "analytics").credentials()).url; const rightAnalyticsUrl = (yield* service(right, "analytics").credentials()).url; if (leftAnalyticsUrl === undefined || rightAnalyticsUrl === undefined) @@ -481,6 +485,7 @@ export const parallel = (runtime: Runtime) => "whole_stack_items", "UPDATE", ); + console.log(`PHASE ${runtime} stop-left ${Date.now()}`); yield* stopWithDiagnostics(left); const reopenedLeft = yield* open({ ...left.locations, id: left.stack.id }); yield* setStackOwner(left, reopenedLeft); @@ -491,6 +496,7 @@ export const parallel = (runtime: Runtime) => }; yield* clearIdleTimers(reopenedLeftFixture); yield* refreshLogTails(reopenedLeftFixture); + console.log(`PHASE ${runtime} reopen-start ${Date.now()}`); yield* reopenedLeft.composition.start; expect( yield* sql( @@ -499,6 +505,7 @@ export const parallel = (runtime: Runtime) => ), ).toBe("parallel-shared"); yield* assertStoredImage(reopenedLeftFixture, "parallel-shared"); + console.log(`PHASE ${runtime} reopen-exercise ${Date.now()}`); const reopenedLedger = yield* exerciseStack( reopenedLeftFixture, "parallel-left-reopened", @@ -507,11 +514,13 @@ export const parallel = (runtime: Runtime) => const reopenedWorkloads = yield* captureWorkloads(runtime, reopenedLeftFixture); yield* assertOwnedPathsPresent(reopenedLeftFixture); const ownerPid = yield* captureOwnerPid(reopenedLeftFixture.locations, reopenedLeft.id); + console.log(`PHASE ${runtime} destroy-left ${Date.now()}`); yield* reopenedLeft.destroy; yield* assertOwnerExited(ownerPid); yield* clearStackOwner(left); yield* assertWorkloadsGone(runtime, reopenedLeftFixture, reopenedWorkloads, true); yield* assertOwnedPathsGone(reopenedLeftFixture); + console.log(`PHASE ${runtime} right-checks ${Date.now()}`); expect((yield* rightRest.status).lifecycle).toBe("running"); yield* assertStoredMarker(right, "parallel-shared"); const update = yield* jsonRequest( @@ -535,7 +544,9 @@ export const parallel = (runtime: Runtime) => }); }), ); + console.log(`PHASE ${runtime} right-after ${Date.now()}`); yield* exerciseStack(right, "parallel-right-after"); + console.log(`PHASE ${runtime} end ${Date.now()}`); }), ); From 6b122981115bfedf1108e4ae723062b024c14803 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 23:58:37 +0200 Subject: [PATCH 21/28] test(stack): time exercise phases Co-Authored-By: Claude Opus 5.5 --- packages/stack/tests/whole-stack/scenarios.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/stack/tests/whole-stack/scenarios.ts b/packages/stack/tests/whole-stack/scenarios.ts index 554100347c..e48e78c3f1 100644 --- a/packages/stack/tests/whole-stack/scenarios.ts +++ b/packages/stack/tests/whole-stack/scenarios.ts @@ -181,6 +181,7 @@ const exerciseStack = Effect.fn("WholeStack.exerciseStack")( (fixture: WholeStack, phase: string, resourceKey?: string) => Effect.scoped( Effect.gen(function* () { + console.log(`XPHASE ${phase} schema ${Date.now()}`); yield* prepareSchema(fixture); const authUrl = (yield* service(fixture, "auth").credentials()).url; const restUrl = (yield* service(fixture, "rest").credentials()).url; @@ -188,6 +189,7 @@ const exerciseStack = Effect.fn("WholeStack.exerciseStack")( return yield* Effect.die("Auth/REST URL missing"); const email = `whole-${phase}-${fixture.stack.id}@example.test`; const password = "whole-stack-password"; + console.log(`XPHASE ${phase} auth-rest ${Date.now()}`); const signup = yield* jsonRequest("POST", `${authUrl}/signup`, { email, password }); expect(signup.status, signup.body).toBe(200); const signupBody = yield* Schema.decodeUnknownEffect( @@ -246,6 +248,7 @@ const exerciseStack = Effect.fn("WholeStack.exerciseStack")( Schema.fromJsonString(Schema.Array(Schema.Struct({ id: Schema.String }))), )(otherRead.body), ).toEqual([]); + console.log(`XPHASE ${phase} storage-functions ${Date.now()}`); yield* exerciseStorageAndFunctions( fixture, signupBody.access_token, @@ -255,6 +258,7 @@ const exerciseStack = Effect.fn("WholeStack.exerciseStack")( ); yield* Effect.scoped( Effect.gen(function* () { + console.log(`XPHASE ${phase} mail-realtime ${Date.now()}`); const mailUrl = (yield* service(fixture, "mail").credentials()).url; const realtimeUrl = (yield* service(fixture, "realtime").credentials()).url; if (mailUrl === undefined || realtimeUrl === undefined) @@ -294,10 +298,13 @@ const exerciseStack = Effect.fn("WholeStack.exerciseStack")( expect(event.record).toEqual({ id: rowId, value: `${phase}-realtime` }); }), ); + console.log(`XPHASE ${phase} analytics ${Date.now()}`); const analyticsUrl = (yield* service(fixture, "analytics").credentials()).url; if (analyticsUrl === undefined) return yield* Effect.die("Analytics URL missing"); yield* exerciseAnalytics(analyticsUrl, fixture.secret, fixture.stack.id, phase); + console.log(`XPHASE ${phase} lifecycle-check ${Date.now()}`); yield* assertLifecycle(fixture, "running"); + console.log(`XPHASE ${phase} end ${Date.now()}`); return { accessToken: loginBody.access_token, rowId, From d5fa95de3587d4f9d3d2b682a7c37385bef53ed0 Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 15 Oct 2026 00:06:08 +0200 Subject: [PATCH 22/28] Revert "test(stack): time exercise phases" This reverts commit 6b122981115bfedf1108e4ae723062b024c14803. --- packages/stack/tests/whole-stack/scenarios.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/stack/tests/whole-stack/scenarios.ts b/packages/stack/tests/whole-stack/scenarios.ts index e48e78c3f1..554100347c 100644 --- a/packages/stack/tests/whole-stack/scenarios.ts +++ b/packages/stack/tests/whole-stack/scenarios.ts @@ -181,7 +181,6 @@ const exerciseStack = Effect.fn("WholeStack.exerciseStack")( (fixture: WholeStack, phase: string, resourceKey?: string) => Effect.scoped( Effect.gen(function* () { - console.log(`XPHASE ${phase} schema ${Date.now()}`); yield* prepareSchema(fixture); const authUrl = (yield* service(fixture, "auth").credentials()).url; const restUrl = (yield* service(fixture, "rest").credentials()).url; @@ -189,7 +188,6 @@ const exerciseStack = Effect.fn("WholeStack.exerciseStack")( return yield* Effect.die("Auth/REST URL missing"); const email = `whole-${phase}-${fixture.stack.id}@example.test`; const password = "whole-stack-password"; - console.log(`XPHASE ${phase} auth-rest ${Date.now()}`); const signup = yield* jsonRequest("POST", `${authUrl}/signup`, { email, password }); expect(signup.status, signup.body).toBe(200); const signupBody = yield* Schema.decodeUnknownEffect( @@ -248,7 +246,6 @@ const exerciseStack = Effect.fn("WholeStack.exerciseStack")( Schema.fromJsonString(Schema.Array(Schema.Struct({ id: Schema.String }))), )(otherRead.body), ).toEqual([]); - console.log(`XPHASE ${phase} storage-functions ${Date.now()}`); yield* exerciseStorageAndFunctions( fixture, signupBody.access_token, @@ -258,7 +255,6 @@ const exerciseStack = Effect.fn("WholeStack.exerciseStack")( ); yield* Effect.scoped( Effect.gen(function* () { - console.log(`XPHASE ${phase} mail-realtime ${Date.now()}`); const mailUrl = (yield* service(fixture, "mail").credentials()).url; const realtimeUrl = (yield* service(fixture, "realtime").credentials()).url; if (mailUrl === undefined || realtimeUrl === undefined) @@ -298,13 +294,10 @@ const exerciseStack = Effect.fn("WholeStack.exerciseStack")( expect(event.record).toEqual({ id: rowId, value: `${phase}-realtime` }); }), ); - console.log(`XPHASE ${phase} analytics ${Date.now()}`); const analyticsUrl = (yield* service(fixture, "analytics").credentials()).url; if (analyticsUrl === undefined) return yield* Effect.die("Analytics URL missing"); yield* exerciseAnalytics(analyticsUrl, fixture.secret, fixture.stack.id, phase); - console.log(`XPHASE ${phase} lifecycle-check ${Date.now()}`); yield* assertLifecycle(fixture, "running"); - console.log(`XPHASE ${phase} end ${Date.now()}`); return { accessToken: loginBody.access_token, rowId, From d2d5133819affc8a1bb58cb7758119fed85d1bba Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 15 Oct 2026 00:06:09 +0200 Subject: [PATCH 23/28] Revert "test(stack): time parallel isolation phases" This reverts commit 8385f052ea48d0b3d55d4ea4ef84af53af504da7. --- packages/stack/tests/whole-stack/scenarios.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/stack/tests/whole-stack/scenarios.ts b/packages/stack/tests/whole-stack/scenarios.ts index 554100347c..9c3ac0ff43 100644 --- a/packages/stack/tests/whole-stack/scenarios.ts +++ b/packages/stack/tests/whole-stack/scenarios.ts @@ -401,7 +401,6 @@ export const allEager = (runtime: Runtime) => export const parallel = (runtime: Runtime) => Effect.scoped( Effect.gen(function* () { - console.log(`PHASE ${runtime} create ${Date.now()}`); const left = yield* wholeStack(runtime); const right = yield* wholeStack(runtime); yield* Effect.all([assertDefaultPolicy(left), assertDefaultPolicy(right)], { @@ -410,12 +409,10 @@ export const parallel = (runtime: Runtime) => yield* Effect.all([clearIdleTimers(left), clearIdleTimers(right)], { concurrency: "unbounded", }); - console.log(`PHASE ${runtime} start ${Date.now()}`); yield* Effect.all([left.stack.composition.start, right.stack.composition.start], { concurrency: "unbounded", }); expect(left.stack.id).not.toBe(right.stack.id); - console.log(`PHASE ${runtime} exercise ${Date.now()}`); const [leftLedger, rightLedger] = yield* Effect.all( [ exerciseStack(left, "parallel-left", "parallel-shared"), @@ -446,7 +443,6 @@ export const parallel = (runtime: Runtime) => expect(new Set([...leftEndpoints, ...rightEndpoints]).size).toBe( leftEndpoints.length + rightEndpoints.length, ); - console.log(`PHASE ${runtime} checks ${Date.now()}`); const leftAnalyticsUrl = (yield* service(left, "analytics").credentials()).url; const rightAnalyticsUrl = (yield* service(right, "analytics").credentials()).url; if (leftAnalyticsUrl === undefined || rightAnalyticsUrl === undefined) @@ -485,7 +481,6 @@ export const parallel = (runtime: Runtime) => "whole_stack_items", "UPDATE", ); - console.log(`PHASE ${runtime} stop-left ${Date.now()}`); yield* stopWithDiagnostics(left); const reopenedLeft = yield* open({ ...left.locations, id: left.stack.id }); yield* setStackOwner(left, reopenedLeft); @@ -496,7 +491,6 @@ export const parallel = (runtime: Runtime) => }; yield* clearIdleTimers(reopenedLeftFixture); yield* refreshLogTails(reopenedLeftFixture); - console.log(`PHASE ${runtime} reopen-start ${Date.now()}`); yield* reopenedLeft.composition.start; expect( yield* sql( @@ -505,7 +499,6 @@ export const parallel = (runtime: Runtime) => ), ).toBe("parallel-shared"); yield* assertStoredImage(reopenedLeftFixture, "parallel-shared"); - console.log(`PHASE ${runtime} reopen-exercise ${Date.now()}`); const reopenedLedger = yield* exerciseStack( reopenedLeftFixture, "parallel-left-reopened", @@ -514,13 +507,11 @@ export const parallel = (runtime: Runtime) => const reopenedWorkloads = yield* captureWorkloads(runtime, reopenedLeftFixture); yield* assertOwnedPathsPresent(reopenedLeftFixture); const ownerPid = yield* captureOwnerPid(reopenedLeftFixture.locations, reopenedLeft.id); - console.log(`PHASE ${runtime} destroy-left ${Date.now()}`); yield* reopenedLeft.destroy; yield* assertOwnerExited(ownerPid); yield* clearStackOwner(left); yield* assertWorkloadsGone(runtime, reopenedLeftFixture, reopenedWorkloads, true); yield* assertOwnedPathsGone(reopenedLeftFixture); - console.log(`PHASE ${runtime} right-checks ${Date.now()}`); expect((yield* rightRest.status).lifecycle).toBe("running"); yield* assertStoredMarker(right, "parallel-shared"); const update = yield* jsonRequest( @@ -544,9 +535,7 @@ export const parallel = (runtime: Runtime) => }); }), ); - console.log(`PHASE ${runtime} right-after ${Date.now()}`); yield* exerciseStack(right, "parallel-right-after"); - console.log(`PHASE ${runtime} end ${Date.now()}`); }), ); From 8bb8faaf395af4388c0c54f7e6be91d129f9bb46 Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 15 Oct 2026 00:06:20 +0200 Subject: [PATCH 24/28] test(stack): wake Mail and Realtime while the earlier service checks run Co-Authored-By: Claude Opus 5.5 --- packages/stack/tests/whole-stack/scenarios.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/stack/tests/whole-stack/scenarios.ts b/packages/stack/tests/whole-stack/scenarios.ts index 9c3ac0ff43..ee93a8f92e 100644 --- a/packages/stack/tests/whole-stack/scenarios.ts +++ b/packages/stack/tests/whole-stack/scenarios.ts @@ -182,6 +182,18 @@ const exerciseStack = Effect.fn("WholeStack.exerciseStack")( Effect.scoped( Effect.gen(function* () { yield* prepareSchema(fixture); + // Waking Mail and Realtime is the slowest step; start it while auth, REST and storage run. + yield* Effect.forEach(["mail", "realtime"] as const, (name) => + service(fixture, name) + .credentials() + .pipe( + Effect.flatMap(({ url }) => + url === undefined ? Effect.void : requestWithHeaders(url, {}), + ), + Effect.ignore, + Effect.forkScoped, + ), + ); const authUrl = (yield* service(fixture, "auth").credentials()).url; const restUrl = (yield* service(fixture, "rest").credentials()).url; if (authUrl === undefined || restUrl === undefined) From be10f5b9aab00b40ea377a693eb9feaa88405cf0 Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 15 Oct 2026 00:19:46 +0200 Subject: [PATCH 25/28] test(cli): pack e2e shards against half the workers to spread Docker load Co-Authored-By: Claude Opus 5.5 --- apps/cli/tests/e2e-sequencer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/cli/tests/e2e-sequencer.ts b/apps/cli/tests/e2e-sequencer.ts index c45c6ae8db..aec487fedb 100644 --- a/apps/cli/tests/e2e-sequencer.ts +++ b/apps/cli/tests/e2e-sequencer.ts @@ -154,8 +154,10 @@ export class E2eSequencer extends BaseSequencer { key, exclusive: spec.project.name === E2E_EXCLUSIVE_PROJECT, })); - const workers = + const maxWorkers = files.find((spec) => spec.project.name === E2E_PROJECT)?.project.config.maxWorkers ?? 1; + // Concurrent files share one Docker daemon and slow each other, so pack against half the slots. + const workers = Math.max(1, Math.floor(maxWorkers / 2)); const assigned = packShards(shardFiles, timings, shard.count, workers)[shard.index - 1] ?? []; return assigned.flatMap((key) => { const spec = byKey.get(key); From fcfa59d8a59109b426f6f3573ddc2f6c7cb2130a Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 15 Oct 2026 00:30:36 +0200 Subject: [PATCH 26/28] Revert "test(stack): wake Mail and Realtime while the earlier service checks run" This reverts commit 8bb8faaf395af4388c0c54f7e6be91d129f9bb46. --- packages/stack/tests/whole-stack/scenarios.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/stack/tests/whole-stack/scenarios.ts b/packages/stack/tests/whole-stack/scenarios.ts index ee93a8f92e..9c3ac0ff43 100644 --- a/packages/stack/tests/whole-stack/scenarios.ts +++ b/packages/stack/tests/whole-stack/scenarios.ts @@ -182,18 +182,6 @@ const exerciseStack = Effect.fn("WholeStack.exerciseStack")( Effect.scoped( Effect.gen(function* () { yield* prepareSchema(fixture); - // Waking Mail and Realtime is the slowest step; start it while auth, REST and storage run. - yield* Effect.forEach(["mail", "realtime"] as const, (name) => - service(fixture, name) - .credentials() - .pipe( - Effect.flatMap(({ url }) => - url === undefined ? Effect.void : requestWithHeaders(url, {}), - ), - Effect.ignore, - Effect.forkScoped, - ), - ); const authUrl = (yield* service(fixture, "auth").credentials()).url; const restUrl = (yield* service(fixture, "rest").credentials()).url; if (authUrl === undefined || restUrl === undefined) From 041ca0f80b958edcf9b0d4854a996371d322cabb Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 15 Oct 2026 00:42:43 +0200 Subject: [PATCH 27/28] ci: try six CLI end-to-end shards Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f2fe2ca454..1594fa635e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -376,11 +376,12 @@ jobs: fail-fast: false matrix: include: - - { suite: cli, shard: 1, shards: 5, name: cli 1/5 } - - { suite: cli, shard: 2, shards: 5, name: cli 2/5 } - - { suite: cli, shard: 3, shards: 5, name: cli 3/5 } - - { suite: cli, shard: 4, shards: 5, name: cli 4/5 } - - { suite: cli, shard: 5, shards: 5, name: cli 5/5 } + - { suite: cli, shard: 1, shards: 6, name: cli 1/6 } + - { suite: cli, shard: 2, shards: 6, name: cli 2/6 } + - { suite: cli, shard: 3, shards: 6, name: cli 3/6 } + - { suite: cli, shard: 4, shards: 6, name: cli 4/6 } + - { suite: cli, shard: 5, shards: 6, name: cli 5/6 } + - { suite: cli, shard: 6, shards: 6, name: cli 6/6 } - { suite: cli-e2e, shard: 1, shards: 2, name: cli-e2e 1/2 } - { suite: cli-e2e, shard: 2, shards: 2, name: cli-e2e 2/2 } steps: From 828a4e0753c6985d9744af6d881792dd6ca48937 Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 15 Oct 2026 00:53:13 +0200 Subject: [PATCH 28/28] Revert "ci: try six CLI end-to-end shards" This reverts commit 041ca0f80b958edcf9b0d4854a996371d322cabb. --- .github/workflows/test.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1594fa635e..f2fe2ca454 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -376,12 +376,11 @@ jobs: fail-fast: false matrix: include: - - { suite: cli, shard: 1, shards: 6, name: cli 1/6 } - - { suite: cli, shard: 2, shards: 6, name: cli 2/6 } - - { suite: cli, shard: 3, shards: 6, name: cli 3/6 } - - { suite: cli, shard: 4, shards: 6, name: cli 4/6 } - - { suite: cli, shard: 5, shards: 6, name: cli 5/6 } - - { suite: cli, shard: 6, shards: 6, name: cli 6/6 } + - { suite: cli, shard: 1, shards: 5, name: cli 1/5 } + - { suite: cli, shard: 2, shards: 5, name: cli 2/5 } + - { suite: cli, shard: 3, shards: 5, name: cli 3/5 } + - { suite: cli, shard: 4, shards: 5, name: cli 4/5 } + - { suite: cli, shard: 5, shards: 5, name: cli 5/5 } - { suite: cli-e2e, shard: 1, shards: 2, name: cli-e2e 1/2 } - { suite: cli-e2e, shard: 2, shards: 2, name: cli-e2e 2/2 } steps: