feat(local): auto-apply registry patches with a build-env trial-build gate (#124)
All checks were successful
ci/crow/cron/process-updates/14 Pipeline was successful
ci/crow/cron/process-updates/13 Pipeline was successful
ci/crow/cron/process-updates/16 Pipeline was successful
ci/crow/cron/process-updates/18 Pipeline was successful
ci/crow/cron/process-updates/15 Pipeline was successful
ci/crow/cron/process-updates/12 Pipeline was successful
ci/crow/cron/process-updates/17 Pipeline was successful
ci/crow/cron/process-updates/6 Pipeline was successful
ci/crow/cron/process-updates/5 Pipeline was successful
ci/crow/cron/process-updates/11 Pipeline was successful
ci/crow/cron/process-updates/1 Pipeline was successful
ci/crow/cron/process-updates/2 Pipeline was successful
ci/crow/cron/process-updates/7 Pipeline was successful
ci/crow/cron/process-updates/8 Pipeline was successful

Closes the classifier loop (issue #115, step 3): turns the auto-proposable candidates into an actual PR, gated by a real trial build **in our own build-env images**.

Chosen model (from the design discussion): **autonomous PR**, **PR-first with a CI trial-build gate**, **bounded top-N batch** per run.

## Creating the patch PR

- `propose-patches.R` gains:
  - `--limit N` -- act on the top-N candidates by failure volume; the rest defer to the next run (logged).
  - `--open-pr` -- write the entries onto the reused `auto/registry-patch-proposals` branch, push (with `REPO_RW_TOKEN`), and open/update **one** PR via the Forgejo API (so re-runs update the same PR instead of piling up).
- `.crow/auto-apply-patches.yaml` -- a single job that runs `--open-pr --limit` on a cron/manual trigger. Needs `FORGEJO_TOKEN` + a write-scoped `REPO_RW_TOKEN`.

Novel source diffs and unknown signatures are still never proposed; nothing merges.

## The merge gate (our build-env images)

- `.crow/trial-build-registry.yaml` -- matrixed over the real `OS/IMG` build-env matrix (alpine:3.24, redhat:8/9/10, ubuntu:jammy/noble/**resolute** for ubuntu-2604). Each platform runs `local/trial-build-registry.R`, which diffs the branch registry against `main` and trial-builds **only the entries the branch adds** that apply to that platform, inside `reg.devxy.io/rpkgs/build-env-*`. Green only if every new entry builds; a platform with no new entries is a fast no-op. Nothing is uploaded/archived/recorded.
- The base-registry read **fails loud** if it can't read `registry.json` at `main`, rather than silently treating the base as empty and trial-building the whole registry.

## Notes / follow-up

- The repo uses **no `pull_request` triggers**, so the gate runs manually or on a cron against the branch (`--var patch_branch=...`). Wiring it to fire automatically on the PR needs `event: pull_request` enabled on the Forgejo webhook -- a one-line addition once that's on.
- Two new crons to register in the crow UI: `auto-apply-patches` and `trial-build-registry`. New secret needed: `REPO_RW_TOKEN` (write scope) for the push.

## Verification

- New pure helpers `entry_applies_to_os()` / `new_registry_packages()` covered by tests (platform codename/family/wildcard matching; added-vs-unchanged entry detection; per-platform filtering).
- `--limit` smoke (stubbed DB): top-2 by volume proposed, 3 deferred, candidate registry validates.
- Full suite: 105 tests pass; all pre-commit hooks pass (air, prettier, markdownlint, yamllint, validate-patches).

Reviewed-on: #124
This commit is contained in:
Patrick Schratz 2026-07-15 08:28:15 +00:00 committed by Patrick Schratz
commit 4bca17e4ac

View file

@ -162,6 +162,57 @@ test_that("blocked_summary lists each dependency and its dependent count", {
expect_true(b[[1L]]$packages_truncated)
})
test_that("entry_applies_to_os matches codename, family, and wildcard", {
expect_true(entry_applies_to_os(list("ubuntu-2604"), "ubuntu-2604"))
expect_true(entry_applies_to_os(list("ubuntu"), "ubuntu-2604")) # family
expect_true(entry_applies_to_os(list("*"), "ubuntu-2604"))
expect_true(entry_applies_to_os(list("alpine", "ubuntu-2604"), "ubuntu-2604"))
expect_false(entry_applies_to_os(list("alpine-324"), "ubuntu-2604"))
expect_false(entry_applies_to_os(list("ubuntu-2404"), "ubuntu-2604")) # other codename
})
test_that("new_registry_packages returns only added entries for the platform", {
base <- list(
list(
package = "RcppParallel",
platforms = list("alpine", "ubuntu-2604"),
versions = "*"
)
)
current <- list(
base[[1L]], # unchanged -> not "new"
list(package = "BFpack", platforms = list("ubuntu-2604"), versions = "*"),
list(
package = "someAlpinePkg",
platforms = list("alpine-324"),
versions = "*"
)
)
# For ubuntu-2604: only the newly-added BFpack (RcppParallel is unchanged,
# someAlpinePkg does not apply to this OS).
expect_identical(
new_registry_packages(current, base, os = "ubuntu-2604"),
"BFpack"
)
# For alpine-324: the alpine package is new and applies.
expect_identical(
new_registry_packages(current, base, os = "alpine-324"),
"someAlpinePkg"
)
# Without an OS filter, both additions are returned.
expect_setequal(
new_registry_packages(current, base),
c("BFpack", "someAlpinePkg")
)
# A changed platform set on the same package counts as a new entry.
widened <- list(list(
package = "RcppParallel",
platforms = list("*"),
versions = "*"
))
expect_identical(new_registry_packages(widened, base), "RcppParallel")
})
test_that("retirement_candidates flags entries whose package no longer fails", {
entries <- list(
list(package = "RcppParallel"),