refresh: ask who authorised a run, not whether it is CI

Both overrides stand in for a person deciding something, and the gate that
enforced it asked the coarser question. `isCi()` got the case that matters
wrong: a `workflow_dispatch` sets CI=true, so somebody clicking "Run workflow"
was refused exactly as the cron was, which made both flags unreachable from the
workflow at all. A dispatch is a person, and a better-evidenced one than a local
shell — GitHub records which one, and the run log keeps it.

`runAttendance` draws the line where it belongs: a local shell is a person, a
dispatch is a person and names the actor, and a schedule — or any other runner
event, erring that way on purpose — is not. The run now prints which override
was used and who authorised it, because a local shell leaves no trace anybody
else can read.

The schedule stays refused, and for the robots override that is not ceremony.
From an address that gets a challenge we never receive robots.txt at all, so a
cron standing on the recorded permission has no way to notice the host
withdrawing it, and that permission has no expiry. The challenge-or-refusal
narrowing does not close this: a plain 403 still stops us, but a robots.txt
edited to disallow us would be invisible, because a challenge arrives instead of
a file. A person re-reading it is the only thing that ever re-validates it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-20 00:01:11 +02:00
co-authored by Claude Opus 5
parent d0b0f2e00c
commit 2fb944135d
4 changed files with 164 additions and 34 deletions
+63
View File
@@ -8,6 +8,7 @@ import {
DEFAULT_HOST_GAP_MS,
outputs,
parseArgs,
runAttendance,
runRefresh,
stepSummary,
type RefreshOptions,
@@ -1076,3 +1077,65 @@ describe("flags", () => {
);
});
});
/**
* The override gates ask who authorised the run, not whether a runner is
* present. That distinction is the whole reason the workflow can offer these
* toggles at all: a `workflow_dispatch` sets `CI=true`, so the old `isCi()`
* check refused a human clicking "Run workflow" exactly as it refused the cron.
*
* What must not regress is the other half — a *schedule* must never be able to
* assert either override, because an override on every cycle is the new default
* with extra steps, and for the robots one a cron cannot re-read the permission
* it would be standing on.
*/
describe("runAttendance", () => {
test("a local shell is a person", () => {
const a = runAttendance({});
expect(a.attended).toBe(true);
expect(a.attended && a.by).toBe("a local shell");
});
test("a scheduled run is not, and says so", () => {
const a = runAttendance({ CI: "true", GITHUB_EVENT_NAME: "schedule" });
expect(a.attended).toBe(false);
expect(!a.attended && a.why).toBe("schedule");
});
test("a manual dispatch is a person, and names the actor", () => {
const a = runAttendance({
CI: "true",
GITHUB_ACTIONS: "true",
GITHUB_EVENT_NAME: "workflow_dispatch",
GITHUB_ACTOR: "StereotypicalCat",
});
expect(a.attended).toBe(true);
expect(a.attended && a.by).toBe("a manual dispatch by StereotypicalCat");
});
test("a dispatch with no actor recorded is still a person", () => {
const a = runAttendance({ CI: "true", GITHUB_EVENT_NAME: "workflow_dispatch" });
expect(a.attended).toBe(true);
expect(a.attended && a.by).toBe("a manual dispatch");
});
test("any other runner event counts as unattended", () => {
// Erring towards "no person" — a push- or repository_dispatch-triggered run
// gets no overrides either, so a schedule cannot be dressed up as one.
for (const event of ["push", "repository_dispatch", "pull_request", ""]) {
const a = runAttendance({ CI: "true", GITHUB_EVENT_NAME: event });
expect(`${event}: ${a.attended}`).toBe(`${event}: false`);
}
});
test("GITHUB_ACTIONS alone is enough to count as a runner", () => {
const a = runAttendance({ GITHUB_ACTIONS: "true", GITHUB_EVENT_NAME: "schedule" });
expect(a.attended).toBe(false);
});
test("an empty CI variable is not a runner", () => {
// `CI=` is how a shell unsets it in practice; treating it as a runner would
// refuse a person their own flags.
expect(runAttendance({ CI: "" }).attended).toBe(true);
});
});