From acb590e7cbb0b48c0072ef7f645266527524d67f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 00:20:32 +0000 Subject: [PATCH 1/2] Add script to delete auto-generated releases while keeping tags Add scripts/delete_automated_releases.sh, which finds every GitHub Release whose body starts with "Automated release for" or "Dispatch release for" (the markers emitted by the automated/dispatch release CI workflows) and deletes the Release entries via the gh CLI, leaving the underlying git tags intact (it never passes `gh release delete --cleanup-tag`). The script enumerates releases dynamically with `gh api --paginate`, defaults to a dry run that only lists matches, requires --apply (plus a confirmation prompt unless --yes) to delete, and backs up the matching release objects to JSON before deleting. The backup file is gitignored. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01GuovjAVb7evQGBXY7ZRkXx --- .gitignore | 3 + scripts/delete_automated_releases.sh | 152 +++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100755 scripts/delete_automated_releases.sh diff --git a/.gitignore b/.gitignore index 9dc7955f8dc..533bcfcec8e 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ package-lock.json .claude/.worktrees .claude/settings.local.json CLAUDE.local.md + +# Backups written by scripts/delete_automated_releases.sh +automated-releases-backup-*.json diff --git a/scripts/delete_automated_releases.sh b/scripts/delete_automated_releases.sh new file mode 100755 index 00000000000..1dcea99f622 --- /dev/null +++ b/scripts/delete_automated_releases.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# +# Delete GitHub Releases whose body starts with one of the auto-release +# markers ("Automated release for" / "Dispatch release for") while KEEPING the +# underlying git tags. +# +# These Release entries are created by the automated/dispatch release CI +# workflows (one per sub-package version bump) and clutter the Releases page. +# Deleting the Release object removes only the GitHub Releases UI entry; the +# git tag -- and therefore the commit it points at and anything that resolves +# it (pip installs, changelog links, etc.) -- is left untouched, because we +# never pass `gh release delete --cleanup-tag`. +# +# Usage: +# scripts/delete_automated_releases.sh # dry run: list matches, delete nothing +# scripts/delete_automated_releases.sh --apply # delete (asks for confirmation once) +# scripts/delete_automated_releases.sh --apply --yes # delete without the confirmation prompt +# REPO=owner/name scripts/delete_automated_releases.sh # target a different repo (default below) +# scripts/delete_automated_releases.sh --repo=owner/name --apply +# +# Requires: gh (https://cli.github.com/, authenticated via `gh auth login`) and jq. + +set -euo pipefail + +REPO="${REPO:-reflex-dev/reflex}" + +# Body prefixes that identify an auto-generated release. Matching is exact +# (no leading-whitespace tolerance). Add/remove entries here to adjust scope. +PREFIXES=("Automated release for" "Dispatch release for") + +usage() { + # Print the header comment block (from line 3 to the first non-comment line), + # stripping the leading "# " so --help reads as plain help text. + awk 'NR>=3 { if (/^#/) { sub(/^# ?/, ""); print; next } else exit }' "$0" +} + +apply=false +assume_yes=false +for arg in "$@"; do + case "$arg" in + --apply) apply=true ;; + -y | --yes) assume_yes=true ;; + --repo=*) REPO="${arg#--repo=}" ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "error: unknown argument '$arg' (try --help)" >&2 + exit 2 + ;; + esac +done + +command -v gh >/dev/null 2>&1 || { + echo "error: gh CLI not found -- install from https://cli.github.com/" >&2 + exit 1 +} +command -v jq >/dev/null 2>&1 || { + echo "error: jq not found -- install jq to run this script" >&2 + exit 1 +} +gh auth status >/dev/null 2>&1 || { + echo "error: gh is not authenticated -- run 'gh auth login'" >&2 + exit 1 +} + +# Build the jq selector by OR-ing a startswith() clause per prefix. Each prefix +# is JSON-encoded via jq so any quotes/backslashes are escaped safely. +select_expr="" +for p in "${PREFIXES[@]}"; do + enc=$(jq -cn --arg s "$p" '$s') + select_expr+="${select_expr:+ or }(.body | startswith($enc))" +done +match_filter="select(.body != null and ($select_expr))" + +echo "Repository : $REPO" +echo "Matching : body starts with one of:" +for p in "${PREFIXES[@]}"; do echo " - \"$p\""; done +echo "Tags : preserved (releases deleted without --cleanup-tag)" +echo + +# Discover matching releases across all pages. gh applies --jq per page when +# paginating, so this streams one "\t" row per match. +echo "Fetching releases for $REPO ..." >&2 +if ! rows_raw=$(gh api "repos/$REPO/releases" --paginate \ + --jq "[.[] | $match_filter] | .[] | [.tag_name, (.body | split(\"\n\")[0] | rtrimstr(\"\r\"))] | @tsv"); then + echo "error: failed to list releases for $REPO" >&2 + exit 1 +fi + +tags=() +if [[ -n "$rows_raw" ]]; then + while IFS=$'\t' read -r tag firstline; do + [[ -z "$tag" ]] && continue + tags+=("$tag") + printf ' %-42s %s\n' "$tag" "$firstline" + done <<<"$rows_raw" +fi + +count=${#tags[@]} +echo +echo "Found $count matching release(s)." + +if [[ "$count" -eq 0 ]]; then + echo "Nothing to do." + exit 0 +fi + +if [[ "$apply" != true ]]; then + echo + echo "Dry run -- no releases were deleted. Re-run with --apply to delete them." + exit 0 +fi + +# Back up the full matching release objects before deleting, so the entries can +# be inspected (or recreated) later. --paginate emits one array per page; merge +# them with a local 'jq -s add'. +backup="automated-releases-backup-${REPO//\//-}.json" +echo +echo "Backing up matching releases to $backup ..." +gh api "repos/$REPO/releases" --paginate --jq "[.[] | $match_filter]" | jq -s 'add' >"$backup" + +if [[ "$assume_yes" != true ]]; then + echo + read -r -p "Delete these $count release(s) from $REPO (tags will be kept)? [y/N] " reply + case "$reply" in + [yY] | [yY][eE][sS]) ;; + *) + echo "Aborted. No releases deleted." + exit 0 + ;; + esac +fi + +echo +deleted=0 +failed=0 +for tag in "${tags[@]}"; do + # No --cleanup-tag: the Release is removed but the git tag is kept. + if gh release delete "$tag" --repo "$REPO" --yes; then + echo " deleted release: $tag (tag kept)" + deleted=$((deleted + 1)) + else + echo " FAILED to delete: $tag" >&2 + failed=$((failed + 1)) + fi +done + +echo +echo "Done. Deleted $deleted release(s); $failed failure(s). Tags were not touched." +[[ "$failed" -eq 0 ]] From 541b65ee775ee73fd3444703303a39a98ce553e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 00:35:12 +0000 Subject: [PATCH 2/2] Preserve top-level non-prerelease releases; backup/flag nits Restrict the deletion scope so the script never removes top-level reflex releases (the main package's bare "vX.Y.Z" tags) unless they are pre-releases. The "Dispatch release for" prefix matches the main package's release entries too (the dispatch planner appends a `package: "reflex"` release whenever reflex-base is released, and those are the only dispatch releases not marked --latest=false), so the previous filter would have removed finalized core entries like v0.9.5 from the Releases page. Top-level alpha pre-releases (e.g. v0.9.4a1) and all sub-package releases remain in scope. Also address two review nits: - Timestamp the backup filename so retrying after a partial failure no longer clobbers an earlier run's backup (gitignore glob still matches). - Print a note when --yes is passed without --apply, which is otherwise a silent no-op. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01GuovjAVb7evQGBXY7ZRkXx --- scripts/delete_automated_releases.sh | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/scripts/delete_automated_releases.sh b/scripts/delete_automated_releases.sh index 1dcea99f622..c0b04a59e04 100755 --- a/scripts/delete_automated_releases.sh +++ b/scripts/delete_automated_releases.sh @@ -11,6 +11,13 @@ # it (pip installs, changelog links, etc.) -- is left untouched, because we # never pass `gh release delete --cleanup-tag`. # +# Scope: top-level reflex releases -- the main package's bare "vX.Y.Z" tags, as +# opposed to "-v..." sub-package tags -- are KEPT in full when they are NOT +# pre-releases, since those finalized core releases are the canonical entries on +# the Releases page. Their pre-release (alpha) counterparts (e.g. v0.9.4a1) and +# every sub-package release (reflex-base-v..., reflex-components-*-v..., etc.) +# are still removed. +# # Usage: # scripts/delete_automated_releases.sh # dry run: list matches, delete nothing # scripts/delete_automated_releases.sh --apply # delete (asks for confirmation once) @@ -72,12 +79,19 @@ for p in "${PREFIXES[@]}"; do enc=$(jq -cn --arg s "$p" '$s') select_expr+="${select_expr:+ or }(.body | startswith($enc))" done -match_filter="select(.body != null and ($select_expr))" + +# A release is eligible for deletion when its body matches a prefix, UNLESS it is +# a top-level reflex release (bare "vX.Y.Z" tag, i.e. the main package rather +# than a "-v..." sub-package) that is not a pre-release -- those finalized +# core entries are kept. test("^v[0-9]") matches only the main package's tags. +keep_toplevel='(.tag_name | test("^v[0-9]")) and (.prerelease == false)' +match_filter="select(.body != null and ($select_expr)) | select(($keep_toplevel) | not)" echo "Repository : $REPO" echo "Matching : body starts with one of:" for p in "${PREFIXES[@]}"; do echo " - \"$p\""; done -echo "Tags : preserved (releases deleted without --cleanup-tag)" +echo "Keeping : top-level non-pre-release reflex (vX.Y.Z) entries, e.g. v0.9.5" +echo "Tags : preserved (releases deleted without --cleanup-tag)" echo # Discover matching releases across all pages. gh applies --jq per page when @@ -109,14 +123,18 @@ fi if [[ "$apply" != true ]]; then echo + if [[ "$assume_yes" == true ]]; then + echo "Note: --yes has no effect without --apply." + fi echo "Dry run -- no releases were deleted. Re-run with --apply to delete them." exit 0 fi # Back up the full matching release objects before deleting, so the entries can # be inspected (or recreated) later. --paginate emits one array per page; merge -# them with a local 'jq -s add'. -backup="automated-releases-backup-${REPO//\//-}.json" +# them with a local 'jq -s add'. The timestamp keeps each run's backup distinct +# so retrying after a partial failure never clobbers an earlier backup. +backup="automated-releases-backup-${REPO//\//-}-$(date +%Y%m%d-%H%M%S).json" echo echo "Backing up matching releases to $backup ..." gh api "repos/$REPO/releases" --paginate --jq "[.[] | $match_filter]" | jq -s 'add' >"$backup"