Skip to content

Commit 79201d1

Browse files
committed
chore: update git configuration and documentation
Update CLAUDE.md with proper git configuration requirements ensuring all commits use josedacosta/contact@josedacosta.info
1 parent de78a1b commit 79201d1

2 files changed

Lines changed: 343 additions & 336 deletions

File tree

scripts/git/cleanup-actions.ts

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
#!/usr/bin/env tsx
2+
3+
/**
4+
* Clean up GitHub Actions workflow runs that are not successful
5+
* This script removes all workflow runs with status other than 'success'
6+
*
7+
* Usage: tsx scripts/git/cleanup-workflow-runs.ts [options]
8+
* Options:
9+
* --dry-run Show what would be deleted without actually deleting
10+
* --keep-last Keep the most recent run for each workflow regardless of status
11+
* --verbose Show detailed output
12+
* --workflow Filter by specific workflow name or ID
13+
* --max-age Only delete runs older than N days (e.g., --max-age 30)
14+
*/
15+
16+
/**
17+
* Clean up GitHub Actions workflow runs that are not successful
18+
* This script removes all workflow runs with status other than 'success'
19+
*
20+
* Usage: tsx scripts/git/cleanup-workflow-runs.ts [options]
21+
* Options:
22+
* --dry-run Show what would be deleted without actually deleting
23+
* --keep-last Keep the most recent run for each workflow regardless of status
24+
* --verbose Show detailed output
25+
* --workflow Filter by specific workflow name or ID
26+
* --max-age Only delete runs older than N days (e.g., --max-age 30)
27+
*/
28+
import { execSync } from 'child_process';
29+
30+
interface WorkflowRun {
31+
id: number;
32+
name: string;
33+
workflow_id: number;
34+
status: string;
35+
conclusion: string | null;
36+
created_at: string;
37+
updated_at: string;
38+
html_url: string;
39+
run_number: number;
40+
event: string;
41+
}
42+
43+
interface Workflow {
44+
id: number;
45+
name: string;
46+
path: string;
47+
state: string;
48+
}
49+
50+
// Parse command line arguments
51+
const args = process.argv.slice(2);
52+
const options = {
53+
dryRun: args.includes('--dry-run'),
54+
keepLast: args.includes('--keep-last'),
55+
verbose: args.includes('--verbose'),
56+
workflow: args.find((arg, i) => args[i - 1] === '--workflow') || null,
57+
maxAge: parseInt(args.find((arg, i) => args[i - 1] === '--max-age') || '0'),
58+
};
59+
60+
// Colors for terminal output
61+
const colors = {
62+
reset: '\x1b[0m',
63+
bright: '\x1b[1m',
64+
dim: '\x1b[2m',
65+
red: '\x1b[31m',
66+
green: '\x1b[32m',
67+
yellow: '\x1b[33m',
68+
blue: '\x1b[34m',
69+
magenta: '\x1b[35m',
70+
cyan: '\x1b[36m',
71+
gray: '\x1b[90m',
72+
};
73+
74+
// Status emojis
75+
const statusEmojis: Record<string, string> = {
76+
success: '✅',
77+
failure: '❌',
78+
cancelled: '🚫',
79+
skipped: '⏭️',
80+
timed_out: '⏱️',
81+
action_required: '⚠️',
82+
stale: '📅',
83+
neutral: '⚪',
84+
startup_failure: '🔥',
85+
};
86+
87+
function log(message: string, color: string = colors.reset) {
88+
console.log(`${color}${message}${colors.reset}`);
89+
}
90+
91+
function verbose(message: string) {
92+
if (options.verbose) {
93+
log(message, colors.gray);
94+
}
95+
}
96+
97+
function formatDate(dateString: string): string {
98+
const date = new Date(dateString);
99+
return date.toLocaleString('en-US', {
100+
year: 'numeric',
101+
month: 'short',
102+
day: 'numeric',
103+
hour: '2-digit',
104+
minute: '2-digit',
105+
});
106+
}
107+
108+
function getDaysOld(dateString: string): number {
109+
const date = new Date(dateString);
110+
const now = new Date();
111+
const diffTime = Math.abs(now.getTime() - date.getTime());
112+
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
113+
}
114+
115+
function executeCommand(command: string): string {
116+
try {
117+
return execSync(command, { encoding: 'utf-8' }).trim();
118+
} catch (error: any) {
119+
if (error.status !== 0) {
120+
throw new Error(`Command failed: ${command}\n${error.message}`);
121+
}
122+
return '';
123+
}
124+
}
125+
126+
async function getWorkflows(): Promise<Workflow[]> {
127+
try {
128+
verbose('Fetching workflows...');
129+
const result = executeCommand('gh api repos/josedacosta/mcp-jetbrains-code-inspections/actions/workflows --paginate');
130+
const data = JSON.parse(result);
131+
return data.workflows || [];
132+
} catch (error) {
133+
log('Error fetching workflows', colors.red);
134+
throw error;
135+
}
136+
}
137+
138+
async function getWorkflowRuns(workflowId?: string): Promise<WorkflowRun[]> {
139+
try {
140+
verbose(`Fetching workflow runs${workflowId ? ` for workflow ${workflowId}` : ''}...`);
141+
142+
let endpoint = 'gh api repos/josedacosta/mcp-jetbrains-code-inspections/actions/runs';
143+
144+
// Add workflow filter if specified
145+
if (workflowId) {
146+
// Check if it's a numeric ID or a name
147+
if (/^\d+$/.test(workflowId)) {
148+
endpoint = `gh api repos/josedacosta/mcp-jetbrains-code-inspections/actions/workflows/${workflowId}/runs`;
149+
} else {
150+
// For workflow names, we'll filter after fetching
151+
endpoint = 'gh api repos/josedacosta/mcp-jetbrains-code-inspections/actions/runs';
152+
}
153+
}
154+
155+
endpoint += ' --paginate';
156+
157+
const result = executeCommand(endpoint);
158+
const data = JSON.parse(result);
159+
let runs = data.workflow_runs || [];
160+
161+
// Filter by workflow name if specified and not a numeric ID
162+
if (workflowId && !/^\d+$/.test(workflowId)) {
163+
runs = runs.filter((run: WorkflowRun) => run.name.toLowerCase().includes(workflowId.toLowerCase()));
164+
}
165+
166+
return runs;
167+
} catch (error) {
168+
log('Error fetching workflow runs', colors.red);
169+
throw error;
170+
}
171+
}
172+
173+
async function deleteWorkflowRun(runId: number): Promise<boolean> {
174+
if (options.dryRun) {
175+
log(` [DRY RUN] Would delete workflow run ${runId}`, colors.cyan);
176+
return true;
177+
}
178+
179+
try {
180+
executeCommand(`gh api -X DELETE repos/josedacosta/mcp-jetbrains-code-inspections/actions/runs/${runId}`);
181+
return true;
182+
} catch (error) {
183+
log(` Failed to delete workflow run ${runId}`, colors.red);
184+
verbose(` Error: ${error}`);
185+
return false;
186+
}
187+
}
188+
189+
async function main() {
190+
log('\n🧹 GitHub Actions Workflow Runs Cleanup Script', colors.bright);
191+
log('='.repeat(50), colors.gray);
192+
193+
if (options.dryRun) {
194+
log('🔍 Running in DRY RUN mode (no changes will be made)', colors.yellow);
195+
}
196+
197+
if (options.workflow) {
198+
log(`📋 Filtering by workflow: ${options.workflow}`, colors.blue);
199+
}
200+
201+
if (options.maxAge > 0) {
202+
log(`📅 Only deleting runs older than ${options.maxAge} days`, colors.blue);
203+
}
204+
205+
try {
206+
// Check if gh CLI is installed and authenticated
207+
try {
208+
executeCommand('gh auth status');
209+
} catch (error) {
210+
log('\n❌ GitHub CLI is not authenticated', colors.red);
211+
log(' Run: gh auth login', colors.yellow);
212+
process.exit(1);
213+
}
214+
215+
// Get all workflow runs
216+
const runs = await getWorkflowRuns(options.workflow || undefined);
217+
218+
if (runs.length === 0) {
219+
log('\n✅ No workflow runs found', colors.green);
220+
return;
221+
}
222+
223+
log(`\n📊 Found ${runs.length} workflow run(s)`, colors.blue);
224+
225+
// Group runs by workflow for better organization
226+
const runsByWorkflow = runs.reduce((acc: Record<string, WorkflowRun[]>, run) => {
227+
if (!acc[run.name]) {
228+
acc[run.name] = [];
229+
}
230+
acc[run.name].push(run);
231+
return acc;
232+
}, {});
233+
234+
// Statistics
235+
let successCount = 0;
236+
let deletedCount = 0;
237+
let failedCount = 0;
238+
let skippedCount = 0;
239+
let tooRecentCount = 0;
240+
241+
// Process each workflow's runs
242+
for (const [workflowName, workflowRuns] of Object.entries(runsByWorkflow)) {
243+
// Sort runs by created_at descending (newest first)
244+
workflowRuns.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
245+
246+
log(`\n📁 ${colors.bright}${workflowName}${colors.reset} (${workflowRuns.length} runs)`, colors.blue);
247+
248+
for (let i = 0; i < workflowRuns.length; i++) {
249+
const run = workflowRuns[i];
250+
const isLast = i === 0; // First in sorted array is the most recent
251+
const daysOld = getDaysOld(run.created_at);
252+
const emoji = statusEmojis[run.conclusion || 'neutral'] || '❓';
253+
254+
verbose(`\nChecking run #${run.run_number} (ID: ${run.id})...`);
255+
256+
// Skip the last run if keepLast option is set
257+
if (isLast && options.keepLast) {
258+
log(
259+
` ${emoji} #${run.run_number} - ${colors.yellow}KEPT${colors.reset} (most recent) - ${run.conclusion || run.status}`,
260+
colors.gray,
261+
);
262+
skippedCount++;
263+
continue;
264+
}
265+
266+
// Skip if run is too recent based on max-age
267+
if (options.maxAge > 0 && daysOld < options.maxAge) {
268+
log(
269+
` ${emoji} #${run.run_number} - ${colors.cyan}SKIPPED${colors.reset} (${daysOld} days old) - ${run.conclusion || run.status}`,
270+
colors.gray,
271+
);
272+
tooRecentCount++;
273+
continue;
274+
}
275+
276+
// Format run info
277+
const runInfo = `#${run.run_number} [${run.event}] - ${formatDate(run.created_at)} (${daysOld} days ago)`;
278+
279+
if (run.conclusion === 'success') {
280+
log(` ${emoji} ${runInfo} - ${colors.green}SUCCESS${colors.reset}`, colors.dim);
281+
successCount++;
282+
} else {
283+
const statusColor = run.conclusion === 'failure' ? colors.red : run.conclusion === 'cancelled' ? colors.yellow : colors.gray;
284+
285+
log(` ${emoji} ${runInfo} - ${statusColor}${(run.conclusion || run.status).toUpperCase()}${colors.reset}`);
286+
287+
// Delete non-successful run
288+
const deleted = await deleteWorkflowRun(run.id);
289+
290+
if (deleted) {
291+
if (!options.dryRun) {
292+
log(` ✓ Deleted successfully`, colors.green);
293+
}
294+
deletedCount++;
295+
} else {
296+
failedCount++;
297+
}
298+
}
299+
}
300+
}
301+
302+
// Print summary
303+
log('\n' + '='.repeat(50), colors.gray);
304+
log('📈 Summary:', colors.bright);
305+
log(` • Total workflow runs: ${runs.length}`, colors.blue);
306+
log(` • Successful: ${successCount}`, colors.green);
307+
log(` • Deleted: ${deletedCount}`, colors.yellow);
308+
309+
if (failedCount > 0) {
310+
log(` • Failed to delete: ${failedCount}`, colors.red);
311+
}
312+
313+
if (skippedCount > 0) {
314+
log(` • Kept (most recent): ${skippedCount}`, colors.cyan);
315+
}
316+
317+
if (tooRecentCount > 0) {
318+
log(` • Too recent: ${tooRecentCount}`, colors.cyan);
319+
}
320+
321+
if (options.dryRun) {
322+
log('\n💡 This was a dry run. Run without --dry-run to actually delete workflow runs', colors.yellow);
323+
} else if (deletedCount > 0) {
324+
log('\n✨ Cleanup completed successfully!', colors.green);
325+
} else {
326+
log('\n✨ No workflow runs needed cleanup', colors.green);
327+
}
328+
329+
// Show link to Actions page
330+
log(`\n🔗 View remaining runs: ${colors.cyan}https://github.com/josedacosta/mcp-jetbrains-code-inspections/actions${colors.reset}`);
331+
} catch (error: any) {
332+
log('\n❌ An error occurred:', colors.red);
333+
log(error.message, colors.red);
334+
process.exit(1);
335+
}
336+
}
337+
338+
// Run the script
339+
main().catch((error) => {
340+
log('Unexpected error:', colors.red);
341+
console.error(error);
342+
process.exit(1);
343+
});

0 commit comments

Comments
 (0)