diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06be797d..d93793e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -398,11 +398,12 @@ jobs: # most of the workspace, and the failure mode of getting it wrong is a stale # site with no signal that it is stale. A redundant build is the cheaper error. # - # On improvise-864a (concurrent merges racing on one Pages environment, the - # superseded run failing "Found 0 artifacts"): building once shortens the gap - # between upload and deploy — 182s before, ~115s after, on the run 35535225637 - # shape — so the window in which a superseding run can expire this run's - # artifact is smaller. It does not close the race; it does not widen it. + # On improvise-864a: the mechanism is NOT supersession and NOT queue depth — + # another run's SUCCESSFUL deployment invalidates this run's `github-pages` + # artifact, measured over 100 runs. Building once shortens the upload→deploy + # window (182s → ~115s on the run 35535225637 shape), and the window is where + # the whole risk lives. It narrows the race; it does not close it. The + # evidence and what it rules out: context/code/tooling.md §3. # `needs` is EVERY check job, which is the only rule that does not go stale. # `kb` was the one absent, on the unstated ground that kb.pl is not shipped by # the site — but neither is `harness-lint`'s crate, and it was in the list, so diff --git a/context/code/tooling.md b/context/code/tooling.md index afa47a50..c27c9233 100644 --- a/context/code/tooling.md +++ b/context/code/tooling.md @@ -209,3 +209,70 @@ the job keys and the `needs:` list off indentation, tracking which job a the deploy's and pass. `README.md` therefore states the rule and deliberately **does not list the jobs**: a second copy with nothing holding it to the first is what went stale, twice. + +### Why the Pages deploy fails on "Found 0 artifact(s)" (improvise-864a) + +`actions/deploy-pages@v5` intermittently fails a push to `main` with *No +artifacts named "github-pages" were found for this workflow run*, while the +upload step was green in that same run. The ticket framed this as concurrent +merges racing on one Pages environment, and PR #46 proposed skipping the deploy +unless the commit is the tip of `main`. **Both the framing and the guard are +wrong, and measurement says so.** + +**What was measured.** All 100 push-to-`main` CI runs from 2026-09-19T22:02Z to +2026-09-21T05:02Z, from the run, job, step and log APIs (job metadata and logs +outlive artifacts, which expire in a day and are all 404 now — so this is +reproducible where the ticket's own artifact-count table is not): + +| deploys | with another run's SUCCESSFUL deploy completing inside this run's own upload→deploy window | +|---|---| +| 12 artifact-not-found failures | **12 (100%)** | +| 56 successes | 2 (3.6%) | +| 4 other failures | 0 — all four are the unrelated *Create Pages site failed: Resource not accessible by integration*, from before `enablement: false` (improvise-3gy) | + +**The direction is settled, and it is neither of the two the ticket offered.** +The oldest run of the 17:47Z batch failed at 17:47:23Z — *before* the next +deploy in the batch had started (17:47:31Z) and before any Pages deployment +existed at all, since `deploy-pages` fetches the artifact metadata *before* it +creates a deployment. So the failure cannot have been caused by this run's own +deploy, by supersession, or by a concurrent deploy. The artifact was already +gone. **The reaper is another run's *successful* deployment.** + +**It is scoped by artifact NAME, not by run.** In run 35560726456 the build job +finalized three artifacts seconds apart — `harness-serve`, `wasm-bundles` and +`github-pages`, each with an id — and 2m21s later that run's own lookup reported +*Found 2 artifact(s)* and no `github-pages`. Same run, same token, same call, +same retention: the two siblings survived and only `github-pages` was missing. +That one observation rules out the whole family of general explanations — +service flakiness, eventual consistency, retention, a token scope, a v3/v4 +backend mismatch — because any of them would have taken the siblings too. + +**Four things this kills.** Queue depth is not the mechanism: the three deploys +of the 17:47Z batch never queued, running 9s and 17s apart with each failure +taking one second, so the `pages` group serialized nothing. Retention is not it +either — all three shared `retention-days: 1`, and since the *newest*-finalized +expires last, expiry can produce 0/1/1 or 0/0/1 but never the ticket's 0/1/0. +Nothing in `deploy-pages` deletes an artifact (an upstream feature request asks +for exactly that capability, which is only open because nothing does). And the +action-version theory dies on the interleaving: successes and failures alternate +minute by minute on identically-resolved action SHAs. + +**One clause of the ticket's own correction is falsified, and the conclusion +still holds.** The correction argued the tip-of-`main` guard would have skipped +the one deploy that published "and let 52507a6 deploy and fail anyway" — which +assumed 52507a6's artifact loss was independent of its neighbour. It was not: +5f871f7's deploy completed at 17:47:40Z, four seconds before 52507a6's began. +Fewer successful deploys really is the right lever, so PR #46 aimed at the right +quantity for the wrong reason. **It should still not be shipped**, because the +association is not a mechanism and the mechanism is not deterministic: 5f871f7 +had a foreign deploy land inside its own window too and survived. A guard +measured at nothing, against a probabilistic effect, is +`doctrine(measure_the_obvious_fix_before_reporting_it_as_the_fix)` exactly. + +**What the evidence does license.** The risk lives entirely in the upload→deploy +window — every victim had a foreign deploy land inside its own — so shortening +that window is the one lever with evidence behind it, and consolidating the +build already did some of that. Closing it properly means no two runs holding a +`github-pages` artifact across a deployment, which is a real change to the job +graph with real costs and is not attempted here. A retry is **not** a fix: the +artifact is gone rather than transiently unlistable, so a retry cannot find it.