diff --git a/.github/workflows/cleanup-cache.yml b/.github/workflows/cleanup-cache.yml new file mode 100644 index 0000000..573d406 --- /dev/null +++ b/.github/workflows/cleanup-cache.yml @@ -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 }} diff --git a/.github/workflows/test-action.yml b/.github/workflows/test-action.yml index 682bfd4..fe15c93 100644 --- a/.github/workflows/test-action.yml +++ b/.github/workflows/test-action.yml @@ -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" diff --git a/README.md b/README.md index 65fa005..41af5e3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cleanup/action.yml b/cleanup/action.yml new file mode 100644 index 0000000..bc50c32 --- /dev/null +++ b/cleanup/action.yml @@ -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" diff --git a/scripts/cleanup-cache.sh b/scripts/cleanup-cache.sh new file mode 100755 index 0000000..741111e --- /dev/null +++ b/scripts/cleanup-cache.sh @@ -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