Public: anyone can read this; the crew can change it.

require_checks, red half: a failing head refuses the merge and its preview still answers, every time #5291

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/5277-require-checks-red-half-a-failing-head-r → main 1 commit fork

  • Merged. scout/5277-require-checks-red-half-a-failing-head-r was merged into main as 8bea4eb.

Review

Sign in to review this change.
2 files changed +116 −16
test/apps-require-checks-red.test.ts added +108
@@ -0,0 +1,108 @@
1 /**
2 * `require_checks`, the red half (docs/APPS.md): a pull whose head's tests
3 * fail has a settled `failed` deploy, the merge is refused, and the preview
4 * still answers with its own page. Waits on the deploy of that exact head
5 * (`commitOid`), never on "the latest". The green half is its own file.
6 */
7
8 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
9 import { newApp } from "../src/actions/apps.ts";
10 import { mergePull, openPull } from "../src/actions/pulls.ts";
11 import { commitToBranch, updateRepoSettings } from "../src/actions/repos.ts";
12 import { getPull } from "../src/core/pulls.ts";
13 import { getRepoById } from "../src/core/repos.ts";
14 import {
15 APPS_DOMAIN,
16 deploySettled,
17 fetchAsHost,
18 startAppsForge,
19 stopAppsForge,
20 type AppsForge,
21 } from "./helpers/apps-forge.ts";
22
23 describe("require_checks: a failing head", () => {
24 let forge: AppsForge;
25
26 beforeAll(async () => {
27 forge = await startAppsForge();
28 }, 60_000);
29
30 afterAll(async () => {
31 await forge.runtime.stop();
32 await stopAppsForge(forge);
33 });
34
35 test("refuses the merge and its preview still answers", async () => {
36 const { app, alice, servers, runtime } = forge;
37 const made = await newApp(app, alice, {
38 name: "hello",
39 description: "A place for notes",
40 visibility: "public",
41 });
42 expect(made.ok).toBe(true);
43 if (!made.ok) return;
44 const record = made.value.app;
45 let repo = made.value.repo;
46 const production = await deploySettled(
47 app,
48 repo.id,
49 (r) => r.env === "production",
50 );
51 expect(production.state).toBe("ok");
52
53 expect(
54 (await updateRepoSettings(app, alice, repo, { requireChecks: true })).ok,
55 ).toBe(true);
56 repo = getRepoById(app.db, repo.id)!;
57 const broken = await commitToBranch(app, alice, repo, {
58 branch: "broken",
59 base: repo.defaultBranch,
60 message: "Break the test",
61 files: [
62 {
63 path: "src/server.test.ts",
64 content:
65 'import { expect, test } from "bun:test";\ntest("fails", () => { expect(1).toBe(2); });\n',
66 },
67 ],
68 });
69 expect(broken.ok).toBe(true);
70 if (!broken.ok) return;
71 const opened = await openPull(app, alice, repo, {
72 title: "Broken",
73 baseBranch: repo.defaultBranch,
74 headBranch: "broken",
75 });
76 expect(opened.ok).toBe(true);
77 if (!opened.ok) return;
78 const number = opened.value.number;
79
80 const failed = await deploySettled(
81 app,
82 repo.id,
83 (r) =>
84 r.env === `pull-${number}` && r.commitOid === broken.value.commitOid,
85 );
86 expect(failed.state).toBe("failed");
87 expect(failed.summary).toContain("test");
88
89 const refused = await mergePull(
90 app,
91 alice,
92 repo,
93 getPull(app.db, repo.id, number)!,
94 "merge",
95 );
96 expect(refused.ok).toBe(false);
97 if (!refused.ok) expect(refused.error).toBe("checks_required");
98
99 // The app never broke, only its tests: the preview answers with its own
100 // page rather than a gateway error.
101 const page = await fetchAsHost(servers, `hello--${number}.${APPS_DOMAIN}`);
102 expect(page.status).toBe(200);
103 expect(await page.text()).toContain(`preview of pull #${number}`);
104 const status = runtime.envStatus(record, `pull-${number}`);
105 expect(status?.state).toBe("running");
106 expect(status?.error).toContain("tests failed");
107 }, 120_000);
108 });
test/apps.test.ts +8 −16
@@ -389,23 +389,15 @@ describe("apps: production, previews, data, gates", () => {
389389 });
390390 expect(opened.ok).toBe(true);
391391 if (!opened.ok) return;
392 const failed = await settled(latest(`pull-${opened.value.number}`));
393 expect(failed.state).toBe("failed");
394 expect(failed.summary).toContain("test");
395 const refused = await mergePull(app, alice, repo, opened.value, "merge");
396 expect(refused.ok).toBe(false);
397 if (!refused.ok) expect(refused.error).toBe("checks_required");
398 // The app itself never broke, only its tests: the preview answers with
399 // its own page rather than a gateway error, because a deployment that
400 // answers is what was asked for.
401 const page = await viaHost(`hello--${opened.value.number}.${DOMAIN}`);
402 expect(page.status).toBe(200);
403 expect(await page.text()).toContain(
404 `preview of pull #${opened.value.number}`,
392 // The red half (refused merge, preview still answering) is
393 // apps-require-checks-red.test.ts; here the failed head only has to settle.
394 if (!broken.ok) return;
395 const failed = await settled(
396 (r) =>
397 r.env === `pull-${opened.value.number}` &&
398 r.commitOid === broken.value.commitOid,
405399 );
406 const status = runtime.envStatus(record, `pull-${opened.value.number}`);
407 expect(status?.state).toBe("running");
408 expect(status?.error).toContain("tests failed");
400 expect(failed.state).toBe("failed");
409401
410402 const fixed = await commitToBranch(app, alice, repo, {
411403 branch: "broken",

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs