Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions bin/prune-revisions
Original file line number Diff line number Diff line change
@@ -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