build-cran-binaries/local/retire-slot.R
pat-s cc9d7b6046
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.
2026-09-11 08:21:23 +00:00

240 lines
7.3 KiB
R

### Retire a slot: strip it from the pipelines and delete its objects from S3.
#
# Rscript local/retire-slot.R <codename> [--apply] [--config-only|--assets-only]
#
# Retiring a distro retires both architectures, because `local.rpkgs_slots` in
# `cdn.tf` is a setproduct over them.
#
# Order: remove the slot from `local.rpkgs_slots` (and the website's
# `DistroId`) and apply that first, then run this. Deleting objects while a
# pipeline still targets the slot just means the next nightly rebuilds them.
#
# Cron jobs live on the Crow server, not here, and are printed at the end.
#
# Only structural YAML is edited. Prose mentioning the slot is left alone, so
# grep for the name afterwards.
#
# Dry run by default.
options(error = function() {
cat("ERROR:", geterrmessage(), "\n", file = stdout())
q(status = 1)
})
args <- commandArgs(trailingOnly = TRUE)
apply_changes <- "--apply" %in% args
config_only <- "--config-only" %in% args
assets_only <- "--assets-only" %in% args
args <- args[!args %in% c("--apply", "--config-only", "--assets-only")]
if (length(args) != 1L || (config_only && assets_only)) {
stop(
"usage: retire-slot.R <codename> [--apply] [--config-only|--assets-only]",
call. = FALSE
)
}
codename <- args[[1L]]
# The bucket and `CODENAME:` say `alpine322`; `OS:` and the options say
# `alpine-322`. Both have to go.
pipeline_os <- if (grepl("^rhel[0-9]+$", codename)) {
sub("^rhel", "redhat-", codename)
} else if (grepl("^alpine[0-9]+$", codename)) {
sub("^alpine", "alpine-", codename)
} else {
# Single bracket so an unknown codename yields NA, not an error.
c(resolute = "ubuntu-2604", noble = "ubuntu-2404", jammy = "ubuntu-2204")[
codename
]
}
if (is.na(pipeline_os) || length(pipeline_os) == 0L) {
stop(sprintf("Unrecognised codename '%s'.", codename), call. = FALSE)
}
# The `OS_VERSION` dropdowns are keyed on the image tag, not the slot name.
image_tag <- if (grepl("^alpine-", pipeline_os)) {
sub("^alpine-([0-9])([0-9]+)$", "\\1.\\2", pipeline_os)
} else if (grepl("^redhat-", pipeline_os)) {
sub("^redhat-", "", pipeline_os)
} else {
codename
}
# Refuse a slot that is still routed: deleting its objects would leave the edge
# serving indexes that name files which no longer exist.
tf <- readLines("cdn.tf", warn = FALSE)
start <- grep("rpkgs_slots\\s*=", tf)
if (length(start) != 1L) {
stop(
"Could not find exactly one `rpkgs_slots` local in cdn.tf.",
call. = FALSE
)
}
block <- paste(tf[start:min(start + 10L, length(tf))], collapse = "\n")
if (grepl(sprintf('"%s"', codename), block, fixed = TRUE)) {
stop(
sprintf(
"'%s' is still listed in `local.rpkgs_slots` in cdn.tf, so the edge still routes it. Remove it there (and from the website's DistroId union) and apply the OpenTofu change first.", # nolint
codename
),
call. = FALSE
)
}
cat(sprintf(
"Retiring %s (pipelines call it '%s', images tag it '%s')\n\n",
codename,
pipeline_os,
image_tag
))
## 1. Pipeline definitions ----------------------------------------------------
# Line-based on purpose: a YAML round trip would reflow all 18 files.
strip_slot <- function(lines) {
keep <- rep(TRUE, length(lines))
i <- 1L
while (i <= length(lines)) {
line <- lines[[i]]
row <- regexec(
sprintf(
"^(\\s*)-\\s*(OS|CODENAME):\\s*(%s|%s)\\s*$",
pipeline_os,
codename
),
line
)
m <- regmatches(line, row)[[1L]]
if (length(m) == 4L) {
indent <- nchar(m[[2L]])
keep[[i]] <- FALSE
j <- i + 1L
while (j <= length(lines)) {
nxt <- lines[[j]]
if (
!nzchar(trimws(nxt)) || nchar(sub("^(\\s*).*$", "\\1", nxt)) <= indent
) {
break
}
keep[[j]] <- FALSE
j <- j + 1L
}
i <- j
next
}
if (grepl(sprintf("^\\s*-\\s*%s-(amd64|arm64)\\s*$", pipeline_os), line)) {
keep[[i]] <- FALSE
}
if (grepl(sprintf("^\\s*-\\s*'%s'\\s*$", image_tag), line)) {
keep[[i]] <- FALSE
}
i <- i + 1L
}
out <- lines[keep]
# A dropdown cannot default to an option that no longer exists.
bad_default <- grepl(sprintf("^\\s*default:\\s*'%s'\\s*$", image_tag), out)
if (any(bad_default)) {
for (k in which(bad_default)) {
opts <- grep("^\\s*-\\s*'[^']+'\\s*$", out[seq_len(k - 1L)])
if (length(opts) == 0L) {
next
}
# Walk back to this block's first option, not the nearest one.
first <- max(opts)
while (first > 1L && grepl("^\\s*-\\s*'[^']+'\\s*$", out[[first - 1L]])) {
first <- first - 1L
}
replacement <- sub("^\\s*-\\s*'([^']+)'\\s*$", "\\1", out[[first]])
out[[k]] <- sub("'[^']+'", sprintf("'%s'", replacement), out[[k]])
cat(sprintf(" default moved to '%s'\n", replacement))
}
}
out
}
if (!assets_only) {
files <- sort(list.files(".crow", pattern = "\\.yaml$", full.names = TRUE))
touched <- 0L
for (f in files) {
lines <- readLines(f, warn = FALSE)
out <- strip_slot(lines)
if (length(out) == length(lines)) {
next
}
touched <- touched + 1L
cat(sprintf(
" %s %-45s -%d lines\n",
if (apply_changes) "EDIT " else "would edit",
f,
length(lines) - length(out)
))
if (apply_changes) {
writeLines(out, f)
}
}
if (touched == 0L) {
cat(" No pipeline references the slot.\n")
} else if (apply_changes) {
cat(sprintf(
" %d file(s) edited. Verify with `crow lint .crow/` and `Rscript local/check-slots.R`.\n",
touched
))
}
cat("\n")
}
## 2. Bucket objects ----------------------------------------------------------
if (!config_only) {
library(s3fs, quietly = TRUE)
if (!nzchar(Sys.getenv("B2_S3_ACCESS_KEY"))) {
stop(
"B2_S3_ACCESS_KEY is unset. B2 lists nothing without credentials, which is indistinguishable from an already-clean slot.", # nolint
call. = FALSE
)
}
bucket <- "devxy-rpkgs-binaries"
s3fs::s3_file_system(
aws_access_key_id = Sys.getenv("B2_S3_ACCESS_KEY"),
aws_secret_access_key = Sys.getenv("B2_S3_SECRET_KEY"),
endpoint = "https://s3.eu-central-003.backblazeb2.com",
region_name = "eu-central-003",
refresh = TRUE
)
for (arch in c("amd64", "arm64")) {
prefix <- sprintf("%s/%s/%s", bucket, arch, codename)
if (!s3fs::s3_dir_exists(prefix)) {
cat(sprintf(" %s/%s: nothing in the bucket.\n", arch, codename))
next
}
objects <- s3fs::s3_dir_ls(prefix, recurse = TRUE)
cat(sprintf(
" %s %s/%s: %d object(s)\n",
if (apply_changes) "DELETE" else "would delete",
arch,
codename,
length(objects)
))
if (apply_changes && length(objects) > 0L) {
s3fs::s3_dir_delete(prefix)
cat(sprintf(" deleted s3://%s\n", prefix))
}
}
cat("\n")
}
## 3. Crons ------------------------------------------------------------------
if (!apply_changes) {
cat("Dry run: nothing was edited or deleted. Re-run with --apply.\n\n")
}
cat("Crons still to remove on the Crow server (they are not in this repo):\n")
cat(sprintf(
" crow-at ci.devxy.io repo cron ls devxy/build-cran-binaries | grep %s\n",
pipeline_os
))
cat(" # `crow repo cron rm` 404s here; the API base is /api/v1\n")
cat(" eval \"$(crow-at --export ci.devxy.io)\"\n")
cat(" curl -s -X DELETE -H \"Authorization: Bearer $CROW_TOKEN\" \\\n")
cat(" https://ci.devxy.io/api/v1/repos/5/cron/<cron-id>\n")