Skip to content

Commit 8652c99

Browse files
authored
Make the test suite parallel-safe and speed up CI (#1273)
* Make self-host tests CI-safe * Await cloud db teardown * Give local e2e boots more headroom * Keep main CI runs alive * Cache CI dependencies and image layers * Wait for recorded introspection requests in graphql plugin tests * Size self-host test budget for loaded runners * Probe test http servers for readiness before use * Die on recorder wait timeout instead of typing a string error * Type the recorder wait timeout as a tagged error * Update e2e locators for the integration copy renames * Keep stale data rendered while a refresh is waiting * Guard create-org input against hydration clobbering * Match the new sign-in heading in the selfhost callback test
1 parent 9c7e4a6 commit 8652c99

27 files changed

Lines changed: 252 additions & 102 deletions

.github/workflows/ci.yml

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ on:
77
- main
88

99
concurrency:
10-
group: ci-${{ github.ref }}
11-
cancel-in-progress: true
10+
group: ci-${{ github.event_name == 'push' && format('{0}-{1}', github.ref, github.sha) || github.ref }}
11+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
1212

1313
jobs:
1414
format:
@@ -21,6 +21,14 @@ jobs:
2121
with:
2222
bun-version: 1.3.11
2323

24+
- name: Cache Bun package cache
25+
uses: actions/cache@v4
26+
with:
27+
path: ~/.bun/install/cache
28+
key: ${{ runner.os }}-bun-1.3.11-${{ hashFiles('bun.lock') }}
29+
restore-keys: |
30+
${{ runner.os }}-bun-1.3.11-
31+
2432
- run: bun install --frozen-lockfile
2533

2634
- run: bun run format:check
@@ -35,6 +43,14 @@ jobs:
3543
with:
3644
bun-version: 1.3.11
3745

46+
- name: Cache Bun package cache
47+
uses: actions/cache@v4
48+
with:
49+
path: ~/.bun/install/cache
50+
key: ${{ runner.os }}-bun-1.3.11-${{ hashFiles('bun.lock') }}
51+
restore-keys: |
52+
${{ runner.os }}-bun-1.3.11-
53+
3854
- run: bun install --frozen-lockfile
3955

4056
- run: bun run lint
@@ -49,20 +65,38 @@ jobs:
4965
with:
5066
bun-version: 1.3.11
5167

68+
- name: Cache Bun package cache
69+
uses: actions/cache@v4
70+
with:
71+
path: ~/.bun/install/cache
72+
key: ${{ runner.os }}-bun-1.3.11-${{ hashFiles('bun.lock') }}
73+
restore-keys: |
74+
${{ runner.os }}-bun-1.3.11-
75+
5276
- run: bun install --frozen-lockfile
5377

5478
- run: bun run typecheck
5579

5680
test:
5781
name: Test
5882
runs-on: ubuntu-latest
83+
env:
84+
TURBO_TEST_CONCURRENCY: 3
5985
steps:
6086
- uses: actions/checkout@v4
6187

6288
- uses: oven-sh/setup-bun@v2
6389
with:
6490
bun-version: 1.3.11
6591

92+
- name: Cache Bun package cache
93+
uses: actions/cache@v4
94+
with:
95+
path: ~/.bun/install/cache
96+
key: ${{ runner.os }}-bun-1.3.11-${{ hashFiles('bun.lock') }}
97+
restore-keys: |
98+
${{ runner.os }}-bun-1.3.11-
99+
66100
# apps/cloud's test script invokes `node` directly; undici 8.x (pulled
67101
# in by @cloudflare/vitest-pool-workers) calls webidl.markAsUncloneable
68102
# which only exists in Node 22.10+. Pin a known-good runtime.
@@ -105,6 +139,14 @@ jobs:
105139
with:
106140
bun-version: 1.3.11
107141

142+
- name: Cache Bun package cache
143+
uses: actions/cache@v4
144+
with:
145+
path: ~/.bun/install/cache
146+
key: ${{ runner.os }}-bun-1.3.11-${{ hashFiles('bun.lock') }}
147+
restore-keys: |
148+
${{ runner.os }}-bun-1.3.11-
149+
108150
# The dev stacks spawn Node sidecars (vite/workerd tooling); pin the
109151
# same known-good runtime the unit-test job uses.
110152
- uses: actions/setup-node@v4
@@ -113,6 +155,12 @@ jobs:
113155

114156
- run: bun install --frozen-lockfile
115157

158+
- name: Cache Playwright browsers
159+
uses: actions/cache@v4
160+
with:
161+
path: ~/.cache/ms-playwright
162+
key: ${{ runner.os }}-playwright-1.60.0
163+
116164
# Install from e2e so bunx resolves ITS pinned playwright (the version
117165
# the tests run against) rather than floating to the latest.
118166
- name: Install Playwright Chromium
@@ -152,6 +200,14 @@ jobs:
152200
with:
153201
bun-version: 1.3.11
154202

203+
- name: Cache Bun package cache
204+
uses: actions/cache@v4
205+
with:
206+
path: ~/.bun/install/cache
207+
key: ${{ runner.os }}-bun-1.3.11-${{ hashFiles('bun.lock') }}
208+
restore-keys: |
209+
${{ runner.os }}-bun-1.3.11-
210+
155211
# The local scenarios boot a real `executor web` (which spawns a Node
156212
# sidecar) and some drive a browser, so pin Node 22 and install Chromium.
157213
- uses: actions/setup-node@v4
@@ -160,6 +216,12 @@ jobs:
160216

161217
- run: bun install --frozen-lockfile
162218

219+
- name: Cache Playwright browsers
220+
uses: actions/cache@v4
221+
with:
222+
path: ~/.cache/ms-playwright
223+
key: ${{ runner.os }}-playwright-1.60.0
224+
163225
# `chromium` and the new `chromium-headless-shell` ship as separate
164226
# downloads; the browser-driven scenarios launch the headless shell.
165227
# Install from e2e so bunx resolves ITS pinned playwright (the version the
@@ -190,6 +252,14 @@ jobs:
190252
with:
191253
bun-version: 1.3.11
192254

255+
- name: Cache Bun package cache
256+
uses: actions/cache@v4
257+
with:
258+
path: ~/.bun/install/cache
259+
key: ${{ runner.os }}-bun-1.3.11-${{ hashFiles('bun.lock') }}
260+
restore-keys: |
261+
${{ runner.os }}-bun-1.3.11-
262+
193263
- run: bun install --frozen-lockfile
194264

195265
- name: Build web app
@@ -221,3 +291,5 @@ jobs:
221291
file: apps/host-selfhost/Dockerfile
222292
push: false
223293
tags: executor-selfhost:ci
294+
cache-from: type=gha
295+
cache-to: type=gha,mode=max

apps/cloud/src/db/db.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,12 @@ const makePostgresResource = (): DbResource => {
7373
sql,
7474
db: drizzle(sql, { schema: combinedSchema }) as DrizzleDb,
7575
close: () =>
76-
Effect.sync(() => {
77-
void Effect.runFork(
78-
Effect.ignore(
79-
Effect.tryPromise({
80-
try: () => sql.end({ timeout: 0 }),
81-
catch: (cause) => cause,
82-
}),
83-
),
84-
);
85-
}),
76+
Effect.ignore(
77+
Effect.tryPromise({
78+
try: () => sql.end({ timeout: 0 }),
79+
catch: (cause) => cause,
80+
}),
81+
),
8682
};
8783
};
8884

apps/host-selfhost/src/boot.test.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,26 @@ import { mkdtempSync } from "node:fs";
22
import { tmpdir } from "node:os";
33
import { join } from "node:path";
44

5-
import { afterAll, expect, test } from "@effect/vitest";
5+
import { afterAll, beforeAll, expect, test } from "@effect/vitest";
66

77
// Config reads the environment, so point it at a throwaway data dir before
88
// importing the app graph.
99
process.env.EXECUTOR_DATA_DIR = mkdtempSync(join(tmpdir(), "eh-boot-"));
1010

11-
const { makeSelfHostTestApp, singleAdminIdentityLayer } = await import("./testing/test-app");
11+
let handler!: (request: Request) => Promise<Response>;
12+
let dispose: () => Promise<void> = async () => {};
1213

13-
const { handler, dispose } = await makeSelfHostTestApp({
14-
identity: singleAdminIdentityLayer({
15-
userId: "admin",
16-
organizationId: "default-org",
17-
organizationName: "Default",
18-
}),
14+
beforeAll(async () => {
15+
const { makeSelfHostTestApp, singleAdminIdentityLayer } = await import("./testing/test-app");
16+
const app = await makeSelfHostTestApp({
17+
identity: singleAdminIdentityLayer({
18+
userId: "admin",
19+
organizationId: "default-org",
20+
organizationName: "Default",
21+
}),
22+
});
23+
handler = app.handler;
24+
dispose = app.dispose;
1925
});
2026
afterAll(() => dispose());
2127

apps/host-selfhost/src/multi-user.test.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { mkdtempSync } from "node:fs";
22
import { tmpdir } from "node:os";
33
import { join } from "node:path";
44

5-
import { afterAll, expect, test } from "@effect/vitest";
5+
import { afterAll, beforeAll, expect, test } from "@effect/vitest";
66
import { connectionIdentifier } from "@executor-js/sdk/shared";
77

88
import { mintInviteCode } from "./testing/mint-invite";
@@ -13,9 +13,15 @@ process.env.BETTER_AUTH_SECRET = "multi-user-secret-0123456789-abcdefghij-klmn";
1313
process.env.EXECUTOR_BOOTSTRAP_ADMIN_EMAIL = "admin@multi.test";
1414
process.env.EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD = "admin-pass-123456";
1515

16-
const { makeSelfHostApiHandler } = await import("./app");
16+
let handler!: (request: Request) => Promise<Response>;
17+
let dispose: () => Promise<void> = async () => {};
1718

18-
const { handler, dispose } = await makeSelfHostApiHandler();
19+
beforeAll(async () => {
20+
const { makeSelfHostApiHandler } = await import("./app");
21+
const app = await makeSelfHostApiHandler();
22+
handler = app.handler;
23+
dispose = app.dispose;
24+
});
1925
afterAll(() => dispose());
2026

2127
const BASE = "http://localhost:4788";

apps/host-selfhost/src/scope-isolation.test.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { mkdtempSync } from "node:fs";
22
import { tmpdir } from "node:os";
33
import { join } from "node:path";
44

5-
import { afterAll, expect, test } from "@effect/vitest";
5+
import { afterAll, beforeAll, expect, test } from "@effect/vitest";
66
import { connectionIdentifier } from "@executor-js/sdk/shared";
77

88
process.env.EXECUTOR_DATA_DIR = mkdtempSync(join(tmpdir(), "eh-iso-"));
@@ -13,10 +13,16 @@ process.env.EXECUTOR_DATA_DIR = mkdtempSync(join(tmpdir(), "eh-iso-"));
1313
// request-scoped. Each identity is its own (org, user): in v2 the org is the
1414
// tenant (catalog partition) and the user is the acting subject (drives
1515
// `owner: "user"` rows).
16-
const { makeSelfHostTestApp, headerIdentityLayer } = await import("./testing/test-app");
16+
let handler!: (request: Request) => Promise<Response>;
17+
let dispose: () => Promise<void> = async () => {};
1718

18-
const { handler, dispose } = await makeSelfHostTestApp({
19-
identity: headerIdentityLayer,
19+
beforeAll(async () => {
20+
const { makeSelfHostTestApp, headerIdentityLayer } = await import("./testing/test-app");
21+
const app = await makeSelfHostTestApp({
22+
identity: headerIdentityLayer,
23+
});
24+
handler = app.handler;
25+
dispose = app.dispose;
2026
});
2127
afterAll(() => dispose());
2228

@@ -135,7 +141,7 @@ test("concurrent requests with distinct identities get disjoint, correct executo
135141
expect(addresses.some((a) => a.includes(connectionNameForUser(other)))).toBe(false);
136142
}
137143
});
138-
}, 30_000);
144+
});
139145

140146
test("a request with no identity is rejected", async () => {
141147
const res = await handler(new Request("http://localhost/api/connections"));

apps/host-selfhost/src/secrets-integration.test.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
44
import { join } from "node:path";
55

66
import { Effect, Layer } from "effect";
7-
import { afterAll, expect, test } from "@effect/vitest";
7+
import { afterAll, beforeAll, expect, test } from "@effect/vitest";
88

99
import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk";
1010
import { makeScopedExecutor } from "@executor-js/api/server";
@@ -32,13 +32,20 @@ const createScopedExecutor = (
3232
Effect.provide(SelfHostScopedExecutorSeams),
3333
);
3434

35-
const dbHandle = await createSelfHostDb({
36-
path: dbPath,
37-
namespace: "executor_selfhost",
38-
version: "1.0.0",
35+
let dbLayer!: Layer.Layer<SelfHostDb>;
36+
let dbHandle: Awaited<ReturnType<typeof createSelfHostDb>> | undefined;
37+
38+
beforeAll(async () => {
39+
dbHandle = await createSelfHostDb({
40+
path: dbPath,
41+
namespace: "executor_selfhost",
42+
version: "1.0.0",
43+
});
44+
dbLayer = Layer.succeed(SelfHostDb)(dbHandle);
45+
});
46+
afterAll(async () => {
47+
await dbHandle?.close();
3948
});
40-
const dbLayer = Layer.succeed(SelfHostDb)(dbHandle);
41-
afterAll(() => dbHandle.close());
4249

4350
const TINY_SPEC = JSON.stringify({
4451
openapi: "3.0.0",

apps/host-selfhost/src/sources-mcp.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
33
import { join } from "node:path";
44

55
import { Effect, Layer } from "effect";
6-
import { afterAll, expect, test } from "@effect/vitest";
6+
import { afterAll, beforeAll, expect, test } from "@effect/vitest";
77

88
import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk";
99
import { makeScopedExecutor } from "@executor-js/api/server";
@@ -50,8 +50,15 @@ const TINY_SPEC = JSON.stringify({
5050
},
5151
});
5252

53-
const { makeSelfHostApiHandler } = await import("./app");
54-
const { handler, dispose } = await makeSelfHostApiHandler({ dbPath });
53+
let handler!: (request: Request) => Promise<Response>;
54+
let dispose: () => Promise<void> = async () => {};
55+
56+
beforeAll(async () => {
57+
const { makeSelfHostApiHandler } = await import("./app");
58+
const app = await makeSelfHostApiHandler({ dbPath });
59+
handler = app.handler;
60+
dispose = app.dispose;
61+
});
5562
afterAll(() => dispose());
5663

5764
const BASE = "http://localhost:4788";

apps/host-selfhost/src/sources.test.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
33
import { join } from "node:path";
44

55
import { Effect, Layer } from "effect";
6-
import { afterAll, expect, test } from "@effect/vitest";
6+
import { afterAll, beforeAll, expect, test } from "@effect/vitest";
77

88
import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk";
99
import { makeScopedExecutor } from "@executor-js/api/server";
@@ -28,13 +28,20 @@ const createScopedExecutor = (
2828
const dataDir = mkdtempSync(join(tmpdir(), "eh-src-"));
2929
process.env.EXECUTOR_DATA_DIR = dataDir;
3030

31-
const dbHandle = await createSelfHostDb({
32-
path: join(dataDir, "data.db"),
33-
namespace: "executor_selfhost",
34-
version: "1.0.0",
31+
let dbLayer!: Layer.Layer<SelfHostDb>;
32+
let dbHandle: Awaited<ReturnType<typeof createSelfHostDb>> | undefined;
33+
34+
beforeAll(async () => {
35+
dbHandle = await createSelfHostDb({
36+
path: join(dataDir, "data.db"),
37+
namespace: "executor_selfhost",
38+
version: "1.0.0",
39+
});
40+
dbLayer = Layer.succeed(SelfHostDb)(dbHandle);
41+
});
42+
afterAll(async () => {
43+
await dbHandle?.close();
3544
});
36-
const dbLayer = Layer.succeed(SelfHostDb)(dbHandle);
37-
afterAll(() => dbHandle.close());
3845

3946
// Inline OpenAPI spec so the test doesn't depend on the network to register.
4047
const TINY_SPEC = JSON.stringify({

0 commit comments

Comments
 (0)