-
Notifications
You must be signed in to change notification settings - Fork 2
rollback and service logs get their happy paths, on a fixture that actually boots #208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6b13edf
ae2409a
d7d9e78
7f6d4ae
bfb2e8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| /** | ||
| * The deployment verbs, against a service this file deploys to. | ||
| * | ||
| * Every command here needs a deployment to act on, which is why they | ||
|
|
@@ -8,11 +8,19 @@ | |
| * | ||
| * The blocks run in file order and share one service: it is deployed | ||
| * once, read by the middle blocks, then stopped and deleted at the end. | ||
| * Teardown must delete the deployment before the scratch project can go. | ||
| * The rollback block adds a second deployment, promotes it, and rolls | ||
| * back to the first, so the later blocks still act on a live first | ||
| * deployment. Teardown must delete every deployment before the scratch | ||
| * project can go. | ||
| */ | ||
| import { afterAll, expect, it } from "vitest"; | ||
|
|
||
| import { deleteDeployment, deployService } from "./deployed-service"; | ||
| import { | ||
| createDeployment, | ||
| deleteDeployment, | ||
| deployService, | ||
| } from "./deployed-service"; | ||
| import type { CliRun } from "./harness"; | ||
| import { scratchName } from "./harness"; | ||
| import { useScratchProject } from "./scratch"; | ||
| import { describeCommand } from "./suite"; | ||
|
|
@@ -25,6 +33,8 @@ | |
| | { serviceId: string; serviceName: string; deploymentId: string } | ||
| | undefined; | ||
|
|
||
| let secondDeployment: { id: string; serviceName: string } | undefined; | ||
|
|
||
| function requireDeployed(): { | ||
| serviceId: string; | ||
| serviceName: string; | ||
|
|
@@ -45,6 +55,9 @@ | |
| } | ||
|
|
||
| afterAll(async () => { | ||
| if (secondDeployment !== undefined) { | ||
| await deleteDeployment(scratch, secondDeployment); | ||
| } | ||
| if (deployed !== undefined) { | ||
| await deleteDeployment(scratch, { | ||
| id: deployed.deploymentId, | ||
|
|
@@ -145,6 +158,65 @@ | |
| }); | ||
| }); | ||
|
|
||
| describeCommand("service deployment rollback", () => { | ||
| it("rolls production back to the previously live deployment", async () => { | ||
| const existing = requireDeployed(); | ||
| // Rolling back needs somewhere to roll back from: a second | ||
| // deployment, promoted over the first. It is tracked for teardown | ||
| // before anything can throw, because `project remove` refuses while | ||
| // it exists. | ||
| const secondId = await createDeployment(existing.serviceId); | ||
| secondDeployment = { id: secondId, serviceName: existing.serviceName }; | ||
| await scratch.run([ | ||
| "service", | ||
| "deployment", | ||
| "start", | ||
| secondId, | ||
| "--service", | ||
| existing.serviceName, | ||
| ]); | ||
| await scratch.run([ | ||
| "service", | ||
| "deployment", | ||
| "promote", | ||
| secondId, | ||
| "--service", | ||
| existing.serviceName, | ||
| ]); | ||
|
|
||
| // No --to: the default target is the deployment before the live | ||
| // one, which is the first. --confirm must name that target. | ||
| const run = await scratch.run([ | ||
| "service", | ||
| "deployment", | ||
| "rollback", | ||
| "--service", | ||
| existing.serviceName, | ||
| "--confirm", | ||
| existing.deploymentId, | ||
| ]); | ||
| const rolledBack = run.envelope.result as { | ||
| readonly service: { readonly id: string }; | ||
| readonly deployment: DeploymentRow; | ||
| readonly previousLiveDeploymentId: string | null; | ||
| }; | ||
|
|
||
| expect(rolledBack.service.id).toBe(existing.serviceId); | ||
| expect(rolledBack.deployment.id).toBe(existing.deploymentId); | ||
| expect(rolledBack.deployment.live).toBe(true); | ||
| expect(rolledBack.previousLiveDeploymentId).toBe(secondId); | ||
|
|
||
| const shown = await scratch.run([ | ||
| "service", | ||
| "deployment", | ||
| "show", | ||
| existing.deploymentId, | ||
| ]); | ||
| const after = shown.envelope.result as { deployment: DeploymentRow }; | ||
| expect(after.deployment.live).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describeCommand("service open", () => { | ||
| it("answers with the service's URL rather than opening one", async () => { | ||
| const existing = requireDeployed(); | ||
|
|
@@ -169,6 +241,109 @@ | |
| }); | ||
| }); | ||
|
|
||
| /** The log lines of a `--json` run: `output` frames on the `logs` | ||
| * source's data channel, which is where the command reports each line | ||
| * the platform captured from the app. */ | ||
| function logLines(run: CliRun): string[] { | ||
| return run.stdout | ||
| .split("\n") | ||
| .map((line) => line.trim()) | ||
| .filter((line) => line.startsWith("{")) | ||
| .flatMap((line) => { | ||
| try { | ||
| return [ | ||
| JSON.parse(line) as { | ||
| kind?: string; | ||
| source?: string; | ||
| channel?: string; | ||
| line?: string; | ||
| }, | ||
| ]; | ||
| } catch { | ||
| return []; | ||
| } | ||
| }) | ||
| .filter( | ||
| (frame) => | ||
| frame.kind === "output" && | ||
| frame.source === "logs" && | ||
| frame.channel === "data" && | ||
| typeof frame.line === "string", | ||
| ) | ||
| .map((frame) => frame.line as string); | ||
| } | ||
|
|
||
| describeCommand("service logs", () => { | ||
| it("reads back what the deployment wrote while serving a request", async () => { | ||
| const existing = requireDeployed(); | ||
| // Rollback made the first deployment live again, so it is what | ||
| // `service logs` reads by default. Serve one request against it so | ||
| // there is a line whose ingestion this run can be pinned to. | ||
| const shown = await scratch.run([ | ||
| "service", | ||
| "deployment", | ||
| "show", | ||
| existing.deploymentId, | ||
| ]); | ||
| const url = (shown.envelope.result as { deployment: DeploymentRow }) | ||
| .deployment.url; | ||
| expect(url).toMatch(HTTPS_URL); | ||
| // A fresh hostname does not serve on the first try — the edge is | ||
| // still setting up routing and TLS for it — so the request retries | ||
| // until the app answers. | ||
| const serveDeadline = Date.now() + 60_000; | ||
| let servedStatus: number | string = "never reached"; | ||
| for (;;) { | ||
| try { | ||
| const served = await fetch(`${url}/e2e-logs-probe`); | ||
| servedStatus = served.status; | ||
| if (served.ok) { | ||
| break; | ||
| } | ||
| } catch (failure) { | ||
| servedStatus = failure instanceof Error ? failure.message : "error"; | ||
| } | ||
| if (Date.now() > serveDeadline) { | ||
| throw new Error( | ||
| `the deployment at ${url} never served the probe request; ` + | ||
| `last answer: ${servedStatus}`, | ||
| ); | ||
| } | ||
| await new Promise((resolve) => setTimeout(resolve, 3000)); | ||
| } | ||
|
|
||
| // Ingestion lags the request by some unspecified amount, so poll | ||
| // until the probe's line arrives rather than asserting on one read. | ||
| const deadline = Date.now() + 90_000; | ||
| let lines: string[] = []; | ||
| for (;;) { | ||
| const run = await scratch.run([ | ||
| "service", | ||
| "logs", | ||
| "--service", | ||
| existing.serviceName, | ||
| ]); | ||
| lines = logLines(run); | ||
| if ( | ||
| lines.some((line) => line.includes("e2e-fixture served /e2e-logs-probe")) | ||
| ) { | ||
| break; | ||
| } | ||
| if (Date.now() > deadline) { | ||
| break; | ||
| } | ||
| await new Promise((resolve) => setTimeout(resolve, 5000)); | ||
| } | ||
|
|
||
| expect( | ||
| lines.some((line) => line.includes("e2e-fixture listening")), | ||
| ).toBe(true); | ||
| expect( | ||
| lines.some((line) => line.includes("e2e-fixture served /e2e-logs-probe")), | ||
| ).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
|
Comment on lines
+276
to
+346
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Restore lint compliance for the retry test. Lint fails at Lines 298 and 320 for 🧰 Tools🪛 GitHub Check: Lint[failure] 320-325: lint/performance/noAwaitInLoops [failure] 298-298: lint/performance/noAwaitInLoops [failure] 277-277: lint/complexity/noExcessiveCognitiveComplexity 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| describeCommand("service deployment stop", () => { | ||
| it("stops the running deployment", async () => { | ||
| const existing = requireDeployed(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: prisma/prisma-cli
Length of output: 50373
🏁 Script executed:
Repository: prisma/prisma-cli
Length of output: 50373
🏁 Script executed:
Repository: prisma/prisma-cli
Length of output: 50373
Stop
secondDeploymentbefore deleting it.The delete API rejects running deployments, including the previous live deployment after rollback.
rollbackdoes not stopsecondDeployment, and teardown suppresses the failed delete. Stop it beforedeleteDeploymentso the deployment does not block scratch-project cleanup.🤖 Prompt for AI Agents