From b253b3450e2792f09ca8bada33184231eb24a8b5 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Thu, 11 Jun 2026 01:07:17 -0700 Subject: [PATCH] fix(ci): restrict staging ECR cleanup to cudly-staging repos The staging cleanup workflow force-deleted any ECR repository named exactly 'cudly', which is also the default production repository name in rollback.yml. Drop the legacy '== cudly' clause from the JMESPath filter (the one-off pre-suffix teardown it served is done) and add an in-loop guard that refuses to delete any repository not matching the cudly-staging* naming pattern, so a future filter change cannot widen the blast radius silently. Also remove the stale 'cudly' fallback for vars.ECR_REPOSITORY in rollback.yml: the validate job now fails loudly when the variable is unset instead of guessing a repository name that diverges from the suffixed cudly-prod- Terraform naming scheme. Validated with actionlint and a shell simulation of the guard (bare 'cudly' and cudly-prod-* refused, cudly-staging* deleted). Closes #1185 --- .github/workflows/cleanup-staging.yml | 24 ++++++++++++++++++++++-- .github/workflows/rollback.yml | 16 ++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cleanup-staging.yml b/.github/workflows/cleanup-staging.yml index 52f4e641b..1b43786ad 100644 --- a/.github/workflows/cleanup-staging.yml +++ b/.github/workflows/cleanup-staging.yml @@ -77,8 +77,18 @@ jobs: - name: Force-delete all staging ECR repos run: | for REPO in $(aws ecr describe-repositories \ - --query "repositories[?starts_with(repositoryName,'cudly-staging') || repositoryName=='cudly'].repositoryName" \ + --query "repositories[?starts_with(repositoryName,'cudly-staging')].repositoryName" \ --output text 2>/dev/null); do + # Defense in depth: refuse anything outside the staging naming + # pattern (in particular the bare 'cudly' prod-adjacent repo) + # even if the query filter above is ever loosened. + case "$REPO" in + cudly-staging*) ;; + *) + echo "Refusing to delete non-staging ECR repo '$REPO'; staging cleanup only deletes cudly-staging* repos" + exit 1 + ;; + esac echo "Force-deleting ECR repo $REPO..." aws ecr delete-repository --repository-name "$REPO" --force 2>/dev/null \ || echo " Failed to delete $REPO (may already be gone)" @@ -137,8 +147,18 @@ jobs: - name: Force-delete all staging ECR repos run: | for REPO in $(aws ecr describe-repositories \ - --query "repositories[?starts_with(repositoryName,'cudly-staging') || repositoryName=='cudly'].repositoryName" \ + --query "repositories[?starts_with(repositoryName,'cudly-staging')].repositoryName" \ --output text 2>/dev/null); do + # Defense in depth: refuse anything outside the staging naming + # pattern (in particular the bare 'cudly' prod-adjacent repo) + # even if the query filter above is ever loosened. + case "$REPO" in + cudly-staging*) ;; + *) + echo "Refusing to delete non-staging ECR repo '$REPO'; staging cleanup only deletes cudly-staging* repos" + exit 1 + ;; + esac echo "Force-deleting ECR repo $REPO..." aws ecr delete-repository --repository-name "$REPO" --force 2>/dev/null \ || echo " Failed to delete $REPO (may already be gone)" diff --git a/.github/workflows/rollback.yml b/.github/workflows/rollback.yml index 500a47263..73066541a 100644 --- a/.github/workflows/rollback.yml +++ b/.github/workflows/rollback.yml @@ -58,6 +58,8 @@ jobs: - name: Validate inputs id: check + env: + ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} run: | IS_VALID=true IMAGE_URI="" @@ -71,7 +73,12 @@ jobs: # Construct image URI based on cloud provider case "${{ inputs.cloud }}" in aws-lambda|aws-fargate) - IMAGE_URI="${{ vars.AWS_ACCOUNT_ID }}.dkr.ecr.${{ vars.AWS_REGION || 'us-east-1' }}.amazonaws.com/${{ vars.ECR_REPOSITORY || 'cudly' }}:${{ inputs.image_tag }}" + if [ -z "$ECR_REPOSITORY" ]; then + echo "❌ ECR_REPOSITORY repository variable is not set; refusing to guess the repo name" + IS_VALID=false + else + IMAGE_URI="${{ vars.AWS_ACCOUNT_ID }}.dkr.ecr.${{ vars.AWS_REGION || 'us-east-1' }}.amazonaws.com/${ECR_REPOSITORY}:${{ inputs.image_tag }}" + fi ;; gcp) IMAGE_URI="${{ vars.GCP_REGION || 'us-central1' }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ vars.ARTIFACT_REGISTRY_REPO || 'cudly' }}/cudly:${{ inputs.image_tag }}" @@ -127,7 +134,12 @@ jobs: if: startsWith(inputs.cloud, 'aws') run: | IMAGE_TAG="${{ inputs.image_tag }}" - REPO="${{ vars.ECR_REPOSITORY || 'cudly' }}" + REPO="${{ vars.ECR_REPOSITORY }}" + + if [ -z "$REPO" ]; then + echo "❌ ECR_REPOSITORY repository variable is not set" + exit 1 + fi echo "Checking if image exists: $REPO:$IMAGE_TAG"