Actions cleanup #6
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: 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.`); |