name: Shared - Delete Merged Branch
on: workflow_call: inputs: branch_name: description: 'Branch name to delete (if empty, detects from merge commit)' required: false type: string default: '' dry_run: description: 'Only log what would be deleted, do not actually delete' required: false type: boolean default: false fail_on_protected: description: 'Hard-fail when target branch is protected (develop/main/master/release/*). Default true for explicit branch_name inputs.' required: false type: boolean default: true
outputs: deleted_branch: description: 'Name of deleted branch (empty if not deleted)' value: ${{ jobs.delete.outputs.branch }} status: description: 'Status: deleted, skipped, protected, not_found' value: ${{ jobs.delete.outputs.status }}
jobs: delete: name: Delete Branch runs-on: ubuntu-latest
outputs: branch: ${{ steps.delete.outputs.branch }} status: ${{ steps.delete.outputs.status }}
steps: - name: Checkout uses: actions/checkout@v5 with: fetch-depth: 0
- name: Detect and delete branch id: delete env: GH_TOKEN: ${{ github.token }} INPUT_BRANCH: ${{ inputs.branch_name }} DRY_RUN: ${{ inputs.dry_run }} FAIL_ON_PROTECTED: ${{ inputs.fail_on_protected }} run: | # Protected branches - never delete PROTECTED_BRANCHES="main master develop"
# Determine branch to delete EXPLICIT_INPUT=false if [ -n "$INPUT_BRANCH" ]; then BRANCH="$INPUT_BRANCH" EXPLICIT_INPUT=true else COMMIT_SHA=$(git rev-parse HEAD) COMMIT_MSG=$(git log -1 --pretty=%B)
# Method 1: Merge commit message if echo "$COMMIT_MSG" | grep -q "Merge pull request"; then BRANCH=$(echo "$COMMIT_MSG" | grep -oP "from [^/]+/\K[^\s]+" | head -1) elif echo "$COMMIT_MSG" | grep -q "Merge branch"; then BRANCH=$(echo "$COMMIT_MSG" | grep -oP "Merge branch '\K[^']+" | head -1) fi
# Method 2: GitHub API (handles squash merges) if [ -z "$BRANCH" ]; then echo "Trying GitHub API to find PR for commit $COMMIT_SHA" BRANCH=$(gh api "repos/${{ github.repository }}/commits/${COMMIT_SHA}/pulls" \ --jq '.[0].head.ref // empty' 2>/dev/null || echo "") fi
if [ -z "$BRANCH" ]; then echo "Could not detect source branch from commit" echo "status=not_found" >> $GITHUB_OUTPUT echo "branch=" >> $GITHUB_OUTPUT exit 0 fi fi
echo "Detected branch: $BRANCH"
# Check if branch is protected IS_PROTECTED=false for protected in $PROTECTED_BRANCHES; do if [ "$BRANCH" = "$protected" ]; then IS_PROTECTED=true break fi done if echo "$BRANCH" | grep -q "^release/"; then IS_PROTECTED=true fi
if [ "$IS_PROTECTED" = "true" ]; then echo "::warning::Branch '$BRANCH' is protected (main/master/develop/release/*) — refusing to delete." echo "status=protected" >> $GITHUB_OUTPUT echo "branch=$BRANCH" >> $GITHUB_OUTPUT if [ "$EXPLICIT_INPUT" = "true" ] && [ "$FAIL_ON_PROTECTED" = "true" ]; then echo "::error::Refusing to delete protected branch '$BRANCH' (explicit input + fail_on_protected=true)." exit 1 fi exit 0 fi
# Check if branch exists if ! git ls-remote --heads origin "$BRANCH" | grep -q .; then echo "Branch '$BRANCH' does not exist on remote" echo "status=not_found" >> $GITHUB_OUTPUT echo "branch=$BRANCH" >> $GITHUB_OUTPUT exit 0 fi
# Delete branch if [ "$DRY_RUN" = "true" ]; then echo "[DRY RUN] Would delete branch: $BRANCH" echo "status=skipped" >> $GITHUB_OUTPUT else echo "Deleting branch: $BRANCH" gh api -X DELETE "repos/${{ github.repository }}/git/refs/heads/$BRANCH" || true echo "status=deleted" >> $GITHUB_OUTPUT fi
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
# Summary echo "### Branch Cleanup" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ "$DRY_RUN" = "true" ]; then echo "**[DRY RUN]** Would delete: \`$BRANCH\`" >> $GITHUB_STEP_SUMMARY else echo "Deleted branch: \`$BRANCH\`" >> $GITHUB_STEP_SUMMARY fi Shared (cross-cutting)· Reusable workflow ·on: workflow_call
Shared Delete Branch
Shared - Delete Merged Branch
.github/workflows/shared-delete-branch.yml