homebrew: publish and verify installable Kandelo bottles #424
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Staging cleanup | |
| # Deletes pr-<NNN>-staging pre-releases when the corresponding PR closes | |
| # and deletes same-repo branches for merged `ready-to-ship` PRs. The daily | |
| # sweep classifies every merge-candidate release from current GitHub state. | |
| # Only authoritative ready candidates and recent terminal-rejection evidence | |
| # survive after merge; API uncertainty always retains evidence. | |
| # | |
| # See docs/plans/2026-04-29-pr-package-builds-design.md §4.3. | |
| on: | |
| pull_request: | |
| types: [closed] | |
| schedule: | |
| - cron: '0 8 * * *' # daily at 08:00 UTC | |
| workflow_dispatch: # manual sweep | |
| jobs: | |
| cleanup-on-close: | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| steps: | |
| - name: Delete pr-${{ github.event.pull_request.number }}-staging | |
| run: | | |
| TAG="pr-${{ github.event.pull_request.number }}-staging" | |
| if gh release view "$TAG" --repo ${{ github.repository }} >/dev/null 2>&1; then | |
| gh release delete "$TAG" --repo ${{ github.repository }} --yes --cleanup-tag | |
| echo "Deleted $TAG" | |
| else | |
| echo "$TAG does not exist; nothing to clean up" | |
| fi | |
| - name: Delete merged ready-to-ship PR branch | |
| if: | | |
| github.event.pull_request.merged == true && | |
| github.event.pull_request.head.repo.full_name == github.repository && | |
| contains(github.event.pull_request.labels.*.name, 'ready-to-ship') | |
| env: | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| HEAD_REF: ${{ github.event.pull_request.head.ref }} | |
| PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| run: | | |
| if [ -z "$HEAD_REF" ]; then | |
| echo "::warning::PR #$PR_NUMBER has no head ref; skipping branch cleanup" | |
| exit 0 | |
| fi | |
| if [ -z "$PR_HEAD_SHA" ]; then | |
| echo "::warning::PR #$PR_NUMBER has no head SHA; skipping branch cleanup" | |
| exit 0 | |
| fi | |
| if [ "$HEAD_REF" = "$DEFAULT_BRANCH" ]; then | |
| echo "::warning::Refusing to delete default branch $HEAD_REF for PR #$PR_NUMBER" | |
| exit 0 | |
| fi | |
| if ! git check-ref-format --branch "$HEAD_REF" >/dev/null 2>&1; then | |
| echo "::warning::Refusing to delete invalid branch ref $HEAD_REF for PR #$PR_NUMBER" | |
| exit 0 | |
| fi | |
| git init --quiet "$RUNNER_TEMP/branch-cleanup" | |
| cd "$RUNNER_TEMP/branch-cleanup" | |
| REMOTE="https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" | |
| remote_sha=$(git ls-remote "$REMOTE" "refs/heads/$HEAD_REF" | awk '{print $1}') | |
| if [ -n "$remote_sha" ]; then | |
| if [ "$remote_sha" != "$PR_HEAD_SHA" ]; then | |
| echo "::warning::Refusing to delete branch $HEAD_REF for PR #$PR_NUMBER because it now points at $remote_sha, not merged head $PR_HEAD_SHA" | |
| exit 0 | |
| fi | |
| if git push "$REMOTE" ":refs/heads/$HEAD_REF"; then | |
| echo "Deleted merged PR #$PR_NUMBER branch $HEAD_REF" | |
| else | |
| echo "::warning::Failed to delete merged PR #$PR_NUMBER branch $HEAD_REF" | |
| fi | |
| else | |
| echo "Merged PR #$PR_NUMBER branch $HEAD_REF is already absent" | |
| fi | |
| sweep: | |
| if: | | |
| github.event_name == 'schedule' || | |
| (github.event_name == 'workflow_dispatch' && | |
| github.ref_name == github.event.repository.default_branch) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read | |
| contents: write | |
| pull-requests: read | |
| statuses: read | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| steps: | |
| - uses: actions/checkout@v7.0.0 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| sparse-checkout: | | |
| .github/scripts/cleanup-merge-candidates.sh | |
| .github/scripts/github-api-get.sh | |
| .github/scripts/latest-merge-gate-status.sh | |
| .github/scripts/state-lock.sh | |
| sparse-checkout-cone-mode: false | |
| - name: Sweep orphan pr-*-staging tags | |
| run: | | |
| # Up to 200 most recent pre-releases; pr-<N>-staging is the | |
| # only naming pattern we own, so we filter on it directly. | |
| gh release list --repo ${{ github.repository }} --limit 200 \ | |
| --json tagName,isPrerelease \ | |
| -q '.[] | select(.isPrerelease and (.tagName | startswith("pr-")) and (.tagName | endswith("-staging"))) | .tagName' \ | |
| | while read -r TAG; do | |
| # Extract PR number from pr-<N>-staging. | |
| PR=${TAG#pr-} | |
| PR=${PR%-staging} | |
| if [ -z "$PR" ] || ! [[ "$PR" =~ ^[0-9]+$ ]]; then | |
| echo "WARN: cannot parse PR number from $TAG, skipping" | |
| continue | |
| fi | |
| STATE=$(gh pr view "$PR" --repo ${{ github.repository }} --json state -q .state 2>/dev/null || echo "MISSING") | |
| if [ "$STATE" != "OPEN" ]; then | |
| echo "Deleting orphan $TAG (PR state: $STATE)" | |
| gh release delete "$TAG" --repo ${{ github.repository }} --yes --cleanup-tag | |
| else | |
| echo "Keeping $TAG (PR is OPEN)" | |
| fi | |
| done | |
| - name: Sweep completed merge-candidate releases | |
| run: | | |
| bash .github/scripts/cleanup-merge-candidates.sh \ | |
| --max-pages 50 \ | |
| --per-page 100 \ | |
| --max-asset-pages 50 \ | |
| --asset-per-page 100 \ | |
| --rejected-retention-days 14 | |
| - name: Sweep merged ready-to-ship PR branches | |
| env: | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| run: | | |
| git init --quiet "$RUNNER_TEMP/branch-cleanup" | |
| cd "$RUNNER_TEMP/branch-cleanup" | |
| REMOTE="https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" | |
| delete_branch() { | |
| local pr_number="$1" | |
| local head_ref="$2" | |
| local expected_sha="$3" | |
| if [ -z "$head_ref" ]; then | |
| echo "::warning::PR #$pr_number has no head ref; skipping branch cleanup" | |
| return 0 | |
| fi | |
| if [ -z "$expected_sha" ]; then | |
| echo "::warning::PR #$pr_number has no head SHA; skipping branch cleanup" | |
| return 0 | |
| fi | |
| if [ "$head_ref" = "$DEFAULT_BRANCH" ]; then | |
| echo "::warning::Refusing to delete default branch $head_ref for PR #$pr_number" | |
| return 0 | |
| fi | |
| if ! git check-ref-format --branch "$head_ref" >/dev/null 2>&1; then | |
| echo "::warning::Refusing to delete invalid branch ref $head_ref for PR #$pr_number" | |
| return 0 | |
| fi | |
| remote_sha=$(git ls-remote "$REMOTE" "refs/heads/$head_ref" | awk '{print $1}') | |
| if [ -n "$remote_sha" ]; then | |
| if [ "$remote_sha" != "$expected_sha" ]; then | |
| echo "::warning::Refusing to delete branch $head_ref for PR #$pr_number because it now points at $remote_sha, not merged head $expected_sha" | |
| return 0 | |
| fi | |
| if git push "$REMOTE" ":refs/heads/$head_ref"; then | |
| echo "Deleted merged PR #$pr_number branch $head_ref" | |
| else | |
| echo "::warning::Failed to delete merged PR #$pr_number branch $head_ref" | |
| fi | |
| else | |
| echo "Merged PR #$pr_number branch $head_ref is already absent" | |
| fi | |
| } | |
| gh pr list \ | |
| --repo ${{ github.repository }} \ | |
| --state merged \ | |
| --base "$DEFAULT_BRANCH" \ | |
| --label ready-to-ship \ | |
| --limit 200 \ | |
| --json number,headRefName,headRefOid,isCrossRepository \ | |
| -q '.[] | select(.isCrossRepository == false) | [.number, .headRefName, .headRefOid] | @tsv' \ | |
| | while IFS=$'\t' read -r pr_number head_ref head_sha; do | |
| delete_branch "$pr_number" "$head_ref" "$head_sha" | |
| done |