The propagation gate: what bioc-registry publishes, and why
r-universe and bioc-build build Bioconductor packages. Only some of those builds become what bioc-registry publishes: installable repositories at https://bioc-registry.seandavi.workers.dev/repo/{bioc,bioc-release}, and the propagation index that the /next/ preview pages are built from. This page states the rule that decides, read from the code that runs it.
bioconductor.org’s repositories and package pages do not read from bioc-registry. Packages reach bioconductor.org through Bioconductor’s own propagation (below) and the hourly bioc-sync from master; its landing pages take versions and download links from the repositories’ own PACKAGES indexes. Switching bioconductor.org’s software repositories to bioc-registry is a documented plan with open blockers: Registry switch.
Bioconductor’s production propagation to bioconductor.org runs in core infrastructure next to BBS and is known here only from conversation. It is described on Propagation in the legacy system, together with the push hooks and the measured agreement between r-universe’s checks and nebbiolo1’s. The two are compared at the end of this page.
The gate, check by check
Since ADR 0011 (2026-09-04) there is one decision function, gate() in src/repo.ts, with three callers and no gating anywhere else:
| Caller | Producer | Config |
|---|---|---|
evaluate() in src/index.ts, on every r-universe wave |
r-universe | gating R from config.yaml, replace_seed: false |
POST /publish (maintenance key required) |
bioc-build | the R version bioc-build’s image ships, replace_seed: true, plus manifest rules |
POST /gate (public, read-only) |
whatever the caller sends | defaults as for r-universe |
Everything below is read from bioc-registry at commit f10719e (main, 2026-09-28). A candidate propagates when every rule passes. Each decision returns every rule’s verdict, pass or fail, under these names:
build-status: the producer’s build status issuccess. For r-universe that is_status;_failureis not read. A green status on a stale commit is caught byversion-gatein practice, because the stale version has already propagated. Before the gate runs,evaluate()drops any r-universe package with no source sha256.families: at least one platform family passes, judged only on jobs running the gating R version. The gating R is resolved at evaluation time frombioconductor.org/config.yaml(r_ver_for_bioc_verfor the universe’s Bioconductor version) and matched against each job’s actual R version, not its label. Three families are gated (passingFamilies):linux(thesourcejob pluslinux-*x86_64),win(windows-*x86_64) andmac(macos-*arm64). A family passes when it has at least one gating job and none of them isERROR,FAILorFAILURE.NOTEandWARNINGpass. Other configs gate nothing:bioc-checks,wasm, linux arm64, mac x86_64. bioc-build reports onelinux-x86_64job.bioccheck: advisory in every caller today; it always passes and records the verdict. A caller could set it toblocking, and thenERROR/FAILwould block.version-parse: the version is digits separated by.or-.version-gate: the version is a strict bump over the one in the propagation index, or the package is new. A push without a bump never propagates. Withreplace_seed(bioc-build only), a build at the same version may also replace abioconductorseed; r-universe must bump.manifest-state,manifest-git-url,manifest-stream,manifest-component(bioc-build only): the package’s entry in bioc-manifest, read at the commit the build recorded, isactive, names the git URL that was built, lists the build’s stream, and hascomponentequal toprofile.deps, run last over everything the other rules cleared (approveByDeps): everyDepends,ImportsandLinkingToentry naming a package that is itself in the propagation index must be present at a version meeting its>=or>constraint. Dependencies not in the index (CRAN, base R, and annotation, experiment and workflow packages until they have a build home) pass untouched. Candidates are approved in waves until a fixpoint, so a package lands as soon as what it waits on does. A dependency that is in neither the index nor CRAN is not caught; that is bioc-registry#56 (open), and finding 2 on Registry switch.
What propagates for an r-universe package that passes: the source tarball always; binaries only for the families that passed, and only those built at the same version with status success. wasm binaries ride along with no gate of their own. The passing families are recorded on the index entry as archs, the BiocCheck verdict as bioccheck.
Every new version that fails (its version-gate passed, something else did not) is written to prop/{universe}/blocked/ with its full list of rule verdicts. To ask why a package did not propagate, or whether it would, POST /gate runs the same function against the live index and writes nothing:
curl -s -X POST https://bioc-registry.seandavi.workers.dev/gate \
-H 'content-type: application/json' \
-d '{"universe":"bioc-release","candidates":[{"package":"limma","version":"99.0.0",
"jobs":[{"config":"source","r":"4.6.0","check":"OK"}]}]}'
# 2026-09-30: propagate true, archs ["linux"], gating_r 4.6, "99.0.0 > 3.68.5"What this gate does not check, and the legacy production gate or the push hooks do:
- The
nebbiolo1sha match. The gate has no provenance check for r-universe builds. (bioc-build’s sha256 andgh attestation verifyare checked by its publisher; per ADR 0011 they are integrity checks, not gate rules.) - The push-hook rules: version parity,
xandyunchanged, the 5 MiB file limit, merge markers. It inherits them by trusting that the commit reachedgit.bioconductor.org.
Binaries that arrive after publication
The gate decides binaries in the wave that first publishes a version, and version-gate keeps that version out of every later wave. Since bioc-registry#50 (PR #51, merged 2026-09-29 02:30 UTC), every wave ends with a top-up (topUpBinaries, topUp):
- For an r-universe entry, if r-universe now has a successful binary at the published version for a family that passed then or passes now on the gating R, it is added. This covers binaries that finish after the source was published and platforms whose check passes on a same-version rebuild.
- For a seeded entry, Bioconductor’s binaries are fetched once they reach the seeded version.
Top-up is additive only: the source, its sha256 and the version never change, a family that passed is never withdrawn, and a slot that already has a binary keeps it. Each addition is recorded under prop/{universe}/topup/. POST /topup (maintenance key) runs the same for a backfill.
Three ways into the index
Each index entry carries an origin:
origin |
How it got there | bioc (devel) |
bioc-release |
|---|---|---|---|
r-universe (or no field: entries from before the field existed) |
passed the gate above; archs says on which families |
2,292 | 2,235 |
bioconductor |
seeded, never faced the gate; empty archs, null bioccheck |
124 | 153 |
bioc-build |
passed the gate through POST /publish |
2 | 2 |
Counts from the live index, 2026-09-30.
Seeds exist because some packages build in BBS and fail in r-universe’s environment, usually because an example reaches a network service the runner cannot (about 300 when measured 2026-08-04; 277 seeded entries today). They are copied from Bioconductor’s repository for the universe’s branch: the devel repository for bioc, the release repository for bioc-release (seedPlan() in src/index.ts). Seeding only fills holes. A seed is replaced by an r-universe build when the maintainer bumps the version, or by a bioc-build build at the same version.
What r-universe already computes
A common assumption, encouraged by “r-universe doesn’t care about R CMD check”, is that r-universe skips checking. It does not. It does not gate on the result, which is a different claim. It runs R CMD check on every platform in its matrix, and for Bioconductor packages it also runs BiocCheck.
All of it is exposed in the per-package API, e.g. https://bioc.r-universe.dev/api/packages/limma (verified live, 2026-08-04):
| Field | Contents |
|---|---|
_jobs[] |
one entry per matrix leg: {job, time, config, r, check, artifact}. config values include source, bioc-checks, linux-{devel,release}-{arm64,x86_64}, macos-*, windows-*, wasm-release. check is OK / NOTE / WARNING / ERROR / FAIL. |
_bioccheck |
BiocCheck summary as counts: {error, warning, note}. First-class API field; no download required. |
_commit |
{id, author, committer, message, time}: the source commit sha and metadata. |
_status |
overall deploy status for the package. |
_failure |
present when the newest commit failed to produce a source package. |
_buildurl |
the GitHub Actions run URL, in r-universe/{universe}. |
_binaries[] |
per-artifact provenance: {r, os, arch, distro, version, date, commit, fileid, status, check, buildurl}. |
Build logs and the full BiocCheck artifact are retrievable without authentication via /api/actions/logs/{job} and /api/actions/artifacts/{id} (both 302 to a signed blob URL).
The gate therefore does not need to run checks. It needs to read them.
Why the registry polls
r-universe emits no webhook, no repository_dispatch, and no subscribable status endpoint. The feature request for build notifications (r-universe-org/help#101) is open with no comments (checked 2026-09-30). The one push-style signal, a GitHub commit status on the upstream repo (context r-universe/{universe}/{package}/deploy), needs the r-universe GitHub App installed on that repo; checked against bioc/limma on 2026-08-04 at the sha r-universe built, there were no statuses.
So the registry polls. Its Worker cron runs every 15 minutes (*/15 * * * * in wrangler.jsonc). Each run fetches /api/packages for both universes with a fixed field list and hashes the response; a new wave (gate, propagate, top-up) starts only when the hash changes. The field list leaves out volatile fields such as _stars so the hash stays quiet when nothing build-relevant changed. The same cron also captures logs and compacts the observation archive.
For scale: r-universe discovers new commits once an hour (sync.yml in r-universe-org/control-room, cron 25 * * * *, checked 2026-09-30), and rebuilds nightly (rebuilds.yml, 30 1 * * *). Measured against bioc-release on 2026-08-04, /api/files with fields=_created,_buildurl,_commit was 1.1 MB in 0.27 s; /api/packages with a field list 6.4 MB in 0.9 s; with no field list about 69 MB. Always pass fields=. PATCH /api/sync is public, unauthenticated, and self-throttles to 60 seconds, if a sync is needed ahead of the cron.
Two traps in the data
_status can be green on a stale commit. When the newest commit fails to produce a source package, the package keeps its previous successful _status and the failure appears separately in _failure. A gate reading only _status will propagate while the current head is broken. This gate reads only _status and relies on version-gate (rule 1 above).
Key the delta on _commit.id, not build time. 453 release packages were rebuilt in the 24 hours before the 2026-08-04 measurement, but r-universe also rebuilds on dependency changes, R-version changes, the nightly rebuild cron, and a reverse-dependency cascade after any version change. Most of that churn carries no new source.
Relation to the legacy production gate
| Legacy production gate | This gate | |
|---|---|---|
| Feeds | bioconductor.org/packages/... (the site and its mirrors) |
bioc-registry (/repo/..., the propagation index, the /next/ pages) |
| Runs in | Bioconductor core infrastructure, alongside BBS | a Cloudflare Worker |
| Described from | conversation with a core team member, 2026-08-04, not re-verified since | the code, pinned at commit f10719e |
| Provenance check | source sha must match what nebbiolo1 built |
none for r-universe: pushes to git.bioconductor.org are assumed hook-checked upstream |
| Check verdict | “the check is clean”: one all-or-nothing verdict; the source does not say it covers more than one platform | per platform family on the gating R; at least one family must pass |
| Version rule | version must match nebbiolo1’s build |
strict bump over the last propagated version |
| Dependency rule | unknown | hard deps that this registry publishes must be satisfiable |
| BiocCheck | advisory | advisory, recorded on the index entry |
| Late binaries | unknown | added by top-up at the same version |
Whether WARNINGS counts as clean in the legacy gate is still undetermined; this gate answers it explicitly (families: yes).
Sources
seandavi/bioc-registryatf10719e:src/repo.ts(gate,passingFamilies,approveByDeps,topUpBinaries,seedBinaryTopUp,verGt),src/index.ts(evaluate,gatingRMinor,poll,topUp,seedPlan,/gate),wrangler.jsonc- ADR 0011: one propagation gate for every producer
r-universe-org/frontend:routes/packages.js,routes/universe.js;r-universe-org/workflows:build.yml,deploy.yml;r-universe-org/control-room:sync.yml,rebuilds.yml- Live endpoints:
bioc-release.r-universe.dev/api/{packages,files},bioc.r-universe.dev/api/packages/limma(2026-08-04);bioc-registry.seandavi.workers.dev/data/prop/{bioc,bioc-release}/index.jsonand/gate(2026-09-30) - Related: Propagation in the legacy system, Registry switch, Builder Transition