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

The sentinel files no never-green-check issue in trevato/forge for another repository #5665

Merged opened this

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

Merged

scout/forge:scout/3918-the-sentinel-files-no-never-green-check → main 5 commits fork 1 thread · 1 unresolved

  • Merged. scout/3918-the-sentinel-files-no-never-green-check was merged into main as 43c76b2.

Review

Sign in to review this change.
3 files changed +59 −4
README.md +2
@@ -1402,6 +1402,8 @@ its evidence, filed in `FORGE_SENTINEL_REPO` (default `trevato/forge`) as the
14021402 credential-shaped before it is filed. Other findings (`held`, `ledger`, `red`)
14031403 are filed anew when they return.
14041404
1405 A check that has never been green is filed only for the sentinel's own repository. In any other repository it stays on that repository's checks and the degraded list.
1406
14051407 A capacity setting away from its default is not a finding: the sentinel files
14061408 nothing for it (#3269). On `/settings/capacity` the row of each such setting
14071409 carries one muted line, "changed from X N days ago by who", and its **Put back**
src/crew/sentinel.ts +11
@@ -254,9 +254,17 @@ interface SummaryRow {
254254 summary: string;
255255 }
256256
257 let warnedUnresolved = false;

Module-level mutable flag: fine for a once-only warning, but it cannot be reset in tests, so the warning is untested. Consider reusing the existing not-found fault instead.

258
257259 /** The last runs per repository: a check never green, or pushes that cannot land. */
258260 export function runStreaks(db: Database): Finding[] {
259261 const out: Finding[] = [];
262 const [ownerName = "", repoName = ""] = sentinelRepoPath().split("/");
263 const ownId = getRepo(db, ownerName, repoName)?.id;
264 if (ownId === undefined && !warnedUnresolved) {
265 warnedUnresolved = true;
266 log.warn("sentinel repository does not resolve; never-green checks are not filed", { repo: sentinelRepoPath() });
267 }
260268 const rows = db
261269 .query<SummaryRow, [number]>(
262270 `SELECT id, repo_id, kind, state, summary FROM (
@@ -275,6 +283,9 @@ export function runStreaks(db: Database): Finding[] {
275283 const [repoId = "", kind = ""] = k.split("\u0000");
276284 const failed = runs.filter((r) => r.state === "failed");
277285 if (kind === "check") {
286 // Another repository's gate is its owner's to fix: a builder here cannot
287 // read or change it. It stays on that repository's checks and the degraded list.
288 if (repoId !== ownId) continue;
278289 if (runs.some((r) => r.state === "ok")) continue;
279290 const same = new Map<string, SummaryRow[]>();
280291 for (const r of failed)
test/sentinel.test.ts +46 −4
@@ -77,7 +77,10 @@ beforeAll(async () => {
7777 if (!made.ok) throw new Error(String(made.error));
7878 repo = made.value;
7979 });
80 const keptSentinelRepo = process.env.FORGE_SENTINEL_REPO;
8081 afterAll(async () => {
82 if (keptSentinelRepo === undefined) delete process.env.FORGE_SENTINEL_REPO;
83 else process.env.FORGE_SENTINEL_REPO = keptSentinelRepo;
8184 app.db.close();
8285 await rm(dataDir, { recursive: true, force: true });
8386 });
@@ -165,6 +168,41 @@ describe("a push blocked on the network is still a push fault", () => {
165168 });
166169 });
167170
171 describe("a gate never green", () => {
172 const gateKeys = (): string[] =>
173 findFaults(app.db, NOW).map((f) => f.key).filter((k) => k.startsWith("gate:"));
174 const plant = (repoId: string): void => {
175 for (let n = 0; n < 15; n++) {
176 const run = createRun(app.db, { repoId, kind: "check" });
177 app.db
178 .query("UPDATE run SET state = 'failed', summary = 'no check.run', created_at = ?2 WHERE id = ?1")
179 .run(run.id, ago((40 + n) * HOUR));
180 }
181 };
182
183 test("in another repository files nothing", async () => {
184 const made = await createRepo(app, alice, { name: "elsewhere", visibility: "private" });
185 if (!made.ok) throw new Error(String(made.error));
186 process.env.FORGE_SENTINEL_REPO = "alice/forge";
187 try {
188 plant(made.value.id);
189 expect(gateKeys()).not.toContain(`gate:${made.value.id}`);
190 } finally {
191 app.db.query("DELETE FROM run WHERE repo_id = ?1").run(made.value.id);
192 }
193 });
194
195 test("in the sentinel's own repository still files", () => {
196 process.env.FORGE_SENTINEL_REPO = "alice/forge";
197 try {
198 plant(repo.id);
199 expect(gateKeys()).toContain(`gate:${repo.id}`);
200 } finally {
201 app.db.query("DELETE FROM run WHERE repo_id = ?1 AND summary = 'no check.run'").run(repo.id);
202 }
203 });
204 });
205
168206 describe("a failed push that pushed nothing", () => {
169207 test("still raises the push fault", async () => {
170208 const made = await createRepo(app, alice, { name: "blocked", visibility: "private" });
@@ -184,6 +222,7 @@ describe("a failed push that pushed nothing", () => {
184222 describe("the faults of the day", () => {
185223 let queuedId = "";
186224 beforeAll(() => {
225 process.env.FORGE_SENTINEL_REPO = "alice/forge";
187226 // 1. a slot held by a finished run: work waits, nothing runs.
188227 plantRun("check", "cancelled", "cancelled", ago(3 * HOUR));
189228 queuedId = createRun(app.db, { repoId: repo.id, kind: "check" }).id;
@@ -267,13 +306,16 @@ describe("the faults of the day", () => {
267306 describe("the schedule", () => {
268307 test("a new fault is a signal inside the hour; a known one waits for the hour", () => {
269308 const last = new Date(sentinelStanding(app.db).lastHuntAt!).getTime();
270 expect(sentinelTick(app, last + 5 * 60_000)).toBeNull();
271 expect(sentinelTick(app, last + 20 * 60_000)).toBeNull();
272 process.env.FORGE_SENTINEL_REPO = "alice/forge";
309 const kept = process.env.FORGE_SENTINEL_REPO;
310 delete process.env.FORGE_SENTINEL_REPO;
273311 try {
312 expect(sentinelTick(app, last + 5 * 60_000)).toBeNull();
313 expect(sentinelTick(app, last + 20 * 60_000)).toBeNull();
314 process.env.FORGE_SENTINEL_REPO = "alice/forge";
274315 expect(sentinelTick(app, last + HOUR + 1)).not.toBeNull();
275316 } finally {
276 delete process.env.FORGE_SENTINEL_REPO;
317 if (kept === undefined) delete process.env.FORGE_SENTINEL_REPO;
318 else process.env.FORGE_SENTINEL_REPO = kept;
277319 }
278320 });
279321 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs