Public: anyone can read this; the crew can change it.
GET …/runs/{id} names its Server-Timing phases: session, run, render #5333
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5316-get-runs-id-names-its-server-timing-phas → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +25
−6
src/http/api.ts +7 −6
| @@ -6126,15 +6126,16 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 6126 | 6126 | }); |
| 6127 | 6127 | |
| 6128 | 6128 | router.add("GET", "/api/v1/repos/:owner/:name/runs/:id", (ctx) => { |
| 6129 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 6129 | 6130 | const repo = loadRepo(ctx); |
| 6130 | 6131 | if (!repo) return notFound(); |
| 6131 | const run = getRepoRun(ctx.app, ctx.auth.user, repo, ctx.params.id ?? ""); | |
| 6132 | if (!run.ok) return notFound(); | |
| 6133 | // The merged pulls a finished production deploy carries (`core/carried.ts`). | |
| 6134 | return json({ | |
| 6135 | ...run.value, | |
| 6136 | carries: carriedBy(ctx.app.db, run.value.id), | |
| 6132 | const found = timePhase(ctx.request, "run", () => { | |
| 6133 | const run = getRepoRun(ctx.app, ctx.auth.user, repo, ctx.params.id ?? ""); | |
| 6134 | // The merged pulls a finished production deploy carries (`core/carried.ts`). | |
| 6135 | return run.ok ? { ...run.value, carries: carriedBy(ctx.app.db, run.value.id) } : null; | |
| 6137 | 6136 | }); |
| 6137 | if (!found) return notFound(); | |
| 6138 | return timePhase(ctx.request, "render", () => json(found)); | |
| 6138 | 6139 | }); |
| 6139 | 6140 | |
| 6140 | 6141 | // The log: text, or a live tail as server-sent events (`?follow=1`, or |
test/api-runs-timing.test.ts +18
| @@ -5,6 +5,7 @@ import { tmpdir } from "node:os"; | ||
| 5 | 5 | import { join } from "node:path"; |
| 6 | 6 | import { createApp, type App } from "../src/app.ts"; |
| 7 | 7 | import { createRepo } from "../src/actions/repos.ts"; |
| 8 | import { createRun } from "../src/core/runs.ts"; | |
| 8 | 9 | import { createSession } from "../src/core/sessions.ts"; |
| 9 | 10 | import { createUser } from "../src/core/users.ts"; |
| 10 | 11 | import { serve, type Servers } from "../src/http/server.ts"; |
| @@ -16,6 +17,7 @@ let dataDir: string; | ||
| 16 | 17 | let app: App; |
| 17 | 18 | let servers: Servers; |
| 18 | 19 | let cookie = ""; |
| 20 | let runId = ""; | |
| 19 | 21 | |
| 20 | 22 | const cells = process.env["FORGE_CELLS"]; |
| 21 | 23 | |
| @@ -37,6 +39,7 @@ beforeAll(async () => { | ||
| 37 | 39 | cookie = `forge_session=${createSession(app.db, alice.id).token}`; |
| 38 | 40 | const repo = await createRepo(app, alice, { name: "r", visibility: "public" }); |
| 39 | 41 | if (!repo.ok) throw new Error(String(repo.error)); |
| 42 | runId = createRun(app.db, { repoId: repo.value.id, kind: "deploy", env: "production" }).id; | |
| 40 | 43 | }); |
| 41 | 44 | afterAll(async () => { |
| 42 | 45 | await servers.stop(); |
| @@ -65,3 +68,18 @@ test("a missing repository is a 404 naming session", async () => { | ||
| 65 | 68 | expect(got.status).toBe(404); |
| 66 | 69 | expect(got.names).toEqual(["session"]); |
| 67 | 70 | }); |
| 71 | ||
| 72 | test("one run's read names session, run, render in order, answer unchanged", async () => { | |
| 73 | const got = await phases(`/api/v1/repos/alice/r/runs/${runId}`); | |
| 74 | expect(got.status).toBe(200); | |
| 75 | expect(got.names).toEqual(["session", "run", "render"]); | |
| 76 | const body = JSON.parse(got.body); | |
| 77 | expect(body.id).toBe(runId); | |
| 78 | expect(body.carries).toEqual([]); | |
| 79 | }); | |
| 80 | ||
| 81 | test("an unknown run is a 404 naming session, run", async () => { | |
| 82 | const got = await phases("/api/v1/repos/alice/r/runs/nope"); | |
| 83 | expect(got.status).toBe(404); | |
| 84 | expect(got.names).toEqual(["session", "run"]); | |
| 85 | }); |
new above
offline
Good: the
okcheck andcarriedByboth sit insiderun, so the phase measures the whole read and a miss never reaches the carried query.