### Fail when a pipeline still targets a slot the edge does not route. # # 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(). # # 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. 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]}" # ] published_codenames <- function(path = "cdn.tf") { if (!file.exists(path)) { stop( sprintf("%s not found; run this from the repository root.", path), call. = FALSE ) } lines <- readLines(path, warn = FALSE) start <- grep("rpkgs_slots\\s*=", lines) if (length(start) != 1L) { stop( "Could not find exactly one `rpkgs_slots` local in cdn.tf.", call. = FALSE ) } 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 call. = FALSE ) } codenames <- regmatches(arrays[[2L]], gregexpr('"[^"]+"', arrays[[2L]]))[[1L]] codenames <- gsub('"', "", codenames) if (length(codenames) == 0L) { stop( "Parsed an empty distro list from cdn.tf; refusing to report every pipeline slot as unpublished.", call. = FALSE ) } codenames } # Pipelines name a slot `-`, 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", jammy = "ubuntu-2204" ) as_pipeline_os <- function(codename) { ifelse( codename %in% names(UBUNTU_CODENAMES), UBUNTU_CODENAMES[codename], ifelse( grepl("^rhel[0-9]+$", codename), sub("^rhel", "redhat-", codename), ifelse( grepl("^alpine[0-9]+$", codename), sub("^alpine([0-9])([0-9]+)$", "alpine-\\1\\2", codename), NA_character_ ) ) ) } # 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`). 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(opt, function(m) if (length(m) == 4L) m[[2L]] else NA_character_, "") ) hits <- hits[!is.na(hits)] # `OS: alpine` (build-all-versions' bare image name) is not a slot. unique(hits[grepl("^(alpine|redhat|ubuntu)-?[0-9]", hits)]) } codenames <- published_codenames() published_os <- unname(as_pipeline_os(codenames)) if (anyNA(published_os)) { stop( sprintf( "Unrecognised codename(s) in cdn.tf: %s", paste(codenames[is.na(published_os)], collapse = ", ") ), call. = FALSE ) } # A slot is written either way depending on the file, so accept both spellings. published <- unique(c(published_os, codenames)) cat(sprintf( "Published slots (cdn.tf): %s\n\n", paste(sort(published_os), collapse = ", ") )) 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) if (length(stale) > 0L) { findings[[f]] <- stale cat(sprintf( "%-45s targets unpublished: %s\n", f, paste(sort(stale), collapse = ", ") )) } } covered <- unique(unlist(lapply(files, slots_in_pipeline))) uncovered <- setdiff(published_os, covered) if (length(uncovered) > 0L) { cat(sprintf( "\nNote: %s appear(s) in no pipeline at all. That is a gap, not necessarily a bug.\n", paste(sort(uncovered), collapse = ", ") )) } if (length(findings) == 0L) { cat("All pipeline slots are published and routed.\n") quit(status = 0) } cat(sprintf( "\n%d file(s) target a slot the edge does not route. Retire it with:\n just retire-slot \n", length(findings) )) quit(status = 1)