Repository navigation
fix(ci): verify the rollback image in AWS_ACCOUNT_ID's registry - #1112
Conversation
The AWS rollback jobs deploy IMAGE_URI, built from the AWS_ACCOUNT_ID repository variable, but the "Verify image exists" step called `aws ecr describe-images` with no --registry-id, so it checked the registry of the account behind AWS_ROLE_TO_ASSUME instead. When those two admin-set variables pointed at different accounts, the step reported the image present in a registry Terraform does not pull from. Both rollback-aws-lambda and rollback-aws-fargate now: - refuse to run when AWS_ACCOUNT_ID is unset, when the caller identity cannot be read, or when AWS_ACCOUNT_ID differs from the assumed role's account (which also guards the Terraform apply, not just the lookup); - pin describe-images to --registry-id "$AWS_ACCOUNT_ID". scripts/test-rollback-ecr-registry.sh lifts the step out of rollback.yml and runs it against a stubbed aws CLI; ci.yml runs it as the rollback-ecr-registry job, required by ci-success. Closes LeanerCloud#142 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. Walkthrough
Merge Risk:
|
|
Independent review 2 (claude-fleet, cold): REQUEST_CHANGES fb9a915 The merge into origin/main is clean, and rollback.yml has not changed on main since the base. There is no migration and no Go change. Findings:
Against #142:
Attacked:
Verified locally:
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
…ploys The AWS rollback jobs bind environment:, so vars.AWS_ACCOUNT_ID, AWS_REGION and ECR_REPOSITORY resolve environment-first there but repo/org-level in the validate job that builds IMAGE_URI. An environment-scoped AWS_ACCOUNT_ID could therefore make the verify step check one registry while Terraform deploys from another (LeanerCloud#142 again). Both verify steps now read IMAGE_URI from needs.validate.outputs.image_uri, parse account, region, repository and tag out of it (failing closed on anything that is not an ECR image URI), compare that account with the STS caller account, and run describe-images against the parsed values. This also drops the "repository variable is not set" messages. scripts/test-rollback-ecr-registry.sh runs every case with environment-scoped decoys under the old variable names, adds malformed URI rows (empty, no tag, 11-digit account, non-ECR host, GCP URI, trailing text, multi-line) that must fail before any AWS call, and replaces the env-line source-equivalence check with an assertion that the verify and Terraform steps read the same job output. Refs LeanerCloud#142 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Addressing Independent review 2 (on [blocking] Environment-scoped vars can make verify and deploy see different accounts. Fixed as suggested.
[non-blocking] The source-equivalence check misses the [cosmetic] "repository variable is not set". Those checks and messages are gone from the verify steps. The step no longer reads vars, and the parse failure says "Image URI from the validate job is not an ECR image URI". I left the validate job's own messages ( PR body: the Fix, test, verification and mutation-probe sections are updated. I also added an out-of-scope note: validate still builds Verification on the merged tree (unit/fixture level only, no workflow run, no AWS call):
The PR is still a draft. |
Brings in main's CI fixes (LeanerCloud#1236, LeanerCloud#1252) and CI changes since, so this PR gets a fresh CI run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review 2 (claude-fleet, cold), re-review: APPROVE ad2e5c2 My blocker from fb9a915 is fixed (fix commit db42209).
Blocker check:
Findings (none blocking):
Attacked:
Verified locally:
Same reviewer as the earlier review, re-reviewing the new head. Review only: nothing was pushed, and the GitHub review state is unchanged. |
|
VERDICT: APPROVE ad2e5c2 Re-review of #1112 (fixes #142). The head moved past the requested db42209: ad2e5c2 = db42209 + a merge of origin/main (503eb2e, one commit behind 6af022f, no overlap). The PR-content delta since fb9a915 is db42209 only. Delta (fb9a915 -> db42209): both AWS "Verify image exists" steps (.github/workflows/rollback.yml:239-286 and the Fargate copy) now take only Findings: none blocking.
Attacked / results (all on the merge-tree of origin/main 6af022f + ad2e5c2, tree a2a9f4fb, clean merge):
|
|
Local verification at
|
|
Marked ready and merged by the owner's agent after 2 independent reviews and local verification (see the review and verification comments above, all at |
Bring the branch up to date with main. Conflicted files: - .github/workflows/ci.yml: main (LeanerCloud#1112) added the rollback-ecr-registry job directly above azure-kv-access-policy; this branch rewrote the azure-kv-access-policy job's header comment (the guard now parses HCL and covers nested access_policy blocks, so main's "nested form is not covered" note is stale). Kept main's new job unchanged, followed by the branch's comment and job (with its Set up Go step). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bring the branch up to date with main. Conflicted files: - .github/workflows/ci.yml: this branch appended a "Run Function URL reader self-tests" step to the aws-tfstate-platform-key job; main (LeanerCloud#1112) added a new rollback-ecr-registry job right after that job. Kept both: the step stays at the end of aws-tfstate-platform-key and main's job follows it unchanged. rollback.yml merged cleanly: main's rewritten "Verify image exists" steps and this branch's TF_VAR_lambda_deployed_function_url export touch different steps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bring the tfstate concurrency-group guard up to date with main. Conflicted files: - .github/workflows/ci.yml: main added the rollback-ecr-registry job (LeanerCloud#1112) right after aws-tfstate-platform-key, where this branch adds tfstate-concurrency-group. Both are independent shell-only guard jobs, so both are kept (this branch's job first, then main's), and both stay in CI Success's needs list alongside main's new migration-down-gate and gcp-import-dev-state entries. The guard still passes (41/41) against main's current workflows, including the rollback.yml change from LeanerCloud#1112. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Root cause
In
.github/workflows/rollback.yml, the AWS rollback jobs deployIMAGE_URI, which the validate job builds from theAWS_ACCOUNT_IDvariable (<AWS_ACCOUNT_ID>.dkr.ecr...). The "Verify image exists" step inrollback-aws-lambdaandrollback-aws-fargatecalledaws ecr describe-imageswith no--registry-id, so the lookup ran against the registry of the account behindAWS_ROLE_TO_ASSUME. Those two admin-set variables only point at the same account by convention. If they ever differ, the step reports "Image exists in ECR" for a registry Terraform doesn't pull from.Fix
Both AWS verify steps now check the exact string Terraform deploys:
IMAGE_URIitself. The step's only env input isIMAGE_URI: ${{ needs.validate.outputs.image_uri }}, the same expression the "Rollback with Terraform" step applies. The step parses account, region, repository and tag out of it. It does not re-readvars.*, because the rollback jobs bindenvironment:and the validate job does not, so the same variable names can resolve to different values in the two jobs (environment-scoped first in the rollback jobs, repo/org-level in validate). Anything that isn't<12-digit account>.dkr.ecr.<region>.amazonaws.com/<repository>:<tag>(empty, no tag, wrong host, trailing text, multi-line) fails closed before any AWS call.aws sts get-caller-identity --query Account --output text. It refuses to roll back if that account differs from the account inIMAGE_URI, or if the caller identity can't be read. This protects the Terraform apply as well as the lookup: Terraform applies with the assumed role, and the deploy workflows push images to that role's own ECR repository.describe-imagespasses--registry-id,--repository-name,imageTag=and--region, all parsed fromIMAGE_URI.sts:GetCallerIdentityneeds no IAM permission, so the deploy role needs no new grant.Behavior change: a setup where
AWS_ACCOUNT_IDdeliberately names a different (shared ECR) account from the deploy role's account now fails the rollback. Nothing in this repo builds that shape:terraform/environments/awscreates the ECR repository in the deploy account.Also included:
scripts/test-rollback-ecr-registry.sh. It extracts the shippedrun:block of each AWS verify step fromrollback.ymland runs it withbash -eo pipefailagainst a stubbedawsCLI. The stub mimics the real CLI's defaults:describe-imageswith no--registry-iduses the caller's account, andget-caller-identityprints JSON unless--query Account --output textis passed.PATHcontains only the stub directory plus/usr/bin:/bin, so no real AWS CLI can be reached. Every case also exportsAWS_ACCOUNT_ID,ECR_REPOSITORY,AWS_REGIONandIMAGE_TAGset to environment-scoped decoys that disagree withIMAGE_URI, so a step that reads them instead ofIMAGE_URIfails. A structural check asserts that the verify step and the Terraform step both takeIMAGE_URIfromneeds.validate.outputs.image_uri. Account IDs are built at runtime, so the file contains no 12-digit token for git-secrets to flag.ci.ymlgets a newrollback-ecr-registryjob that runs it, and the job is added toci-success'sneeds..github/workflows/README.md: one sentence under the rollback "Image Verification" bullet.Closes #142
Verification
All of this evidence is unit/fixture level: a stubbed
awsCLI running the step text extracted from the workflow. No rollback workflow was run and no real AWS call was made. Real-cloud gap: nothing here checks the step against real STS/ECR, e.g. thatget-caller-identityoutput has no stray whitespace under the configure-aws-credentials session. The AWS CLI documents that--output textfor a scalar--queryprints the bare value.Fail before / pass after. The same suite against older copies of
rollback.yml:origin/main: 6 passed, 46 failed (exit 1).fb9a915f(which comparedvars.AWS_ACCOUNT_ID): 8 passed, 44 failed. That includes the environment-scope case from review:Mutation probes on the fixed workflow (each one makes the suite fail):
$AWS_ACCOUNT_IDinstead of the parsed account (lambda only)--registry-id "$AWS_ACCOUNT_ID"--region "$AWS_REGION"(lambda only)--repository-name "$ECR_REPOSITORY"$IMAGE_TAGfalls back to the env value (lambda only)$anchor^anchor[0-9]{12}changed to[0-9]+IMAGE_URItaken from another expression--registry-id(lambda only)Other checks:
actionlint1.7.12 onci.ymlandrollback.yml: clean.zizmor==1.29.0 --offline --persona=pedantic --min-severity=mediumon both: no findings.shellcheck scripts/test-rollback-ecr-registry.sh: clean.markdownlint-cli@0.47.0on the README: clean.git diff --check: clean.scripts/*.shall still pass:test-ecr-delete-selection.sh59/0,test-rds-deletion-protection-scope.sh58/0,test-aws-tfstate-platform-key.sh26/0,test-cloud-sql-delete-scope.sh62/0.scripts/setup-git-secrets.shplusgit secrets --scanin a freshgit initcopy with an isolatedHOME: clean.scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.internal/mocksguards don't apply.Out of scope (noted, not changed)
describe-imagesstderr to/dev/null. AnAccessDeniedor throttling error therefore shows up as "Image not found in ECR". This was already the case before this PR.rollback.yml's header "Required GitHub Variables" lists onlyGCP_PROJECT_ID, although the AWS jobs also needAWS_ACCOUNT_ID,ECR_REPOSITORYandAWS_ROLE_TO_ASSUME.IMAGE_URIfrom repo/org-level variables, because it binds no environment. If prod's account is set only at environment scope, this PR makes the rollback refuse; it does not make validate read the environment's values. Changing that would mean bindingenvironment:on validate, which adds a second approval prompt (see the note above the AWS jobs).Labels
I can't apply labels from this account. Could a maintainer please mirror the issue's triage labels onto this PR:
priority/p2,severity/medium,urgency/this-quarter,impact/internal,effort/xs,type/bug,triaged?🤖 Generated with Claude Code
Summary by CodeRabbit