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,39 +1,20 @@
### Retire a slot: strip it from the pipelines and delete its objects from S3.
#
# Usage:
# Rscript local/retire-slot.R <codename> [--apply] [--config-only|--assets-only]
#
# Rscript local/retire-slot.R alpine322 # dry run, both halves
# Rscript local/retire-slot.R alpine322 --config-only # edit .crow/, no S3
# Rscript local/retire-slot.R alpine322 --assets-only --apply
#
# Retiring a distro retires both architectures, because `local.rpkgs_slots` in
# `cdn.tf` is a setproduct over `["amd64", "arm64"]` and there is no way to
# publish one without the other.
# `cdn.tf` is a setproduct over them.
#
# Three things have to happen and nothing ties them together on its own:
# 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.
#
# 1. the slot leaves `local.rpkgs_slots` in `cdn.tf` (and `DistroId` in the
# website's `src/src/data/repos.ts`), which is what stops the edge routing
# it. Do this first and by hand: it is the decision, the rest is cleanup.
# 2. the slot leaves every `.crow/*.yaml` matrix, which is what stops the
# fleet building, indexing and verifying it. This script does that.
# 3. the objects leave the bucket. This script does that too.
# Cron jobs live on the Crow server, not here, and are printed at the end.
#
# A fourth step cannot be scripted from here: the slot's cron jobs live on the
# Crow server, not in this repository, and removing a matrix row does not remove
# the cron that targets it. The script prints the ones it finds by name at the
# end, with the API call to delete them.
# Only structural YAML is edited. Prose mentioning the slot is left alone, so
# grep for the name afterwards.
#
# Dry run by default. Nothing is edited or deleted without --apply.
#
# Order matters. Deleting the objects while a pipeline still targets the slot
# means the next nightly rebuilds them.
#
# Only structural YAML is edited: matrix rows, dropdown options and the
# `OS_VERSION` tag. Prose that happens to mention the slot (a usage example in
# a header comment, a dropdown `description:`) is left alone, because rewriting
# free text automatically is guesswork. Grep for the name afterwards.
# Dry run by default.
options(error = function() {
cat("ERROR:", geterrmessage(), "\n", file = stdout())
@ -53,15 +34,14 @@ if (length(args) != 1L || (config_only && assets_only)) {
}
codename <- args[[1L]]
# `alpine322` is how the bucket, the edge and `CODENAME:` spell it; `alpine-322`
# is how `OS:` and the dropdown options spell it. Both have to go.
# 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 and the check below
# reports it rather than raising "subscript out of bounds".
# Single bracket so an unknown codename yields NA, not an error.
c(resolute = "ubuntu-2604", noble = "ubuntu-2404", jammy = "ubuntu-2204")[
codename
]
@ -79,9 +59,8 @@ image_tag <- if (grepl("^alpine-", pipeline_os)) {
codename
}
# Refuse to retire a slot that is still routed. Without this the script is a
# fast way to break a live slot: the edge would keep sending clients to indexes
# whose objects no longer exist, and every install would 404.
# 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) {
@ -110,9 +89,7 @@ cat(sprintf(
## 1. Pipeline definitions ----------------------------------------------------
# Drops a matrix row and everything indented under it, a dropdown option, and
# an `OS_VERSION` entry for the retired tag. Line-based on purpose: round
# tripping through a YAML parser would reflow all 18 files and bury the change.
# Line-based on purpose: a YAML round trip would reflow all 18 files.
strip_slot <- function(lines) {
keep <- rep(TRUE, length(lines))
i <- 1L
@ -153,8 +130,7 @@ strip_slot <- function(lines) {
i <- i + 1L
}
out <- lines[keep]
# A dropdown whose default was the retired tag would reject its own default,
# so move it to the first surviving option of the same block.
# 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)) {
@ -162,8 +138,7 @@ strip_slot <- function(lines) {
if (length(opts) == 0L) {
next
}
# Walk back to the first option of this block rather than the last, so the
# default lands on the lowest surviving version instead of an unrelated one.
# 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
@ -213,7 +188,7 @@ if (!config_only) {
library(s3fs, quietly = TRUE)
if (!nzchar(Sys.getenv("B2_S3_ACCESS_KEY"))) {
stop(
"B2_S3_ACCESS_KEY is unset. B2 needs credentials even to list, and an unauthenticated list returns empty, which is indistinguishable from an already-clean slot.", # nolint
"B2_S3_ACCESS_KEY is unset. B2 lists nothing without credentials, which is indistinguishable from an already-clean slot.", # nolint
call. = FALSE
)
}
@ -248,7 +223,7 @@ if (!config_only) {
cat("\n")
}
## 3. What is left for a human ------------------------------------------------
## 3. Crons ------------------------------------------------------------------
if (!apply_changes) {
cat("Dry run: nothing was edited or deleted. Re-run with --apply.\n\n")
@ -259,9 +234,7 @@ 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 on this server version; use the API, whose base is /api/v1\n"
)
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")