Artifact cleanup #1
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: Artifact cleanup | |
| # Deletes GitHub Actions artifacts older than a threshold so storage doesn't | |
| # accumulate and trip the account-wide quota. | |
| # | |
| # Three triggers: | |
| # - schedule: Monday 06:00 UTC, same slot as branch-protection-sync, | |
| # security, and dependabot for weekly-governance clustering. | |
| # - workflow_dispatch: on-demand cleanup with inputs for threshold + | |
| # dry-run safety. | |
| # - push: not wired — cleanup doesn't need to fire on code changes. | |
| # | |
| # Uses the default GITHUB_TOKEN (built-in `actions: write` scope is | |
| # sufficient for artifact deletion — no PAT needed). The scheduled run | |
| # uses the hard-coded default (7 days, live delete); manual runs default | |
| # to dry-run for safety. | |
| on: | |
| schedule: | |
| - cron: "0 6 * * 1" | |
| workflow_dispatch: | |
| inputs: | |
| days_to_keep: | |
| description: "Delete artifacts older than this many days" | |
| required: true | |
| default: "7" | |
| dry_run: | |
| description: "List only, no deletion (safe to flip to false when ready)" | |
| type: boolean | |
| default: true | |
| permissions: | |
| actions: write # for artifact deletion | |
| contents: read | |
| concurrency: | |
| group: artifact-cleanup | |
| cancel-in-progress: false | |
| jobs: | |
| cleanup: | |
| name: Prune old artifacts | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Compute threshold | |
| id: threshold | |
| env: | |
| DAYS: ${{ inputs.days_to_keep || '7' }} | |
| run: | | |
| days="${DAYS}" | |
| threshold_secs=$(( days * 86400 )) | |
| echo "days=${days}" >> "$GITHUB_OUTPUT" | |
| echo "secs=${threshold_secs}" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Pruning artifacts older than ${days} day(s)." | |
| - name: List candidates | |
| id: list | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| THRESHOLD_SECS: ${{ steps.threshold.outputs.secs }} | |
| run: | | |
| gh api --paginate \ | |
| "/repos/${GITHUB_REPOSITORY}/actions/artifacts?per_page=100" \ | |
| --jq ".artifacts[] | |
| | select((now - (.created_at | fromdateiso8601)) > ${THRESHOLD_SECS}) | |
| | [.id, .created_at, | |
| ((.size_in_bytes/1024/1024*100|floor)/100|tostring), | |
| (.workflow_run.name // \"<unknown>\"), | |
| .name] | |
| | @tsv" \ | |
| > candidates.tsv | |
| count=$(wc -l < candidates.tsv | tr -d ' ') | |
| echo "count=${count}" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Found ${count} artifact(s) older than ${{ steps.threshold.outputs.days }} day(s)." | |
| - name: Summary (candidates) | |
| if: always() | |
| run: | | |
| { | |
| echo "### Artifact cleanup — dry_run=${{ inputs.dry_run || 'false (scheduled)' }}" | |
| echo "" | |
| echo "**Threshold:** older than ${{ steps.threshold.outputs.days }} day(s)" | |
| echo "**Matched:** ${{ steps.list.outputs.count }} artifact(s)" | |
| echo "" | |
| echo "| id | created_at | size (MB) | workflow | name |" | |
| echo "|---|---|---|---|---|" | |
| awk -F'\t' '{ printf "| %s | %s | %s | %s | %s |\n", $1, $2, $3, $4, $5 }' \ | |
| candidates.tsv || true | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Delete (skipped when dry_run=true) | |
| if: ${{ inputs.dry_run == false || inputs.dry_run == 'false' || github.event_name == 'schedule' }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # Individual 404s are expected — an artifact can expire between the | |
| # list call and the delete call. A mixed outcome still passes. Only | |
| # the every-attempt-failed signal escalates (token lost | |
| # `actions: write`, API rate-limit, network flake). | |
| deleted=0 | |
| failed=0 | |
| while IFS=$'\t' read -r id _rest; do | |
| if [ -n "${id}" ]; then | |
| if gh api -X DELETE "/repos/${GITHUB_REPOSITORY}/actions/artifacts/${id}" > /dev/null 2>&1; then | |
| deleted=$((deleted + 1)) | |
| else | |
| echo "::warning::failed to delete artifact ${id}" | |
| failed=$((failed + 1)) | |
| fi | |
| fi | |
| done < candidates.tsv | |
| echo "::notice::Deleted ${deleted} artifact(s); ${failed} failure(s)." | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "**Deleted:** ${deleted} artifact(s); ${failed} failure(s)." >> "$GITHUB_STEP_SUMMARY" | |
| if [ "${failed}" -gt 0 ] && [ "${deleted}" -eq 0 ]; then | |
| echo "::error::All ${failed} delete attempts failed. Check token scope (actions: write) or API rate limits." | |
| exit 1 | |
| fi |