Repository navigation
Conversation
Replace automatic image deletion with repository-linked inventory and reviewed plans. Add release and graph protection, a checked publication freeze, protected approval, bounded apply, durable results, and operator guidance. Signed-off-by: nicolejms <nicolej@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: nicolejms <nicolej@microsoft.com>
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned Files
|
Allow verified test-func runtime tags in functional cleanup profiles without bypassing protection. Pass both environment secret names through the apply caller and add regression checks and operator guidance. Signed-off-by: nicolejms <nicolej@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Hold candidates without per-release and combined protected manifest-closure approval. Check known child, subject, and referrer edges against the supplied closure. Expose the bounded API budget in both manual wrappers and document full-inventory restart limits. Signed-off-by: nicolejms <nicolej@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Artifact confidentiality, legacy signature-subject protection, and expiry enforcement need correction before approval.
4 open issues
What changed in this PR
Replaces automatic package deletion with release-aware inventory, saved plans, and separately approved cleanup. Deletion remains disabled by default.
Changes:
- Adds release evidence, dependency protection, and bound deletion plans.
- Introduces guarded apply with approval, publication-freeze checks, and durable reports.
- Adds offline tests and an operator guide.
| File | Description |
|---|---|
docs/contributing/README.md |
Links the cleanup guide. |
docs/contributing/package-cleanup.md |
Documents setup, approval, and recovery. |
docs/contributing/contributing-code/contributing-code-github-workflows/README.md |
Updates cleanup behavior and testing guidance. |
build/test.mk |
Integrates engine tests. |
.github/workflows/purge-old-images.yaml |
Makes scheduled/manual cleanup plan-only. |
.github/workflows/package-cleanup-apply.yaml |
Adds explicitly activated apply dispatch. |
.github/workflows/__package-cleanup.yaml |
Coordinates planning, approval, apply, and artifacts. |
.github/scripts/purge-old-images.sh |
Provides bounded, fixed-origin transport. |
.github/scripts/purge-old-images_test.sh |
Tests transport retries and restrictions. |
.github/scripts/package-cleanup.mjs |
Implements inventory, protection, planning, and apply. |
.github/scripts/package-cleanup_test.mjs |
Tests engine behavior and workflow contracts. |
.github/package-cleanup-evidence.json |
Seeds an intentionally incomplete evidence ledger. |
Review effort: Balanced
Unit Tests 2 files ± 0 424 suites +3 15m 24s ⏱️ - 2m 21s Results for commit b4e3d54. ± Comparison against base commit 765c6ad. This pull request removes 3 and adds 66 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #13368 +/- ##
==========================================
+ Coverage 60.97% 61.09% +0.12%
==========================================
Files 746 749 +3
Lines 44216 44439 +223
==========================================
+ Hits 26959 27152 +193
- Misses 17257 17287 +30 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Save bounded redacted response evidence before parsing or rate handling. Corroborate version absence with read-only requests under the same workflow credential, and stop on contradictory or inconclusive access checks. Cover both revalidation and DELETE 404 paths without changing release or publication gates. Signed-off-by: nicolejms <nicolej@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Distinguish package Admin from repository and Manage Actions Write access. Require the credential owner to confirm the actual workflow token principal and scopes without inferring a historical 404 cause from a named account's role. Signed-off-by: nicolejms <nicolej@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Project release, PR and run metadata before checkpoints. Stop public inventory for non-public or unknown package visibility without saving identifiers. Protect legacy Cosign subjects before graph propagation. Check live freeze and plan deadlines after revalidation before every DELETE, preserving partial results on expiry. Signed-off-by: nicolejms <nicolej@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Radius functional test overviewClick here to see the test run details
Test Status⌛ Building Radius and pushing container images for functional tests... |

Summary
Replace automatic container deletion with repository-linked public inventory, saved deletion plans, and a separate guarded apply workflow. Scheduled runs now create plans only. Deletion remains disabled after merge.
The shared engine protects historical releases and both RC formats, release aliases and digest copies, platform manifests, CLI packages, charts, signatures, SBOMs, attestations, and active references. It holds unknown tags/history, untagged versions, unavailable metadata, and incomplete graphs. The
functionalandreviewed-testsprofiles recognize runtimetest-func<10 hex>-<run>-<attempt>tags, includingdev/ucpd. Bicep recipe paths still need explicit reviewed evidence; mixed release tags and active references still retain the version. Valid legacy Cosignsha256-<64 lowercase hex>.sig/.att/.sbomtags protect their encoded subjects before copy/descendant propagation, even without an OCIsubjectfield.Each historical release needs explicit
closureComplete: trueapproval and a complete list of OCI manifest digests, including platform children, subjects, and referrers. Other protected digests and seeded baselines need combinedprotectedClosureCompleteapproval. Known graph edges check these claims. External roots are not treated as traversed: missing closure proof holds candidates, and supplied child digests protect in-scope copies. Both manual wrappers exposeapi_budget(default 4,000; integer range 1..20,000) through the reusable workflow. Partial inventory cannot be approved or resumed by replay.Public evidence boundary: Release responses are projected to
id,tag_name, andupdated_atbefore checkpoints; draft bodies, titles, assets, and authors are never saved. PRs retain onlynumber; active runs retain onlyid/path. Listing visibility is checked before any package-specific request. Private/internal/unknown entries stop inventory as incomplete, saving only a withheld count, not identifiers or raw bodies. Package details must also confirm public visibility before version reads. Other-repository packages remain excluded. A restricted evidence path for non-public inventory is not implemented; missing coverage cannot silently be called complete.Apply binds the approved plan to its trusted main run/attempt, source and policy hashes, metadata, and evidence. It requires a plan under six hours old, protected human approval, and a checked publication freeze. Release/PR/version revalidation happens before the final publication gate. The gate reads a live clock after all its API calls; apply reads the clock again immediately before each DELETE and checks both the lease and plan lifetime/future validity. Expiry during inventory, revalidation, gate reads, or later candidates stops with partial status, completed counts preserved, and remaining work unfinished. Whole-package deletion is forbidden. The apply caller passes both
CLEANUP_PACKAGE_TOKENandCLEANUP_PUBLICATION_FREEZEby name; protected-environment values take precedence and must be nonempty.404 correctness: A DELETE or exact-version revalidation GET 404 is not proof of absence. Each exact-version GET/DELETE and diagnostic GET saves its method, fixed API path, UTC time, status, redacted body (at most 4,096 characters), truncation flag, and allowlisted GitHub request ID in
report.jsonbefore parsing or rate handling. Follow-up checks use the same workflow credential. A follow-up GET 200 or still-listed version stops all further DELETEs. Only another 404 plus a matching accessible package and complete valid version list excluding the ID establishalready-absent. Inconclusive checks fail closed and retain descendants. Rate/budget limits remain partial with evidence. HTTP 204 is the only deleted result; exact known HTTP 400 download protection still warns and continues with independent candidates. No specific permission cause is inferred.Reason for change
A test tag does not prove that a version is safe to remove. Release content can share tags or digests with test artifacts, and revalidation alone cannot prevent a publication race. A 404 can conceal an access or visibility problem. Privileged inventory responses must not expose private package or draft-release content through public artifacts. This extends the bounded curl retry helper and preserves the exact protected-download behavior merged in #13363.
Fixes: N/A; maintainer-directed cleanup strategy.
MANUAL SETUP / HUMAN APPROVAL before deletion
closureCompleteand combinedprotectedClosureCompleteonly after review. A release-manifest asset is not assumed. If organization listing includes non-public/unknown-visibility packages, this public evidence path blocks rather than disclosing or silently omitting them; a separately approved restricted path would need a later implementation.package-cleanupwith required reviewers, prevent self-review, disallow admin bypass, and permit exactlymain. Confirm organization-secret access forGH_RAD_CI_BOT_PAT. Configure environment-onlyCLEANUP_PACKAGE_TOKENandCLEANUP_PUBLICATION_FREEZE. The credential/package owner must verify the actual token principal, not infer it from a secret name or assumed bot account. A classic PAT needsread:packages,delete:packagesfor apply, repository/draft/Actions metadata access (reposcope), and package Admin access. Repository Write, inherited access, and Manage Actions Write do not by themselves prove package Admin access. Only one listed reviewer must approve; preventing self-review requires a second person when the dispatcher is a reviewer.__package-cleanup.yaml, and stop other runs. Coordinate external, sibling, and manual publication/tag changes for the full window. Set the SHA-bound freeze lease for at most six hours. Cleanup concurrency does not lock publishers.purge-old-images.yamlonmainand review its artifacts. If inventory exhausts its budget, start a new full inventory with a largerapi_budget, up to 20,000. Explicitly setPACKAGE_CLEANUP_ENABLE_APPLY=true, dispatchpackage-cleanup-apply.yamlwith the exact plan run ID/hash, matching profile/retention, and a sufficient bounded budget, then obtain independent environment approval. Disable activation and restore prior workflow states after apply stops.docs/contributing/package-cleanup.md.Limits: Production/legacy/air-gap/CLI/chart/devcontainer cleanup is not enabled. Untagged versions remain held. Non-public/unknown package visibility blocks public inventory; a restricted evidence destination is not provided. Unavailable registry metadata and incomplete linked graphs block safe selection. External closure needs reviewed proof; this engine cannot discover omitted external manifests. Full inventories above 20,000 logical requests remain blocked and need separately reviewed partition/checkpoint work. External/manual publication exclusion needs human coordination. Artifacts last 30 days, subject to repository limits. No storage-reclaimed claim is made. The earlier main-run 404 cause and actual workflow credential subject/scopes remain unconfirmed. A separate user-directed rerun of the old main workflow is not validation or activation of this engine.
No package was deleted by this work, no live purge was enabled, and no GitHub setting, secret, or environment was changed by this session. Live package inventory/apply were not attempted. No token refresh, scope expansion, or duplicate workflow run was performed.
How to test
Use Node.js 24, Bash, curl, jq, ShellCheck, Go, and the root pnpm dependencies. Run these PowerShell commands from the repository root. Offline tests need no GitHub credential. Expect shell transport checks and 40 Node tests to pass without real deletion. Protected/rate-limit warnings are fixture output. The Make target runs in existing unit-test CI through
make test.bash -c 'make test-purge-old-images'shellcheck -x .github\scripts\purge-old-images.sh .github\scripts\purge-old-images_test.shgo run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 -shellcheck= .github\workflows\purge-old-images.yaml .github\workflows\__package-cleanup.yaml .github\workflows\package-cleanup-apply.yamlnpx --yes pnpm@11.24.0 exec prettier --config .github\linters\.prettierrc.yml --check .github\scripts\package-cleanup.mjs .github\scripts\package-cleanup_test.mjs .github\package-cleanup-evidence.jsonnpx --yes pnpm@11.24.0 exec markdownlint-cli2 docs\contributing\package-cleanup.md --config .github\linters\.markdownlint-cli2.yamlnpx --yes pnpm@11.24.0 exec markdown-table-formatter docs\contributing\package-cleanup.md --checkbash -c 'make spellcheck'git --no-pager diff --cached --checkCoverage includes scope/pagination, mixed tags, historical/RC protection, unknown evidence, Bicep media types, retained/shared children, deletion order, exact protected-download continuation, stale/tampered plans, changed metadata, malformed API responses, rate limits/caps, plan-only behavior, trusted artifacts, and approval/freeze contracts. Runtime
dev/ucpdfixtures retain evidence/namespace/graph guards. External closure and API-budget fixtures remain covered.New review regressions use draft body/asset/author, PR/run, private package/tag, changed visibility, and failed API canaries. They verify every saved checkpoint, the embedded public plan, and error output; incomplete private inventory cannot produce a plan or issue private package-specific requests. Legacy Cosign tests cover all three suffixes, subject copies/platform descendants, and near matches. Deterministic clocks advance during inventory, release/PR/version reads, early/final gate API reads, and a later candidate; no DELETE occurs after expiry, and completed counts/unfinished results persist. Future-dated plans also stop.
The 404 fixtures cover both origins, contradictory 200/still-listed results (including later pages), corroborated absence, denied/hidden access, malformed bodies/records, body bounds/redaction, request IDs, safe JSON errors, and diagnostic rate/budget partials. The shell fixture checks body/request-ID transport. All apply HTTP responses are fake.
Latest signed revision:
b4e3d54619686519a0413885d9f0186639d12814. GitHub verifies its GPG signature as valid. It has the requested sign-off and final Copilot App co-author trailer; no commit was amended. All four review findings were replied to and resolved after verification. Live workflow execution/deletion was not run and still needs the setup, complete evidence, and separately approved freeze window.File change summary
.github/scripts/package-cleanup.mjs.github/scripts/package-cleanup_test.mjs.github/scripts/purge-old-images.sh.github/scripts/purge-old-images_test.sh.github/package-cleanup-evidence.json.github/workflows/__package-cleanup.yaml.github/workflows/purge-old-images.yaml.github/workflows/package-cleanup-apply.yamlbuild/test.mkdocs/contributing/package-cleanup.mddocs/contributing/README.mddocs/contributing/contributing-code/contributing-code-github-workflows/README.md