From e10c1b158d5fabd90db6db1dbf93f049d47fa88e Mon Sep 17 00:00:00 2001 From: Mikolaj Matuszny Date: Tue, 17 Mar 2026 13:16:39 +0100 Subject: [PATCH 1/4] BUILD-10739 Test cache cleanup --- scripts/cleanup-cache.sh | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100755 scripts/cleanup-cache.sh diff --git a/scripts/cleanup-cache.sh b/scripts/cleanup-cache.sh new file mode 100755 index 0000000..fc39819 --- /dev/null +++ b/scripts/cleanup-cache.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -euo pipefail + +# Self-service S3 cache cleanup script. +# Deletes cache objects matching a branch and optional key pattern. +# +# Required environment variables: +# - CLEANUP_BRANCH: Branch name (e.g., "refs/heads/feature/my-branch" or "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. + +# Normalize branch name: ensure it has refs/heads/ prefix +BRANCH="${CLEANUP_BRANCH}" +if [[ "$BRANCH" != refs/heads/* && "$BRANCH" != refs/pull/* ]]; then + BRANCH="refs/heads/${BRANCH}" +fi + +S3_PREFIX="s3://${S3_BUCKET}/cache/${GITHUB_REPOSITORY}/" +KEY_PATTERN="${CLEANUP_KEY:-}" + +if [[ -n "$KEY_PATTERN" ]]; then + INCLUDE_PATTERN="*/${BRANCH}/${KEY_PATTERN}*" + echo "Deleting cache entries matching branch='${BRANCH}' key='${KEY_PATTERN}*'" +else + INCLUDE_PATTERN="*/${BRANCH}/*" + echo "Deleting ALL cache entries for branch='${BRANCH}'" +fi + +echo "S3 prefix: ${S3_PREFIX}" +echo "Include pattern: ${INCLUDE_PATTERN}" + +DRYRUN_FLAG="" +if [[ "${DRY_RUN:-false}" == "true" ]]; then + DRYRUN_FLAG="--dryrun" + echo "" + echo "=== DRY RUN MODE - no objects will be deleted ===" + echo "" +fi + +aws s3 rm "${S3_PREFIX}" \ + --recursive \ + --exclude "*" \ + --include "${INCLUDE_PATTERN}" \ + ${DRYRUN_FLAG} + +echo "" +echo "Cache cleanup completed." From 7c0f755a8b87e7af6ce8ec652c21643728911b00 Mon Sep 17 00:00:00 2001 From: Mikolaj Matuszny Date: Tue, 17 Mar 2026 13:44:50 +0100 Subject: [PATCH 2/4] BUILD-10739 feat: add cache cleanup reusable workflow Co-Authored-By: Claude Opus 4.6 --- .github/workflows/cleanup-cache.yml | 55 +++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/cleanup-cache.yml diff --git a/.github/workflows/cleanup-cache.yml b/.github/workflows/cleanup-cache.yml new file mode 100644 index 0000000..2977812 --- /dev/null +++ b/.github/workflows/cleanup-cache.yml @@ -0,0 +1,55 @@ +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: false + +jobs: + cleanup: + runs-on: github-ubuntu-latest-s + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup AWS credentials + id: creds + uses: SonarSource/gh-action_cache/credential-setup@v1 + with: + environment: ${{ inputs.environment }} + + - name: Run cache cleanup + 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.creds.outputs.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ steps.creds.outputs.AWS_SECRET_ACCESS_KEY }} + AWS_SESSION_TOKEN: ${{ steps.creds.outputs.AWS_SESSION_TOKEN }} + AWS_DEFAULT_REGION: eu-central-1 + AWS_PROFILE: "" + AWS_DEFAULT_PROFILE: "" + run: bash scripts/cleanup-cache.sh From 69ae6a7e160e6f77c0632e04f2e14d8fd7be975f Mon Sep 17 00:00:00 2001 From: Mikolaj Matuszny Date: Tue, 17 Mar 2026 13:46:03 +0100 Subject: [PATCH 3/4] BUILD-10739 test: add cache cleanup script tests Co-Authored-By: Claude Opus 4.6 --- __tests__/cleanup-cache.test.sh | 185 ++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100755 __tests__/cleanup-cache.test.sh diff --git a/__tests__/cleanup-cache.test.sh b/__tests__/cleanup-cache.test.sh new file mode 100755 index 0000000..332ab91 --- /dev/null +++ b/__tests__/cleanup-cache.test.sh @@ -0,0 +1,185 @@ +#!/bin/bash +set -euo pipefail + +# Tests for scripts/cleanup-cache.sh +# Uses a mock aws CLI to capture arguments and verify correctness. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +CLEANUP_SCRIPT="$PROJECT_ROOT/scripts/cleanup-cache.sh" + +PASS=0 +FAIL=0 + +# ── helpers ────────────────────────────────────────────────────────── + +assert_contains() { + local label="$1" file="$2" expected="$3" + if grep -qF -- "$expected" "$file"; then + echo "PASS: ${label}" + PASS=$((PASS + 1)) + else + echo "FAIL: ${label}" + echo " Expected to find: ${expected}" + echo " In: $(cat "$file")" + FAIL=$((FAIL + 1)) + fi +} + +assert_not_contains() { + local label="$1" file="$2" unexpected="$3" + if grep -qF -- "$unexpected" "$file"; then + echo "FAIL: ${label}" + echo " Did NOT expect to find: ${unexpected}" + echo " In: $(cat "$file")" + FAIL=$((FAIL + 1)) + else + echo "PASS: ${label}" + PASS=$((PASS + 1)) + fi +} + +# Create a temporary directory for mock and logs +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +MOCK_AWS="$TMPDIR/aws" +AWS_LOG="$TMPDIR/aws_args.log" + +# Create mock aws CLI that logs all arguments +cat > "$MOCK_AWS" <<'MOCK' +#!/bin/bash +echo "$@" >> "${AWS_LOG}" +MOCK +chmod +x "$MOCK_AWS" + +# Put mock first in PATH +export PATH="$TMPDIR:$PATH" + +# Helper to run cleanup script with given env vars and capture aws args +run_cleanup() { + # Clear log from previous run + : > "$AWS_LOG" + # Export the log path so the mock can find it + export AWS_LOG + # Run the script, capturing stdout (we don't assert on stdout here) + bash "$CLEANUP_SCRIPT" >/dev/null 2>&1 || true +} + +# ── Test 1: Branch-only cleanup (bare branch, no key) ─────────────── + +echo "--- Test 1: Branch-only cleanup (no key) ---" +export CLEANUP_BRANCH="feature/my-branch" +export S3_BUCKET="my-bucket" +export GITHUB_REPOSITORY="SonarSource/my-repo" +unset CLEANUP_KEY 2>/dev/null || true +unset DRY_RUN 2>/dev/null || true + +run_cleanup + +assert_contains "includes bare branch pattern" \ + "$AWS_LOG" "--include */feature/my-branch/*" +assert_contains "includes full ref pattern" \ + "$AWS_LOG" "--include */refs/heads/feature/my-branch/*" +assert_contains "uses correct S3 prefix" \ + "$AWS_LOG" "s3://my-bucket/cache/SonarSource/my-repo/" + +# ── Test 2: Branch + key cleanup ──────────────────────────────────── + +echo "" +echo "--- Test 2: Branch + key cleanup ---" +export CLEANUP_BRANCH="develop" +export S3_BUCKET="my-bucket" +export GITHUB_REPOSITORY="SonarSource/my-repo" +export CLEANUP_KEY="sccache-Linux-" +unset DRY_RUN 2>/dev/null || true + +run_cleanup + +assert_contains "includes bare branch + key pattern" \ + "$AWS_LOG" "--include */develop/sccache-Linux-*" +assert_contains "includes full ref + key pattern" \ + "$AWS_LOG" "--include */refs/heads/develop/sccache-Linux-*" + +# ── Test 3: Dry run mode ──────────────────────────────────────────── + +echo "" +echo "--- Test 3: Dry run mode ---" +export CLEANUP_BRANCH="main" +export S3_BUCKET="my-bucket" +export GITHUB_REPOSITORY="SonarSource/my-repo" +export DRY_RUN="true" +unset CLEANUP_KEY 2>/dev/null || true + +run_cleanup + +assert_contains "dryrun flag is present" \ + "$AWS_LOG" "--dryrun" + +# ── Test 4: Bare branch input normalization ───────────────────────── + +echo "" +echo "--- Test 4: Bare branch input normalization ---" +export CLEANUP_BRANCH="my-feature" +export S3_BUCKET="my-bucket" +export GITHUB_REPOSITORY="SonarSource/my-repo" +unset CLEANUP_KEY 2>/dev/null || true +unset DRY_RUN 2>/dev/null || true + +run_cleanup + +assert_contains "bare branch include pattern" \ + "$AWS_LOG" "--include */my-feature/*" +assert_contains "full ref include pattern derived from bare input" \ + "$AWS_LOG" "--include */refs/heads/my-feature/*" + +# ── Test 5: Full ref input normalization ──────────────────────────── + +echo "" +echo "--- Test 5: Full ref input (refs/heads/main) ---" +export CLEANUP_BRANCH="refs/heads/main" +export S3_BUCKET="my-bucket" +export GITHUB_REPOSITORY="SonarSource/my-repo" +unset CLEANUP_KEY 2>/dev/null || true +unset DRY_RUN 2>/dev/null || true + +run_cleanup + +assert_contains "bare branch derived from full ref" \ + "$AWS_LOG" "--include */main/*" +assert_contains "full ref kept as-is" \ + "$AWS_LOG" "--include */refs/heads/main/*" + +# ── Test 6: --exclude and --recursive always present ──────────────── + +echo "" +echo "--- Test 6: --exclude * and --recursive always present ---" + +# Re-run a simple case to check structural flags +export CLEANUP_BRANCH="some-branch" +export S3_BUCKET="my-bucket" +export GITHUB_REPOSITORY="SonarSource/my-repo" +unset CLEANUP_KEY 2>/dev/null || true +unset DRY_RUN 2>/dev/null || true + +run_cleanup + +assert_contains "recursive flag present" \ + "$AWS_LOG" "--recursive" +assert_contains "exclude-all pattern present" \ + "$AWS_LOG" '--exclude *' + +# Also verify dryrun is NOT present when DRY_RUN is not set +assert_not_contains "dryrun flag absent when DRY_RUN unset" \ + "$AWS_LOG" "--dryrun" + +# ── Summary ───────────────────────────────────────────────────────── + +echo "" +echo "==============================" +echo "Results: ${PASS} passed, ${FAIL} failed" +echo "==============================" + +if (( FAIL > 0 )); then + exit 1 +fi From e076b582ffcc333dd4c67d8e4b12416335edae2e Mon Sep 17 00:00:00 2001 From: Mikolaj Matuszny Date: Tue, 17 Mar 2026 13:47:52 +0100 Subject: [PATCH 4/4] BUILD-10739 Cache cleanup workflow --- .github/workflows/cleanup-cache.yml | 28 ++--- .github/workflows/test-action.yml | 15 +++ README.md | 56 +++++++++ __tests__/cleanup-cache.test.sh | 185 ---------------------------- cleanup/action.yml | 54 ++++++++ scripts/cleanup-cache.sh | 67 +++++++--- 6 files changed, 181 insertions(+), 224 deletions(-) delete mode 100755 __tests__/cleanup-cache.test.sh create mode 100644 cleanup/action.yml diff --git a/.github/workflows/cleanup-cache.yml b/.github/workflows/cleanup-cache.yml index 2977812..573d406 100644 --- a/.github/workflows/cleanup-cache.yml +++ b/.github/workflows/cleanup-cache.yml @@ -24,32 +24,22 @@ on: description: "Preview mode - show what would be deleted without deleting" required: false type: boolean - default: false + 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: Setup AWS credentials - id: creds - uses: SonarSource/gh-action_cache/credential-setup@v1 + - name: Cleanup cache + uses: ./cleanup with: + branch: ${{ inputs.branch }} + key: ${{ inputs.key }} environment: ${{ inputs.environment }} - - - name: Run cache cleanup - 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.creds.outputs.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ steps.creds.outputs.AWS_SECRET_ACCESS_KEY }} - AWS_SESSION_TOKEN: ${{ steps.creds.outputs.AWS_SESSION_TOKEN }} - AWS_DEFAULT_REGION: eu-central-1 - AWS_PROFILE: "" - AWS_DEFAULT_PROFILE: "" - run: bash scripts/cleanup-cache.sh + 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/__tests__/cleanup-cache.test.sh b/__tests__/cleanup-cache.test.sh deleted file mode 100755 index 332ab91..0000000 --- a/__tests__/cleanup-cache.test.sh +++ /dev/null @@ -1,185 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Tests for scripts/cleanup-cache.sh -# Uses a mock aws CLI to capture arguments and verify correctness. - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -CLEANUP_SCRIPT="$PROJECT_ROOT/scripts/cleanup-cache.sh" - -PASS=0 -FAIL=0 - -# ── helpers ────────────────────────────────────────────────────────── - -assert_contains() { - local label="$1" file="$2" expected="$3" - if grep -qF -- "$expected" "$file"; then - echo "PASS: ${label}" - PASS=$((PASS + 1)) - else - echo "FAIL: ${label}" - echo " Expected to find: ${expected}" - echo " In: $(cat "$file")" - FAIL=$((FAIL + 1)) - fi -} - -assert_not_contains() { - local label="$1" file="$2" unexpected="$3" - if grep -qF -- "$unexpected" "$file"; then - echo "FAIL: ${label}" - echo " Did NOT expect to find: ${unexpected}" - echo " In: $(cat "$file")" - FAIL=$((FAIL + 1)) - else - echo "PASS: ${label}" - PASS=$((PASS + 1)) - fi -} - -# Create a temporary directory for mock and logs -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -MOCK_AWS="$TMPDIR/aws" -AWS_LOG="$TMPDIR/aws_args.log" - -# Create mock aws CLI that logs all arguments -cat > "$MOCK_AWS" <<'MOCK' -#!/bin/bash -echo "$@" >> "${AWS_LOG}" -MOCK -chmod +x "$MOCK_AWS" - -# Put mock first in PATH -export PATH="$TMPDIR:$PATH" - -# Helper to run cleanup script with given env vars and capture aws args -run_cleanup() { - # Clear log from previous run - : > "$AWS_LOG" - # Export the log path so the mock can find it - export AWS_LOG - # Run the script, capturing stdout (we don't assert on stdout here) - bash "$CLEANUP_SCRIPT" >/dev/null 2>&1 || true -} - -# ── Test 1: Branch-only cleanup (bare branch, no key) ─────────────── - -echo "--- Test 1: Branch-only cleanup (no key) ---" -export CLEANUP_BRANCH="feature/my-branch" -export S3_BUCKET="my-bucket" -export GITHUB_REPOSITORY="SonarSource/my-repo" -unset CLEANUP_KEY 2>/dev/null || true -unset DRY_RUN 2>/dev/null || true - -run_cleanup - -assert_contains "includes bare branch pattern" \ - "$AWS_LOG" "--include */feature/my-branch/*" -assert_contains "includes full ref pattern" \ - "$AWS_LOG" "--include */refs/heads/feature/my-branch/*" -assert_contains "uses correct S3 prefix" \ - "$AWS_LOG" "s3://my-bucket/cache/SonarSource/my-repo/" - -# ── Test 2: Branch + key cleanup ──────────────────────────────────── - -echo "" -echo "--- Test 2: Branch + key cleanup ---" -export CLEANUP_BRANCH="develop" -export S3_BUCKET="my-bucket" -export GITHUB_REPOSITORY="SonarSource/my-repo" -export CLEANUP_KEY="sccache-Linux-" -unset DRY_RUN 2>/dev/null || true - -run_cleanup - -assert_contains "includes bare branch + key pattern" \ - "$AWS_LOG" "--include */develop/sccache-Linux-*" -assert_contains "includes full ref + key pattern" \ - "$AWS_LOG" "--include */refs/heads/develop/sccache-Linux-*" - -# ── Test 3: Dry run mode ──────────────────────────────────────────── - -echo "" -echo "--- Test 3: Dry run mode ---" -export CLEANUP_BRANCH="main" -export S3_BUCKET="my-bucket" -export GITHUB_REPOSITORY="SonarSource/my-repo" -export DRY_RUN="true" -unset CLEANUP_KEY 2>/dev/null || true - -run_cleanup - -assert_contains "dryrun flag is present" \ - "$AWS_LOG" "--dryrun" - -# ── Test 4: Bare branch input normalization ───────────────────────── - -echo "" -echo "--- Test 4: Bare branch input normalization ---" -export CLEANUP_BRANCH="my-feature" -export S3_BUCKET="my-bucket" -export GITHUB_REPOSITORY="SonarSource/my-repo" -unset CLEANUP_KEY 2>/dev/null || true -unset DRY_RUN 2>/dev/null || true - -run_cleanup - -assert_contains "bare branch include pattern" \ - "$AWS_LOG" "--include */my-feature/*" -assert_contains "full ref include pattern derived from bare input" \ - "$AWS_LOG" "--include */refs/heads/my-feature/*" - -# ── Test 5: Full ref input normalization ──────────────────────────── - -echo "" -echo "--- Test 5: Full ref input (refs/heads/main) ---" -export CLEANUP_BRANCH="refs/heads/main" -export S3_BUCKET="my-bucket" -export GITHUB_REPOSITORY="SonarSource/my-repo" -unset CLEANUP_KEY 2>/dev/null || true -unset DRY_RUN 2>/dev/null || true - -run_cleanup - -assert_contains "bare branch derived from full ref" \ - "$AWS_LOG" "--include */main/*" -assert_contains "full ref kept as-is" \ - "$AWS_LOG" "--include */refs/heads/main/*" - -# ── Test 6: --exclude and --recursive always present ──────────────── - -echo "" -echo "--- Test 6: --exclude * and --recursive always present ---" - -# Re-run a simple case to check structural flags -export CLEANUP_BRANCH="some-branch" -export S3_BUCKET="my-bucket" -export GITHUB_REPOSITORY="SonarSource/my-repo" -unset CLEANUP_KEY 2>/dev/null || true -unset DRY_RUN 2>/dev/null || true - -run_cleanup - -assert_contains "recursive flag present" \ - "$AWS_LOG" "--recursive" -assert_contains "exclude-all pattern present" \ - "$AWS_LOG" '--exclude *' - -# Also verify dryrun is NOT present when DRY_RUN is not set -assert_not_contains "dryrun flag absent when DRY_RUN unset" \ - "$AWS_LOG" "--dryrun" - -# ── Summary ───────────────────────────────────────────────────────── - -echo "" -echo "==============================" -echo "Results: ${PASS} passed, ${FAIL} failed" -echo "==============================" - -if (( FAIL > 0 )); then - exit 1 -fi 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 index fc39819..741111e 100755 --- a/scripts/cleanup-cache.sh +++ b/scripts/cleanup-cache.sh @@ -4,8 +4,13 @@ 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., "refs/heads/feature/my-branch" or "feature/my-branch") +# - 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 # @@ -13,39 +18,61 @@ set -euo pipefail # - 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. -# Normalize branch name: ensure it has refs/heads/ prefix -BRANCH="${CLEANUP_BRANCH}" -if [[ "$BRANCH" != refs/heads/* && "$BRANCH" != refs/pull/* ]]; then - BRANCH="refs/heads/${BRANCH}" +: "${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_PATTERN="*/${BRANCH}/${KEY_PATTERN}*" - echo "Deleting cache entries matching branch='${BRANCH}' key='${KEY_PATTERN}*'" + 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_PATTERN="*/${BRANCH}/*" - echo "Deleting ALL cache entries for branch='${BRANCH}'" + INCLUDE_BARE="*/${BARE_BRANCH}/*" + INCLUDE_FULL="*/${FULL_REF_BRANCH}/*" + echo "Deleting ALL cache entries for branch='${BARE_BRANCH}'" fi -echo "S3 prefix: ${S3_PREFIX}" -echo "Include pattern: ${INCLUDE_PATTERN}" +echo "Repository: ${GITHUB_REPOSITORY}" +echo "Bucket: ${S3_BUCKET}" -DRYRUN_FLAG="" +CMD=(aws s3 rm "${S3_PREFIX}" --recursive --exclude "*" --include "${INCLUDE_BARE}" --include "${INCLUDE_FULL}") if [[ "${DRY_RUN:-false}" == "true" ]]; then - DRYRUN_FLAG="--dryrun" + CMD+=(--dryrun) echo "" echo "=== DRY RUN MODE - no objects will be deleted ===" echo "" fi -aws s3 rm "${S3_PREFIX}" \ - --recursive \ - --exclude "*" \ - --include "${INCLUDE_PATTERN}" \ - ${DRYRUN_FLAG} +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 -echo "" -echo "Cache cleanup completed." +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