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

A landed pull checks main's new head, so a red main clears on the next land #1888

Merged opened this

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

Merged

scout/forge:scout/1883-a-landed-pull-checks-main-s-new-head-so → main 2 commits fork

  • Merged. scout/1883-a-landed-pull-checks-main-s-new-head-so was merged into main as b378611.

Review

Sign in to review this change.
3 files changed +112 −0
scripts/gate.ts +1
@@ -289,6 +289,7 @@ export const GATE: readonly string[] = [
289289 "test/check-budget.test.ts",
290290 "test/check-proven.test.ts",
291291 "test/check-list.test.ts",
292 "test/check-land-main.test.ts",
292293 "test/check-tiers.test.ts",
293294 "test/pull-check-again.test.ts",
294295 "test/holds.test.ts",
src/checks/service.ts +17
@@ -898,6 +898,23 @@ export function startChecks(app: App, options: ChecksOptions): ChecksHandle {
898898 inFlight.get(run.id)?.abort.abort();
899899 announceRun(app, run);
900900 }
901 // A land moves the default branch without a push event: check its
902 // new head, or a red main stays red until someone pushes to it.
903 const mergeOid = event.payload["mergeOid"];
904 if (
905 event.kind === "pull.merged" &&
906 pull.baseBranch === repo.defaultBranch &&
907 typeof mergeOid === "string" &&
908 /^[0-9a-f]{40}$/.test(mergeOid)
909 )
910 await request(
911 repo,
912 repo.defaultBranch,
913 mergeOid,
914 "push",
915 undefined,
916 actor,
917 );
901918 return;
902919 }
903920 default:
test/check-land-main.test.ts added +94
@@ -0,0 +1,94 @@
1 /**
2 * A landed pull checks main's new head (#1883): landing emits pull.merged,
3 * not a push, so a red main only cleared when someone pushed to it.
4 */
5 import { afterAll, beforeAll, expect, test } from "bun:test";
6 import { mkdtemp, rm } from "node:fs/promises";
7 import { tmpdir } from "node:os";
8 import { join } from "node:path";
9 import { createApp, type App } from "../src/app.ts";
10 import { mergePull, openPull } from "../src/actions/pulls.ts";
11 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
12 import { redMain } from "../src/checks/red_main.ts";
13 import { startChecks, type ChecksHandle } from "../src/checks/service.ts";
14 import { listRuns, type Run } from "../src/core/runs.ts";
15 import { createUser, type User } from "../src/core/users.ts";
16 import type { Repo } from "../src/core/repos.ts";
17 import { serve, type Servers } from "../src/http/server.ts";
18
19 let dataDir: string;
20 let app: App;
21 let servers: Servers;
22 let checks: ChecksHandle;
23 let alice: User;
24 let repo: Repo;
25
26 const checkJson = (run: string): string =>
27 `{"check": {"run": ${JSON.stringify(run)}, "install": ""}}\n`;
28
29 async function finished(where: (r: Run) => boolean): Promise<Run> {
30 const deadline = Date.now() + 30_000;
31 while (Date.now() < deadline) {
32 const run = listRuns(app.db, repo.id, { kind: "check", limit: 50 }).find(where);
33 if (run && (run.state === "ok" || run.state === "failed")) return run;
34 await Bun.sleep(50);
35 }
36 throw new Error("check did not settle");
37 }
38
39 beforeAll(async () => {
40 dataDir = await mkdtemp(join(tmpdir(), "forge-land-main-"));
41 app = createApp({ dataDir, port: 0 });
42 servers = await serve(app);
43 checks = startChecks(app, { forgeUrl: servers.url, concurrency: 1 });
44 const a = await createUser(app.db, {
45 trusted: true,
46 username: "alice",
47 email: "[email protected]",
48 password: "a-good-passphrase",
49 adminIfFirst: true,
50 });
51 if (!a.ok) throw new Error(a.error);
52 alice = a.value;
53 const made = await createRepo(app, alice, { name: "lib", visibility: "public" });
54 if (!made.ok) throw new Error(String(made.error));
55 repo = made.value;
56 });
57
58 afterAll(async () => {
59 await checks.stop();
60 servers.stop();
61 app.close();
62 await rm(dataDir, { recursive: true, force: true });
63 });
64
65 test("landing a pull on a red main checks the merge commit, and a green one clears the red", async () => {
66 const red = await commitToBranch(app, alice, repo, {
67 message: "Declare a check that fails",
68 files: [{ path: "forge.json", content: checkJson("test -f OK.md") }],
69 });
70 if (!red.ok) throw new Error(JSON.stringify(red.error));
71 const failed = await finished((r) => r.commitOid === red.value.commitOid);
72 expect(failed.state).toBe("failed");
73 expect(redMain(app.db, repo.id)).toBeDefined();
74
75 const branched = await commitToBranch(app, alice, repo, {
76 branch: "fix",
77 message: "Make the check pass",
78 files: [{ path: "OK.md", content: "ok\n" }],
79 });
80 if (!branched.ok) throw new Error(JSON.stringify(branched.error));
81 const opened = await openPull(app, alice, repo, {
82 title: "Fix",
83 baseBranch: "main",
84 headBranch: "fix",
85 });
86 if (!opened.ok) throw new Error(String(opened.error));
87 const landed = await mergePull(app, alice, repo, opened.value, "merge");
88 if (!landed.ok) throw new Error(JSON.stringify(landed.error));
89 const { mergeOid } = landed.value;
90
91 const own = await finished((r) => r.commitOid === mergeOid);
92 expect(own).toMatchObject({ env: "main", issueId: null, state: "ok" });
93 expect(redMain(app.db, repo.id)).toBeUndefined();
94 }, 60_000);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs