Repository navigation
Conversation
Since ubuntu-latest moved to Ubuntu 26.04, Docker attaches a provenance attestation to the Cloud Run image, so each release pushes an image index that references the image manifest and an attestation manifest. The cleanup counts every manifest, keeps the newest 15, and fails when it tries to delete a child manifest whose index it keeps. These tests describe the fix: the build turns off the provenance attestation, and the cleanup keeps the newest releases by tag, deletes an old release's tagged manifest before the untagged manifests it references, and skips a manifest that another manifest still references. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cleanup now keeps every untagged manifest uploaded since the oldest kept release, so KEEP must name at least one release. Reject KEEP values that aren't a whole number of 1 or more, before listing anything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ubuntu-latest runner moved to Ubuntu 26.04 (Docker 29.4.2, which
stores images in containerd) between the 2026-11-10 and 2026-11-11
releases. Docker 28.0.4 on 24.04 dropped BuildKit's default provenance
attestation; Docker 29 keeps it, so each release pushed an image index
that references the image manifest and an attestation manifest. The
cleanup counted manifests, so its 15 kept about 5 releases, and when the
limit fell inside a release it kept the index but tried to delete a
child. Artifact Registry refuses that ("manifest is referenced by parent
manifests"), and every release run since 2026-11-12 22:58 UTC has failed
in this job.
- build_cloud_run_image.sh passes --provenance=false to docker build, so
each release pushes one image manifest.
- cleanup_cloud_run_images.sh lists tags and counts releases as tagged
manifests. It keeps the newest KEEP releases, deletes older tagged
manifests first, then the untagged manifests uploaded before the oldest
kept release. A deletion refused because another manifest still
references the manifest is logged and not counted as a failure. KEEP
must be 1 or more. Same-time manifests sort with LC_ALL=C.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Say exactly which kept-index manifests the upload-time comparison keeps, and which ones Artifact Registry's refusal protects instead. - Correct the test helper's description of how its digests sort. - Make the fake registry drop deleted manifests from later listings, and test that a manifest refused because its untagged index still existed is deleted by the next run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Say that a manifest uploaded just before its kept index is tried, and refused, only on runs in which that index is the oldest kept release, and test it with KEEP=2 over three runs. - Read the per-run logs without a fallback that can no longer happen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Say that a manifest uploaded just before its kept index gets a refused deletion request on runs in which it is older than the oldest kept release, which is normally while that index is the oldest kept release, and name the test for what it checks. 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 #4311 +/- ##
=======================================
Coverage 93.47% 93.47%
=======================================
Files 369 369
Lines 21455 21455
Branches 3400 3400
=======================================
Hits 20056 20056
Misses 816 816
Partials 583 583
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ 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.
Fixes #4308
Summary
The release workflow's last job, "Clean up old Cloud Run images", has failed in every release run that reached it since the 2026-11-12 22:58 UTC release. Since
ubuntu-latestbecame Ubuntu 26.04, each release pushes three manifests instead of one, and the cleanup deletes them in an order that Artifact Registry refuses. This PR makes the build push one manifest again and makes the cleanup count, keep and delete by release..github/scripts/build_cloud_run_image.shpasses--provenance=falsetodocker build, in both the dry-run and the real invocation (now one shared command array), so each release pushes a single image manifest..github/scripts/cleanup_cloud_run_images.shlists tags (--include-tags) and counts a release as a tagged manifest:KEEP(15) tagged manifests are the kept releases; older tagged manifests are deleted first. For the releases built on 26.04 so far, those are image indexes.referenced by parent manifestsis logged asKept <ref>: another manifest still references it.and not counted as a failure. That happens to a manifest uploaded just before the kept index that references it, normally while that index is the oldest kept release, and to a manifest whose untagged index (left when a tag is pushed again) is deleted later in the same run; the next run deletes that one.KEEPmust be a whole number of 1 or more (checked before listing). Rows sort withLC_ALL=C, so manifests with the same upload time always sort in the same order.A failed listing or any other deletion failure still fails the job,
DRY_RUN=1still only prints, and the script still runs on bash 3.2.The script's header comment, the comment above the workflow step, and a changelog fragment (
changelog.d/cloud-run-image-cleanup.fixed.md).Evidence
Runner and build. The 2026-11-10 release (run 42898604898,
df394c589) built onImage: ubuntu-24.04(runner imageubuntu24/20261101.360, Docker 28.0.4). Its "Deploy and test staging Cloud Run candidate" log shows#15 writing image sha256:4ceca0b3…. From the 2026-11-11 release (run 43051821762,8b906f8c0) the job runs onImage: ubuntu-26.04(ubuntu26/20261111.196, Docker 29.4.2). Run 43489854522 (f9c619dd8) shows:Docker 29 stores images in containerd, which keeps the provenance attestation BuildKit attaches by default, so the push is an image index (tagged with the release commit) that references the image manifest and an attestation manifest (both untagged).
--provenance=falsegives one manifest. Locally, Docker 29.6.2 with the containerd image store (driver-type io.containerd.snapshotter.v1) anddocker build --platform linux/amd64 --metadata-file …of aFROM scratchimage:containerimage.descriptor.mediaTypeexporting manifest,exporting attestation manifest,exporting manifest listapplication/vnd.oci.image.index.v1+json--provenance=falseexporting manifestonlyapplication/vnd.oci.image.manifest.v1+jsonNo SBOM attestation was attached by default, so
--sbom=falseisn't needed. Nothing was pushed to Artifact Registry.Cleanup counts. On 24.04 the cleanup listed 16 manifests and deleted 1 per release (run 42898604898). On 26.04 it listed 18 and deleted 3 (run 43051821762), so
KEEP=15kept about 5 releases. The failed runs:2eae02aeb7dcceef4…(releasecd513dacc)9b02ce57b81744ed7…(release3fa2de575)f843c3f5a72b84d95…(releasec4419dcea)f9c619dd80bbc9874…(releasedf9d51c64), twiced7042b2261546b9e1…(release2eae02aeb), twiceEach refusal reads like this one from run 43489854522:
Working the counts through from the first 26.04 release, every manifest of the refused child's release was due for deletion in each of these runs. The order of the deletions caused the failures, not a 15-manifest limit falling inside a release. Each refused child's digest sorts above its index's digest (
dddf4ca5>7dcceef4,e8294dd0>81744ed7,e13d0247>72b84d95,77689f2fand646094b7>0bbc9874,c30b7f0eand984e8447>1546b9e1). That fits the three manifests of a release being listed with the samecreateTime: the oldsort -ron whole rows then ordered them by digest. The old script didn't printcreateTime, so this is inferred, not observed. The new script doesn't depend on it: it deletes tagged manifests before untagged ones, and logs a refusal instead of failing. Deleting an index left its children in the registry, and later runs listed and deleted them.The listing format comes from the Google Cloud SDK source (579.0.0,
command_lib/artifacts/docker_util.py): for a repository,gcloud artifacts docker images list --include-tagsreturnscreateTime(the upload time),package,version(the digest) andtags. A local check with the SDK's ownvalue[separator='|']printer showed several tags joined with;and an empty field for an untagged manifest. The job installsversion: latestof the SDK.Environment variables and secrets
No variable or secret is added. What the changed scripts read, and where each comes from:
cleanup_cloud_run_images.sh: jobcleanup-cloud-run-images(environment: production), step "Prune old Cloud Run images", which has no job or stepenv:.KEEP,DRY_RUN,AR_LOCATION,AR_PROJECT,AR_REPOCLOUD_RUN_*values below)env:entry.CLOUD_RUN_REGION,CLOUD_RUN_PROJECT,CLOUD_RUN_ARTIFACT_REPOSITORYcloud_run_env.sh(us-central1,policyengine-api,policyengine-api)CLOUD_RUN_*andV2_*names incloud_run_set_defaultscloud_run_env.sh;V2_SUPABASE_PROJECT_REFandV2_SUPABASE_ENVIRONMENTare only exportedGITHUB_SHA,GITHUB_RUN_NUMBERcloud_run_set_defaults; not used by the cleanup.CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE,GOOGLE_APPLICATION_CREDENTIALS,CLOUDSDK_CORE_PROJECT, …)google-github-actions/auth@v2, from the GitHub Actions secretsGCP_WORKLOAD_IDENTITY_PROVIDERandGCP_DEPLOY_SERVICE_ACCOUNTid-token: write. The auth step exports the credential variables to later steps, as run 43489854522's log shows for the cleanup step.build_cloud_run_image.sh: job "Deploy and test staging Cloud Run candidate" (environment: staging), step "Build and push Cloud Run image".CLOUD_RUN_IMAGE_TAG,CLOUD_RUN_TAGcompute_cloud_run_metadata.sh, fromGITHUB_SHAandGITHUB_RUN_NUMBER)env:sets both fromsteps.cloud_run.outputs.CLOUD_RUN_IMAGE_PLATFORM,CLOUD_RUN_IMAGE_URI,CLOUD_RUN_IMAGE_NAME,CLOUD_RUN_REGION,CLOUD_RUN_PROJECT,CLOUD_RUN_ARTIFACT_REPOSITORYcloud_run_env.sh(linux/amd64, and the URI built from the others and the tag)env:setsCLOUD_RUN_SERVICEand runtime-cache settings, which the build doesn't use.CLOUD_RUN_DRY_RUN0google-github-actions/auth@v2;gcloud auth configure-dockersets up DockerUnverified: the cleanup's deletions need
artifactregistry.versions.deleteandartifactregistry.tags.delete, and its listingartifactregistry.dockerimages.list, on the deploy service account. I can't read the IAM policy from here. The same account listed and deleted manifests in the failed runs (2 deleted in run 43489854522), and--include-tagsdoesn't change the API call that the current SDK makes for a repository listing.Testing
Only the tests this PR adds or changes, by node ID:
tests/unit/test_workflow_guard_scripts.py: the 13test_cleanup_*tests plus 4 cases oftest_cleanup_rejects_a_keep_below_one_release. The fakegcloudworks like Artifact Registry in two ways: it refuses to delete a child manifest while its parent index still exists, and it drops deleted manifests from later listings. The test releases mirror the failed runs: same upload time for the three manifests, and an attestation manifest that sorts ahead of its index. Two tests run the cleanup several times against one fake registry: a manifest refused because its untagged index still existed is deleted by the next run; and withKEEP=2, a manifest uploaded a second before its index gets no deletion request while that index is the newer kept release, a refused one while it is the oldest kept release, and is deleted after the index once the index is no longer kept.tests/unit/test_cloud_run_candidate_deploy.py:test_build_cloud_run_image_dry_run_turns_off_provenance_attestations,test_build_cloud_run_image_pushes_one_manifest_without_provenance(real path, fakegcloudanddocker), and the two existing build tests.The new tests were committed first and failed against the old scripts (11 of 13, then 4 of 4). All 21 pass now, and the cleanup tests also pass under macOS's
/bin/bash3.2.57.make qualitypassed in the pre-commit hook, andshellcheck, run the way theci-scriptsguard runs it, reports nothing for the changed scripts.Not tested: the
LC_ALL=Con the sort. It only fixes the order in which manifests with the same upload time are tried. A different order can at most leave a refused manifest for the next run, as in the two-run test. The fake listing prints its rows whatever the arguments, so the;-joined tags and thecreateTimemapping rest on the SDK source and the printer check above, not on the tests.After merging
The first release after this merges builds a single-manifest image. Its cleanup keeps all the releases in the registry (there are fewer than 15), deletes the image and attestation manifests left behind by the failed runs, and keeps the 26.04 releases' indexes and the manifests they reference until they age out of the newest 15.
🤖 Generated with Claude Code