Skip to content
Merged
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
48 changes: 48 additions & 0 deletions .github/workflows/release-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Nightly builds are helpful validation, but useless when old.
# This script deletes any "nightly" titled release older than DAYS_OLD.
# Dry run is default, and must be explicitly passed as 'false' to act.
name: Release cleanup

on:
schedule:
- cron: "30 3 * * *"

workflow_dispatch:
inputs:
dry_run:
description: "Dry run (no deletions)"
type: boolean
default: true

Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow is missing explicit permissions declaration. All other workflows in this repository explicitly declare permissions (e.g., release.yml has 'contents: write', update-issue-status.yml has 'contents: write', etc.). This workflow needs 'contents: write' permission to delete releases. Add a 'permissions:' section at the workflow level after the 'on:' section, before 'jobs:', with 'contents: write'.

Suggested change
permissions:
contents: write

Copilot uses AI. Check for mistakes.
jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Clean up old nightly releases
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DRY_RUN: ${{ github.event_name != 'schedule' && (github.event_name != 'workflow_dispatch' || inputs.dry_run) }}
run: |
set -euo pipefail

DAYS_OLD=7
CUTOFF_DATE=$(date -u -d "${DAYS_OLD} days ago" +%Y-%m-%dT%H:%M:%SZ)

gh release list -R "$GITHUB_REPOSITORY" \
--limit 1000 \
--json tagName,publishedAt,name \
--jq ".[] | select(.name | test(\"nightly\"; \"i\")) | select(.publishedAt < \"${CUTOFF_DATE}\") | .tagName" |
while read -r tag; do
if [[ "$DRY_RUN" == "true" ]]; then
echo "DRY-RUN: $tag"
else
if ! gh release delete "$tag" -R "$GITHUB_REPOSITORY" --yes; then
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'gh release delete' command only deletes the GitHub release but leaves the git tag intact. For nightly builds with timestamp-based tags (e.g., v9.0.12-abc1234-20260227T040000Z), this will result in orphaned tags accumulating in the repository over time. Consider adding the '--cleanup-tag' flag to also delete the associated tag, or document this as intentional behavior if you want to preserve the tag history.

Suggested change
if ! gh release delete "$tag" -R "$GITHUB_REPOSITORY" --yes; then
if ! gh release delete "$tag" -R "$GITHUB_REPOSITORY" --cleanup-tag --yes; then

Copilot uses AI. Check for mistakes.
echo "Error: release deletion: $tag" >&2
exit 1
else
echo "Release deleted: $tag"
fi
fi
Comment on lines +33 to +47
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The limit of 1000 releases may not be sufficient if the repository has more than 1000 total releases. Since 'gh release list' returns releases in reverse chronological order by default, older nightly releases beyond the 1000th release would never be deleted. Consider either using pagination to fetch all releases, or documenting this limitation. However, given that nightly builds are created daily and kept for 7 days, this limit should be adequate for most cases unless the repository has hundreds of non-nightly releases per day.

Suggested change
gh release list -R "$GITHUB_REPOSITORY" \
--limit 1000 \
--json tagName,publishedAt,name \
--jq ".[] | select(.name | test(\"nightly\"; \"i\")) | select(.publishedAt < \"${CUTOFF_DATE}\") | .tagName" |
while read -r tag; do
if [[ "$DRY_RUN" == "true" ]]; then
echo "DRY-RUN: $tag"
else
if ! gh release delete "$tag" -R "$GITHUB_REPOSITORY" --yes; then
echo "Error: release deletion: $tag" >&2
exit 1
else
echo "Release deleted: $tag"
fi
fi
PAGE=1
PAGE_SIZE=100
while :; do
TAGS=$(gh release list -R "$GITHUB_REPOSITORY" \
--limit "$PAGE_SIZE" \
--page "$PAGE" \
--json tagName,publishedAt,name \
--jq ".[] | select(.name | test(\"nightly\"; \"i\")) | select(.publishedAt < \"${CUTOFF_DATE}\") | .tagName")
# If no tags were returned on this page, we're done.
if [[ -z "$TAGS" ]]; then
break
fi
while read -r tag; do
# Skip empty lines just in case
[[ -z "$tag" ]] && continue
if [[ "$DRY_RUN" == "true" ]]; then
echo "DRY-RUN: $tag"
else
if ! gh release delete "$tag" -R "$GITHUB_REPOSITORY" --yes; then
echo "Error: release deletion: $tag" >&2
exit 1
else
echo "Release deleted: $tag"
fi
fi
done <<< "$TAGS"
PAGE=$((PAGE + 1))

Copilot uses AI. Check for mistakes.
done
Loading