flowchart LR
DEV["maintainer push"] --> HOOKS{"gitolite pre-hooks<br/>version rules · 5 MiB limit<br/>merge markers"}
HOOKS -->|rejected| X["push fails"]
HOOKS -->|accepted| GIT["git.bioconductor.org<br/>canonical source"]
GIT --> MIRROR["GitHub mirror<br/>github.com/bioc/*"]
GIT --> NEB["nebbiolo1<br/>R CMD build / check"]
MIRROR --> RU["r-universe<br/>build + R CMD check + BiocCheck"]
NEB --> GATE{"gate<br/>sha + version match<br/>check clean"}
RU --> GATE
GATE -->|pass| WEB["bioconductor.org"]
GATE -->|hold| NOP["not propagated"]
classDef g fill:#f3fbf4,stroke:#3d8b47,color:#12263f
classDef r fill:#fdf0ef,stroke:#b4453c,color:#12263f
classDef n fill:#eef4ff,stroke:#4a6fa5,color:#12263f
class GIT,NEB,WEB g
class X,NOP r
class MIRROR,RU n
What propagates, and why: the r-universe gate
r-universe builds Bioconductor packages continuously, but only some of those builds become what bioconductor.org serves. This page documents the rule that decides, why it is shaped the way it is, and what it would take to run the same decision as a GitHub Actions job.
It matters for this project because the decommission plan eventually has to answer “who decides a package is fit to publish” without nebbiolo1 in the room.
The rule itself was described by a Bioconductor core team member in conversation on 2026-08-04 and is paraphrased below. Everything else is either a live measurement taken the same day (marked as such, with the command or endpoint) or a reading of public source (linked). Where something could not be confirmed, the page says so. As with the rest of this site, the numbers are point-in-time.
The experimental gate implemented in the bioc-prop pilot has moved past the all-or-nothing rule described below: a package propagates when it builds and its checks pass on at least one architecture (linux, windows, mac — judged per-architecture from the same BBS-equivalent check jobs on the branch’s gating R). Only the passing architectures’ binaries propagate; a failing architecture is simply not available until a later version passes there. The source tarball always accompanies an eligible package, NOTE/WARNING pass, ERROR/FAIL block only the architecture they occur on, and BiocCheck remains advisory — recorded, never blocking. The passing set is recorded on each propagation-index entry.
The rule
Paraphrased:
r-universe does not care about
R CMD check; we do — that is the first big difference. And they build and propagate on any new commit, regardless of a version bump. So right now we match on the sha and version of thenebbiolo1build to r-universe, because that Linux host still has all our pre-hooks valid — preventing bad version numbers, large files, and the rest that we also check. And then we make sure the check is actually clean.
So the gate is three conditions, not one:
- the r-universe build’s version matches what
nebbiolo1built, - its source commit sha matches too, and
- the check is clean.
Why it keys on nebbiolo1
The sha match looks redundant next to the version match, and the choice of nebbiolo1 specifically looks arbitrary. Neither is.
Bioconductor’s push-time checks are gitolite server-side hooks on git.bioconductor.org (see The pre-hooks below). r-universe does not build from gitolite — it builds from a GitHub mirror, because it requires a GitHub remote. nebbiolo1 builds from canonical git.
Matching the sha against nebbiolo1 is therefore a provenance check wearing the costume of an equality check. It establishes that the commit came through the git server, and so had the pre-hooks applied to it. Bioconductor inherits that guarantee without re-running anything.
The practical consequence: a gate that runs outside this path must replace the provenance guarantee, either by verifying the sha traces back to canonical git or by re-implementing the hooks. The former is cheaper and more faithful.
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.
How closely does r-universe agree with nebbiolo1?
This is the load-bearing question for any plan that lets r-universe’s check stand in for BBS’s, so it was measured rather than assumed.
Comparing r-universe’s linux-release-x86_64 job against nebbiolo1’s checksrc stage in BUILD_STATUS_DB.txt for release 3.23, across all 2,416 packages present in both, excluding the 90 where BBS skipped the check or r-universe had no job:
| Reading of “shippable” | Agreement |
|---|---|
WARNINGS counts as clean |
2,249 / 2,326 = 96.7% |
WARNINGS counts as dirty |
2,196 / 2,326 = 94.4% |
The residual is asymmetric: 59 packages r-universe calls ERROR that nebbiolo1 passes, against 18 the other way.
For reference, nebbiolo1’s own 3.23 distribution:
| Stage | OK | WARNINGS | ERROR | skipped | TIMEOUT |
|---|---|---|---|---|---|
install |
2382 | — | 34 | — | — |
buildsrc |
2330 | — | 86 | — | — |
checksrc |
1870 | 391 | 68 | 86 | 1 |
Close, but not a drop-in substitute. That 3.3% is precisely the risk surface of ever treating r-universe’s check as authoritative.
An earlier version of this comparison used r-universe’s bioc-checks job and found only ~20% agreement. That job is BiocCheck, not R CMD check — a different check with different severities. The platform jobs (linux-release-x86_64 and siblings) are the right comparand.
There is no build notification to subscribe to
The natural design is event-driven: r-universe finishes a build, something fires, the gate runs. That is not available.
r-universe emits no webhook, no repository_dispatch, and no subscribable status endpoint. The feature request for build notifications (r-universe-org/help#101) has been open and uncommented since 2021.
The one push-style signal it does emit is a GitHub commit status on the upstream repo (context r-universe/{universe}/{package}/deploy), which an on: status workflow could catch — but only if the r-universe GitHub App is installed on that repo. Checked live against bioc/limma at the exact sha r-universe built: zero statuses, no contexts. That path is closed for Bioconductor packages today.
So: poll, and don’t poll fast
r-universe discovers new commits through its own hourly cron (41 * * * *, in r-universe-org/control-room), which then dispatches per-monorepo sync and build. Polling faster than hourly buys nothing — it re-reads the same state.
The cheapest useful endpoint (measured against bioc-release, 2026-08-04):
GET /api/files?type=src&fields=_created,_buildurl,_commit
# 1.1 MB · 0.27 s · 2,399 NDJSON rowsEach row carries package, version, commit id and time, build time, and the Actions run URL — everything a delta needs. Two comparisons for scale: the same universe via /api/packages with a field list is 6.4 MB / 0.9 s, and with no field list at all it is ~69 MB. Always pass fields=.
Polling /api/files this way is the sanctioned pattern — r-universe’s own rebuilds package does exactly this. If a sync is needed ahead of the cron, PATCH /api/sync is public, unauthenticated, and self-throttles to 60 seconds.
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. Read both.
Key the delta on _commit.id, not build time. 453 release packages were rebuilt in the 24 hours before measurement, but r-universe also rebuilds on dependency changes, R-version changes, a nightly retry cron (30 1 * * *), and a reverse-dependency cascade after any version change. Most of that churn carries no new source.
The pre-hooks
Public, at Bioconductor/git-hooks. Two caveats before trusting any of it:
- The default
develbranch is stale Python 2. The live code is almost certainly theConvertToPython3branch, which was never merged. Which branch is actually deployed could not be confirmed — that needs access to the git server itself. - A server-side
hooks.confcan disable any hook per package. Its contents are not public. These are defaults, not guarantees.
File size (prevent_large_files.py): exactly 5,242,880 bytes (5 MiB, strict >), raised from 5,000,000 decimal in August 2020. Applies to software packages only — data experiment, workflow and book packages are not size-gated by the hook. The implementation sizes files at the push tip rather than walking the push range, so a large blob added and deleted within one push passes.
Version numbers (prevent_bad_version_numbers.py): format must match ^\d+\.\d+\.\d+$, then:
| Branch | Rule |
|---|---|
RELEASE_* |
x unchanged; y unchanged and even; z non-decreasing |
| devel | x unchanged; y odd; y unchanged unless 99; z non-decreasing unless y is 99 |
Two properties matter for the gate. The hook never requires a bump — a push with no version change is accepted, and that acceptance is exactly what makes “no bump, no propagation” the gate’s job rather than the hook’s. And the hook has no knowledge of the current Bioconductor release number; it enforces parity and stability, not “y matches this cycle.”
The parity rule holds almost perfectly in practice. Across the release universe, 2,398 of 2,399 packages have an even y. The single exception is BiocVersion 3.23.1, whose version is the Bioconductor version — so any reimplementation needs one hardcoded exception.
Also present: a merge-conflict-marker check (known leaky — git-hooks#2) and a duplicate-commit check that is pure SVN-transition legacy and can be dropped. Force-push and branch-creation restrictions come from gitolite ACLs rather than hooks; GitHub branch protection covers those natively.
BiocCheck does not substitute for the hooks
Since r-universe runs BiocCheck, it is tempting to treat that as covering the pre-hooks. It does not:
| Hook rule | BiocCheck | Covered? |
|---|---|---|
| 5,242,880-byte file limit | .MAX_FILE_SIZE <- 5e+6L, emits WARNING |
No — different threshold, different severity |
| version parity | checkVersionNumber(), emits WARNING |
Partial and weaker |
| x unchanged, y unchanged, z non-decreasing | — | No. Structurally impossible: BiocCheck never sees the previous version |
| merge markers | — | No |
That last row is the useful one. A gate does have the previous version — it is already in the state it keeps for delta detection — so it is naturally positioned to enforce exactly the rules BiocCheck cannot, at no extra data cost.
If the size check is ported, Bioconductor/BiocContributions’ scripts/validate.py already uses 5 * 1024 * 1024, matching the hook exactly.
What a GitHub Actions gate would look like
Given the above, the job is a reconciler over metadata, not a check farm: no matrix, no re-running checks, no webhook receiver.
on:
schedule: [{cron: '55 * * * *'}] # after r-universe's :41 sync has had time to build
workflow_dispatch:
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: python3 gate.py # diff vs state, evaluate, rewrite state
- run: |
git add state/gate.json
git diff --cached --quiet || { git commit -m "gate $(date -u +%FT%TZ)"; git push; }Per changed package, gate.py evaluates: version-diff rules against its own state, check outcome from _jobs, BiocCheck counts from _bioccheck, _failure for the stale-green case, and sha provenance. State lives in a committed file rather than the Actions cache — it survives past the cache’s seven-day eviction, and git log becomes a free audit trail answering “why did this propagate.”
Two modes, and the order to build them in
Shadow. BBS stays authoritative; the workflow only reconciles r-universe against BUILD_STATUS_DB.txt and reports disagreements. Every input is public, so this needs no builder access and no Bioconductor-side change. It also supplies the check-status axis that model-binary-flow.py currently lacks.
Reference. Actions runs the canonical Linux check itself and becomes what nebbiolo1 is now. This is the mode that actually retires a builder — and where the 59-vs-18 divergence has to be explained rather than merely measured.
Shadow first, and let the divergence data decide whether reference mode is safe.
What could not be determined
- Whether
checksrc: WARNINGScounts as “clean” for propagation. 391 packages — 16% of the repo — sit in that state. The answer changes whether the gate reads as ~77% or ~93% permissive. This is a question for the Bioconductor core team, not something to infer. - Which branch of
git-hooksis deployed, and the contents of the per-packagehooks.confthat can disable individual hooks. - Measured commit-to-build latency. It is bounded below by the hourly sync cron, but no primary source gives a wall-clock figure.
Sources
- Conversation with a Bioconductor core team member, 2026-08-04 (the rule)
Bioconductor/git-hooks—prevent_large_files.py,prevent_bad_version_numbers.py,pre-receive-hook-software; branchConvertToPython3Bioconductor/BiocCheck—R/checks.R,R/checkDESCRIPTION.R;Bioconductor/BiocContributions—scripts/validate.pyr-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, 2026-08-04:
bioc-release.r-universe.dev/api/{packages,files},bioc.r-universe.dev/api/packages/limma,bioconductor.org/checkResults/3.23/bioc-LATEST/BUILD_STATUS_DB.txt - Related: Builder Transition, VIEWS & r-universe, Decommission Plan