Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
24c2491
feat(ci): Add revdep3, pair checks in isolated containers on a shared…
claude Aug 17, 2026
76260ae
fix(ci): Always run the old half; a stored old result is a second opi…
claude Aug 17, 2026
a350845
docs(ci): Align the shared README with the second-opinion baseline po…
claude Aug 17, 2026
c759c0f
fix(ci): Apply the clean-context review findings to the shared core
claude Aug 17, 2026
68a0c11
fix(ci): Build the dev binary without vignettes
claude Aug 17, 2026
3e4c84e
fix(ci): Harden the universe commit and the dev binary against the on…
claude Aug 18, 2026
33ecd8a
fix(ci): Put the universe commit under a watchdog and publish its evi…
claude Aug 18, 2026
625671d
fix(ci): Keep temporary files out of the universe layer, and re-chunk…
claude Aug 18, 2026
f31d55d
perf(ci): Isolate failing universe packages by trisection instead of …
claude Aug 18, 2026
d44fcc4
fix(ci): Give the bind-mounted /tmp real /tmp permissions
claude Aug 18, 2026
cac319a
feat(ci): Check under R 4.6.1 with a virtual display, and finish the …
claude Aug 18, 2026
6ae2a4a
ci: TEMPORARY push trigger for the revdep3 live test run (most, depth 2)
claude Aug 18, 2026
c6541dd
Revert "ci: TEMPORARY push trigger for the revdep3 live test run (mos…
claude Aug 18, 2026
284536e
fix(ci): Never truncate the shared manifest between check slices
claude Aug 18, 2026
5bf3c3f
fix(ci): Publish the universe image the containerd store's way
claude Aug 18, 2026
d3c3033
fix(ci): Run docker on the classic graphdriver store
claude Aug 18, 2026
47cb586
ci: TEMPORARY push trigger for the revdep3 live test run (most, depth 2)
claude Aug 18, 2026
91ae7b1
chore: Update revdep report from revdep3 run 32196879628
github-actions[bot] Aug 19, 2026
fb9ee04
feat(ci): Resolve Bioconductor dependencies, diagnose compile OOMs, c…
claude Aug 19, 2026
04f1190
Revert "ci: TEMPORARY push trigger for the revdep3 live test run (mos…
claude Aug 19, 2026
5cafd96
feat(ci): Compile checks with -g0 and -j1 inside the memory cap; docu…
claude Aug 19, 2026
5b2c0fc
fix(ci): Carry the committed record through subset runs; add Rust to …
claude Aug 19, 2026
0114f7c
fix(ci): Ship the tcl/tk runtime in the base image
claude Aug 19, 2026
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
1,392 changes: 1,392 additions & 0 deletions .github/workflows/revdep3.yaml

Large diffs are not rendered by default.

78 changes: 78 additions & 0 deletions .github/workflows/revdep3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# revdep3: the pair engine

The engine-specific part of the containerized reverse-dependency check;
everything else — planning, image lifecycle, comparison,
collection, artifacts — is shared with revdep4
and documented in `../revdepx/README.md`.

## What this engine does

For each package of a shard, in plan order (heaviest first),
the driver (`../revdepx/shard.R`) calls `check-pair.sh`,
which starts **two containers at the same time**:
one checks the package against the released CRAN version of igraph,
one against the dev build,
each with `R_LIBS` stacking its single-package half library
in front of the universe library baked into the image.
The pair still halves a package's wall clock,
exactly as it did in revdep2 —
what changed is that the halves can no longer see each other.

## Why containers, not more patches

revdep2's two halves shared one host.
R's `parallel` picks its PSOCK port once per session
from the RNG and the clock;
`set.seed()` in examples and tests makes it deterministic,
and two halves starting together drew the same port —
`cia` and `TDApplied` were reported broken with nothing wrong.
The fix (`R_PARALLEL_PORT=20000/30000`) worked,
but it was a patch for one mechanism.
The halves still shared `/tmp`,
the ccache and pak caches, DBus, X, the loopback interface —
every singleton a check may assume it owns.

A container per half ends the class:
own network namespace (any port, twice, no collision),
own PID space, own mounts, own `/tmp`
(bind-mounted from the big disk, discarded per package).
The port variable is gone from the environment entirely,
because nothing needs it any more.

Two side effects are worth having on their own:

- a **memory cap** per container
(`REVDEPX_MEMORY_PER_CHECK`, 6g by default;
`(RAM − 2 GiB) / 2` per half when it is cleared,
and a caller-set `REVDEPX_MEMORY` wins over both),
so a hungry check kills its own container —
one package's recorded result —
instead of starving the runner agent
("The hosted runner lost communication with the server");
- a **pinned check platform**:
the halves run under the resolved `r-version` (default oldrel)
on the fixed base image,
so results stay comparable across runs
while the runner image drifts underneath.

## Trade-offs against revdep4

- Wall clock per package is the slower half,
not the sum — the pair is the faster engine
for a full sweep with few reusable baselines.
- Both halves always run fresh, here and in revdep4:
a stored old result from an earlier run
is a second opinion (`baseline_agrees`),
never a substitute for the old check.
- Per-half timings remain inseparable
(both are recorded as the pair's wall clock);
revdep4 measures real ones.
- Concurrency within a shard is fixed at two checks;
revdep4 scales with `REVDEPX_WORKERS`.

The two engines publish interchangeable artifacts,
so the choice is per dispatch, not forever:
a revdep4 run can retry a revdep3 run's leftovers,
reuse its baselines,
and start from the universe image it refreshed —
and the other way around.
90 changes: 90 additions & 0 deletions .github/workflows/revdep3/check-pair.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Check one package against both versions of the package under test at once.
#
# The two checks are independent -- separate work directories, separate
# library stacks that differ in exactly one package -- so there is no reason
# to run them one after the other. Running the pair concurrently halves a
# package's wall clock and, when one of them hangs, still gets the other's
# answer instead of never reaching it.
#
# Each half now runs in a container of its own (check-half.sh), and that is
# a categorical change, not a tuning one. On the shared host the two halves
# collided on anything with a global name. The collision that actually
# struck was the PSOCK port: `parallel` picks its default port once per
# session, from the RNG and the clock; `set.seed()` makes the draw
# deterministic; and in run 31893156685 that cost `cia` and `TDApplied`,
# both reported newly broken with nothing wrong with them. The fix then was
# a hand-picked R_PARALLEL_PORT per half -- a patch for exactly that one
# mechanism, with the shared /tmp, the shared process table and every other
# global name still waiting their turn. Per-container namespaces retire the
# whole class at once: each half gets its own loopback, its own /tmp on its
# own disk budget, its own PID space. The port dodge is gone because nothing
# is shared to dodge.
#
# The library stacks still cascade, but the shared trunk is baked into the
# image: check-half.sh puts /revdepx/lib-half in front of /opt/revdepx/lib
# on R_LIBS, and the mounted half-library holds exactly one package -- the
# CRAN release for `old`, the dev build for `new`.
#
# The host design also ran the checks under nice/ionice so the runner agent
# would never starve behind them; per-container `--cpu-shares` (set in
# check-half.sh) does that job now, with the same property of only biting
# when there is more work than cores.
#
# Usage:
# check-pair.sh <tarball> <workdir> <lib-old> <lib-new> <seconds>
#
# The old shared-library argument is gone: the shared library lives in the
# universe image named by REVDEPX_IMAGE, which -- with the other REVDEPX_*
# knobs -- passes straight through to check-half.sh. Paths must be absolute.
#
# Leaves <workdir>/{old,new}/ holding the .Rcheck directory, `driver.log`
# (what R CMD check said), `status` (its exit code; 124 is the timeout, per
# coreutils `timeout`) and possibly `oom` -- see check-half.sh for the full
# inventory. Always exits 0: which of the two failed, and how, is for the
# caller to read out of those files.

set -u

tarball=$1
work=$2
lib_old=$3
lib_new=$4
seconds=$5

check_half="$(dirname "$0")/../revdepx/check-half.sh"

# Each half is capped at half the machine's memory, less 2 GB kept back for
# the runner agent and the docker daemon, with a 2 GB floor so a small
# machine still gets a workable cap. What the cap buys is *where* the OOM
# kill lands. Uncapped, a hungry check takes the runner down -- "The hosted
# runner lost communication with the server", every package on the shard
# lost. Capped, the kernel kills the one container, check-half.sh records
# `oom` next to the status, and the manifest names the one package. A
# REVDEPX_MEMORY already in the environment wins: the caller sized it
# deliberately. REVDEPX_MEMORY_PER_CHECK is the same knob under the name the
# two engines share -- the yaml sets it to 6g by default; the derivation
# below is the fallback when neither is set.
if [ -z "${REVDEPX_MEMORY:-}" ] && [ -n "${REVDEPX_MEMORY_PER_CHECK:-}" ]; then
REVDEPX_MEMORY=${REVDEPX_MEMORY_PER_CHECK}
export REVDEPX_MEMORY
fi
if [ -z "${REVDEPX_MEMORY:-}" ]; then
mem_kb=$(awk '/^MemTotal:/ { print $2 }' /proc/meminfo 2> /dev/null || echo 0)
half_mb=$(((mem_kb / 1024 - 2048) / 2))
if [ "${half_mb}" -lt 2048 ]; then
half_mb=2048
fi
REVDEPX_MEMORY="${half_mb}m"
export REVDEPX_MEMORY
fi

"${check_half}" old "${tarball}" "${work}" "${lib_old}" "${seconds}" &
old_pid=$!
"${check_half}" new "${tarball}" "${work}" "${lib_new}" "${seconds}" &
new_pid=$!

wait "${old_pid}"
wait "${new_pid}"

exit 0
Loading
Loading