diff --git a/.github/workflows/deployment.artifact-cleanup-schedule.yml b/.github/workflows/deployment.artifact-cleanup-schedule.yml new file mode 100644 index 00000000..0e1bf6b5 --- /dev/null +++ b/.github/workflows/deployment.artifact-cleanup-schedule.yml @@ -0,0 +1,169 @@ +### +# Deletes stale artifacts from the deployment-delivery-pipeline-artifacts +# bucket: trunk (main/master) keeps the newest `trunk-revisions-to-keep`, +# other branches keep `branch-revisions-to-keep` and expire after +# `branch-days-to-keep` days. Call from a repo's own `schedule:` workflow, +# like deployment.preview-cleanup-schedule.yml. +### +on: + workflow_call: + inputs: + s3-bucket-name: + required: false + type: string + description: "Name of the S3 bucket to clean up. Defaults to '-deployment-delivery-pipeline-artifacts'." + default: "" + trunk-branches: + required: false + type: string + description: "Comma-separated list of branch names considered trunk." + default: "main,master" + trunk-revisions-to-keep: + required: false + type: number + description: "Minimum number of trunk revisions to keep before older ones are deleted." + default: 5 + branch-revisions-to-keep: + required: false + type: number + description: "Minimum number of revisions to keep per non-trunk branch." + default: 2 + branch-days-to-keep: + required: false + type: number + description: "Number of days to keep non-trunk branch revisions beyond branch-revisions-to-keep, before they're deleted." + default: 30 + aws-region: + required: false + type: string + description: "AWS region to use." + default: 'eu-west-1' + +name: Pipeline artifact cleanup + +permissions: + contents: read + id-token: write + +jobs: + cleanup-artifacts: + name: Delete Stale Pipeline Artifacts + runs-on: ubuntu-latest + environment: Service + steps: + - name: Authenticate with AWS + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: ${{ inputs.aws-region }} + role-to-assume: "arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/${{ vars.AWS_DEPLOYMENT_ROLE_NAME }}" + + - name: Delete stale artifacts + env: + S3_BUCKET: ${{ inputs.s3-bucket-name != '' && inputs.s3-bucket-name || format('{0}-deployment-delivery-pipeline-artifacts', vars.AWS_ACCOUNT_ID) }} + REPO_NAME: ${{ github.repository }} + TRUNK_BRANCHES: ${{ inputs.trunk-branches }} + TRUNK_KEEP: ${{ inputs.trunk-revisions-to-keep }} + BRANCH_KEEP: ${{ inputs.branch-revisions-to-keep }} + BRANCH_DAYS: ${{ inputs.branch-days-to-keep }} + run: | + set -euo pipefail + + REPO_NAME_WITHOUT_OWNER="${REPO_NAME##*/}" + echo "Cleaning up s3://${S3_BUCKET}/${REPO_NAME_WITHOUT_OWNER}/" + + OBJECTS_JSON=$(aws s3api list-objects-v2 \ + --bucket "${S3_BUCKET}" \ + --prefix "${REPO_NAME_WITHOUT_OWNER}/" \ + --query 'Contents[].{Key:Key,LastModified:LastModified}' \ + --output json) + + if [ "$OBJECTS_JSON" = "null" ] || [ -z "$OBJECTS_JSON" ]; then + echo "No artifacts found under ${REPO_NAME_WITHOUT_OWNER}/" + exit 0 + fi + + # Enrich every object with its branch (read from the `tags` metadata + # written on upload, e.g. '["-SHA","-branch"]') and the + # directory it lives in, so revisions can be grouped correctly. + ENRICHED="[]" + OBJECT_COUNT=$(echo "$OBJECTS_JSON" | jq 'length') + for ((i=0; i/dev/null || echo "") + BRANCH=$(echo "$TAGS_RAW" | grep -oE '"[^"]+-branch"' | sed -E 's/^"//; s/-branch"$//' || true) + + if [ -z "$BRANCH" ]; then + echo "::warning::Could not determine branch for $KEY, skipping" + continue + fi + + ENRICHED=$(echo "$ENRICHED" | jq --arg key "$KEY" --arg dir "$DIR" --arg branch "$BRANCH" --arg lm "$LAST_MODIFIED" \ + '. += [{"key": $key, "dir": $dir, "branch": $branch, "lastModified": $lm}]') + done + + TO_DELETE="[]" + CURRENT_TIME=$(date +%s) + CUTOFF_SECONDS=$(( BRANCH_DAYS * 24 * 60 * 60 )) + + DIRS=$(echo "$ENRICHED" | jq -r '[.[].dir] | unique | .[]') + for DIR in $DIRS; do + # Trunk revisions: keep the newest TRUNK_KEEP, delete the rest regardless of age. + TRUNK_ITEMS=$(echo "$ENRICHED" | jq -c --arg dir "$DIR" --arg branches "$TRUNK_BRANCHES" \ + '($branches | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $tb | [.[] | select(.dir == $dir) | select(.branch as $b | $tb | index($b) != null)] | sort_by(.lastModified) | reverse') + TRUNK_COUNT=$(echo "$TRUNK_ITEMS" | jq 'length') + if [ "$TRUNK_COUNT" -gt "$TRUNK_KEEP" ]; then + STALE=$(echo "$TRUNK_ITEMS" | jq -c ".[${TRUNK_KEEP}:]") + TO_DELETE=$(echo "$TO_DELETE" | jq --argjson stale "$STALE" '. + $stale') + fi + + # Branch revisions: keep the newest BRANCH_KEEP per branch, delete the + # rest only once older than BRANCH_DAYS days. + BRANCHES_IN_DIR=$(echo "$ENRICHED" | jq -r --arg dir "$DIR" --arg branches "$TRUNK_BRANCHES" \ + '($branches | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $tb | [.[] | select(.dir == $dir) | select(.branch as $b | $tb | index($b) == null) | .branch] | unique | .[]') + for BRANCH in $BRANCHES_IN_DIR; do + BRANCH_ITEMS=$(echo "$ENRICHED" | jq -c --arg dir "$DIR" --arg branch "$BRANCH" \ + '[.[] | select(.dir == $dir and .branch == $branch)] | sort_by(.lastModified) | reverse') + BRANCH_COUNT=$(echo "$BRANCH_ITEMS" | jq 'length') + if [ "$BRANCH_COUNT" -le "$BRANCH_KEEP" ]; then + continue + fi + + CANDIDATES=$(echo "$BRANCH_ITEMS" | jq -c ".[${BRANCH_KEEP}:]") + CANDIDATE_COUNT=$(echo "$CANDIDATES" | jq 'length') + for ((j=0; j