Skip to content

Actions cleanup

Actions cleanup #6

name: Actions cleanup
on:
workflow_run:
workflows:
- "Build Office MCP Windows Executable"
types: [completed]
workflow_dispatch:
permissions:
actions: write
contents: read
jobs:
cleanup:
name: Prune old Actions artifacts, caches, and runs
if: >-
github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- name: Delete stale artifacts, branch-scoped caches, and old completed runs
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
async function retry(label, fn) {
let lastError;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
core.warning(`${label} failed on attempt ${attempt}: ${error.message}`);
if (attempt < 3) {
await new Promise(resolve => setTimeout(resolve, attempt * 2000));
}
}
}
throw lastError;
}
const repoInfo = await github.rest.repos.get({ owner, repo });
const defaultBranch = repoInfo.data.default_branch || 'main';
const reusableCacheRefs = new Set([
`refs/heads/${defaultBranch}`,
'refs/heads/main',
'refs/heads/master',
]);
const runs = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, {
owner,
repo,
per_page: 100,
status: 'completed',
});
runs.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
const retentionRuns = runs.filter(run => run.name !== 'Actions cleanup');
const keepRunIds = new Set(retentionRuns.slice(0, 10).map(run => run.id));
const artifacts = await github.paginate(github.rest.actions.listArtifactsForRepo, {
owner,
repo,
per_page: 100,
});
artifacts.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
const keepArtifactIds = new Set(artifacts.slice(0, 10).map(artifact => artifact.id));
let artifactCount = 0;
for (const artifact of artifacts) {
const created = new Date(artifact.created_at).getTime();
const runId = artifact.workflow_run && artifact.workflow_run.id;
const beyondRunWindow = runId ? !keepRunIds.has(runId) : !keepArtifactIds.has(artifact.id);
if (artifact.expired || created < cutoff || beyondRunWindow) {
await retry(`delete artifact ${artifact.id}`, () =>
github.rest.actions.deleteArtifact({ owner, repo, artifact_id: artifact.id })
);
artifactCount++;
}
}
const caches = await github.paginate(github.rest.actions.getActionsCacheList, {
owner,
repo,
per_page: 100,
});
let cacheCount = 0;
for (const cache of caches) {
const lastUsed = new Date(cache.last_accessed_at || cache.created_at).getTime();
const branchScoped = cache.ref && cache.ref.startsWith('refs/heads/') && !reusableCacheRefs.has(cache.ref);
if (lastUsed < cutoff || branchScoped) {
await retry(`delete cache ${cache.id}`, () =>
github.rest.actions.deleteActionsCacheById({ owner, repo, cache_id: cache.id })
);
cacheCount++;
}
}
let runCount = 0;
for (const run of runs) {
if (run.id === context.runId) continue;
const created = new Date(run.created_at).getTime();
if (created < cutoff) {
try {
await retry(`delete workflow run ${run.id}`, () =>
github.rest.actions.deleteWorkflowRun({ owner, repo, run_id: run.id })
);
runCount++;
} catch (error) {
core.warning(`Could not delete workflow run ${run.id} after retries: ${error.message}`);
}
}
}
core.notice(`Deleted ${artifactCount} artifacts, ${cacheCount} caches, and ${runCount} completed workflow runs.`);