diff --git a/README.md b/README.md index 9a4e427..cdfc18e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ board **at request time**, across four hosts. | [`prs.bounded.tools`](https://prs.bounded.tools) | what is open and awaiting a check | `selectPrs` | | [`desk.bounded.tools`](https://desk.bounded.tools) | all three at a glance — the front door | `selectOverview` | +The front door also carries a fourth section, **Repo health** (#81): which public repos run +the org's standard CI and whether it passes, read from the snapshot `bounded-systems/.github` +publishes daily (`selectCi`). It has no host of its own yet, so it links to the snapshot. + One Worker, selected by hostname, because the selection rules **are** the product: four Workers would be four deploys, four broker entries, and four chances for "claimable" to come to mean four different things. diff --git a/src/render.js b/src/render.js index 0822f19..372ffa2 100644 --- a/src/render.js +++ b/src/render.js @@ -165,9 +165,11 @@ const row = ({ marker, markerLabel = "", title, url, repo, number, noun, suffix ? `${esc(c.subject)} ${esc(c.delta)}` : esc(title) } - ${vh(" — ")}${esc(shortRepo(repo))}${vh( - noun + " ", - )}${esc(number)}${suffix ? `${esc(suffix)}` : ""} + ${vh(" — ")}${esc(shortRepo(repo))}${ + number == null + ? "" + : `${vh(noun + " ")}${esc(number)}` + }${suffix ? `${esc(suffix)}` : ""} `; }; @@ -663,20 +665,44 @@ const SECTION_COPY = { markerLabel: "Status", noun: "issue" }, prs: { title: "PRs", label: "open", blurb: "changes awaiting a check", markerLabel: "Pull request", noun: "pull request" }, + ci: { title: "Repo health", label: "with findings", + blurb: "which public repos run the standard CI, and whether it passes — measured daily by the standard's own repo", + markerLabel: "Findings", noun: "repo" }, }; const EMPTY_COPY = { issues: "Nothing claimable right now.", claims: "Nothing is claimed right now.", prs: "No open pull requests.", + ci: "Every public repo calls the standard CI, and every standard run is green.", }; +/** + * The repo-health denominator, said out loud (desk#81). "42 with findings" on + * its own hides the fact that matters most — how many repos call the standard + * at all, and whether the ones that do are green — so the section carries the + * lane's own totals and this turns them into one sentence. Gaps are named as + * gaps: "could not be measured" is not "healthy", and the lane keeps them in a + * separate field for exactly this reason. + */ +function ciSummary(s) { + const t = s.totals; + if (!t || !t.caller || !t.standard_run) return ""; + const rows = t.rows ?? "?"; + const gaps = t.gaps ? ` ${esc(t.gaps)} could not be measured.` : ""; + const selftest = s.standard ? ` The standard's own selftest is ${esc(s.standard)}.` : ""; + return `

${esc(t.caller.present)} of ${esc(rows)} public repos call the standard: ${esc(t.standard_run.green)} green, ${esc(t.standard_run.red)} red. ${esc(t.caller.absent)} do not call it.${gaps}${selftest}

`; +} + function overviewSection(s) { const copy = SECTION_COPY[s.key] || { title: s.key, label: "", blurb: "", markerLabel: "", noun: "item" }; + // A section with no host of its own links wherever its feed lives (repo + // health links to the snapshot itself until a `ci.` host exists). + const link = s.href || `https://${s.host}`; // The heading carries the id the
points at, so each section is a // NAMED landmark rather than three anonymous regions a reader has to count. const heading = `
-

${esc(copy.title)}

+

${esc(copy.title)}

${s.ok ? `${esc(s.count)} ${esc(copy.label)}` : "unreadable"}

`; @@ -689,7 +715,7 @@ function overviewSection(s) { ${heading}
This section could not be read. It is not empty — the feed behind - ${esc(s.host)} did not answer in a way this page can stand behind, + ${esc(s.host)} did not answer in a way this page can stand behind, so nothing is shown rather than a count that would be made up.

${esc(s.reason)}

@@ -726,12 +752,13 @@ function overviewSection(s) { const more = !s.count ? "" : s.count > s.items.length - ? `

Showing the first ${esc(s.items.length)} of ${esc(s.count)} — the rest are at ${esc(s.host)}.

` - : `

All of them, in full, at ${esc(s.host)}.

`; + ? `

Showing the first ${esc(s.items.length)} of ${esc(s.count)} — the rest are at ${esc(s.host)}.

` + : `

All of them, in full, at ${esc(s.host)}.

`; return `
${heading}

${esc(copy.blurb)}

+ ${s.key === "ci" ? ciSummary(s) : ""} ${body} ${more}
`; diff --git a/src/select.js b/src/select.js index d459187..3a136ea 100644 --- a/src/select.js +++ b/src/select.js @@ -5,7 +5,8 @@ // issues.bounded.tools what is worth picking up select() // claims.bounded.tools what someone is already on selectClaims() // prs.bounded.tools what is open and awaiting a check selectPrs() -// desk.bounded.tools all three at a glance selectOverview() +// desk.bounded.tools all three at a glance, plus selectOverview() +// repo health from the standard CI selectCi() // // THE RANK IS THE BOARD'S. Nothing here scores. `Score` is carried through // unchanged and only sorted on; a ranking computed here would be a different @@ -246,8 +247,90 @@ export function selectPrs(feed) { }; } +// ── repo health (desk#81) ──────────────────────────────────────────────────── + +/** + * Where the repo-standard conformance snapshot is published — `.github`#381's + * lane, daily, as main plus one API commit on a branch of the repo that owns + * the standard. Until a host of its own exists, the section links here. + */ +export const CI_SNAPSHOT_URL = + "https://raw.githubusercontent.com/bounded-systems/.github/repo-standard-conformance/repo-standard-conformance.json"; +export const CI_SECTION_HOST = "github.com/bounded-systems/.github"; + +/** + * The finding codes the conformance lane emits, as sentences. A code this file + * does not know passes through AS WRITTEN rather than being dropped: the lane + * may grow a finding before this page learns its name, and an unnamed finding + * is still a finding. + */ +export const FINDING_COPY = { + "caller-absent": "does not call the standard CI", + "pin-not-sha": "calls the standard at a ref that is not a commit SHA", + "pull-request-missing": "the caller has no pull_request trigger", + "pull-request-filtered": "the caller's pull_request trigger is path-filtered, so it does not report on every PR", + "pull-request-no-synchronize": "the caller's pull_request trigger does not re-run on a push", + "test-lane-absent": "carries a toolchain but no test lane — its tests, if any, gate nothing", + "standard-run-red": "the latest standard run on its default branch is red", +}; + /** - * Compose the three selections into the front door — desk.bounded.tools. + * Reduce the conformance snapshot to the repos with findings — the fourth + * section of desk.bounded.tools. + * + * NOTHING IS RE-COUNTED. `totals` are the lane's own and are carried through + * as published; this page sorts and truncates. A FINDING is the repo's (no + * caller, an unpinned ref, a red run); a GAP is the lane's (a listing it could + * not read). The lane keeps them in separate fields and never sums them, and + * neither does this — `count` is repos with findings, and the gaps ride along + * in `totals` for the summary line to say out loud. + * + * Worst first: most findings, then name — the same order the lane publishes. + */ +export function selectCi(feed) { + requireBoardFeed( + feed, + "repo-standard-conformance", + "Only the conformance snapshot may be rendered as repo health — any other feed is the wrong page's data.", + ); + + const repos = Array.isArray(feed.repos) ? feed.repos : []; + const t = feed.totals && typeof feed.totals === "object" ? feed.totals : {}; + const flagged = repos.filter((r) => Array.isArray(r.findings) && r.findings.length > 0); + const sorted = [...flagged].sort( + (a, b) => b.findings.length - a.findings.length || String(a.repo).localeCompare(String(b.repo)), + ); + + return { + generated_at: feed.generated_at, + href: CI_SNAPSHOT_URL, + count: sorted.length, + totals: { + rows: t.rows ?? null, + caller: t.caller ?? null, + standard_run: t.standard_run ?? null, + test_lane: t.test_lane ?? null, + findings: t.findings ?? null, + gaps: t.gaps ?? null, + }, + // The one "is the org CI good" signal that exists today: the standard's + // own selftest on main (`.github`#382 is what it still lacks). + standard: feed.standard?.selftest?.state ?? null, + items: sorted.map((r) => ({ + repo: r.repo, + url: `https://github.com/${r.repo}`, + findings: r.findings, + // hasOwn, not a bare lookup: a code named like an Object.prototype member + // ("constructor", "toString") must read as the text it is, not resolve to + // a function whose source becomes the sentence. + summary: r.findings.map((f) => (Object.hasOwn(FINDING_COPY, f) ? FINDING_COPY[f] : String(f))).join("; "), + standard_run: r.standard_run?.state ?? null, + })), + }; +} + +/** + * Compose the four selections into the front door — desk.bounded.tools. * * TAKES OUTCOMES, NOT FEEDS, and that is the point: each section is fetched and * selected independently, so this function is where "one of the three could not @@ -266,7 +349,7 @@ export function selectPrs(feed) { * make one page's "12 claimed" mean the same as another's is for both to be the * same expression. */ -export function selectOverview({ issues, claims, prs }, head = OVERVIEW_HEAD) { +export function selectOverview({ issues, claims, prs, ci }, head = OVERVIEW_HEAD) { const section = (key, host, outcome, shape) => outcome?.ok ? { key, host, ok: true, ...shape(outcome.value), generated_at: outcome.value.generated_at } @@ -296,6 +379,18 @@ export function selectOverview({ issues, claims, prs }, head = OVERVIEW_HEAD) { repo: i.repo, number: i.number, title: i.title, url: i.url, note: `#${i.number}`, })), })), + // Repo health (desk#81). No host of its own yet, so `href` points at the + // snapshot; rows are repos, not issues, so they carry no number. + section("ci", CI_SECTION_HOST, ci, (d) => ({ + count: d.count, + shown: d.items.length, + href: d.href, + totals: d.totals, + standard: d.standard, + items: d.items.slice(0, head).map((i) => ({ + repo: i.repo, number: null, title: i.summary, url: i.url, note: String(i.findings.length), + })), + })), ]; // The OLDEST readable stamp, not the newest. The page shows three feeds side diff --git a/src/worker.js b/src/worker.js index 98a04a7..994facc 100644 --- a/src/worker.js +++ b/src/worker.js @@ -34,6 +34,7 @@ import { select, selectClaims, selectPrs, + selectCi, selectOverview, DEFAULT_LIMIT, FeedError, @@ -1335,9 +1336,12 @@ export default { // No destination is baked in: where the filtered feeds are published is a // maintainer decision (see site#241), so they arrive as configuration and // an absent one is reported rather than guessed at. - const [board, prsFeed] = await Promise.all([ + const [board, prsFeed, ciFeed] = await Promise.all([ readFeed(env.FEED_URL, "FEED_URL"), readFeed(env.PRS_FEED_URL, "PRS_FEED_URL"), + // Repo health (desk#81): the conformance snapshot the standard's own + // repo publishes. Fails closed per section like the other two. + readFeed(env.CI_FEED_URL, "CI_FEED_URL"), ]); // Both issue-side sections read the SAME feed — one origin read, and the // two pages can never disagree about which snapshot they are describing. @@ -1345,6 +1349,7 @@ export default { issues: selected(board, (f) => select(f, limit)), claims: selected(board, selectClaims), prs: selected(prsFeed, selectPrs), + ci: selected(ciFeed, selectCi), }); const status = overview.ok ? 200 : 502; return wantsJson diff --git a/test/render.test.mjs b/test/render.test.mjs index b61f2fb..b3b4657 100644 --- a/test/render.test.mjs +++ b/test/render.test.mjs @@ -308,12 +308,27 @@ const section = (key, host, o = {}) => ({ items: [{ repo: "bounded-systems/prx", number: 434, title: `${key} row`, url: "https://e/1", note: "n" }], ...o, }); +// Repo health (desk#81): rows are repos, not issues — no number — and the +// section carries the lane's totals for the summary sentence. +const ciSection = (o = {}) => section("ci", "github.com/bounded-systems/.github", { + href: "https://raw.example/ci.json", + totals: { + rows: 90, caller: { present: 49, absent: 40, unreadable: 1 }, + standard_run: { green: 49, red: 0, other: 0, none: 0, unreadable: 0 }, findings: 42, gaps: 3, + }, + standard: "green", + count: 42, shown: 1, + items: [{ repo: "bounded-systems/bare", number: null, title: "does not call the standard CI", + url: "https://github.com/bounded-systems/bare", note: "1" }], + ...o, +}); const overview = (o = {}) => ({ ok: true, generated_at: "2026-08-25T12:00:00Z", head: 5, sections: [ section("issues", "issues.bounded.tools"), section("claims", "claims.bounded.tools"), section("prs", "prs.bounded.tools"), + ciSection(), ], ...o, }); @@ -692,3 +707,27 @@ test("no motion ships without a reduced-motion guard", async () => { } assert.equal(declared, guarded, `${declared} motion declarations, ${guarded} inside a reduced-motion guard`); }); + +// ── repo health on the overview (desk#81) ──────────────────────────────────── + +test("the repo-health section links to its snapshot, says its denominator, and rows carry no number", () => { + const html = renderOverview(overview(), AT, 60); + assert.match(html, /Repo health/); + assert.ok(html.includes('href="https://raw.example/ci.json"'), "links to the href, not a host"); + assert.match(html, /49 of 90 public repos call the standard: 49 green, 0 red\. 40 do not call it\. 3 could not be measured\. The standard's own selftest is green\.|49 of 90 public repos call the standard: 49 green, 0 red\. 40 do not call it\. 3 could not be measured\. The standard's own selftest is green\./); + assert.match(html, /does not call the standard CI/); + assert.match(html, /42 with findings/); + assert.doesNotMatch(html, /repo null/); + assert.doesNotMatch(html, /issue null/); +}); + +test("a repo-health section with nothing flagged says so, and only then", () => { + const html = renderOverview(overview({ sections: [ciSection({ count: 0, items: [] })] }), AT, 60); + assert.match(html, /every standard run is green/); + const failed = renderOverview(overview({ + ok: false, + sections: [{ key: "ci", host: "github.com/bounded-systems/.github", ok: false, reason: "feed responded 504", count: null, items: [] }], + }), AT, 60); + assert.doesNotMatch(failed, /every standard run is green/); + assert.match(failed, /feed responded 504/); +}); diff --git a/test/select.test.mjs b/test/select.test.mjs index f5117b4..b119f73 100644 --- a/test/select.test.mjs +++ b/test/select.test.mjs @@ -1,8 +1,8 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { - select, selectPrs, selectClaims, selectOverview, - FeedError, DEFAULT_LIMIT, OVERVIEW_HEAD, + select, selectPrs, selectClaims, selectOverview, selectCi, + FeedError, DEFAULT_LIMIT, OVERVIEW_HEAD, CI_SNAPSHOT_URL, FINDING_COPY, } from "../src/select.js"; const item = (o = {}) => ({ @@ -256,20 +256,50 @@ const boardFeed = feed([ item({ number: 2, fields: { Status: "Todo", Score: 9 } }), claimed({ number: 3, fields: { Status: "In Progress", Score: 1 } }), ]); +// A conformance snapshot the way `.github`'s lane publishes it (desk#81): +// totals are the lane's, rows carry findings (the repo's) and gaps (the lane's). +const ciRepo = (o = {}) => ({ + repo: "bounded-systems/x", findings: [], gaps: [], + caller: { state: "present" }, standard_run: { state: "green" }, extra: [], + ...o, +}); +const ciFeed = (repos = [ + ciRepo({ repo: "bounded-systems/bare", findings: ["caller-absent"], caller: { state: "absent" }, standard_run: null }), + ciRepo({ repo: "bounded-systems/clean" }), +], o = {}) => ({ + feed: "repo-standard-conformance", generated_at: "2026-09-04T20:38:14Z", + totals: { + rows: 2, caller: { present: 1, absent: 1, unreadable: 0 }, + standard_run: { green: 1, red: 0, other: 0, none: 0, unreadable: 0 }, + test_lane: { present: 1, absent: 1, "n/a": 0, unmeasured: 0 }, findings: 1, gaps: 0, + }, + standard: { selftest: { state: "green" } }, + repos, ...o, +}); const outcomes = (o = {}) => ({ issues: ok(select(boardFeed)), claims: ok(selectClaims(boardFeed)), prs: ok(selectPrs(prFeed([prItem({ number: 4 })]))), + ci: ok(selectCi(ciFeed())), ...o, }); -test("the overview carries all three sections, in reading order", () => { +test("the overview carries all four sections, in reading order", () => { const r = selectOverview(outcomes()); - assert.deepEqual(r.sections.map((s) => s.key), ["issues", "claims", "prs"]); + assert.deepEqual(r.sections.map((s) => s.key), ["issues", "claims", "prs", "ci"]); assert.deepEqual(r.sections.map((s) => s.host), [ - "issues.bounded.tools", "claims.bounded.tools", "prs.bounded.tools", + "issues.bounded.tools", "claims.bounded.tools", "prs.bounded.tools", "github.com/bounded-systems/.github", ]); assert.equal(r.ok, true); + // Repo health has no host of its own yet, so it says where its feed lives. + assert.equal(r.sections[3].href, CI_SNAPSHOT_URL); +}); + +test("a missing repo-health outcome fails the overview closed, like any other section", () => { + const r = selectOverview(outcomes({ ci: undefined })); + const ci = r.sections.find((s) => s.key === "ci"); + assert.equal(ci.ok, false); + assert.equal(r.ok, false); }); // The whole point of composing rather than re-counting: the overview's number @@ -280,6 +310,7 @@ test("every count comes from the selector that owns it", () => { assert.equal(by.issues.count, select(boardFeed).items.length); assert.equal(by.claims.count, selectClaims(boardFeed).count); assert.equal(by.prs.count, 1); + assert.equal(by.ci.count, selectCi(ciFeed()).count); }); test("a section that could not be read keeps its slot and its reason", () => { @@ -301,12 +332,13 @@ test("the overview's age is the OLDEST readable stamp, not the newest", () => { issues: ok(select(feed([], { generated_at: "2026-08-25T10:00:00Z" }))), claims: ok(selectClaims(feed([], { generated_at: "2026-08-25T10:00:00Z" }))), prs: ok(selectPrs(prFeed([], { generated_at: "2026-08-27T10:00:00Z" }))), + ci: ok(selectCi(ciFeed([], { generated_at: "2026-08-26T10:00:00Z" }))), }); assert.equal(r.generated_at, "2026-08-25T10:00:00Z"); }); test("an overview with nothing readable states no age rather than inventing one", () => { - const r = selectOverview({ issues: bad("x"), claims: bad("x"), prs: bad("x") }); + const r = selectOverview({ issues: bad("x"), claims: bad("x"), prs: bad("x"), ci: bad("x") }); assert.equal(r.generated_at, null); assert.equal(r.ok, false); }); @@ -316,9 +348,69 @@ test("each section shows only its head, and says how many it counted", () => { item({ number: n, fields: { Status: "Todo", Score: n } }))); const r = selectOverview({ issues: ok(select(many)), claims: ok(selectClaims(many)), prs: ok(selectPrs(prFeed([]))), + ci: ok(selectCi(ciFeed())), }); const issues = r.sections.find((s) => s.key === "issues"); assert.equal(issues.items.length, OVERVIEW_HEAD); assert.equal(issues.count, 12); assert.equal(r.head, OVERVIEW_HEAD); }); + +// ── selectCi — repo health (desk#81) ───────────────────────────────────────── + +test("selectCi refuses any feed that is not repo-standard-conformance", () => { + assert.throws(() => selectCi(feed([])), FeedError); + assert.throws(() => selectCi(prFeed([])), FeedError); + assert.throws(() => selectCi(ciFeed([], { feed: "front-desk-public" })), /expected the 'repo-standard-conformance' feed/); +}); + +test("selectCi refuses a snapshot it cannot date", () => { + assert.throws(() => selectCi(ciFeed([], { generated_at: "yesterday" })), FeedError); +}); + +test("selectCi lists only repos with findings, worst first, as sentences", () => { + const r = selectCi(ciFeed([ + ciRepo({ repo: "bounded-systems/clean" }), + ciRepo({ repo: "bounded-systems/b", findings: ["caller-absent"] }), + ciRepo({ repo: "bounded-systems/a", findings: ["pin-not-sha", "pull-request-filtered"] }), + ])); + assert.equal(r.count, 2); + assert.deepEqual(r.items.map((i) => i.repo), ["bounded-systems/a", "bounded-systems/b"]); + assert.equal(r.items[1].summary, FINDING_COPY["caller-absent"]); + assert.match(r.items[0].summary, /not a commit SHA; .*path-filtered/); + assert.equal(r.items[0].url, "https://github.com/bounded-systems/a"); + assert.equal(r.href, CI_SNAPSHOT_URL); +}); + +test("selectCi carries the lane's totals through, and never re-counts them", () => { + // The totals say 5 findings; the rows carry 1. The page shows the lane's + // number as the lane's number and its own count as its own — two facts, + // not one reconciled by this file. + const r = selectCi(ciFeed(undefined, { totals: { ...ciFeed().totals, findings: 5, gaps: 3 } })); + assert.equal(r.totals.findings, 5); + assert.equal(r.totals.gaps, 3); + assert.equal(r.count, 1); + assert.deepEqual(r.totals.caller, { present: 1, absent: 1, unreadable: 0 }); + assert.equal(r.standard, "green"); +}); + +test("a finding code this page does not know passes through as written", () => { + const r = selectCi(ciFeed([ciRepo({ repo: "bounded-systems/n", findings: ["something-new"] })])); + assert.equal(r.items[0].summary, "something-new"); + // …including one that happens to name an Object.prototype member. + const p = selectCi(ciFeed([ciRepo({ repo: "bounded-systems/p", findings: ["constructor", "toString"] })])); + assert.equal(p.items[0].summary, "constructor; toString"); +}); + +test("a snapshot with no findings is a real answer, not an error", () => { + const r = selectCi(ciFeed([ciRepo({ repo: "bounded-systems/clean" })])); + assert.equal(r.count, 0); + assert.deepEqual(r.items, []); +}); + +test("a snapshot predating totals or repos degrades to empty rather than throwing", () => { + const r = selectCi({ feed: "repo-standard-conformance", generated_at: "2026-09-04T20:38:14Z" }); + assert.equal(r.count, 0); + assert.equal(r.totals.rows, null); + assert.equal(r.standard, null); +}); diff --git a/test/worker.test.mjs b/test/worker.test.mjs index ade31b8..b3a0b2d 100644 --- a/test/worker.test.mjs +++ b/test/worker.test.mjs @@ -27,9 +27,27 @@ const PRS = { items: [{ repo: "bounded-systems/prx", number: 7, title: "a change", url: "https://e/7", labels: [], claimed: false }], }; +// The repo-standard conformance snapshot (desk#81), the way `.github`'s lane +// publishes it: one repo with a finding, one clean. +const CI = { + feed: "repo-standard-conformance", + generated_at: new Date().toISOString(), + totals: { + rows: 2, caller: { present: 1, absent: 1, unreadable: 0 }, + standard_run: { green: 1, red: 0, other: 0, none: 0, unreadable: 0 }, + test_lane: { present: 1, absent: 1, "n/a": 0, unmeasured: 0 }, findings: 1, gaps: 0, + }, + standard: { selftest: { state: "green" } }, + repos: [ + { repo: "bounded-systems/bare", findings: ["caller-absent"], gaps: [], caller: { state: "absent" }, standard_run: null, extra: [] }, + { repo: "bounded-systems/prx", findings: [], gaps: [], caller: { state: "present" }, standard_run: { state: "green" }, extra: [] }, + ], +}; + const ENV = { FEED_URL: "https://feed.example/board.json", PRS_FEED_URL: "https://feed.example/prs.json", + CI_FEED_URL: "https://feed.example/ci.json", DESK_LIMIT: "25", }; @@ -37,9 +55,9 @@ let realFetch; /** Serve each configured feed, or fail the one the test names. */ function stubFeeds({ fail = null, status = 500 } = {}) { globalThis.fetch = async (url) => { - const which = url === ENV.FEED_URL ? "board" : "prs"; + const which = url === ENV.FEED_URL ? "board" : url === ENV.CI_FEED_URL ? "ci" : "prs"; if (fail === which) return new Response("nope", { status, statusText: "Server Error" }); - return new Response(JSON.stringify(which === "board" ? BOARD : PRS), { + return new Response(JSON.stringify(which === "board" ? BOARD : which === "ci" ? CI : PRS), { status: 200, headers: { "content-type": "application/json" }, }); }; @@ -141,7 +159,7 @@ test("/board.json serves that host's selection", async () => { const claims = await (await get("claims.bounded.tools", "/board.json")).json(); assert.deepEqual(claims.items.map((i) => i.number), [2]); const overview = await (await get("desk.bounded.tools", "/board.json")).json(); - assert.deepEqual(overview.sections.map((s) => s.key), ["issues", "claims", "prs"]); + assert.deepEqual(overview.sections.map((s) => s.key), ["issues", "claims", "prs", "ci"]); }); test("/healthz does not touch the feed, so it answers when the feed does not", async () => { @@ -190,6 +208,46 @@ test("the overview renders what it could read and 502s on what it could not", as assert.doesNotMatch(html, /No open pull requests/); }); +// ── repo health on the overview (desk#81) ──────────────────────────────────── + +test("the overview's repo-health section reads CI_FEED_URL and names the repos with findings", async () => { + const res = await get("desk.bounded.tools"); + assert.equal(res.status, 200); + const html = await res.text(); + assert.match(html, /Repo health/); + assert.match(html, /1 of 2 public repos call the standard: 1 green, 0 red\. 1 do not call it\./); + assert.match(html, /does not call the standard CI/); + // The attribute, not the bare URL: this asserts the LINK TARGET, and a bare + // URL substring is what CodeQL reads as sanitization (js/incomplete-url-substring-sanitization). + assert.ok(html.includes('href="https://github.com/bounded-systems/bare"'), "the row links to the repo"); + assert.doesNotMatch(html, /repo null/); +}); + +test("an unreadable conformance feed keeps its slot and 502s the overview", async () => { + stubFeeds({ fail: "ci" }); + const res = await get("desk.bounded.tools"); + assert.equal(res.status, 502); + const html = await res.text(); + assert.match(html, /pick me up/); // the board sections survived + assert.match(html, /This section could not be read/); + assert.doesNotMatch(html, /every standard run is green/); // never the empty sentence +}); + +test("the board feed served as CI_FEED_URL is refused, not rendered as repo health", async () => { + const inner = globalThis.fetch; + globalThis.fetch = async (url, init) => + url === ENV.CI_FEED_URL ? new Response(JSON.stringify(BOARD), { status: 200 }) : inner(url, init); + const res = await get("desk.bounded.tools"); + assert.equal(res.status, 502); + assert.match(await res.text(), /expected the 'repo-standard-conformance' feed/); +}); + +test("a missing CI_FEED_URL is the Worker's own fault, and says which var", async () => { + const res = await get("desk.bounded.tools", "/", { ...ENV, CI_FEED_URL: "" }); + assert.equal(res.status, 502); + assert.match(await res.text(), /CI_FEED_URL is not configured/); +}); + test("the overview reads the board feed once for both board-side sections", async () => { const seen = []; const inner = globalThis.fetch; diff --git a/wrangler.jsonc b/wrangler.jsonc index b727ff8..c3d4155 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -78,6 +78,12 @@ // (scripts/prs.sh) — the Worker refuses to render it anywhere but the prs // host, and refuses any feed that does not name itself front-desk-prs-public. "PRS_FEED_URL": "https://raw.githubusercontent.com/bounded-systems/front-desk-feed/feed/front-desk-prs.json", + // Repo health (desk#81): the repo-standard conformance snapshot, published + // daily by bounded-systems/.github (its #381 lane) as main plus one API + // commit on a branch of the repo that owns the standard. The Worker refuses + // to render it anywhere but the overview's repo-health section, and refuses + // any feed that does not name itself repo-standard-conformance. + "CI_FEED_URL": "https://raw.githubusercontent.com/bounded-systems/.github/repo-standard-conformance/repo-standard-conformance.json", "DESK_LIMIT": "25" // NOT HERE, and deliberately: SESSION_SECRET, the key desk login's session