refactor(slots): cut the comments down to the essentials

Addresses review on #214.

This change will:

- Trim the header comments of `local/check-slots.R`, `local/retire-slot.R` and
  `.crow/retire-slot.yaml` to what a reader needs, dropping the narrative about
  how Alpine 3.22 got into that state.
- Reduce the README section to the ordered steps and the two constraints that
  matter.

Behaviour is unchanged: against the tree at 523424b the script still reproduces
#213's structural changes, differing only in the same prose-only file, and both
guards still fire.
This commit is contained in:
Patrick Schratz 2026-09-11 08:21:23 +00:00
commit cc9d7b6046
No known key found for this signature in database
GPG key ID: 62050D5BC68AB6DC

View file

@ -1,47 +1,20 @@
### Fail when a pipeline still targets a slot the edge does not route.
### Fail when a pipeline targets a slot that is not in `local.rpkgs_slots`.
#
# Usage:
# Rscript local/check-slots.R
#
# A slot is only reachable by clients if it appears in `local.rpkgs_slots` in
# `cdn.tf`. That list feeds the `UNION_SLOTS` variable, and
# `edge/rpkgs-router.ts` gates all per-R-minor index rewriting on it. A slot
# missing from it is not merely unadvertised: every request falls through to
# the flat index, which holds a single build per package, so clients on any R
# minor other than the one the slot was built against die at dyn.load().
# That list feeds `UNION_SLOTS`, which `edge/rpkgs-router.ts` gates per-R-minor
# routing on. An unrouted slot serves the flat index to every client, so its
# binaries fail at dyn.load() under every R minor but one.
#
# Alpine 3.22 sat in that state for months. It was built nightly, its per-minor
# indexes were correct, and it was never routed. `verify_routing=all` found it
# only because the run happened to include it: 1056 of 1056 checks passed on
# the 16 published slots and every failure in the run was alpine322, on both
# architectures, identical across all three URL forms.
#
# The check is deliberately one-directional. A pipeline targeting an unpublished
# slot is an error, because it spends build capacity on binaries nobody can
# reach and turns `verify_routing=all` permanently red for a reason that is not
# a defect. A published slot missing from *some* pipeline is only reported:
# several pipelines cover a subset on purpose (archive-missed-packages has no
# ubuntu rows, weekly-rebuild-missing has no alpine-324 rows), and the coverage
# a pipeline needs is its own business.
#
# The website's `DistroId` union in `src/src/data/repos.ts` must agree with
# `cdn.tf` too, but it lives in another repository so it cannot be checked here.
# One-directional: a published slot missing from some pipeline is only
# reported, since several pipelines cover a subset on purpose.
options(error = function() {
cat("ERROR:", geterrmessage(), "\n", file = stdout())
q(status = 1)
})
# `cdn.tf` is the source of truth, so read it rather than restating the list.
# The shape this depends on is the second string array inside the
# `rpkgs_slots` local, i.e. the distro list of the setproduct:
#
# rpkgs_slots = [
# for pair in setproduct(
# ["amd64", "arm64"],
# ["resolute", "noble", ...]
# ) : "${pair[0]}/${pair[1]}"
# ]
# Parses the distro list out of the setproduct's second array.
published_codenames <- function(path = "cdn.tf") {
if (!file.exists(path)) {
stop(
@ -59,27 +32,23 @@ published_codenames <- function(path = "cdn.tf") {
}
block <- paste(lines[start:min(start + 10L, length(lines))], collapse = "\n")
arrays <- regmatches(block, gregexpr("\\[[^]]*\\]", block))[[1L]]
# The first array is the setproduct's arch list, the second is the distros.
if (length(arrays) < 2L) {
stop(
"The `rpkgs_slots` local no longer has the two-array setproduct shape this parser expects. Update local/check-slots.R alongside cdn.tf.", # nolint
"`rpkgs_slots` no longer has the two-array setproduct shape this parses.",
call. = FALSE
)
}
codenames <- regmatches(arrays[[2L]], gregexpr('"[^"]+"', arrays[[2L]]))[[1L]]
codenames <- gsub('"', "", codenames)
codenames <- gsub(
'"',
"",
regmatches(arrays[[2L]], gregexpr('"[^"]+"', arrays[[2L]]))[[1L]]
)
if (length(codenames) == 0L) {
stop(
"Parsed an empty distro list from cdn.tf; refusing to report every pipeline slot as unpublished.",
call. = FALSE
)
stop("Parsed an empty distro list from cdn.tf.", call. = FALSE)
}
codenames
}
# Pipelines name a slot `<os>-<version>`, the edge and the bucket name it by
# codename. The ubuntu half of this mapping is also in `edge/rpkgs-router.ts`
# (UBUNTU_CODENAMES) and `local/build-all.R`.
UBUNTU_CODENAMES <- c(
resolute = "ubuntu-2604",
noble = "ubuntu-2404",
@ -102,26 +71,23 @@ as_pipeline_os <- function(codename) {
)
}
# Structural lines only, so a codename mentioned in a comment is not a finding.
# Three shapes carry a slot: a matrix row (`- OS: alpine-323`), a continuation
# key (`CODENAME: alpine323`), and a dropdown option (`- alpine-323-amd64`).
# Structural lines only, so a codename in a comment is not a finding.
slots_in_pipeline <- function(path) {
lines <- readLines(path, warn = FALSE)
hits <- character(0)
row <- regmatches(
lines,
regexec("^\\s*-?\\s*(OS|CODENAME|OS_ID):\\s*(\\S+)\\s*$", lines)
)
hits <- c(
hits,
vapply(row, function(m) if (length(m) == 3L) m[[3L]] else NA_character_, "")
)
opt <- regmatches(
lines,
regexec("^\\s*-\\s*([a-z0-9]+-[a-z0-9]+)-(amd64|arm64)\\s*$", lines)
)
hits <- c(
hits,
vapply(
row,
function(m) if (length(m) == 3L) m[[3L]] else NA_character_,
""
),
vapply(opt, function(m) if (length(m) == 4L) m[[2L]] else NA_character_, "")
)
hits <- hits[!is.na(hits)]
@ -140,7 +106,7 @@ if (anyNA(published_os)) {
call. = FALSE
)
}
# A slot is written either way depending on the file, so accept both spellings.
# Files spell a slot either way, so accept both.
published <- unique(c(published_os, codenames))
cat(sprintf(
@ -151,8 +117,7 @@ cat(sprintf(
files <- sort(list.files(".crow", pattern = "\\.yaml$", full.names = TRUE))
findings <- list()
for (f in files) {
found <- slots_in_pipeline(f)
stale <- setdiff(found, published)
stale <- setdiff(slots_in_pipeline(f), published)
if (length(stale) > 0L) {
findings[[f]] <- stale
cat(sprintf(
@ -163,11 +128,13 @@ for (f in files) {
}
}
covered <- unique(unlist(lapply(files, slots_in_pipeline)))
uncovered <- setdiff(published_os, covered)
uncovered <- setdiff(
published_os,
unique(unlist(lapply(files, slots_in_pipeline)))
)
if (length(uncovered) > 0L) {
cat(sprintf(
"\nNote: %s appear(s) in no pipeline at all. That is a gap, not necessarily a bug.\n",
"\nNote: %s appear(s) in no pipeline at all.\n",
paste(sort(uncovered), collapse = ", ")
))
}
@ -178,7 +145,7 @@ if (length(findings) == 0L) {
}
cat(sprintf(
"\n%d file(s) target a slot the edge does not route. Retire it with:\n just retire-slot <codename>\n",
"\n%d file(s) target an unrouted slot. Retire it with:\n just retire-slot <codename>\n",
length(findings)
))
quit(status = 1)