Repository navigation
Fail the Artifact Registry image cleanup when gcloud fails - #4143
Merged
Merged
Conversation
The cleanup job sent gcloud's output to /dev/null and exited 0 whether or not any deletion worked. Run 40968846055 logged "Deleted 0/108 image(s)" and passed: the deploy service account has roles/artifactregistry.writer, which lacks artifactregistry.tags.delete and artifactregistry.versions.delete. Each failed deletion now prints gcloud's error, and the script exits 1 when any deletion fails. A failed listing also exits 1 instead of being read as an empty repository, since bash ignores a process substitution's exit status. The newest-15 retention is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4143 +/- ##
===========================================
+ Coverage 50.63% 88.15% +37.52%
===========================================
Files 258 274 +16
Lines 14955 16393 +1438
Branches 2399 2689 +290
===========================================
+ Hits 7573 14452 +6879
+ Misses 6751 1271 -5480
- Partials 631 670 +39 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The
cleanup-cloud-run-imagesjob inpush.ymlruns.github/scripts/cleanup_cloud_run_images.shafter a successful production deploy. The script sent gcloud's output to/dev/nulland exited 0 even when every deletion failed. Run 40968846055 (29 Oct 2026) loggedImages: 123 (keeping newest 15),Deleting: 108,Deleted 0/108 image(s).and the job passed. Thepolicyengine-apiArtifact Registry repository is about 42 GB.Changes:
Failed to delete <image>@<digest>:followed by gcloud's error output. If any deletion fails, the script ends with one::error::Failed to delete N/M image(s)annotation and exits 1.gcloud artifacts docker images listnow exits 1 with::error::Could not list images in <repo>.Before this change, the listing ran inside a process substitution, and bash ignores a process substitution's exit status, so a failed listing was read as "0 images" and the job passed.DRY_RUN=1and the target repository are unchanged. The workflow file is unchanged.Cause of the failed deletions
github-deployment@policyengine-api.iam.gserviceaccount.com. TheGCP_DEPLOY_SERVICE_ACCOUNTsecret is masked in the log, but the Admin Activity audit log names this account as the caller for every delete request during the run (23:49–23:51 UTC).roles/artifactregistry.writer, on the repository. There are no Artifact Registry bindings at the project, folder or organization level. That role has neitherartifactregistry.tags.deletenorartifactregistry.versions.delete.gcloud artifacts docker images delete <image>@<digest> --delete-tagsfirst callsDeleteTagfor each tag on the image, then callsDeleteVersion(seeDeleteDockerImageingooglecloudsdk/command_lib/artifacts/docker_util.py).DeleteTagcalls from this run that were refused with status 7 (PERMISSION_DENIED), one per tagged image. For example:Permission 'artifactregistry.tags.delete' denied on resource '//artifactregistry.googleapis.com/projects/policyengine-api/locations/us-central1/repositories/policyengine-api/packages/policyengine-api/tags/cdc25d58…' (or it may not exist).DeleteVersiondirectly, which needsartifactregistry.versions.delete. Those 16 calls left no audit entries, because Data Access audit logs are not enabled in this project. Their failure follows from the role's permission list; it was not observed in the logs.IAM change (applied 30 Oct 2026, outside this PR)
For deletions to succeed, the deploy account needs
artifactregistry.tags.deleteandartifactregistry.versions.deleteon thepolicyengine-apirepository. These were granted at 16:40 UTC on 30 Oct 2026, using the predefined role scoped to this one repository:The repository's IAM policy now binds the account to both
roles/artifactregistry.repoAdminand the existingroles/artifactregistry.writer. On top ofwriter,repoAdminalso allowsfiles.delete,packages.delete,repositories.deleteArtifacts,rules.create/update/deleteandversions.update. A custom role with only the two delete permissions would have been the narrower alternative.The script on
masteralready attempts the deletions. So the next production release run deletes every image older than the newest 15 (108 at the time of writing), whether or not this PR has merged. Cloud Run revisions that use those images will no longer be able to start. Automated rollback goes back to the revision that was serving just before the deploy, by revision name, and that image is always among the newest 15. The script header already records this trade-off. The 6policyengine-api-app-engineimages are not used: App Engine is disabled (USER_DISABLED), and its only version is stopped and uses an image inus.gcr.io.Environment variables
The changed script reads these variables. None are new.
KEEP,DRY_RUN15,0)AR_LOCATION,AR_PROJECT,AR_REPOCLOUD_RUN_REGION,CLOUD_RUN_PROJECT,CLOUD_RUN_ARTIFACT_REPOSITORYCLOUD_RUN_REGION,CLOUD_RUN_PROJECT,CLOUD_RUN_ARTIFACT_REPOSITORY(and the otherCLOUD_RUN_*defaults)cloud_run_set_defaultsincloud_run_env.shus-central1-docker.pkg.dev/policyengine-api/policyengine-api, which matches the run log and the repository the deploy jobs push to.GITHUB_SHA,GITHUB_RUN_NUMBERcloud_run_set_defaultsonly to build tag names. The cleanup does not use them.GCP_WORKLOAD_IDENTITY_PROVIDER,GCP_DEPLOY_SERVICE_ACCOUNTproductionenvironment has no secrets with these names)with:inputs togoogle-github-actions/auth@v2in the same job. The account's IAM policy grantsroles/iam.workloadIdentityUsertoprincipalSet://…/workloadIdentityPools/github/attribute.repository/PolicyEngine/policyengine-api. The audit log confirms the job authenticated asgithub-deployment@.CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE,GOOGLE_APPLICATION_CREDENTIALSgoogle-github-actions/auth@v2$GITHUB_ENV, so later steps in the job see them. The run log shows both set.ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION,OLD_SIMULATION_GATEWAY_URLenv:(a literal value, and a repository variable)Testing
tests/unit/test_workflow_guard_scripts.py: replacedtest_cleanup_counts_only_successful_deletions, which asserted exit 0 when every deletion failed, withtest_cleanup_reports_each_failed_deletion_and_fails. Addedtest_cleanup_fails_when_the_listing_fails. The fakegcloudcan now fail specific images and the listing.test_cleanup_deletes_all_but_the_newest_images,test_cleanup_dry_run_deletes_nothing,test_cleanup_keeps_everything_within_the_limit). All 5 passed.gcloud: it exits 0 withDeleted 0/2 image(s)., and exits 0 withImages: 0when the listing fails. The new script exits 1 in both cases.python scripts/run_quality_guards.py(shellcheck over every script, and the other static guards),ruff format --checkandruff checkon the test file: all pass.🤖 Generated with Claude Code