Some checks failed
ci/crow/manual/retire-slot Pipeline was successful
ci/crow/cron/process-updates/14 Pipeline was successful
ci/crow/cron/process-updates/9 Pipeline was successful
ci/crow/cron/process-updates/16 Pipeline was successful
ci/crow/cron/process-updates/15 Pipeline was successful
ci/crow/cron/process-updates/4 Pipeline was successful
ci/crow/cron/process-updates/10 Pipeline was successful
ci/crow/cron/process-updates/3 Pipeline was successful
ci/crow/cron/auto-apply-patches Pipeline was successful
ci/crow/cron/weekly-audit-missing/1 Pipeline was successful
ci/crow/cron/weekly-audit-missing/2 Pipeline was successful
ci/crow/cron/weekly-audit-missing/4 Pipeline was successful
ci/crow/cron/weekly-audit-missing/5 Pipeline was successful
ci/crow/cron/weekly-audit-missing/6 Pipeline was successful
ci/crow/cron/weekly-audit-missing/7 Pipeline was successful
ci/crow/cron/weekly-audit-missing/8 Pipeline was successful
ci/crow/cron/weekly-audit-missing/9 Pipeline was successful
ci/crow/cron/weekly-audit-missing/10 Pipeline was successful
ci/crow/cron/weekly-audit-missing/3 Pipeline was successful
ci/crow/cron/weekly-audit-missing/11 Pipeline was successful
ci/crow/cron/weekly-audit-missing/12 Pipeline was successful
ci/crow/cron/weekly-audit-missing/13 Pipeline was successful
ci/crow/cron/weekly-audit-missing/14 Pipeline was successful
ci/crow/cron/trial-build-registry/3 Pipeline was successful
ci/crow/cron/trial-build-registry/15 Pipeline was successful
ci/crow/cron/trial-build-registry/7 Pipeline was successful
ci/crow/cron/trial-build-registry/9 Pipeline was successful
ci/crow/cron/trial-build-registry/4 Pipeline failed
ci/crow/cron/trial-build-registry/13 Pipeline was successful
ci/crow/cron/trial-build-registry/11 Pipeline was successful
ci/crow/cron/trial-build-registry/5 Pipeline was successful
ci/crow/cron/trial-build-registry/2 Pipeline was successful
ci/crow/cron/trial-build-registry/6 Pipeline was successful
ci/crow/cron/trial-build-registry/1 Pipeline was successful
ci/crow/cron/trial-build-registry/10 Pipeline was successful
ci/crow/cron/trial-build-registry/14 Pipeline was successful
ci/crow/cron/trial-build-registry/16 Pipeline was successful
ci/crow/cron/trial-build-registry/12 Pipeline was successful
ci/crow/cron/trial-build-registry/8 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/14 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/15 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/13 Pipeline was successful
ci/crow/cron/weekly-rebuild-reindex/5 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/17 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/20 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/21 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/19 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/16 Pipeline was successful
ci/crow/cron/weekly-rebuild-reindex/7 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/1 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/27 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/26 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/18 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/25 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/32 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/31 Pipeline was successful
ci/crow/cron/weekly-rebuild-reindex/6 Pipeline was successful
ci/crow/cron/weekly-rebuild-reindex/9 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/33 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/3 Pipeline was successful
ci/crow/cron/weekly-rebuild-reindex/11 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/4 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/6 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/38 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/22 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/37 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/23 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/39 Pipeline was successful
ci/crow/cron/weekly-rebuild-reindex/13 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/5 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/2 Pipeline was successful
ci/crow/cron/weekly-rebuild-reindex/2 Pipeline was successful
ci/crow/cron/process-updates/5 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/24 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/29 Pipeline was successful
ci/crow/cron/weekly-rebuild-reindex/1 Pipeline was successful
ci/crow/cron/weekly-rebuild-reindex/8 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/28 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/30 Pipeline was successful
ci/crow/cron/process-updates/7 Pipeline was successful
ci/crow/cron/weekly-rebuild-reindex/10 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/36 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/34 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/35 Pipeline was successful
ci/crow/cron/weekly-rebuild-reindex/12 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/40 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/41 Pipeline was successful
ci/crow/cron/weekly-rebuild-missing/42 Pipeline was successful
ci/crow/cron/weekly-rebuild-reindex/14 Pipeline was successful
ci/crow/cron/process-updates/8 Pipeline was successful
ci/crow/cron/process-updates/1 Pipeline was successful
ci/crow/cron/process-updates/6 Pipeline was successful
ci/crow/cron/process-updates/11 Pipeline was successful
ci/crow/cron/process-updates/2 Pipeline was successful
ci/crow/cron/process-updates/12 Pipeline was successful
ci/crow/cron/process-updates/13 Pipeline was successful
## Why
Retiring a slot means three separate things in three separate places, and nothing tied them together.
Alpine 3.22 is what that costs.
It was removed from `local.rpkgs_slots` in `cdn.tf` at some point, so the edge stopped routing it, but it kept:
- a matrix row in **twelve** pipelines
- **six** cron jobs on the Crow server
- roughly **50000 objects** in the bucket
Nightly builds kept producing binaries nobody could reach.
Worse, an unrouted slot is not inert: every request falls through to the flat index, so R 4.4 and R 4.6 clients got `LOAD_FAILED` at `dyn.load()` rather than simply nothing.
It took a fleet-wide `verify_routing=all` run to notice.
## What changed
**`local/check-slots.R`** (`just check-slots`) fails when any `.crow/*.yaml` still targets a slot missing from `local.rpkgs_slots`.
`cdn.tf` stays the source of truth; the checker reads it rather than restating the list.
**`local/retire-slot.R`** (`just retire-slot <codename>`) does the cleanup:
- strips the slot from every pipeline matrix, dropdown option and `OS_VERSION` tag, and moves a dropdown default that pointed at the retired tag
- deletes `amd64/<codename>/` and `arm64/<codename>/` from the bucket
- prints the cron jobs that are left, with the API call, since they live on the Crow server and cannot be removed from here
Dry run by default.
It refuses any slot still listed in `cdn.tf`, so it cannot be pointed at a live slot however it is invoked.
**`.crow/retire-slot.yaml`** runs the asset half in CI, where the B2 credentials exist.
**README** gains a `## Retiring a slot` section with the ordering.
Step 4 before step 2 is the one ordering that actively breaks things: the next nightly rebuilds everything just deleted.
## Verification
Both scripts were run against the tree as it stood at `523424b`, immediately before #213.
The checker names exactly the twelve files that PR edited by hand:
```
.crow/archive-missed-packages.yaml targets unpublished: alpine322
.crow/backfill-flat-safety.yaml targets unpublished: alpine-322
.crow/process-updates.yaml targets unpublished: alpine-322, alpine322
...
12 file(s) target a slot the edge does not route.
```
`retire-slot.R alpine322 --config-only --apply` reproduces that commit's structural changes byte for byte across all 18 pipeline files, and additionally catches the two `OS_VERSION` dropdowns.
The only differences from the hand-made commit are two prose lines, a usage example and a dropdown `description:`, which the script deliberately leaves alone.
Guards exercised: a live slot (`alpine323`) is refused, an unknown codename is rejected by name, and the asset half refuses to run without `B2_S3_ACCESS_KEY` rather than treating B2's empty unauthenticated listing as an already-clean slot.
`crow lint .crow/` reports all 19 configs valid.
`air` and `jarl` are clean on both new scripts.
## Next
`alpine321` and `alpine322` still hold objects in the bucket (22922 and 25308 records respectively, both architectures).
Once this merges they can be removed with the pipeline this PR adds.
Reviewed-on: #214
151 lines
4 KiB
R
151 lines
4 KiB
R
### Fail when a pipeline targets a slot that is not in `local.rpkgs_slots`.
|
|
#
|
|
# Rscript local/check-slots.R
|
|
#
|
|
# 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.
|
|
#
|
|
# 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)
|
|
})
|
|
|
|
# Parses the distro list out of the setproduct's second array.
|
|
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]]
|
|
if (length(arrays) < 2L) {
|
|
stop(
|
|
"`rpkgs_slots` no longer has the two-array setproduct shape this parses.",
|
|
call. = FALSE
|
|
)
|
|
}
|
|
codenames <- gsub(
|
|
'"',
|
|
"",
|
|
regmatches(arrays[[2L]], gregexpr('"[^"]+"', arrays[[2L]]))[[1L]]
|
|
)
|
|
if (length(codenames) == 0L) {
|
|
stop("Parsed an empty distro list from cdn.tf.", call. = FALSE)
|
|
}
|
|
codenames
|
|
}
|
|
|
|
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 in a comment is not a finding.
|
|
slots_in_pipeline <- function(path) {
|
|
lines <- readLines(path, warn = FALSE)
|
|
row <- regmatches(
|
|
lines,
|
|
regexec("^\\s*-?\\s*(OS|CODENAME|OS_ID):\\s*(\\S+)\\s*$", lines)
|
|
)
|
|
opt <- regmatches(
|
|
lines,
|
|
regexec("^\\s*-\\s*([a-z0-9]+-[a-z0-9]+)-(amd64|arm64)\\s*$", lines)
|
|
)
|
|
hits <- c(
|
|
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)]
|
|
# `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
|
|
)
|
|
}
|
|
# Files spell a slot either way, so accept both.
|
|
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) {
|
|
stale <- setdiff(slots_in_pipeline(f), published)
|
|
if (length(stale) > 0L) {
|
|
findings[[f]] <- stale
|
|
cat(sprintf(
|
|
"%-45s targets unpublished: %s\n",
|
|
f,
|
|
paste(sort(stale), collapse = ", ")
|
|
))
|
|
}
|
|
}
|
|
|
|
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.\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 an unrouted slot. Retire it with:\n just retire-slot <codename>\n",
|
|
length(findings)
|
|
))
|
|
quit(status = 1)
|