Skip to content

Commit aa93785

Browse files
author
Agent
committed
test: add portable runtime e2e matrix
1 parent 34377ec commit aa93785

22 files changed

Lines changed: 936 additions & 162 deletions

File tree

.github/workflows/ci.yml

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,45 @@ jobs:
136136
if: steps.check.outputs.affected == 'true'
137137
run: pnpm nx run edge-worker:e2e
138138

139-
# ─────────────────────────────────────── 2a. EDGE-WORKER INTEGRATION ──────────────────────────────────────
139+
# ─────────────────────────────────────── 2a. EDGE-WORKER PORTABLE RUNTIME E2E ──────────────────────────────────────
140+
edge-worker-portable-runtime-e2e:
141+
needs: pre_job
142+
if: needs.pre_job.outputs.should_skip != 'true'
143+
runs-on: ubuntu-latest
144+
env:
145+
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
146+
steps:
147+
- uses: actions/checkout@v4
148+
with:
149+
fetch-depth: 0
150+
151+
- uses: ./.github/actions/setup
152+
with:
153+
github-token: ${{ secrets.GITHUB_TOKEN }}
154+
atlas-cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
155+
156+
- uses: oven-sh/setup-bun@v2
157+
158+
- name: Check if edge-worker is affected
159+
id: check
160+
run: |
161+
if pnpm nx show projects --affected --base=origin/main --head=HEAD | grep -q "^edge-worker$"; then
162+
echo "affected=true" >> $GITHUB_OUTPUT
163+
echo "edge-worker is affected, running portable runtime e2e tests"
164+
else
165+
echo "affected=false" >> $GITHUB_OUTPUT
166+
echo "edge-worker not affected, skipping portable runtime e2e tests"
167+
fi
168+
169+
- name: Pre-start Supabase
170+
if: steps.check.outputs.affected == 'true'
171+
run: ./scripts/ci-prestart-supabase.sh edge-worker
172+
173+
- name: Run edge-worker portable runtime e2e tests
174+
if: steps.check.outputs.affected == 'true'
175+
run: pnpm nx run edge-worker:e2e:portable-runtimes
176+
177+
# ─────────────────────────────────────── 2b. EDGE-WORKER INTEGRATION ──────────────────────────────────────
140178
edge-worker-integration:
141179
needs: pre_job
142180
if: needs.pre_job.outputs.should_skip != 'true'
@@ -172,7 +210,7 @@ jobs:
172210
if: steps.check.outputs.affected == 'true'
173211
run: pnpm nx run edge-worker:test:integration
174212

175-
# ─────────────────────────────────────── 2b. EDGE-WORKER BUN SMOKE ──────────────────────────────────────
213+
# ─────────────────────────────────────── 2c. EDGE-WORKER BUN SMOKE ──────────────────────────────────────
176214
edge-worker-bun-smoke:
177215
needs: pre_job
178216
if: needs.pre_job.outputs.should_skip != 'true'
@@ -206,7 +244,7 @@ jobs:
206244
if: steps.check.outputs.affected == 'true'
207245
run: pnpm nx run edge-worker:smoke:bun
208246

209-
# ─────────────────────────────────────── 2c. CLI E2E ──────────────────────────────────────
247+
# ─────────────────────────────────────── 2d. CLI E2E ──────────────────────────────────────
210248
cli-e2e:
211249
needs: pre_job
212250
if: needs.pre_job.outputs.should_skip != 'true'
@@ -242,7 +280,7 @@ jobs:
242280
if: steps.check.outputs.affected == 'true'
243281
run: pnpm nx run cli:e2e
244282

245-
# ─────────────────────────────────────── 2d. CLIENT E2E ──────────────────────────────────────
283+
# ─────────────────────────────────────── 2e. CLIENT E2E ──────────────────────────────────────
246284
client-e2e:
247285
needs: pre_job
248286
if: needs.pre_job.outputs.should_skip != 'true'
@@ -278,7 +316,7 @@ jobs:
278316
if: steps.check.outputs.affected == 'true'
279317
run: pnpm nx run client:e2e
280318

281-
# ─────────────────────────────────────── 2e. CORE PGTAP ──────────────────────────────────────
319+
# ─────────────────────────────────────── 2f. CORE PGTAP ──────────────────────────────────────
282320
core-pgtap:
283321
needs: pre_job
284322
if: needs.pre_job.outputs.should_skip != 'true'
@@ -321,6 +359,7 @@ jobs:
321359
[
322360
build-and-test,
323361
edge-worker-e2e,
362+
edge-worker-portable-runtime-e2e,
324363
edge-worker-integration,
325364
edge-worker-bun-smoke,
326365
cli-e2e,
@@ -374,6 +413,7 @@ jobs:
374413
[
375414
build-and-test,
376415
edge-worker-e2e,
416+
edge-worker-portable-runtime-e2e,
377417
edge-worker-integration,
378418
edge-worker-bun-smoke,
379419
cli-e2e,

pkgs/edge-worker/project.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,21 @@
199199
"parallel": false
200200
}
201201
},
202+
"e2e:portable-runtimes": {
203+
"executor": "nx:run-commands",
204+
"dependsOn": ["build", "serve:functions:e2e"],
205+
"local": true,
206+
"cache": false,
207+
"inputs": ["default", "^production"],
208+
"options": {
209+
"cwd": "pkgs/edge-worker",
210+
"commands": [
211+
"deno run --allow-net=localhost:50321 \"data:application/typescript,console.log(%22Waiting%20for%20functions%20server%20(port%2050321)...%22)%3B%20const%20deadline%20%3D%20Date.now()%20%2B%20120_000%3B%20while%20(Date.now()%20%3C%20deadline)%20%7B%20try%20%7B%20const%20conn%20%3D%20await%20Deno.connect(%7B%20hostname%3A%20%22localhost%22%2C%20port%3A%2050321%20%7D)%3B%20conn.close()%3B%20console.log(%22%20Ready!%22)%3B%20Deno.exit(0)%3B%20%7D%20catch%20%7B%20await%20new%20Promise((resolve)%20%3D%3E%20setTimeout(resolve%2C%20500))%3B%20%7D%20%7D%20console.error(%22TIMEOUT%3A%20Functions%20server%20not%20available%20on%20port%2050321%20after%20120s%22)%3B%20Deno.exit(1)%3B\"",
212+
"deno test --config deno.test.json --allow-all --env=supabase/functions/.env tests/e2e-portable-runtimes/"
213+
],
214+
"parallel": false
215+
}
216+
},
202217
"test": {
203218
"inputs": ["default", "^production"],
204219
"dependsOn": ["test:types", "test:unit"]

pkgs/edge-worker/src/core/Queries.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class Queries {
4949
RETURNING (w.deprecated_at IS NOT NULL) AS is_deprecated;
5050
`;
5151

52-
return result || { is_deprecated: false };
52+
return result || { is_deprecated: true };
5353
}
5454

5555
async ensureFlowCompiled(

pkgs/edge-worker/src/core/Worker.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,26 @@ export class Worker {
116116
return this.lifecycle.edgeFunctionName;
117117
}
118118

119+
get isCreated() {
120+
return this.lifecycle.isCreated;
121+
}
122+
123+
get isStarting() {
124+
return this.lifecycle.isStarting;
125+
}
126+
127+
get isRunning() {
128+
return this.lifecycle.isRunning;
129+
}
130+
131+
get isDeprecated() {
132+
return this.lifecycle.isDeprecated;
133+
}
134+
135+
get isStopped() {
136+
return this.lifecycle.isStopped;
137+
}
138+
119139
/**
120140
* Log deprecation message only once (prevents duplicate logs when deprecation
121141
* is detected in heartbeat and then stop() is called)

pkgs/edge-worker/src/core/WorkerLifecycle.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ export class WorkerLifecycle<IMessage extends Json> implements ILifecycle {
9999
return this.workerState.isCreated;
100100
}
101101

102+
get isStarting() {
103+
return this.workerState.isStarting;
104+
}
105+
102106
get isRunning() {
103107
return this.workerState.isRunning;
104108
}

pkgs/edge-worker/src/core/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ export interface ILifecycle {
2727
get edgeFunctionName(): string | undefined;
2828
get queueName(): string;
2929
get isCreated(): boolean;
30+
get isStarting(): boolean;
3031
get isRunning(): boolean;
32+
get isDeprecated(): boolean;
3133
get isStopping(): boolean;
3234
get isStopped(): boolean;
3335

pkgs/edge-worker/src/flow/FlowWorkerLifecycle.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ export class FlowWorkerLifecycle<TFlow extends AnyFlow> implements ILifecycle {
166166
return this.workerState.isCreated;
167167
}
168168

169+
get isStarting() {
170+
return this.workerState.isStarting;
171+
}
172+
169173
get isRunning() {
170174
return this.workerState.isRunning;
171175
}

pkgs/edge-worker/src/platform/SupabasePlatformAdapter.ts

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ interface SupabaseEnv extends Record<string, string | undefined> {
4242
export class SupabasePlatformAdapter implements PlatformAdapter<SupabaseResources> {
4343
private edgeFunctionName: string | null = null;
4444
private worker: Worker | null = null;
45+
private workerId: string | null = null;
46+
private workerReplacementPromise: Promise<void> | null = null;
4547
private logger: Logger;
4648
private abortController: AbortController;
4749
private _platformResources: SupabaseResources;
@@ -183,8 +185,9 @@ export class SupabasePlatformAdapter implements PlatformAdapter<SupabaseResource
183185
if (this.worker) {
184186
// Signal death to ensure_workers() cron by setting stopped_at.
185187
// This allows the cron to immediately ping for a replacement worker.
186-
const workerId = this.validatedEnv.SB_EXECUTION_ID;
187-
await this.queries.markWorkerStopped(workerId);
188+
if (this.workerId) {
189+
await this.queries.markWorkerStopped(this.workerId);
190+
}
188191
}
189192

190193
await this.stopWorker();
@@ -206,7 +209,7 @@ export class SupabasePlatformAdapter implements PlatformAdapter<SupabaseResource
206209
}
207210

208211
private setupStartupHandler(createWorkerFn: CreateWorkerFn): void {
209-
this.deps.serve((req: Request) => {
212+
this.deps.serve(async (req: Request) => {
210213
// Validate auth header in production (skipped in local mode)
211214
const authResult = validateServiceRoleAuth(req, this.validatedEnv);
212215
if (!authResult.valid) {
@@ -219,35 +222,66 @@ export class SupabasePlatformAdapter implements PlatformAdapter<SupabaseResource
219222

220223
this.logger.debug(`HTTP Request: ${this.edgeFunctionName}`);
221224

222-
const wasStarted = !this.worker;
223-
224-
if (!this.worker) {
225-
this.edgeFunctionName = this.extractFunctionName(req);
226-
227-
const workerId = this.validatedEnv.SB_EXECUTION_ID;
228-
229-
this.loggingFactory.setWorkerId(workerId);
230-
this.loggingFactory.setWorkerName(this.edgeFunctionName);
231-
232-
// Create the worker using the factory function and the logger
233-
this.worker = createWorkerFn(this.loggingFactory.createLogger);
234-
this.worker.startOnlyOnce({
235-
edgeFunctionName: this.edgeFunctionName,
236-
workerId,
237-
startMode: 'http',
238-
});
239-
}
225+
const wasStarted = await this.ensureWorkerStarted(req, createWorkerFn);
240226

241227
return new Response(JSON.stringify({
242228
status: wasStarted ? 'started' : 'running',
243-
workerId: this.validatedEnv.SB_EXECUTION_ID,
229+
workerId: this.workerId,
244230
functionName: this.edgeFunctionName,
245231
}), {
246232
headers: { 'Content-Type': 'application/json' },
247233
});
248234
});
249235
}
250236

237+
private needsWorkerReplacement(): boolean {
238+
return !this.worker || this.worker.isDeprecated || this.worker.isStopped;
239+
}
240+
241+
private async ensureWorkerStarted(req: Request, createWorkerFn: CreateWorkerFn): Promise<boolean> {
242+
while (this.needsWorkerReplacement()) {
243+
if (this.workerReplacementPromise) {
244+
await this.workerReplacementPromise;
245+
continue;
246+
}
247+
248+
this.workerReplacementPromise = this.replaceWorker(req, createWorkerFn);
249+
try {
250+
await this.workerReplacementPromise;
251+
return true;
252+
} finally {
253+
this.workerReplacementPromise = null;
254+
}
255+
}
256+
257+
return false;
258+
}
259+
260+
private async replaceWorker(req: Request, createWorkerFn: CreateWorkerFn): Promise<void> {
261+
if (this.worker) {
262+
await this.worker.stop();
263+
this.abortController = new AbortController();
264+
}
265+
266+
this.edgeFunctionName = this.extractFunctionName(req);
267+
268+
const workerId = this.workerId === null
269+
? this.validatedEnv.SB_EXECUTION_ID
270+
: globalThis.crypto.randomUUID();
271+
this.workerId = workerId;
272+
273+
this.loggingFactory.setWorkerId(workerId);
274+
this.loggingFactory.setWorkerName(this.edgeFunctionName);
275+
276+
// Create the worker using the factory function and the logger
277+
this.worker = createWorkerFn(this.loggingFactory.createLogger);
278+
this.worker.startOnlyOnce({
279+
edgeFunctionName: this.edgeFunctionName,
280+
workerId,
281+
startMode: 'http',
282+
});
283+
}
284+
251285
/**
252286
* Assertion function that validates environment has all required Supabase fields
253287
* @throws Error if any required environment variable is missing

pkgs/edge-worker/supabase/config.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ verify_jwt = false
8787
import_map = "./functions/deno.json"
8888
entrypoint = "./functions/conn_custom_sql/index.ts"
8989

90+
[functions.conn_max_pg_default]
91+
enabled = true
92+
verify_jwt = false
93+
import_map = "./functions/deno.json"
94+
entrypoint = "./functions/conn_max_pg_default/index.ts"
95+
96+
[functions.conn_max_pg_override]
97+
enabled = true
98+
verify_jwt = false
99+
import_map = "./functions/deno.json"
100+
entrypoint = "./functions/conn_max_pg_override/index.ts"
101+
90102
[functions.stopped_at_test]
91103
enabled = true
92104
verify_jwt = false

0 commit comments

Comments
 (0)