Public: anyone can read this; the crew can change it.
The no_release alarm fires on any repository's oldest merge, so it always claims nothing has gone live #8687
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/7638-the-no-release-alarm-fires-on-any-reposi → main 2 commits fork
4 files
changed +40
−8
src/release/landings.ts +2 −1
| @@ -99,7 +99,8 @@ export function forgetLive(): void { | ||
| 99 | 99 | |
| 100 | 100 | function isLive(row: MergedRow, state: LiveState | undefined): boolean { |
| 101 | 101 | if (state === undefined || row.repo.toLowerCase() !== state.own.toLowerCase()) return true; |
| 102 | return row.oid === "" ? Date.parse(row.at) <= state.at : state.oids.has(row.oid); | |
| 102 | // A merge made before the running commit is behind it, though git was not asked about it (or answered no for a rewritten history). | |
| 103 | return Date.parse(row.at) <= state.at || (row.oid !== "" && state.oids.has(row.oid)); | |
| 103 | 104 | } |
| 104 | 105 | |
| 105 | 106 | /** |
test/changelog.test.ts +3 −2
| @@ -65,7 +65,8 @@ describe("changelog page", () => { | ||
| 65 | 65 | const day = new Date().toISOString().slice(0, 10); |
| 66 | 66 | merge("forge", "First <b>thing</b>", `${day}T01:00:00.000Z`, oids[0]!); |
| 67 | 67 | merge("forge", "Second thing", `${day}T02:00:00.000Z`, oids[1]!); |
| 68 | merge("forge", "Third thing", `${day}T03:00:00.000Z`, oids[2]!); | |
| 68 | // Not before the running commit was made, or it would be behind it. | |
| 69 | merge("forge", "Third thing", `${day}T23:59:59.000Z`, oids[2]!); | |
| 69 | 70 | merge("notes", "A note app change", `${day}T02:30:00.000Z`, ""); |
| 70 | 71 | token = createToken(app.db, alice.id, "rw", "rw").token; |
| 71 | 72 | }, 60_000); |
| @@ -174,7 +175,7 @@ describe("changelog page", () => { | ||
| 174 | 175 | const fork = must(await createRepo(app, alice, { name: "fork", visibility: "public" })); |
| 175 | 176 | const c = must(await commitToBranch(app, alice, fork, { message: "fork work", files: [{ path: "x.txt", content: "x\n" }] })); |
| 176 | 177 | const day = new Date().toISOString().slice(0, 10); |
| 177 | merge("fork", "Fork change", `${day}T23:00:00.000Z`, c.commitOid); | |
| 178 | merge("fork", "Fork change", `${day}T23:59:59.000Z`, c.commitOid); | |
| 178 | 179 | delete app.config.forgeRepo; |
| 179 | 180 | forgetLive(); |
| 180 | 181 | process.env["FORGE_COMMIT"] = c.commitOid; |
test/flow-alarm-no-release.test.ts +33 −4
| @@ -7,7 +7,7 @@ import { createApp } from "../src/app.ts"; | ||
| 7 | 7 | import { createRepo } from "../src/core/repos.ts"; |
| 8 | 8 | import { createUser } from "../src/core/users.ts"; |
| 9 | 9 | import { id } from "../src/kernel/id.ts"; |
| 10 | import { forgetLive, keepLive } from "../src/release/landings.ts"; | |
| 10 | import { forgetLive, keepLive, split, waitingSince } from "../src/release/landings.ts"; | |
| 11 | 11 | import { noReleaseAlarm } from "../src/flow/alarm_no_release.ts"; |
| 12 | 12 | |
| 13 | 13 | const NOW = new Date("2026-10-26T12:00:00.000Z"); |
| @@ -19,16 +19,19 @@ async function seed() { | ||
| 19 | 19 | if (!u.ok) throw new Error(u.error); |
| 20 | 20 | const repo = await createRepo(app.db, app.reposRoot, { ownerId: u.value.id, name: "r", visibility: "public", description: "r" }); |
| 21 | 21 | if (!repo.ok) throw new Error(repo.error); |
| 22 | const other = await createRepo(app.db, app.reposRoot, { ownerId: u.value.id, name: "other", visibility: "public", description: "o" }); | |
| 23 | if (!other.ok) throw new Error(other.error); | |
| 22 | 24 | let n = 0; |
| 23 | 25 | return { |
| 24 | 26 | app, |
| 25 | 27 | /** The running commit was made at `at`: it contains every merge before it. */ |
| 26 | 28 | release: (at: string) => keepLive("alice/r", [], Date.parse(at)), |
| 27 | merge: (at: string) => { | |
| 29 | merge: (at: string, oid?: string, into: "r" | "other" = "r") => { | |
| 30 | const target = into === "r" ? repo.value : other.value; | |
| 28 | 31 | const issueId = id(); |
| 29 | 32 | n++; |
| 30 | app.db.query(`INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state, created_at) VALUES (?1, ?2, ?3, 'pull', 'P', '', ?4, 'closed', ?5)`).run(issueId, repo.value.id, n, u.value.id, at); | |
| 31 | app.db.query(`INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, head_repo_id, merged_at) VALUES (?1, ?2, ?3, '', '', ?4, ?5)`).run(issueId, repo.value.defaultBranch, `b/${n}`, repo.value.id, at); | |
| 33 | app.db.query(`INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state, created_at) VALUES (?1, ?2, ?3, 'pull', 'P', '', ?4, 'closed', ?5)`).run(issueId, target.id, n, u.value.id, at); | |
| 34 | app.db.query(`INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, head_repo_id, merged_at, merged_oid) VALUES (?1, ?2, ?3, '', '', ?4, ?5, ?6)`).run(issueId, target.defaultBranch, `b/${n}`, target.id, at, oid ?? null); | |
| 32 | 35 | }, |
| 33 | 36 | }; |
| 34 | 37 | } |
| @@ -62,4 +65,30 @@ describe("no release alarm", () => { | ||
| 62 | 65 | t.release(minAgo(100)); |
| 63 | 66 | expect(noReleaseAlarm(t.app.db, NOW).fired).toBe(false); |
| 64 | 67 | }); |
| 68 | test("stays quiet with an empty release record: a merge older than the running commit is behind it", async () => { | |
| 69 | const s = await seed(); | |
| 70 | s.merge(minAgo(4000), "cafe"); | |
| 71 | s.release(minAgo(10)); | |
| 72 | expect(noReleaseAlarm(s.app.db, NOW).fired).toBe(false); | |
| 73 | expect(waitingSince(s.app.db)).toBeUndefined(); | |
| 74 | }); | |
| 75 | test("a merge in another repository never fires it", async () => { | |
| 76 | const s = await seed(); | |
| 77 | s.merge(minAgo(4000), "beef", "other"); | |
| 78 | s.release(minAgo(10)); | |
| 79 | expect(noReleaseAlarm(s.app.db, NOW).fired).toBe(false); | |
| 80 | }); | |
| 81 | test("a git miss merged after the running commit still fires it", async () => { | |
| 82 | const s = await seed(); | |
| 83 | s.merge(minAgo(91), "cafe"); | |
| 84 | s.release(minAgo(200)); | |
| 85 | expect(noReleaseAlarm(s.app.db, NOW).fired).toBe(true); | |
| 86 | }); | |
| 87 | test("split counts a miss older than the running commit live, so /changelog agrees", () => { | |
| 88 | const row = (n: number, at: string) => ({ repo: "alice/r", number: n, title: "P", at, oid: `o${n}` }); | |
| 89 | const state = { own: "alice/r", oids: new Set<string>(), misses: new Set(["o1", "o2"]), at: Date.parse(minAgo(100)), commit: "c" }; | |
| 90 | const { live, onItsWay } = split([row(2, minAgo(50)), row(1, minAgo(500))], state); | |
| 91 | expect(live.map((l) => l.number)).toEqual([1]); | |
| 92 | expect(onItsWay.map((l) => l.number)).toEqual([2]); | |
| 93 | }); | |
| 65 | 94 | }); |
test/landings-asked-once.test.ts +2 −1
| @@ -45,7 +45,8 @@ describe("landings ask git once per merge commit", () => { | ||
| 45 | 45 | let n = 0; |
| 46 | 46 | const merge = (oid: string) => { |
| 47 | 47 | n++; |
| 48 | const at = new Date(Date.now() - (10 - n) * 60_000).toISOString(); | |
| 48 | // After the running commit was made, or it would already contain the merge. | |
| 49 | const at = new Date(Date.now() + n * 60_000).toISOString(); | |
| 49 | 50 | const issueId = id(); |
| 50 | 51 | app.db.query(`INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state, created_at) VALUES (?1, ?2, ?3, 'pull', 'P', '', ?4, 'closed', ?5)`).run(issueId, repo.value.id, n, u.value.id, at); |
| 51 | 52 | app.db.query(`INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, head_repo_id, merged_at, merged_oid) VALUES (?1, ?2, ?3, '', '', ?4, ?5, ?6)`).run(issueId, repo.value.defaultBranch, `b/${n}`, repo.value.id, at, oid); |
new above
offline