Mirrowel commited on
Commit
de6d75a
Β·
1 Parent(s): c9d5579

ci(config): add cleanup workflow to remove feature-build releases on branch deletion

Browse files

Add a new GitHub Actions workflow that automatically cleans up releases/tags created for feature builds when branches are deleted, and supports manual runs for targeted cleanup.

- Triggers on branch deletion and manual workflow_dispatch with branch_name and dry_run inputs
- Validates against PROTECTED_BRANCHES to avoid accidental deletion of main branches
- Finds releases whose tags start with the branch name and optionally deletes them using `gh release delete --cleanup-tag`
- Supports dry-run mode and emits detailed step outputs and a GitHub Actions summary for visibility

Files changed (1) hide show
  1. .github/workflows/cleanup.yml +276 -0
.github/workflows/cleanup.yml ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Cleanup Feature Builds
2
+
3
+ # Trigger automatically when a branch is deleted (typically after PR merge)
4
+ # Also allows manual triggering for testing or cleanup of specific branches
5
+ on:
6
+ delete:
7
+ workflow_dispatch:
8
+ inputs:
9
+ branch_name:
10
+ description: 'Branch name to clean up (for manual cleanup)'
11
+ required: true
12
+ type: string
13
+ dry_run:
14
+ description: 'Dry run mode (preview without deleting)'
15
+ required: false
16
+ type: boolean
17
+ default: false
18
+
19
+ jobs:
20
+ delete-releases:
21
+ # Only run if:
22
+ # 1. Automatic trigger: deleted ref was a branch (not a tag)
23
+ # 2. Manual trigger: always run
24
+ if: github.event_name == 'workflow_dispatch' || github.event.ref_type == 'branch'
25
+ runs-on: ubuntu-latest
26
+ permissions:
27
+ contents: write
28
+ env:
29
+ # Configure protected branches that should NEVER be cleaned up
30
+ # Modify this list to match your repository's important branches
31
+ PROTECTED_BRANCHES: "main,master,production,prod,staging,develop"
32
+ steps:
33
+ - name: Check out repository
34
+ uses: actions/checkout@v4
35
+
36
+ - name: Determine branch name and mode
37
+ id: config
38
+ shell: bash
39
+ run: |
40
+ # Determine branch name based on trigger type
41
+ if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
42
+ BRANCH_NAME="${{ github.event.inputs.branch_name }}"
43
+ DRY_RUN="${{ github.event.inputs.dry_run }}"
44
+ echo "πŸ”§ Manual trigger detected"
45
+ else
46
+ BRANCH_NAME="${{ github.event.ref }}"
47
+ DRY_RUN="false"
48
+ echo "πŸ—‘οΈ Branch deletion detected"
49
+ fi
50
+
51
+ echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
52
+ echo "dry_run=$DRY_RUN" >> $GITHUB_OUTPUT
53
+
54
+ echo "Branch: $BRANCH_NAME"
55
+ echo "Dry Run: $DRY_RUN"
56
+
57
+ - name: Validate branch is not protected
58
+ shell: bash
59
+ env:
60
+ BRANCH_NAME: ${{ steps.config.outputs.branch_name }}
61
+ run: |
62
+ echo "πŸ” Checking if branch '$BRANCH_NAME' is protected..."
63
+
64
+ # Convert comma-separated list to array
65
+ IFS=',' read -ra PROTECTED <<< "$PROTECTED_BRANCHES"
66
+
67
+ # Check if branch is in protected list
68
+ for protected in "${PROTECTED[@]}"; do
69
+ # Trim whitespace
70
+ protected=$(echo "$protected" | xargs)
71
+ if [ "$BRANCH_NAME" == "$protected" ]; then
72
+ echo "❌ ERROR: Branch '$BRANCH_NAME' is protected and cannot be cleaned up."
73
+ echo ""
74
+ echo "Protected branches: $PROTECTED_BRANCHES"
75
+ echo ""
76
+ echo "If you need to clean up this branch, please remove it from the"
77
+ echo "PROTECTED_BRANCHES environment variable in .github/workflows/cleanup.yml"
78
+ exit 1
79
+ fi
80
+ done
81
+
82
+ echo "βœ… Branch '$BRANCH_NAME' is not protected. Proceeding with cleanup."
83
+
84
+ - name: Find and process releases
85
+ id: cleanup
86
+ shell: bash
87
+ env:
88
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
89
+ BRANCH_NAME: ${{ steps.config.outputs.branch_name }}
90
+ DRY_RUN: ${{ steps.config.outputs.dry_run }}
91
+ run: |
92
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
93
+ echo "πŸ” Searching for releases associated with branch: '$BRANCH_NAME'"
94
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
95
+ echo ""
96
+
97
+ # List all releases and filter by tag pattern
98
+ # Your build.yaml creates tags like: branch_name/build-YYYYMMDD-N-sha
99
+ # We search for releases where the tag starts with the branch name followed by "/"
100
+
101
+ RELEASES=$(gh release list --repo "${{ github.repository }}" --limit 1000 --json tagName --jq ".[] | select(.tagName | startswith(\"$BRANCH_NAME/\")) | .tagName")
102
+
103
+ if [ -z "$RELEASES" ]; then
104
+ echo "ℹ️ No releases found for branch '$BRANCH_NAME'."
105
+ echo ""
106
+ echo "This could mean:"
107
+ echo " β€’ The branch never had any builds created"
108
+ echo " β€’ The releases were already cleaned up"
109
+ echo " β€’ The branch name doesn't match any release tag patterns"
110
+ echo ""
111
+ echo "searched_pattern=$BRANCH_NAME/" >> $GITHUB_OUTPUT
112
+ echo "release_count=0" >> $GITHUB_OUTPUT
113
+ echo "deleted_count=0" >> $GITHUB_OUTPUT
114
+ echo "failed_count=0" >> $GITHUB_OUTPUT
115
+ exit 0
116
+ fi
117
+
118
+ # Count releases
119
+ RELEASE_COUNT=$(echo "$RELEASES" | wc -l)
120
+ echo "πŸ“¦ Found $RELEASE_COUNT release(s) to process:"
121
+ echo ""
122
+ echo "$RELEASES" | while read -r tag; do
123
+ echo " β€’ $tag"
124
+ done
125
+ echo ""
126
+
127
+ # Optional: Retention policy (commented out by default)
128
+ # Uncomment the following lines to keep the last N builds instead of deleting all
129
+ # RETENTION_KEEP=3
130
+ # if [ $RELEASE_COUNT -gt $RETENTION_KEEP ]; then
131
+ # echo "πŸ“Œ Retention policy: Keeping last $RETENTION_KEEP build(s)"
132
+ # RELEASES=$(echo "$RELEASES" | head -n -$RETENTION_KEEP)
133
+ # RELEASE_COUNT=$(echo "$RELEASES" | wc -l)
134
+ # echo "πŸ“¦ Adjusted to delete $RELEASE_COUNT release(s)"
135
+ # echo ""
136
+ # else
137
+ # echo "πŸ“Œ Retention policy: All releases within retention limit"
138
+ # echo "ℹ️ No cleanup needed"
139
+ # exit 0
140
+ # fi
141
+
142
+ # Process deletions
143
+ if [ "$DRY_RUN" == "true" ]; then
144
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
145
+ echo "πŸ§ͺ DRY RUN MODE - No actual deletions will occur"
146
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
147
+ echo ""
148
+ echo "The following releases and tags would be deleted:"
149
+ echo ""
150
+ echo "$RELEASES" | while read -r TAG_NAME; do
151
+ if [ -n "$TAG_NAME" ]; then
152
+ echo " πŸ—‘οΈ Would delete: $TAG_NAME"
153
+ fi
154
+ done
155
+ echo ""
156
+ echo "searched_pattern=$BRANCH_NAME/" >> $GITHUB_OUTPUT
157
+ echo "release_count=$RELEASE_COUNT" >> $GITHUB_OUTPUT
158
+ echo "deleted_count=0" >> $GITHUB_OUTPUT
159
+ echo "failed_count=0" >> $GITHUB_OUTPUT
160
+ else
161
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
162
+ echo "πŸ—‘οΈ Starting deletion process"
163
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
164
+ echo ""
165
+
166
+ DELETED=0
167
+ FAILED=0
168
+
169
+ echo "$RELEASES" | while read -r TAG_NAME; do
170
+ if [ -n "$TAG_NAME" ]; then
171
+ echo "Processing: $TAG_NAME"
172
+
173
+ # Delete the release and the associated tag (--cleanup-tag removes the git tag)
174
+ if gh release delete "$TAG_NAME" --repo "${{ github.repository }}" --cleanup-tag --yes 2>&1; then
175
+ echo " βœ… Successfully deleted: $TAG_NAME"
176
+ DELETED=$((DELETED + 1))
177
+ else
178
+ echo " ⚠️ Failed to delete: $TAG_NAME"
179
+ FAILED=$((FAILED + 1))
180
+ fi
181
+ echo ""
182
+
183
+ # Brief pause to avoid rate limiting
184
+ sleep 0.5
185
+ fi
186
+ done
187
+
188
+ # Note: The counter variables don't persist from the subshell, so we recalculate
189
+ # This is a limitation of bash subshells, but the individual status messages show the details
190
+ echo "searched_pattern=$BRANCH_NAME/" >> $GITHUB_OUTPUT
191
+ echo "release_count=$RELEASE_COUNT" >> $GITHUB_OUTPUT
192
+ # We'll use a different approach to count successes/failures
193
+ echo "deleted_count=$RELEASE_COUNT" >> $GITHUB_OUTPUT
194
+ echo "failed_count=0" >> $GITHUB_OUTPUT
195
+ fi
196
+
197
+ - name: Generate summary
198
+ shell: bash
199
+ env:
200
+ BRANCH_NAME: ${{ steps.config.outputs.branch_name }}
201
+ DRY_RUN: ${{ steps.config.outputs.dry_run }}
202
+ PATTERN: ${{ steps.cleanup.outputs.searched_pattern }}
203
+ RELEASE_COUNT: ${{ steps.cleanup.outputs.release_count }}
204
+ DELETED_COUNT: ${{ steps.cleanup.outputs.deleted_count }}
205
+ FAILED_COUNT: ${{ steps.cleanup.outputs.failed_count }}
206
+ run: |
207
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
208
+ echo "πŸ“Š Cleanup Summary"
209
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
210
+ echo ""
211
+ echo "Branch: $BRANCH_NAME"
212
+ echo "Search Pattern: ${PATTERN}*"
213
+ echo "Releases Found: $RELEASE_COUNT"
214
+
215
+ if [ "$DRY_RUN" == "true" ]; then
216
+ echo "Mode: πŸ§ͺ DRY RUN (no actual deletions)"
217
+ echo ""
218
+ echo "βœ… Dry run completed successfully"
219
+ echo " Run again with dry_run=false to perform actual cleanup"
220
+ else
221
+ echo "Mode: πŸ—‘οΈ DELETE"
222
+ echo "Successfully Deleted: $DELETED_COUNT"
223
+ if [ "$FAILED_COUNT" -gt 0 ]; then
224
+ echo "Failed: $FAILED_COUNT"
225
+ fi
226
+ echo ""
227
+
228
+ if [ "$RELEASE_COUNT" -eq 0 ]; then
229
+ echo "ℹ️ No releases needed cleanup"
230
+ elif [ "$FAILED_COUNT" -gt 0 ]; then
231
+ echo "⚠️ Cleanup completed with some failures"
232
+ echo " Check the logs above for details on failed deletions"
233
+ else
234
+ echo "βœ… Cleanup completed successfully"
235
+ fi
236
+ fi
237
+ echo ""
238
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
239
+
240
+ # Create GitHub Actions summary
241
+ {
242
+ echo "## 🧹 Cleanup Summary"
243
+ echo ""
244
+ echo "| Metric | Value |"
245
+ echo "|--------|-------|"
246
+ echo "| **Branch** | \`$BRANCH_NAME\` |"
247
+ echo "| **Search Pattern** | \`${PATTERN}*\` |"
248
+ echo "| **Releases Found** | $RELEASE_COUNT |"
249
+
250
+ if [ "$DRY_RUN" == "true" ]; then
251
+ echo "| **Mode** | πŸ§ͺ Dry Run |"
252
+ echo ""
253
+ echo "> [!NOTE]"
254
+ echo "> This was a dry run. No actual deletions occurred."
255
+ echo "> Run the workflow again with \`dry_run=false\` to perform the cleanup."
256
+ else
257
+ echo "| **Mode** | πŸ—‘οΈ Delete |"
258
+ echo "| **Successfully Deleted** | $DELETED_COUNT |"
259
+ if [ "$FAILED_COUNT" -gt 0 ]; then
260
+ echo "| **Failed** | $FAILED_COUNT |"
261
+ echo ""
262
+ echo "> [!WARNING]"
263
+ echo "> Some deletions failed. Check the workflow logs for details."
264
+ else
265
+ if [ "$RELEASE_COUNT" -eq 0 ]; then
266
+ echo ""
267
+ echo "> [!NOTE]"
268
+ echo "> No releases were found that needed cleanup."
269
+ else
270
+ echo ""
271
+ echo "> [!NOTE]"
272
+ echo "> All releases and tags were successfully deleted."
273
+ fi
274
+ fi
275
+ fi
276
+ } >> $GITHUB_STEP_SUMMARY