diff --git a/.github/workflows/branch-cleanup.yml b/.github/workflows/branch-cleanup.yml index c496435..acc371c 100644 --- a/.github/workflows/branch-cleanup.yml +++ b/.github/workflows/branch-cleanup.yml @@ -9,7 +9,8 @@ # it is merged as the head of a PR. # - So this keeps an explicit allowlist instead: the default branch, main, # master, develop, staging, test, gh-pages, deploy-* and release/* are never -# deleted, and neither is a branch that an open PR still uses as its base. +# deleted, and neither is a branch that an open PR still uses as its base or +# head, or one that has moved past the merged commit. # # Self-contained on purpose (no reusable workflow), and it only acts on # same-repository heads, never on a fork's branch. @@ -27,12 +28,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 2 permissions: - contents: write - pull-requests: read + contents: write # delete the merged head branch's ref + pull-requests: read # list open PRs that still use the branch steps: - name: Delete the merged head branch env: HEAD_REF: ${{ github.event.pull_request.head.ref }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} @@ -51,15 +53,36 @@ jobs: ;; esac - children="$(gh pr list --repo "$REPO" --base "$HEAD_REF" --state open --json number --jq length)" - if [ "$children" -gt 0 ]; then - echo "::notice::Keeping $HEAD_REF: $children open PR(s) use it as their base." + bases="$(gh pr list --repo "$REPO" --base "$HEAD_REF" --state open --json number --jq length)" + if [ "$bases" -gt 0 ]; then + echo "::notice::Keeping $HEAD_REF: $bases open PR(s) use it as their base." + exit 0 + fi + heads="$(gh pr list --repo "$REPO" --head "$HEAD_REF" --state open --json number --jq length)" + if [ "$heads" -gt 0 ]; then + echo "::notice::Keeping $HEAD_REF: $heads other open PR(s) use it as their head." exit 0 fi ref="$(jq -rn --arg r "$HEAD_REF" '$r|@uri')" + if ! current="$(gh api "repos/$REPO/git/ref/heads/$ref" --jq .object.sha 2>&1)"; then + if printf '%s' "$current" | grep -Eq 'Not Found|HTTP 404'; then + echo "::notice::Branch $HEAD_REF was already deleted." + exit 0 + fi + printf '%s\n' "$current" >&2 + exit 1 + fi + # Only delete the commit that was merged. REST has no conditional + # delete, so a push in the moment between this check and the DELETE + # is not caught; anything pushed after the merge before now is. + if [ "$current" != "$HEAD_SHA" ]; then + echo "::notice::Keeping $HEAD_REF: it now points at $current, not the merged head $HEAD_SHA." + exit 0 + fi + if out="$(gh api -X DELETE "repos/$REPO/git/refs/heads/$ref" 2>&1)"; then - echo "Deleted branch $HEAD_REF." + echo "Deleted branch $HEAD_REF ($HEAD_SHA)." elif printf '%s' "$out" | grep -Eq 'Reference does not exist|HTTP 404'; then echo "::notice::Branch $HEAD_REF was already deleted." else