Skip to content

ci: add release-safe package cleanup plans and guarded apply - #13368

Draft
nicolejms wants to merge 6 commits into
mainfrom
nicolejms-release-safe-package-cleanup
Draft

nicolejms wants to merge 6 commits into
mainfrom
nicolejms-release-safe-package-cleanup

Conversation

@nicolejms

@nicolejms nicolejms commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

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 functional and reviewed-tests profiles recognize runtime test-func<10 hex>-<run>-<attempt> tags, including dev/ucpd. Bicep recipe paths still need explicit reviewed evidence; mixed release tags and active references still retain the version. Valid legacy Cosign sha256-<64 lowercase hex>.sig/.att/.sbom tags protect their encoded subjects before copy/descendant propagation, even without an OCI subject field.

Each historical release needs explicit closureComplete: true approval and a complete list of OCI manifest digests, including platform children, subjects, and referrers. Other protected digests and seeded baselines need combined protectedClosureComplete approval. 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 expose api_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, and updated_at before checkpoints; draft bodies, titles, assets, and authors are never saved. PRs retain only number; active runs retain only id/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_TOKEN and CLEANUP_PUBLICATION_FREEZE by 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.json before 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 establish already-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

  1. Review and merge complete historical-release, manifest-closure, and active-reference evidence. The initial ledger is deliberately incomplete and expired. For external/unavailable graphs, obtain an independently supported complete manifest inventory, not just roots. Set per-release closureComplete and combined protectedClosureComplete only 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.
  2. Create package-cleanup with required reviewers, prevent self-review, disallow admin bypass, and permit exactly main. Confirm organization-secret access for GH_RAD_CI_BOT_PAT. Configure environment-only CLEANUP_PACKAGE_TOKEN and CLEANUP_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 needs read:packages, delete:packages for apply, repository/draft/Actions metadata access (repo scope), 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.
  3. Record prior workflow states, disable all other repository workflows except both cleanup wrappers and __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.
  4. Run purge-old-images.yaml on main and review its artifacts. If inventory exhausts its budget, start a new full inventory with a larger api_budget, up to 20,000. Explicitly set PACKAGE_CLEANUP_ENABLE_APPLY=true, dispatch package-cleanup-apply.yaml with 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.
  5. Investigate inconclusive 404s read-only with the same workflow credential and saved bodies/request IDs. A named account's role, public browser page, or different credential does not establish the workflow token's access or historical 404 cause. Do not retry DELETE, refresh tokens, add scopes, or treat a diagnosis as removal approval. Resume partial deletion only with a newly generated and reviewed plan. Inventory checkpoints are evidence, not resume cursors. Download-protected versions need a maintainer-led GitHub Support request. Exact inputs, lease fields, restart/resume steps, and Support details are in 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.

Exact command Actual result
bash -c 'make test-purge-old-images' Passed: shell transport checks and 40 Node tests.
shellcheck -x .github\scripts\purge-old-images.sh .github\scripts\purge-old-images_test.sh Passed.
go 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.yaml Passed; workflow safety contracts also pass in the Node suite.
npx --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.json Passed.
npx --yes pnpm@11.24.0 exec markdownlint-cli2 docs\contributing\package-cleanup.md --config .github\linters\.markdownlint-cli2.yaml Passed: no issues.
npx --yes pnpm@11.24.0 exec markdown-table-formatter docs\contributing\package-cleanup.md --check Passed.
bash -c 'make spellcheck' Passed: 351 files, no issues.
git --no-pager diff --cached --check Passed before commit.

Coverage 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/ucpd fixtures 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

File Summary of change
.github/scripts/package-cleanup.mjs Shared inventory/protection, minimal public evidence, bound plans, live per-delete deadlines, Cosign subjects, guarded apply, and same-credential 404 evidence/checks.
.github/scripts/package-cleanup_test.mjs 40 offline tests, including privacy canaries, Cosign subjects/copies, deterministic expiry, 404 diagnostics, closure, and budgets.
.github/scripts/purge-old-images.sh Plan-by-default fixed-origin transport, bounded retries, body capture, and allowlisted request-ID forwarding.
.github/scripts/purge-old-images_test.sh Offline retries, rate limits, unsafe paths, and 404 body/request-ID/header checks.
.github/package-cleanup-evidence.json Incomplete/expired ledger with closure approval false, preventing unreviewed deletion.
.github/workflows/__package-cleanup.yaml Reusable plan, preflight, protected apply, bounded API budget, and 30-day evidence artifacts; environment secret precedence.
.github/workflows/purge-old-images.yaml Scheduled/manual plan-only wrapper; scheduled failure issues and manual API budget.
.github/workflows/package-cleanup-apply.yaml Main-only activated dispatch bound to exact plan run/hash; protected secret names and API budget.
build/test.mk Run Node cleanup tests in existing unit-test CI.
docs/contributing/package-cleanup.md Operator setup, public/restricted evidence boundary, closure, Cosign subjects, live expiry, holds, approval/freeze, budgets, resume, and package Admin/access diagnostics.
docs/contributing/README.md Operator-guide navigation.
docs/contributing/contributing-code/contributing-code-github-workflows/README.md Updated plan-only behavior and test command.

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>
Copilot AI balanced review requested due to automatic review settings October 20, 2026 18:47
@nicolejms
nicolejms requested review from a team as code owners October 20, 2026 18:47
@nicolejms nicolejms added the pr:standard Ongoing maintenance, minor improvements, documentation updates, and routine development work label Oct 20, 2026
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

  • .github/workflows/purge-old-images.yaml

@github-actions github-actions Bot added the pr:waiting-for-review A reviewer owns the next action; required approval is pending or unverifiable label Oct 20, 2026
nicolejms and others added 2 commits October 20, 2026 11:52
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread .github/scripts/package-cleanup.mjs Outdated
Comment thread .github/scripts/package-cleanup.mjs
Comment thread .github/scripts/package-cleanup.mjs Outdated
Comment thread .github/scripts/package-cleanup.mjs
@github-actions github-actions Bot added pr:waiting-for-author The author needs to address human review feedback or human-endorsed inline feedback and removed pr:waiting-for-review A reviewer owns the next action; required approval is pending or unverifiable labels Oct 20, 2026
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Unit Tests

    2 files  ± 0    424 suites  +3   15m 24s ⏱️ - 2m 21s
7 373 tests +63  7 371 ✅ +63  2 💤 ±0  0 ❌ ±0 
8 542 runs  +65  8 540 ✅ +65  2 💤 ±0  0 ❌ ±0 

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.
github.com/radius-project/radius/pkg/rp/util ‑ Test_PathParser
github.com/radius-project/radius/pkg/rp/util ‑ Test_PathParserDigestErr
github.com/radius-project/radius/pkg/rp/util ‑ Test_PathParserErr
github.com/radius-project/radius/pkg/cli/cmd/bicep/publish ‑ TestRunner_Run_PrintsPinnedRecipeURL
github.com/radius-project/radius/pkg/cli/cmd/bicep/publish ‑ TestRunner_extractDestination/registry_port
github.com/radius-project/radius/pkg/rp/util ‑ Test_ReadFromRegistry_References
github.com/radius-project/radius/pkg/rp/util ‑ Test_ReadFromRegistry_References/digest
github.com/radius-project/radius/pkg/rp/util ‑ Test_ReadFromRegistry_References/digest_wins_over_moved_tag
github.com/radius-project/radius/pkg/rp/util ‑ Test_ReadFromRegistry_References/missing_digest
github.com/radius-project/radius/pkg/rp/util ‑ Test_ReadFromRegistry_References/missing_digest_does_not_fall_back_to_tag
github.com/radius-project/radius/pkg/rp/util ‑ Test_ReadFromRegistry_References/tag
github.com/radius-project/radius/pkg/rp/util ‑ Test_parsePath
github.com/radius-project/radius/pkg/rp/util ‑ Test_parsePath/digest
…

♻️ This comment has been updated with latest results.

@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 61.09%. Comparing base (a534953) to head (b4e3d54).
⚠️ Report is 14 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

nicolejms and others added 3 commits October 20, 2026 13:47
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>
@github-actions github-actions Bot added pr:waiting-for-review A reviewer owns the next action; required approval is pending or unverifiable and removed pr:waiting-for-author The author needs to address human review feedback or human-endorsed inline feedback labels Oct 20, 2026
@radius-functional-tests

radius-functional-tests Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Radius functional test overview

🔍 Go to test action run

Click here to see the test run details
Name Value
Repository radius-project/radius
Commit ref b4e3d54
Unique ID funcd5ce0f51b0
Image tag test-funcd5ce0f51b0-39496667367-1
  • Dapr: 1.14.4
  • Azure KeyVault CSI driver: 1.4.2
  • Azure Workload identity webhook: 1.3.0
  • Bicep recipe location ghcr.io/radius-project/dev/test/testrecipes/test-bicep-recipes/<name>:test-funcd5ce0f51b0-39496667367-1
  • Terraform recipe location http://tf-module-server.radius-test-tf-module-server.svc.cluster.local/<name>.zip (in cluster)
  • applications-rp test image location: ghcr.io/radius-project/dev/applications-rp:test-funcd5ce0f51b0-39496667367-1
  • dynamic-rp test image location: ghcr.io/radius-project/dev/dynamic-rp:test-funcd5ce0f51b0-39496667367-1
  • controller test image location: ghcr.io/radius-project/dev/controller:test-funcd5ce0f51b0-39496667367-1
  • ucp test image location: ghcr.io/radius-project/dev/ucpd:test-funcd5ce0f51b0-39496667367-1
  • deployment-engine test image location: ghcr.io/radius-project/deployment-engine:latest

Test Status

⌛ Building Radius and pushing container images for functional tests...
✅ Container images build succeeded
⌛ Publishing Bicep Recipes for functional tests...
✅ Recipe publishing succeeded
⌛ Starting ucp-cloud functional tests...
⌛ Starting corerp-cloud functional tests...
✅ ucp-cloud functional tests succeeded
✅ corerp-cloud functional tests succeeded

@nicolejms
nicolejms marked this pull request as draft October 20, 2026 22:00
@github-actions github-actions Bot added pr:needs-rebase The pull request has merge conflicts and removed pr:waiting-for-review A reviewer owns the next action; required approval is pending or unverifiable labels Oct 20, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:needs-rebase The pull request has merge conflicts pr:standard Ongoing maintenance, minor improvements, documentation updates, and routine development work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants