Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/cleanup-cache.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Cleanup S3 Cache

on:
workflow_dispatch:
inputs:
branch:
description: "Branch name to clean cache for (e.g., 'feature/my-branch')"
required: true
type: string
key:
description: "Cache key prefix to delete (e.g., 'sccache-Linux-'). Leave empty to delete all cache for the branch."
required: false
type: string
default: ""
environment:
description: "Target environment"
required: false
type: choice
options:
- prod
- dev
default: prod
dry-run:
description: "Preview mode - show what would be deleted without deleting"
required: false
type: boolean
default: true

jobs:
cleanup:
runs-on: github-ubuntu-latest-s
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Cleanup cache
uses: ./cleanup
with:
branch: ${{ inputs.branch }}
key: ${{ inputs.key }}
environment: ${{ inputs.environment }}
dry-run: ${{ inputs.dry-run }}
15 changes: 15 additions & 0 deletions .github/workflows/test-action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,18 @@ jobs:
pip install pytest requests

# SUCCESS: credential-guard post step runs, then runs-on/cache saves to S3

test-cleanup-dry-run:
runs-on: github-ubuntu-latest-s
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Dry-run cleanup against real S3
uses: ./cleanup
with:
branch: test-cleanup-nonexistent
environment: dev
dry-run: "true"
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,59 @@ jobs:

The AWS S3 bucket lifecycle rules apply to delete the old files. The content from default branches expires in 60 days and for feature
branches in 30 days.

## Cache Cleanup

Delete S3 cache entries for a specific branch and/or cache key prefix without waiting for the 30-day lifecycle expiry.

### Setup

Add a cleanup workflow to your repository:

```yaml
# .github/workflows/cleanup-cache.yml
name: Cleanup S3 Cache

on:
workflow_dispatch:
inputs:
branch:
description: "Branch name (e.g., 'feature/my-branch')"
required: true
type: string
key:
description: "Cache key prefix (optional). Leave empty to delete all cache for the branch."
required: false
type: string
default: ""
dry-run:
description: "Preview deletions without executing them"
required: false
type: boolean
default: false

jobs:
cleanup:
runs-on: github-ubuntu-latest-s
permissions:
id-token: write
contents: read
steps:
- uses: SonarSource/gh-action_cache/cleanup@v1
with:
branch: ${{ inputs.branch }}
key: ${{ inputs.key }}
dry-run: ${{ inputs.dry-run }}
```

> **Important:** The workflow must be dispatched from a **default/protected branch** (e.g., `main` or `master`).
> This is required by the IAM policy for cross-branch cache deletion.

### Running Cleanup

**Via GitHub Actions UI:**

1. Go to **Actions** > **Cleanup S3 Cache** > **Run workflow**
2. Enter the branch name as you know it (e.g., `feature/my-branch` or `master`)
3. Optionally enter a cache key prefix (e.g., `sccache-Linux-`)
4. Optionally enable **dry-run** to preview what would be deleted
54 changes: 54 additions & 0 deletions cleanup/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: S3 Cache Cleanup
description: Delete S3 cache entries by branch name and optional key prefix
author: SonarSource

inputs:
branch:
description: >
Branch name to clean cache for.
Examples: "master", "feature/my-branch".
Automatically finds cache entries regardless of how they were created (PR or push).
required: true
key:
description: >
Cache key prefix to delete (optional). If empty, deletes ALL cache entries for the branch.
Examples: "sccache-Linux-", "orchestrator-2026-"
required: false
default: ""
environment:
description: Environment to use ('dev' or 'prod')
required: false
default: prod
dry-run:
description: Preview deletions without executing them
required: false
default: "false"

runs:
using: composite
steps:
- name: Set action path
shell: bash
run: |
echo "ACTION_PATH_CLEANUP=${{ github.action_path }}" >> "$GITHUB_ENV"

- name: Setup S3 cache credentials
id: aws-auth
uses: SonarSource/gh-action_cache/credential-setup@v1
with:
environment: ${{ inputs.environment }}

- name: Run cache cleanup
shell: bash
env:
CLEANUP_BRANCH: ${{ inputs.branch }}
CLEANUP_KEY: ${{ inputs.key }}
S3_BUCKET: sonarsource-s3-cache-${{ inputs.environment }}-bucket
GITHUB_REPOSITORY: ${{ github.repository }}
DRY_RUN: ${{ inputs.dry-run }}
AWS_ACCESS_KEY_ID: ${{ steps.aws-auth.outputs.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ steps.aws-auth.outputs.AWS_SECRET_ACCESS_KEY }}
AWS_SESSION_TOKEN: ${{ steps.aws-auth.outputs.AWS_SESSION_TOKEN }}
AWS_DEFAULT_REGION: eu-central-1
AWS_REGION: eu-central-1
run: "$ACTION_PATH_CLEANUP/../scripts/cleanup-cache.sh"
78 changes: 78 additions & 0 deletions scripts/cleanup-cache.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/bin/bash
set -euo pipefail

# Self-service S3 cache cleanup script.
# Deletes cache objects matching a branch and optional key pattern.
#
# The branch name in S3 varies by event type:
# - PR events use GITHUB_HEAD_REF (bare name, e.g., "feat/my-branch")
# - Push events use GITHUB_REF (full ref, e.g., "refs/heads/master")
# This script searches for BOTH forms to cover all cached objects.
#
# Required environment variables:
# - CLEANUP_BRANCH: Branch name (e.g., "feature/my-branch" or "refs/heads/feature/my-branch")
# - S3_BUCKET: S3 bucket name (e.g., "sonarsource-s3-cache-prod-bucket")
# - GITHUB_REPOSITORY: Repository in org/repo format
#
# Optional environment variables:
# - CLEANUP_KEY: Cache key prefix to match (e.g., "sccache-Linux-"). If empty, deletes all cache for the branch.
# - DRY_RUN: Set to "true" to preview deletions without executing them.

: "${CLEANUP_BRANCH:?}" "${S3_BUCKET:?}" "${GITHUB_REPOSITORY:?}"

# Derive both bare and full ref forms of the branch name
INPUT_BRANCH="${CLEANUP_BRANCH}"
if [[ "$INPUT_BRANCH" == refs/heads/* ]]; then
BARE_BRANCH="${INPUT_BRANCH#refs/heads/}"
FULL_REF_BRANCH="$INPUT_BRANCH"
elif [[ "$INPUT_BRANCH" == refs/pull/* ]]; then
# PR ref - use as-is, no bare form
BARE_BRANCH="$INPUT_BRANCH"
FULL_REF_BRANCH="$INPUT_BRANCH"
else
BARE_BRANCH="$INPUT_BRANCH"
FULL_REF_BRANCH="refs/heads/${INPUT_BRANCH}"
fi

S3_PREFIX="s3://${S3_BUCKET}/cache/${GITHUB_REPOSITORY}/"
KEY_PATTERN="${CLEANUP_KEY:-}"

# Build include patterns for both bare (PR) and full ref (push) forms
if [[ -n "$KEY_PATTERN" ]]; then
INCLUDE_BARE="*/${BARE_BRANCH}/${KEY_PATTERN}*"
INCLUDE_FULL="*/${FULL_REF_BRANCH}/${KEY_PATTERN}*"
echo "Deleting cache entries matching branch='${BARE_BRANCH}' key='${KEY_PATTERN}*'"
else
INCLUDE_BARE="*/${BARE_BRANCH}/*"
INCLUDE_FULL="*/${FULL_REF_BRANCH}/*"
echo "Deleting ALL cache entries for branch='${BARE_BRANCH}'"
fi

echo "Repository: ${GITHUB_REPOSITORY}"
echo "Bucket: ${S3_BUCKET}"

CMD=(aws s3 rm "${S3_PREFIX}" --recursive --exclude "*" --include "${INCLUDE_BARE}" --include "${INCLUDE_FULL}")
if [[ "${DRY_RUN:-false}" == "true" ]]; then
CMD+=(--dryrun)
echo ""
echo "=== DRY RUN MODE - no objects will be deleted ==="
echo ""
fi

EXIT_CODE=0
OUTPUT=$("${CMD[@]}" 2>&1) || EXIT_CODE=$?
echo "$OUTPUT"

if [[ $EXIT_CODE -ne 0 ]]; then
echo "" >&2
echo "ERROR: aws s3 rm failed with exit code ${EXIT_CODE}" >&2
exit $EXIT_CODE
fi

if [[ -z "$OUTPUT" ]]; then
echo "No matching cache entries found."
else
MATCH_COUNT=$(echo "$OUTPUT" | grep -c "delete:" || true)
echo ""
echo "Cache cleanup completed. ${MATCH_COUNT} object(s) matched."
fi
Loading