Skip to content

feat(ci): Add revdep4, sequential halves driven by a two-ended bash work queue - #2857

Open
krlmlr wants to merge 27 commits into
mainfrom
claude/revdep-check-collision-cfavj8-seq
Open

feat(ci): Add revdep4, sequential halves driven by a two-ended bash work queue#2857
krlmlr wants to merge 27 commits into
mainfrom
claude/revdep-check-collision-cfavj8-seq

Conversation

@krlmlr

@krlmlr krlmlr commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Proposes revdep4, the queue engine: old and new halves of each reverse dependency checked sequentially, with the lost concurrency won back across packages by a custom bash work queue — one of two sibling proposals (the other is revdep3, #2856). Both reuse the good bits of revdep2 through a shared, engine-agnostic core, and are fully interoperable with each other.

The problem

revdep2 checks each reverse dependency against the released and the dev igraph simultaneously on one host. The PSOCK port collision (patched with R_PARALLEL_PORT) was one member of an open-ended interference class — shared /tmp, caches, locks, any singleton a check assumes it owns. Simultaneously checking the same package with two libraries is not a supported mode of operation.

What revdep4 does

Removes the simultaneity instead of isolating it: per package, the old half runs, then the new half — no overlap, ever. Parallelism moves to the package level: a shard runs workers packages at once (default 4, the runner's cores), each check in its own container, so different packages cannot collide either (stronger than revdep2, which only separated the two halves' ports).

The queue (revdep4/queue.sh, bash, flock-guarded cursors) consumes the shard's heaviest-first package list from both ends: one heavy lane takes the heaviest remaining package — the longest checks start first and are never the straggler discovered last — while the remaining light lanes drain the long tail of cheap packages from the other end; the two meet in the middle. Deadline-aware claiming defers what no longer fits; a crashed claim still writes an error manifest line through a four-rung fallback ladder (compare-one → --error rerun → printf-JSON template → supervisor sweep), so no claimed package can ever vanish silently; three check slices with an upload after each bound what a reclaimed runner can lose.

Heaviness — answering the design question directly: the queue key is expected check seconds — measured on this infrastructure by earlier revdepx runs (either workflow, youngest wins), else CRAN's T_total × a self-calibrating scale, else the cohort median. Package size and dependency count/size are deliberately not in the key: they price installation, which the shared universe image amortizes to zero (dependency count still gates the depfail screen). This is revdep2's proven cost model, re-priced for sequential halves (×2 per package, ÷workers per shard, with a max(heaviest, sum/workers) bound so a giant-dominated shard is not flattered).

Sequential halves make per-half timings real: t_old/t_new become true measurements (revdep2 could only record the pair's wall clock for both), which sharpens the cost model over time.

The old half always runs; a stored old result is a second opinion only. Where the plan certifies an earlier run's old-version result as comparable — same revdep version, our CRAN version, container R, base-image tag, dependency fingerprint, within age — the fresh old check is compared against it and baseline_agrees records whether they match, with disagreements reported as drift. It never substitutes for the check, however tempting the saved wall clock: a fresh old is the only result whose provenance the run fully controls. (revdep2-era baselines lack the base-image key and are never offered — they were measured on a different platform entirely.)

Pipeline (shared with revdep3): plan + base image (parallel) → build (dev binary inside the container) → universe image (whole dependency universe + sysreqs on GHCR, delta-updated, age-bounded) → test shards (queue) → collect. Containers also bring per-check memory caps (an OOM kills one container, not the runner) and a pinned check platform (r-version, default oldrel — a fixed target).

Kept from revdep2 / dropped

Kept (via .github/workflows/revdepx/): the planner with all its inputs (packages/broken/retry-run/part/dry-run), the manifest schema and result vocabulary, baseline lineage, timings + calibration, three-slice uploads, resource sampler, chunked deadline-bounded installs, load test, comparison/diff/salvage, report/collect and the committed revdep/ record. Dropped: the port hack, the preflight job and all prebuilt-library artifacts/donor walks/tar machinery, host toolchain setup on shards, per-shard host installs (kept only as disaster-recovery fallback when the universe image is unavailable). revdep2 itself is untouched by this PR.

Compatibility with revdep3

Both PRs ship .github/workflows/revdepx/ byte-for-byte identical (this PR adds revdep4.yaml + revdep4/; #2856 adds revdep3.yaml + revdep3/; merging both leaves one copy, no conflicts). One artifact family (revdepx-*), one manifest/baseline/timings schema, one comparison code path, one universe-image lineage, one concurrency group per ref. Either workflow reads the other's baselines as second opinions, consumes the other's timings (canonical per-half seconds; each plan prices its own engine from them), retries the other's runs, and starts from whichever universe image was refreshed last.

Notes for review

  • First push creates the GHCR packages; org policy forbidding GITHUB_TOKEN package writes degrades gracefully to artifact transport.
  • revdep2-era baselines/timings are never consumed (different platform, and the base-image key walls them off); the first run checks everything fresh.
  • Validated locally: plan.R dry runs against live CRAN metadata for both engines (775 revdeps → 20 shards; queue weights verified at 2× pair weights, worker-aware shard walls, the giant multinma isolated by the LPT bound); the queue exercised standalone with stub checks (single-claim guarantees, two-ended consumption, deadline defer, all fallback rungs, the final sweep) and end-to-end against a local Docker daemon with rocker/r-ver:4.5.3 — two real CRAN packages checked sequentially in containers with real per-half timings, and the second-opinion path verified (fresh old check ran, stored result consulted, baseline_agrees recorded on the manifest line); bash -n/parse() clean; actionlint findings identical to what revdep2.yaml already carries.
  • The "Smoke test: stock R" failures on the first push were the GitHub outage (HTTP 503 from gh one second into the job); the follow-up push re-triggers CI cleanly.
  • The first live run (most, depth 2) paid for itself in infrastructure findings, all fixed here: docker commit of the universe image filled the runner's single disk (there is no /mnt on ubuntu-26.04 runners) and killed the runner agent mid-copy — the commit step now measures the delta first and skips with a loud warning when the copy cannot fit; the dev binary silently vanished from its artifact because file.rename() across two bind mounts fails with EXDEV and its FALSE went unread — now a checked file.copy(); a resource sampler backgrounded in its own step stops reaching the job log once that step ends, so the long steps now run in-step samplers (a dying runner leaves streamed evidence); and the pak cache never saved because root-in-container wrote it — now chowned back. The shard-local fallback got a production validation for free: with no universe image, every shard built its own from the base image in ~12 minutes and carried on.
  • The branch temporarily carries a push trigger with hardcoded inputs (most, depth 2) that drives the live test runs, because workflow_dispatch registration requires the file on the default branch; it will be reverted once the test run is green.

  • By submitting this pull request, I assign the copyright of my contribution to The igraph development team.

…ork queue

revdep2 checks each reverse dependency's CRAN and dev halves
as two simultaneous `R CMD check` processes on one host.
Simultaneously checking the same package against two libraries
is not a supported mode of operation for the packages being checked:
the PSOCK port collision that needed the `R_PARALLEL_PORT` split
was one failure class of an open-ended family.

revdep4 removes the simultaneity instead of isolating it:
per package, the old half runs, then the new half, never both at once.
The lost concurrency is won back across packages --
a custom bash work queue (revdep4/queue.sh) checks
REVDEPX_WORKERS packages at a time, each half in its own container,
which isolates different packages from each other too.
The queue consumes the shard's heaviest-first list from both ends:
one worker takes from the heavy end,
so the longest checks start first
and are never the straggler discovered last,
while the rest drain the cheap tail from the other end.

Sequential halves make per-half durations real measurements,
and give baseline reuse teeth again:
a valid old-version result from an earlier run of either workflow
skips the old half outright --
sound now, unlike revdep2's abandoned cross-run comparison,
because both eras check inside the same pinned universe image
and are parsed by the same code.

The engine-agnostic core lands in .github/workflows/revdepx/,
shared byte-for-byte with the sibling revdep3 proposal:
one artifact family, one manifest, baseline and timings schema,
one comparison code path, one universe-image lineage --
either workflow reuses the other's baselines, timings,
images and reports, and `retry-run` accepts a run of either.

revdep2 stays untouched;
this is a proposal beside it, not a replacement of it yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
claude and others added 26 commits August 17, 2026 15:53
…nion only

The queue engine briefly revived what revdep2 had retired:
substituting a stored old-version result for the old check
when the plan judged it comparable.
The container platform makes that far safer than it was,
but a fresh old check is the only result
whose provenance the run fully controls --
so the old half now always runs, in both engines,
and a comparable stored result rides along
purely as a second opinion:
`baseline_agrees` records whether the fresh check reproduced it,
and a disagreement is printed as drift.

Concretely: the queue engine's per-package price is always two halves
(no baseline discount in the plan),
the queue file loses its skip_old column,
queue.sh always runs both halves,
compare-one.R always reads a fresh old side,
and compare_halves() loses its baseline-substitution mode.
The baseline artifact, its validity conditions
and the cross-workflow lineage are unchanged --
only what a valid row is *for* has narrowed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
…licy

Two sentences still described the retired skip-old design:
the timings bullet halved the queue plan's bill
where a baseline covered the old half,
and the baseline bullet spoke of reusing rows.
Both halves always run fresh now,
and a stored row is only ever a second opinion.

(This push also re-triggers the checks
that failed during the GitHub outage.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
An independent review of both revdepx branches confirmed four bugs
and a handful of gaps; this applies everything that lives in the
shared core or on this branch.

- The check-slice cut no longer dies on shards with fewer runnable
  packages than slices: `seq(index, n, by = of)` is an R error when
  index exceeds n, which crashed slices of 1-package retry shards and
  -- with an empty runnable set -- erased recorded depfail diagnoses
  into `missing`. The slice is also cut before the source downloads
  now, so each slice fetches only its own tarballs instead of the
  whole shard's three times over. The same seq() trap is fixed in
  plan.R's `part` split.
- The base image now installs callr: util.R's run_with_timeout()
  silently degrades to an unbounded inline call without it, and every
  "bounded" pak call of the in-container universe build ran with no
  clock -- the exact hang class revdep2 added callr for on the host.
  A denied base-image push is now a hard error too: unlike the
  universe image it has no artifact fallback, and everything
  downstream pulls it.
- timing.json's `script_seconds` accumulates across check slices;
  before, the final slice's overwrite dropped the earlier slices'
  driver time and the calibration charged it to per-shard setup,
  inflating every later plan.
- The `compared` commit gate counts only this run's own comparisons:
  carried-over retry results could pass it and let a run that learnt
  nothing overwrite the committed report.
- ensure_check_sysreqs() runs `apt-get update` before its direct
  install -- the base image deletes the apt lists, so the install
  otherwise failed quietly.
- The shared concurrency group is built from `github.ref_name`, so a
  dispatch with the `ref` input filled in serializes against one
  without it on the same branch.
- check-half.sh tells its outer safety-net timeout apart from a real
  check timeout (both exit 124) by when the axe fell, and reports the
  outer case as a runner failure, not a package one.
- The queue's claimed.log and queue-state.json are copied into the
  results artifact per slice, and queue.sh refuses loudly to run
  without flock instead of racing its claim cursors in silence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
The previous run passed every substantive step --
build, check, pkgdown -- and failed only on its final
`gh api` call posting the success status,
which caught a stray HTTP 503 from the outage's tail.
The sibling branch's identical run posted its status
a minute later and went green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
…h 2)

A workflow_dispatch workflow is only registered once its file exists
on the default branch, so a branch-only workflow cannot be dispatched
by API or UI. Fire this first run from the branch itself instead --
the same device the old revdep.yaml uses -- with `which: most` and
`depth: 2` hardcoded, since a push event carries no inputs.

Revert after the run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
The first live run failed in the build container:
"vignette builder 'knitr' not found".
The pak bootstrap installs the package's hard dependencies,
and building vignettes needs the Suggests tree on top --
which revdep2's host build inherited from setup-r-dependencies
without anyone deciding it.

The binary exists to be installed into the checks' new-half
library, and no check ever builds or reads the package-under-test's
own vignettes, so `--no-build-vignettes` removes the requirement
instead of importing it.
Also forward GITHUB_SHA into the container,
so meta.json records the real commit
when git cannot answer inside the mount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
The first live run's universe job died in `docker commit`:
these runners have one disk (there is no /mnt volume),
`docker commit` writes a full copy of the container's rw layer --
the entire installed universe -- onto it,
and when the copy filled the disk the runner agent itself
could no longer write.
The job was killed with its logs, artifacts and cache saves;
the step froze as `in_progress` and the `always()` steps never ran.

So: measure the rw layer (`du` over the overlay upper dir)
and the free space first,
and skip the commit with a loud warning when it cannot fit --
the shards' shard-local build fallback carries the run,
which is the same path a failed universe job already takes,
and which the dying run demonstrated works.
After a successful commit,
drop the build container before the push,
so the original copy of the delta is freed
before the push wants scratch space.
The artifact fallback gets its own space check,
and a new `saved` output gates the artifact upload and download
instead of overloading `pushed`.

The resource sampler turned out to stream nothing after its own step
ended: a backgrounded sampler keeps appending to its RESOURCE_LOG file,
but its stdout stops reaching the job log --
and the streamed lines are all that survives a dying runner,
which is exactly when they are needed.
The long steps (universe build, commit, shard image prep)
now run their own in-step samplers on top of the file-writing one.

Also rename the "Move docker onto the big disk" steps to tell the
truth -- there is no big disk to move to, only room to make --
revert the temporary push trigger and its hardcoded inputs
now that the live run is under way
(removing the trigger in the same push means this push starts no run),
and bump docker/login-action to v4 for the Node.js 24 runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
Every shard of the first live run failed installing the dev binary:
the revdepx-pkg artifact carried meta.json and the source tarball,
but not the binary meta.json named.
build.R moved the binary with file.rename(),
and in the build container the working directory and OUT_DIR are
two different bind mounts --
rename(2) across mounts fails with EXDEV,
which file.rename() reports only as a FALSE nobody read.
Copy with file.copy(), check the result, and fail loudly,
for the source tarball too.

Also chown the pak cache back to the runner user
after the build and universe containers wrote it as root:
their 600 lock files made the cache post-step's tar fail
("Permission denied"), so the cache never saved --
and a warm cache is exactly what makes a retry cheap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
…th 2)

The first live run died on the two bugs the previous commits fix
(the universe commit filling the one disk; the dev binary lost to a
cross-mount file.rename()), so the test run runs again.
Same device as before -- push trigger plus hardcoded inputs,
because a workflow_dispatch workflow is only registered once its file
exists on the default branch -- and this time `run-name` is hardcoded
too, so the run is titled what it actually checks.
Revert this commit after the test run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
…dence first

The guarded commit died again -- run 32084560474, ~34 minutes in,
same as the run before it, runner and all --
and a dead runner turns out to lose even the log lines it already
streamed, so the guard's own measurements died with it.

Three consequences, all in the universe job:

- A new step measures the delta (bytes AND inodes -- `df` can show
  free gigabytes while millions of small package files run the inode
  table dry) and uploads the numbers as an artifact BEFORE the commit
  runs. Whatever kills the copy, the post-mortem now starts with data.
- The guard reads those numbers, refuses an unmeasured delta
  (after two dead runners the burden of proof sits on the commit),
  and checks inodes as well as bytes.
- The commit runs under a watchdog that stops the docker daemon when
  disk, inodes or memory approach zero -- killing the client would
  not abort the daemon-side copy -- then restarts it so the report,
  artifact, cache save and logout still work. A wrong measurement can
  cost the commit, never a third runner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
…ost, depth 2)"

This reverts commit 0a2c237.

The live test run (32084560474) is under way pinned to its own
commit, so the branch no longer needs the push trigger --
and reverting it in the same push as the universe hardening means
that push starts no spurious sibling run.
The workflow is dispatch-only again, with the run title and the
which/depth defaults back on the dispatch expressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
… before retrying singly

Two answers to what the universe build log shows.

The one-at-a-time installs at the end are the salvage pass:
a pak transaction is all-or-nothing,
so one broken package strands its whole 400-package chunk,
and the retry then paid pak's per-call resolution overhead
once per stranded innocent --
hundreds of calls, hours of tail,
and the install deadline cutting off packages
that were never broken at all.
A middle rung now re-tries the missing set in chunks of 50 first;
only what still refuses gets the one-at-a-time treatment
that names each failure on its own.

And the rw layer was carrying temporary files into `docker commit`:
every pak install that hit its timeout died mid-build
and left its extracted sources and objects in the container's /tmp,
and apt's package lists stayed behind from the sysreqs runs --
all of it copied by the commit,
and on the delta path inherited by every descendant image.
The build containers (universe job and shard-local fallback alike)
now bind-mount /tmp from the host,
so build residue never enters the layer at all,
and image.R sweeps apt lists and any remaining /tmp leftovers
before the library is indexed.
The pak download cache was already a host bind mount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
…a flat scan

The one-at-a-time salvage pass paid pak's per-call overhead once per
stranded innocent -- and that overhead is irreducible from the
driver's side: the resolver runs per call, in pak's own private
subprocess, so the only lever is the number of calls.

Divide and conquer, in the driving R process (which risks nothing:
every pak call already runs in its own clocked subprocess).
Retry the missing set whole; a failing set of more than one package
splits into three, down to single-package leaves where a genuine
failure names itself. Subsets without a culprit succeed as one call,
so d culprits hiding in n packages cost about 3 * d * log3(n) calls
instead of n. Simulated with pak's prefix-install behavior modelled:
for n = 400 and d = 1..10, trisection takes 14-78 calls where the
flat scan took 400; fan-outs 3, 4 and 5 are within noise of each
other (k/ln k is minimal at 3), and 2 is ~15% worse.
This supersedes the chunks-of-50 middle rung from the previous
commit; missing_from() re-measures before every call, so nothing a
failing transaction did install is ever asked for twice, and a big
retry that merely times out splits and continues instead of
starting over.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
…th 2)

The first live run died on the two bugs the previous commits fix
(the universe commit filling the one disk; the dev binary lost to a
cross-mount file.rename()), so the test run runs again.
Same device as before -- push trigger plus hardcoded inputs,
because a workflow_dispatch workflow is only registered once its file
exists on the default branch -- and this time `run-name` is hardcoded
too, so the run is titled what it actually checks.
Revert this commit after the test run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
Run 32106802120 showed every apt-get that pak executed failing.
Not privileges -- everything that runs apt runs as root -- but the
/tmp bind mount the previous hygiene commit introduced: a plain
mkdir hands the container a 755 directory where /tmp semantics
demand 1777, apt-key cannot create its temporary config there
("Couldn't create temporary file /tmp/apt.conf.XXXX"), and from
that moment every repository fails signature verification and every
apt-get fails with it.

Reproduced locally against rocker/r-ver:4.5.3:
a 755 host directory mounted at /tmp fails apt-get update on all
four repositories exactly as the run did;
chmod 1777 on the same mount and the same update runs clean.

chmod 1777 on every host directory that becomes a container's /tmp:
the universe build, the shard-local fallback build, and -- for
strict /tmp semantics even single-user -- the check containers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
…hardening

Four changes, each answering something the live runs surfaced.

R 4.6.1, hard-coded: at least one reverse dependency needs R-release,
so the pinned default moves from oldrel to 4.6.1
(dispatch can still pass 'oldrel', 'release' or any version).
The base image tag follows the recipe hash, so the new platform
builds itself on the next run.

Xvfb in the base image, used everywhere R runs: Tk-based packages
(gWidgets2tcltk) initialise Tk while lazy-loading AT INSTALL TIME
and die headless with `[tcl] invalid command name "font"`.
CRAN's own machines check under X; ours now start a virtual
framebuffer -- image.R for installs and load tests, check-half.sh
for the checks themselves.

The universe delta is measured even under the containerd image
store: GraphDriver is empty there ("upper: <none>"), which made the
guard skip a commit that would have fit five times over
(run 32114635495: ~10G delta, 94G free).
`docker ps -as` prices the rw layer instead when du cannot.

And the sharp edges from the same run: the check-sysreqs survey runs
pak::pkg_sysreqs in chunks of 300 (one call over 3435 packages grew
past 14 GB and was OOM-killed), the build containers get memory caps
so a future kill stays inside the container instead of gambling the
runner, the load test computes its roots from Depends+Imports only
(LinkingTo-only packages -- BH, cpp11 -- were counted as covered yet
never actually loaded; now they are roots and load-tested),
and the check slices run in-step resource samplers,
so the minute-by-minute load lines reach the log
from the phase where the minutes actually go.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
…ost, depth 2)"

This reverts commit bf9ac26e2517889113c98d270ff7fa15b40a54cd.

The live test has its completed run (32114635495: 3435 packages,
20 shards, collect green, report committed), so the branch goes back
to dispatch-only. Removing the trigger in its own push means this
push starts no run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
Run 32114635495's manifest recorded 2293 of 3435 packages as
`deferred` with empty messages -- yet every shard's three check
slices each ran for a real hour. The slices did the work;
the bookkeeping destroyed it: the check phase opened with
`file.create(manifest_path)`, and file.create() TRUNCATES an
existing file. Every slice wiped its predecessors' manifest lines
at startup, the account-for-everything sweep then faithfully
re-wrote those packages as `deferred`, and the per-slice artifact
upload (same name, overwrite) propagated the loss. The sweep at the
end was already written to leave existing lines alone --
it just never got to see them.

Create the manifest only when it does not exist yet.
The interim semantics stay exactly as designed: earlier slices'
lines survive, later lines win per package in the collector,
and the last upload carries the whole shard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
The reports committed by runs 32084560474 (cancelled mid-run) and
32114635495 (the slice-truncation bug, fixed in the previous commit)
carried entries that describe harness artifacts as package results:
shard-cancellation "deadline" failures for packages that were never
reached, and `deferred` rows for 2293 checks whose finished results
the truncation destroyed. Rather than hand-prune fabrications,
the report returns to the last trustworthy state (main's), and the
next complete run rewrites it from scratch with real data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
…th 2)

The first live run died on the two bugs the previous commits fix
(the universe commit filling the one disk; the dev binary lost to a
cross-mount file.rename()), so the test run runs again.
Same device as before -- push trigger plus hardcoded inputs,
because a workflow_dispatch workflow is only registered once its file
exists on the default branch -- and this time `run-name` is hardcoded
too, so the run is titled what it actually checks.
Revert this commit after the test run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
Run 32148999976 got everything right up to the handoff:
delta measured via docker ps -as (14.9G -- the SizeRw fallback
works), guarded commit succeeded in ten minutes, runner healthy.
Then three dominoes:

- `docker push` refused the commit-created image outright:
  the containerd store commits an OCI manifest INDEX, and pushing
  one is rejected ("trying to push a manifest list/index").
  Push `--platform linux/amd64` -- the fix the error message itself
  prescribes -- with the plain push kept as fallback for daemons
  without the flag.
- The artifact fallback then `docker save`d a 1336-BYTE
  manifest-only shell of the same index -- no layers -- and shipped
  it. Save with `--platform` too, and refuse to upload anything
  under 100 MB: a universe image compresses to gigabytes, and an
  empty shell costs every shard its run.
- shard-prep docker-loaded the shell, correctly recognised it
  ("no readable /opt/revdepx/lib-index.json") -- and exited instead
  of falling back. A bad image from the pull or the artifact now
  falls through to the shard-local build; only the locally built
  image still fails the shard on that test, because there is no
  further rung.

Also: one failing chunk of the sysreqs survey no longer costs the
whole survey (per-chunk tolerance; the same run lost it to a single
pak subprocess error).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
Run 32158907637 characterised the containerd image store's commit
completely: the committed image is an OCI index whose children the
daemon never pulled, so a plain push is refused ("not all of them
are available locally"), `--platform linux/amd64` cannot select a
platform the commit never labelled ("does not provide the specified
platform"), and a plain save reduces to a manifest-only shell --
which the new 100 MB floor correctly refused, and the shards fell
through to local builds as designed.

The daemon.json this workflow already writes now also disables the
containerd snapshotter, restoring the classic store whose commit is
a plain single-manifest image that pushes, saves and du-measures
exactly as everything here assumes. The --platform fallbacks and
the save floor stay as belt and braces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
…ost, depth 2)"

This reverts commit db4fde9.

Run 32158907637 (most, depth 2, R 4.6.1) is under way pinned to its
own commit and needs no trigger; reverting it here lets the held
classic-store fix ride the same push without spawning a sibling run.
Re-add the trigger only if another push-driven revdep4 test run is
wanted before the PR merges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UB7YutLzVWU7xCTYUvF3kF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants