From 728fc544593f30ca702c8368efcc0bc9f796b151 Mon Sep 17 00:00:00 2001 From: Chris Oliver Date: Wed, 9 Sep 2026 16:24:41 -0500 Subject: [PATCH] Keep only the two newest revision releases of a version Every rebuild left another pinned revision release behind, most versions had five or six, and nothing used any but the newest: mise resolves the newest revision or the floating tag, and the build fingerprint lives in the newest revision's notes. Hundreds of releases also push versions out of the first page of the release list, which is all mise reads by default. bin/prune-revisions deletes a version's oldest revision releases and their tags, keeping the newest KEEP_REVISIONS (2: the current build and the previous one, so a bad rebuild can be rolled back by re-pointing the floating release). publish-release runs it last, after the floating release points at the new build. The floating release is never pruned. Claude-Session: https://claude.ai/code/session_01KeNcsk8V24YRXGfD2HwDZY --- .github/workflows/release.yml | 13 ++++++++++ CLAUDE.md | 1 + README.md | 5 ++++ bin/prune-revisions | 49 +++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+) create mode 100755 bin/prune-revisions diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f799bda..6de6250 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,6 +19,12 @@ on: type: boolean default: false +env: + # Revision releases of a version to keep after publishing a new one: the current build + # and the previous one, so a bad rebuild can be rolled back by re-pointing the floating + # release at the previous revision. Older ones are deleted by bin/prune-revisions. + KEEP_REVISIONS: "2" + # Prevent multiple releases of the same version from running simultaneously concurrency: group: release-${{ inputs.version }} @@ -301,6 +307,8 @@ jobs: GH_REPO: ${{ github.repository }} GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN || github.token }} steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Download artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: @@ -413,6 +421,11 @@ jobs: gh api -X PATCH "repos/${GH_REPO}/git/refs/tags/${VERSION}" \ -f sha="${{ github.sha }}" -F force=true > /dev/null + - name: Prune old revision releases + # Only after the floating release points at the new build, so the revisions being + # deleted are never the one it serves. + run: bin/prune-revisions "${VERSION}" --keep "${KEEP_REVISIONS}" + notify: needs: [plan, build, combine-hashes, create-release, provenance, upload-assets, publish-release] runs-on: ubuntu-latest diff --git a/CLAUDE.md b/CLAUDE.md index e042f99..954918b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,6 +42,7 @@ YJIT builds require `rustup` or `rustc` in `PATH`. Set `JDX_RUBY_RUSTUP_HOME` to - `bin/update-ruby-recipe`: Adds or updates a Ruby entry in `recipes/rubies.yml`; used by autobump. - `bin/package-linux`: Runs `bin/package` for a Linux target inside its pinned manylinux2014 container, mirroring the build workflow, for local Linux builds. - `bin/recipe-info`: Prints a version's effective series settings (`legacy`, `yjit`, ...) for workflows. +- `bin/prune-revisions`: Deletes a version's oldest revision releases and tags, keeping the newest `KEEP_REVISIONS` (2); run by the release workflow after publishing. - `bin/build-fingerprint`: Prints a digest of everything in the repo that affects a version's build; the release workflow skips rebuilding when it matches the newest revision release's. ### Old series diff --git a/README.md b/README.md index fdc2d33..c20e17b 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,11 @@ packaging script and the build workflow), and a run whose fingerprint matches th revision's exits after its first job. Tick `force` to rebuild anyway, for example when a pinned container or the Rust toolchain is the reason. +Each version keeps its two newest revision releases, the current build and the previous one +for rollback; older revisions and their tags are deleted after a publish (`bin/prune-revisions`, +`KEEP_REVISIONS` in the workflow). The floating release is never pruned. A mise lockfile that +pins a pruned revision falls back to the newest one. + [Release New Versions](https://github.com/hatchboxio/precompiled-ruby/actions/workflows/release-new.yml) dispatches that for every recipe that has no release yet; it runs on a schedule, when `recipes/rubies.yml` changes on `main`, or by hand, where its `only` input releases exactly diff --git a/bin/prune-revisions b/bin/prune-revisions new file mode 100755 index 0000000..2a807e4 --- /dev/null +++ b/bin/prune-revisions @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Delete a version's oldest revision releases, and their tags, keeping the newest few. The +# floating release (the bare version) is never touched. +# +# bin/prune-revisions 3.4.9 # keep the newest 2 revisions of 3.4.9 +# bin/prune-revisions 3.4.9 --keep 3 +# bin/prune-revisions 3.4.9 --dry-run # only print what would go +# +# Uses gh; set GH_REPO to target a repository other than the current checkout's. +set -euo pipefail + +version="" +keep=2 +dry_run=false +while [[ $# -gt 0 ]]; do + case "$1" in + --keep) keep="$2"; shift 2 ;; + --keep=*) keep="${1#--keep=}"; shift ;; + --dry-run) dry_run=true; shift ;; + -h|--help) sed -n '2,9p' "$0"; exit 0 ;; + -*) echo "unknown option: $1" >&2; exit 2 ;; + *) version="$1"; shift ;; + esac +done +[[ -n "$version" ]] || { echo "usage: bin/prune-revisions VERSION [--keep N] [--dry-run]" >&2; exit 2; } +[[ "$keep" =~ ^[0-9]+$ && "$keep" -ge 1 ]] || { echo "--keep must be a positive integer" >&2; exit 2; } + +repo="${GH_REPO:-$(gh repo view --json nameWithOwner --jq .nameWithOwner)}" +escaped="$(printf '%s' "$version" | sed 's/[.]/\\./g')" + +# Revision tags of this version, oldest first, from the full release list (drafts excluded). +revisions="$(gh api "repos/${repo}/releases" --paginate --jq '.[] | select(.draft | not) | .tag_name' \ + | { grep -E "^${escaped}-[0-9]+$" || true; } \ + | sed "s/^${escaped}-//" | sort -n | sed "s/^/${version}-/")" + +total=$(printf '%s\n' "$revisions" | grep -c . || true) +if [[ "$total" -le "$keep" ]]; then + echo "${version}: ${total} revision release(s), nothing to prune" + exit 0 +fi + +printf '%s\n' "$revisions" | head -n "$((total - keep))" | while read -r tag; do + if [[ "$dry_run" == "true" ]]; then + echo "would delete ${tag}" + else + echo "deleting ${tag}" + gh release delete "$tag" --repo "$repo" --yes --cleanup-tag + fi +done