Branch Cleanup #12
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: Branch Cleanup | |
| on: | |
| pull_request: | |
| types: [closed] | |
| schedule: | |
| - cron: '0 2 * * 0' # Weekly cleanup on Sundays at 2 AM | |
| jobs: | |
| cleanup-merged-branches: | |
| runs-on: ubuntu-latest | |
| if: github.event.pull_request.merged == true | |
| permissions: | |
| contents: write | |
| pull-requests: read | |
| steps: | |
| - name: Delete merged branch | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const branchName = context.payload.pull_request.head.ref; | |
| const isFromFork = context.payload.pull_request.head.repo.fork; | |
| // Only delete branches from main repo, not forks | |
| if (!isFromFork && !branchName.includes('main') && !branchName.includes('master')) { | |
| try { | |
| // Check if branch has protection rules | |
| let hasProtection = false; | |
| try { | |
| await github.rest.repos.getBranchProtection({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| branch: branchName | |
| }); | |
| hasProtection = true; | |
| console.log(`⚠️ Branch ${branchName} has protection rules, skipping deletion`); | |
| } catch (protectionError) { | |
| // No protection rules, safe to delete | |
| console.log(`Branch ${branchName} has no protection rules`); | |
| } | |
| if (!hasProtection) { | |
| await github.rest.git.deleteRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: `heads/${branchName}` | |
| }); | |
| console.log(`✅ Deleted merged branch: ${branchName}`); | |
| } | |
| } catch (error) { | |
| console.log(`❌ Failed to delete branch ${branchName}: ${error.message}`); | |
| // If deletion fails due to permissions, suggest manual cleanup | |
| if (error.message.includes('Resource not accessible')) { | |
| console.log(`💡 Manual cleanup required for branch ${branchName}`); | |
| console.log('This may be due to branch protection rules or insufficient permissions.'); | |
| } | |
| } | |
| } else if (isFromFork) { | |
| console.log(`🔀 Skipping fork branch: ${branchName}`); | |
| } else { | |
| console.log(`🔒 Skipping protected branch: ${branchName}`); | |
| } | |
| cleanup-stale-branches: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'schedule' | |
| permissions: | |
| contents: write | |
| pull-requests: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Cleanup stale branches | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const { execSync } = require('child_process'); | |
| // Get branches older than 30 days with no recent activity | |
| const cutoffDate = Math.floor(Date.now() / 1000) - (30 * 24 * 60 * 60); | |
| try { | |
| const branchOutput = execSync('git for-each-ref --format="%(refname:short) %(committerdate:unix)" refs/remotes/origin/', { encoding: 'utf8' }); | |
| const branches = branchOutput.trim().split('\n').filter(line => line.trim()); | |
| for (const line of branches) { | |
| const [fullBranch, timestamp] = line.split(' '); | |
| const branchName = fullBranch.replace('origin/', ''); | |
| // Skip protected branches | |
| if (['main', 'master'].includes(branchName) || branchName.startsWith('release/')) { | |
| continue; | |
| } | |
| if (parseInt(timestamp) < cutoffDate) { | |
| const lastActivity = new Date(parseInt(timestamp) * 1000).toLocaleDateString(); | |
| console.log(`🗑️ Stale branch found: ${branchName} (last activity: ${lastActivity})`); | |
| // Check if branch has open PR | |
| try { | |
| const prs = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| head: `${context.repo.owner}:${branchName}`, | |
| state: 'open' | |
| }); | |
| if (prs.data.length === 0) { | |
| // Check for branch protection | |
| let hasProtection = false; | |
| try { | |
| await github.rest.repos.getBranchProtection({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| branch: branchName | |
| }); | |
| hasProtection = true; | |
| console.log(`⚠️ Branch ${branchName} has protection rules, skipping deletion`); | |
| } catch (protectionError) { | |
| // No protection rules | |
| } | |
| if (!hasProtection) { | |
| try { | |
| await github.rest.git.deleteRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: `heads/${branchName}` | |
| }); | |
| console.log(`✅ Deleted stale branch: ${branchName}`); | |
| } catch (deleteError) { | |
| console.log(`❌ Failed to delete ${branchName}: ${deleteError.message}`); | |
| } | |
| } | |
| } else { | |
| console.log(`Branch ${branchName} has open PR, skipping deletion`); | |
| } | |
| } catch (error) { | |
| console.log(`Error checking PRs for ${branchName}: ${error.message}`); | |
| } | |
| } | |
| } | |
| } catch (error) { | |
| console.log('Error during stale branch cleanup:', error.message); | |
| } |