Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion scripts/ci/fleet-refs-audit-lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,35 @@ export function retiredHandleMatches(text, retiredHandles) {
}

/** `uses: owner/repo/path@ref` and `uses: owner/repo@ref`. Local (`./…`) and
* container (`docker://`) references have no owner to be wrong about. */
* container (`docker://`) references have no owner to be wrong about.
*
* LIMITATION, stated rather than assumed: this matches by line shape, not by
* YAML structural position. A `run: |` block whose FIRST physical line
* (after indentation) happens to read literally `uses: owner/repo@ref` —
* e.g. example text in an echoed usage message — would be treated as a real
* `uses:` key. Narrow and not observed in this fleet; a real YAML parse
* would close it at the cost of a dependency this script deliberately has
* none of. */
export const USES = /^\s*uses:\s*([A-Za-z0-9][\w.-]*)\/([\w.-]+)(?:\/[^@\s]+)?@/gm;

/**
* This is the actual mechanism that catches the outage: REST resolves a
* rename/transfer redirect and returns the canonical `full_name`; Actions'
* `uses:` resolver does not resolve it at all. `real` is what `resolve()` in
* fleet-refs-audit.mjs already returned for `slug` — this function makes no
* network call, so it is fully testable without a token.
*
* real === undefined → the lookup itself failed (rate limit, 5xx, etc.) —
* report as unreadable, never as clean and never as
* stale. Silence here would be worse than either.
* real === null → the repo does not exist under this name at all
* real !== slug → it exists, but Actions will resolve a DIFFERENT
* name than what's written — the exact redirect gap
* real === slug → the reference is already canonical
*/
export function verdictFor(slug, real) {
if (real === undefined) return { kind: 'unreadable', message: `${slug} (lookup failed)` };
if (real === null) return { kind: 'stale', message: `uses ${slug} — DOES NOT EXIST` };
if (real !== slug) return { kind: 'stale', message: `uses ${slug} — canonical is ${real} (Actions will NOT follow this)` };
return { kind: 'ok' };
}
11 changes: 4 additions & 7 deletions scripts/ci/fleet-refs-audit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
* broken for exactly that reason, and had no auto-merge.yml to notice.
*/

import { retiredHandleMatches, USES } from './fleet-refs-audit-lib.mjs';
import { retiredHandleMatches, USES, verdictFor } from './fleet-refs-audit-lib.mjs';

const ORG = process.env.FLEET_ORG || 'bitbaum';
const RETIRED = (process.env.RETIRED_HANDLES || 'maonakamoto').split(',').map(s => s.trim()).filter(Boolean);
Expand Down Expand Up @@ -97,12 +97,9 @@ for (const repo of repos) {
for (const [, owner, name] of text.matchAll(USES)) {
const slug = `${owner}/${name}`;
const real = await resolve(slug);
if (real === undefined) { unreadable.push(`${slug} (lookup failed)`); continue; }
if (real === null) {
stale.push(`${repo.full_name}/.github/workflows/${f.name}: uses ${slug} — DOES NOT EXIST`);
} else if (real !== slug) {
stale.push(`${repo.full_name}/.github/workflows/${f.name}: uses ${slug} — canonical is ${real} (Actions will NOT follow this)`);
}
const verdict = verdictFor(slug, real);
if (verdict.kind === 'unreadable') unreadable.push(verdict.message);
else if (verdict.kind === 'stale') stale.push(`${repo.full_name}/.github/workflows/${f.name}: ${verdict.message}`);
}
}
}
Expand Down
74 changes: 72 additions & 2 deletions scripts/test/fleet-refs-audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@
* its first real run (2026-08-28) — the workflow that exists to catch a
* retired-owner reference failed on the one line that DEFINES what a retired
* owner is, not a line that USES one.
*
* Also covers USES and verdictFor — the mechanism that actually caught the
* three real outages this audit exists for (2026-08-26/27/28), and which had
* zero test coverage until this file: only the newer, less consequential
* retired-handle check was pinned. A regex bug in USES would silently miss
* exactly the class of breakage the whole tool was built to catch.
*/
import assert from "node:assert/strict";
import { retiredHandleMatches } from "../ci/fleet-refs-audit-lib.mjs";
import { retiredHandleMatches, USES, verdictFor } from "../ci/fleet-refs-audit-lib.mjs";

const RETIRED = ["maonakamoto"];

Expand Down Expand Up @@ -72,4 +78,68 @@ assert.deepEqual(
"a file with no retired handle anywhere must report nothing"
);

console.log("OK: 5 assertions passed");
// --- USES: what actually gets checked against GitHub -----------------------
const usesOf = (text: string) => [...text.matchAll(USES)].map(([, owner, name]) => `${owner}/${name}`);

assert.deepEqual(
usesOf("jobs:\n x:\n uses: bitbaum/fleetcrown@main\n"),
["bitbaum/fleetcrown"],
"a plain owner/repo@ref uses: line is matched"
);

assert.deepEqual(
usesOf("jobs:\n x:\n uses: bitbaum/fleetcrown/.github/workflows/selfhost-deploy.yml@main\n"),
["bitbaum/fleetcrown"],
"the owner/repo is extracted even with a path and filename after it"
);

assert.deepEqual(
usesOf("jobs:\n x:\n uses: ./.github/actions/local-thing\n"),
[],
"a local action (no owner, no @ref) is not matched"
);

assert.deepEqual(
usesOf("jobs:\n x:\n uses: docker://ghcr.io/owner/image:tag\n"),
[],
"a docker:// reference has no owner/repo to be wrong about and must not match"
);

assert.deepEqual(
usesOf("jobs:\n a:\n uses: bitbaum/one@v1\n b:\n uses: bitbaum/two@v2\n"),
["bitbaum/one", "bitbaum/two"],
"every uses: line in a file is matched independently"
);

assert.deepEqual(
usesOf(" uses: bitbaum/fleetcrown@main\n"),
["bitbaum/fleetcrown"],
"indentation before uses: does not prevent a match"
);

// --- verdictFor: the actual redirect-detection decision --------------------
assert.deepEqual(
verdictFor("bitbaum/fleetcrown", "bitbaum/fleetcrown"),
{ kind: "ok" },
"a reference already naming its canonical owner is fine"
);

assert.deepEqual(
verdictFor("catomean/fleetcrown", "bitbaum/fleetcrown"),
{ kind: "stale", message: "uses catomean/fleetcrown — canonical is bitbaum/fleetcrown (Actions will NOT follow this)" },
"REST resolving a DIFFERENT canonical name is the exact redirect gap Actions falls into"
);

assert.deepEqual(
verdictFor("catomean/does-not-exist", null),
{ kind: "stale", message: "uses catomean/does-not-exist — DOES NOT EXIST" },
"a 404 from REST is reported as stale, not silently skipped"
);

assert.deepEqual(
verdictFor("bitbaum/fleetcrown", undefined),
{ kind: "unreadable", message: "bitbaum/fleetcrown (lookup failed)" },
"a failed lookup (rate limit, 5xx) must be unreadable — never reported as clean, never as a false stale"
);

console.log("OK: 15 assertions passed");
Loading