feat(ci): verify what clients actually receive from every published slot #209

Merged
pat-s merged 1 commit from feat/verify-repo-routing into main 2026-09-09 12:52:17 +00:00
Owner

Why

Everything built so far reasons about the binaries: abi_flat_safety records what an object's symbols say it can load under, and the index writer drops records on that verdict.
None of it proves what a client actually receives, which is the product of the CDN rewrite, the per-minor indexes and the Path: field acting together.

The reprex that found the base64enc failure ran on a workstation, could only reach amd64, and did not survive the session.
This makes it a pipeline.

What it does

For every slot, under every R minor the image carries, it installs a fixed package set through each URL form the website documents and checks that each one loads in a fresh subprocess.

form repository user agent
arch https://cran.rpkgs.com/<arch>/<codename>/latest R's own
generic https://cran.rpkgs.com/ the string the website's snippet builds
flat the arch URL curl/8.5.0, so the edge cannot read an R minor

arch and generic are contractual, so a failure there fails the run.
flat measures what a client hiding its R minor is exposed to: it is reported and never fatal, because pruning the flat slot is a separate decision.

Two details that matter:

  • CRAN is deliberately absent from repos. A package missing from our index must fail loudly rather than be quietly compiled from source, which would hide exactly the gap this exists to find.
  • One process per form. available.packages() caches its index in tempdir() keyed by repository URL, and arch and flat share a URL. In a single process the second form would read the first form's index and prove nothing.

Verification

Run against the live repository in build-env-ubuntu:noble under R 4.6.0, mid-repair:

amd64/noble R 4.6 via arch:    11 of 11 packages usable
amd64/noble R 4.6 via generic: 11 of 11 packages usable
amd64/noble R 4.6 via flat:     8 of 11 packages usable

The three flat failures are Rcpp, data.table and yaml, each Built: R 4.4.3 and each failing dyn.load() under 4.6.
That is the unrouted-client exposure, measured rather than estimated.

The rows also show the routed forms serving a mix of per-minor and flat objects (Rcpp at R 4.6.0, stringi at R 4.4.3), which is the intended behaviour: only objects that cannot load cross-minor get a per-minor copy.

## Why Everything built so far reasons about the *binaries*: `abi_flat_safety` records what an object's symbols say it can load under, and the index writer drops records on that verdict. None of it proves what a **client** actually receives, which is the product of the CDN rewrite, the per-minor indexes and the `Path:` field acting together. The reprex that found the `base64enc` failure ran on a workstation, could only reach amd64, and did not survive the session. This makes it a pipeline. ## What it does For every slot, under every R minor the image carries, it installs a fixed package set through each URL form the website documents and checks that each one loads in a fresh subprocess. | form | repository | user agent | | --- | --- | --- | | `arch` | `https://cran.rpkgs.com/<arch>/<codename>/latest` | R's own | | `generic` | `https://cran.rpkgs.com/` | the string the website's snippet builds | | `flat` | the arch URL | `curl/8.5.0`, so the edge cannot read an R minor | `arch` and `generic` are contractual, so a failure there fails the run. `flat` measures what a client hiding its R minor is exposed to: it is reported and never fatal, because pruning the flat slot is a separate decision. Two details that matter: - **CRAN is deliberately absent from `repos`.** A package missing from our index must fail loudly rather than be quietly compiled from source, which would hide exactly the gap this exists to find. - **One process per form.** `available.packages()` caches its index in `tempdir()` keyed by repository URL, and `arch` and `flat` share a URL. In a single process the second form would read the first form's index and prove nothing. ## Verification Run against the live repository in `build-env-ubuntu:noble` under R 4.6.0, mid-repair: ``` amd64/noble R 4.6 via arch: 11 of 11 packages usable amd64/noble R 4.6 via generic: 11 of 11 packages usable amd64/noble R 4.6 via flat: 8 of 11 packages usable ``` The three `flat` failures are `Rcpp`, `data.table` and `yaml`, each `Built: R 4.4.3` and each failing `dyn.load()` under 4.6. That is the unrouted-client exposure, measured rather than estimated. The rows also show the routed forms serving a mix of per-minor and flat objects (`Rcpp` at `R 4.6.0`, `stringi` at `R 4.4.3`), which is the intended behaviour: only objects that cannot load cross-minor get a per-minor copy.
Add a manual pipeline that installs a fixed package set from a slot under
every R minor its image carries, through each URL form the website documents,
and checks that each package loads.

The ABI bookkeeping so far reasons about the binaries: `abi_flat_safety`
records what an object's symbols say, and the index writer acts on that. None
of it proves what a client receives once the CDN rewrite, the per-minor
indexes and the `Path:` field have had their say. This closes that gap, and
runs on the build agents so arm64 is covered on real hardware rather than
skipped.

- `arch` and `generic` are contractual and fail the run.
- `flat` measures a client that hides its R minor; it is reported, never fatal.
- CRAN is deliberately absent from `repos`, so a package missing from our
  index fails loudly instead of being compiled from source behind our backs.
pat-s merged commit af4cc6f414 into main 2026-09-09 12:52:17 +00:00
pat-s deleted branch feat/verify-repo-routing 2026-09-09 12:52:17 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
devxy/build-cran-binaries!209
No description provided.