Skip to content

Commit a537498

Browse files
authored
Add housekeeping workflow for automated tag/release cleanup (#172)
1 parent 85ca400 commit a537498

File tree

1 file changed

+220
-0
lines changed

1 file changed

+220
-0
lines changed

.github/workflows/housekeeping.yml

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
name: Housekeeping - Cleanup Old Releases
2+
3+
on:
4+
schedule:
5+
# Run every Sunday at 2 AM UTC
6+
- cron: '0 2 * * 0'
7+
workflow_dispatch: # Allow manual triggering
8+
9+
permissions:
10+
contents: write # Required to delete tags
11+
12+
jobs:
13+
cleanup-releases:
14+
name: Cleanup Old Tags and Releases
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0 # Fetch all history for all tags
22+
23+
- name: Setup GitHub CLI
24+
run: |
25+
type -p gh >/dev/null || (echo "GitHub CLI already installed" && gh --version)
26+
27+
- name: Cleanup old releases and tags
28+
env:
29+
GH_TOKEN: ${{ github.token }}
30+
run: |
31+
echo "🧹 Starting housekeeping - cleanup old tags and releases"
32+
echo "========================================================"
33+
echo ""
34+
35+
KEEP_COUNT=3
36+
37+
# Function to get releases from GitHub API
38+
get_github_releases() {
39+
local pattern=$1
40+
gh api repos/${{ github.repository }}/releases \
41+
--paginate \
42+
--jq "[.[] | select(.tag_name | test(\"^${pattern}\")) | {tag: .tag_name, created: .created_at, id: .id}] | sort_by(.created) | reverse"
43+
}
44+
45+
# Function to get tags from GitHub API
46+
get_github_tags() {
47+
local pattern=$1
48+
gh api repos/${{ github.repository }}/tags \
49+
--paginate \
50+
--jq "[.[] | select(.name | test(\"^${pattern}\")) | {tag: .name, commit: .commit.sha}]"
51+
}
52+
53+
# Function to delete release
54+
delete_release() {
55+
local tag=$1
56+
local release_id=$2
57+
echo " 🗑️ Deleting release: $tag (ID: $release_id)"
58+
gh api -X DELETE repos/${{ github.repository }}/releases/$release_id
59+
}
60+
61+
# Function to delete tag
62+
delete_tag() {
63+
local tag=$1
64+
echo " 🗑️ Deleting tag: $tag"
65+
gh api -X DELETE repos/${{ github.repository }}/git/refs/tags/$tag
66+
}
67+
68+
# Process VSCode Extension releases and tags
69+
echo "📦 Processing VS Code Extension (vscode-v*)"
70+
echo "--------------------------------------------"
71+
72+
vscode_releases=$(get_github_releases "vscode-v")
73+
vscode_count=$(echo "$vscode_releases" | jq 'length')
74+
75+
if [ "$vscode_count" -gt 0 ]; then
76+
echo " Found $vscode_count releases"
77+
78+
# Get releases to keep and delete
79+
keep_releases=$(echo "$vscode_releases" | jq -r ".[0:$KEEP_COUNT] | .[] | .tag")
80+
delete_releases=$(echo "$vscode_releases" | jq -r ".[$KEEP_COUNT:] | .[] | {tag: .tag, id: .id} | @json")
81+
82+
echo " ✅ Keeping $(echo "$keep_releases" | wc -l) most recent:"
83+
echo "$keep_releases" | sed 's/^/ - /'
84+
85+
delete_count=$(echo "$delete_releases" | grep -c "^{" || echo "0")
86+
if [ "$delete_count" -gt 0 ]; then
87+
echo " ❌ Deleting $delete_count old releases:"
88+
89+
while IFS= read -r release_json; do
90+
if [ -n "$release_json" ]; then
91+
tag=$(echo "$release_json" | jq -r '.tag')
92+
id=$(echo "$release_json" | jq -r '.id')
93+
echo " - $tag"
94+
delete_release "$tag" "$id"
95+
delete_tag "$tag"
96+
fi
97+
done <<< "$delete_releases"
98+
else
99+
echo " ✅ No old releases to delete"
100+
fi
101+
else
102+
echo " ℹ️ No vscode-v* releases found"
103+
fi
104+
105+
echo ""
106+
107+
# Process MCP Server & CLI releases and tags (excluding vscode-v*)
108+
echo "📦 Processing MCP Server & CLI (v* excluding vscode-v*)"
109+
echo "--------------------------------------------------------"
110+
111+
# Get all v* releases
112+
all_v_releases=$(get_github_releases "v")
113+
# Filter out vscode-v* releases
114+
mcp_releases=$(echo "$all_v_releases" | jq '[.[] | select(.tag | test("^vscode-v") | not)]')
115+
mcp_count=$(echo "$mcp_releases" | jq 'length')
116+
117+
if [ "$mcp_count" -gt 0 ]; then
118+
echo " Found $mcp_count releases"
119+
120+
# Get releases to keep and delete
121+
keep_releases=$(echo "$mcp_releases" | jq -r ".[0:$KEEP_COUNT] | .[] | .tag")
122+
delete_releases=$(echo "$mcp_releases" | jq -r ".[$KEEP_COUNT:] | .[] | {tag: .tag, id: .id} | @json")
123+
124+
echo " ✅ Keeping $(echo "$keep_releases" | wc -l) most recent:"
125+
echo "$keep_releases" | sed 's/^/ - /'
126+
127+
delete_count=$(echo "$delete_releases" | grep -c "^{" || echo "0")
128+
if [ "$delete_count" -gt 0 ]; then
129+
echo " ❌ Deleting $delete_count old releases:"
130+
131+
while IFS= read -r release_json; do
132+
if [ -n "$release_json" ]; then
133+
tag=$(echo "$release_json" | jq -r '.tag')
134+
id=$(echo "$release_json" | jq -r '.id')
135+
echo " - $tag"
136+
delete_release "$tag" "$id"
137+
delete_tag "$tag"
138+
fi
139+
done <<< "$delete_releases"
140+
else
141+
echo " ✅ No old releases to delete"
142+
fi
143+
else
144+
echo " ℹ️ No v* releases found (excluding vscode-v*)"
145+
fi
146+
147+
echo ""
148+
149+
# Cleanup orphaned tags (tags without releases)
150+
echo "🏷️ Checking for orphaned tags (tags without releases)"
151+
echo "-------------------------------------------------------"
152+
153+
# Get all tags from GitHub
154+
all_tags=$(gh api repos/${{ github.repository }}/tags --paginate --jq '.[].name')
155+
156+
# Get all release tags
157+
all_release_tags=$(gh api repos/${{ github.repository }}/releases --paginate --jq '.[].tag_name')
158+
159+
# Find orphaned tags
160+
orphaned_tags=$(comm -23 <(echo "$all_tags" | sort) <(echo "$all_release_tags" | sort))
161+
orphaned_count=$(echo "$orphaned_tags" | grep -v '^$' | wc -l)
162+
163+
if [ "$orphaned_count" -gt 0 ]; then
164+
echo " Found $orphaned_count orphaned tags"
165+
166+
# Process vscode-v* orphaned tags
167+
vscode_orphaned=$(echo "$orphaned_tags" | grep "^vscode-v" || echo "")
168+
if [ -n "$vscode_orphaned" ]; then
169+
vscode_orphaned_array=($(echo "$vscode_orphaned"))
170+
vscode_orphaned_count=${#vscode_orphaned_array[@]}
171+
172+
if [ "$vscode_orphaned_count" -gt "$KEEP_COUNT" ]; then
173+
echo " ❌ Deleting $((vscode_orphaned_count - KEEP_COUNT)) old vscode-v* orphaned tags"
174+
# Keep the last N, delete the rest
175+
for ((i=KEEP_COUNT; i<vscode_orphaned_count; i++)); do
176+
tag="${vscode_orphaned_array[$i]}"
177+
echo " - $tag"
178+
delete_tag "$tag"
179+
done
180+
fi
181+
fi
182+
183+
# Process v* orphaned tags (excluding vscode-v*)
184+
mcp_orphaned=$(echo "$orphaned_tags" | grep "^v" | grep -v "^vscode-v" || echo "")
185+
if [ -n "$mcp_orphaned" ]; then
186+
mcp_orphaned_array=($(echo "$mcp_orphaned"))
187+
mcp_orphaned_count=${#mcp_orphaned_array[@]}
188+
189+
if [ "$mcp_orphaned_count" -gt "$KEEP_COUNT" ]; then
190+
echo " ❌ Deleting $((mcp_orphaned_count - KEEP_COUNT)) old v* orphaned tags"
191+
# Keep the last N, delete the rest
192+
for ((i=KEEP_COUNT; i<mcp_orphaned_count; i++)); do
193+
tag="${mcp_orphaned_array[$i]}"
194+
echo " - $tag"
195+
delete_tag "$tag"
196+
done
197+
fi
198+
fi
199+
200+
if [ -z "$vscode_orphaned" ] && [ -z "$mcp_orphaned" ]; then
201+
echo " ℹ️ No v* or vscode-v* orphaned tags found"
202+
fi
203+
else
204+
echo " ✅ No orphaned tags found"
205+
fi
206+
207+
echo ""
208+
echo "✅ Housekeeping complete!"
209+
210+
- name: Summary
211+
if: always()
212+
run: |
213+
echo "## Housekeeping Summary" >> $GITHUB_STEP_SUMMARY
214+
echo "" >> $GITHUB_STEP_SUMMARY
215+
echo "✅ Cleaned up old tags and releases" >> $GITHUB_STEP_SUMMARY
216+
echo "- Kept last 3 vscode-v* releases" >> $GITHUB_STEP_SUMMARY
217+
echo "- Kept last 3 v* releases (MCP/CLI)" >> $GITHUB_STEP_SUMMARY
218+
echo "- Removed orphaned tags" >> $GITHUB_STEP_SUMMARY
219+
echo "" >> $GITHUB_STEP_SUMMARY
220+
echo "📅 Next run: Every Sunday at 2 AM UTC" >> $GITHUB_STEP_SUMMARY

0 commit comments

Comments
 (0)