ghcr-manager 0.9.6 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/LICENSE +1 -1
  3. package/README.md +45 -63
  4. package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +0 -1
  5. package/dist/cleanup-summary/_cleanup-summary-markdown.js +149 -39
  6. package/dist/cleanup-summary/_cleanup-summary.d.ts +22 -10
  7. package/dist/cleanup-summary/_cleanup-summary.js +26 -11
  8. package/dist/cleanup-summary/index.d.ts +1 -1
  9. package/dist/cli/_cleanup-command.js +82 -23
  10. package/dist/cli/_json-output.d.ts +1 -0
  11. package/dist/cli/_json-output.js +11 -0
  12. package/dist/cli/_tag-selector-resolver.js +36 -13
  13. package/dist/cli/index.js +1 -5
  14. package/dist/core/_types.d.ts +9 -6
  15. package/dist/core/_types.js +8 -1
  16. package/dist/core/index.d.ts +1 -0
  17. package/dist/core/index.js +1 -0
  18. package/dist/db/_cleanup-run-writer.d.ts +1 -1
  19. package/dist/db/_cleanup-run-writer.js +28 -7
  20. package/dist/db/_db-merge-cleanup-copy.js +4 -2
  21. package/dist/db/_db-merge-scan-copy.js +3 -2
  22. package/dist/db/_manifest-reachability.js +47 -8
  23. package/dist/db/_scan-writer.js +0 -9
  24. package/dist/db/index.d.ts +2 -1
  25. package/dist/db/index.js +1 -0
  26. package/dist/db/planner/_planner-direct-target-root-options.d.ts +11 -0
  27. package/dist/db/planner/_planner-direct-target-root-options.js +1 -0
  28. package/dist/db/planner/_planner-direct-target-root-tag-filters.d.ts +9 -0
  29. package/dist/db/planner/_planner-direct-target-root-tag-filters.js +42 -0
  30. package/dist/db/planner/_planner-direct-target-roots-combined-sql.d.ts +7 -0
  31. package/dist/db/planner/_planner-direct-target-roots-combined-sql.js +198 -0
  32. package/dist/db/planner/_planner-direct-target-roots-combined.d.ts +4 -0
  33. package/dist/db/planner/_planner-direct-target-roots-combined.js +10 -0
  34. package/dist/db/planner/_planner-direct-target-roots-tagged.d.ts +4 -0
  35. package/dist/db/planner/_planner-direct-target-roots-tagged.js +125 -0
  36. package/dist/db/planner/_planner-direct-target-roots.d.ts +2 -11
  37. package/dist/db/planner/_planner-direct-target-roots.js +8 -192
  38. package/dist/db/planner/_planner-direct-target-tags.d.ts +1 -1
  39. package/dist/db/planner/_planner-direct-target-tags.js +7 -6
  40. package/dist/db/planner/_planner-output.js +34 -13
  41. package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.d.ts +1 -0
  42. package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.js +65 -0
  43. package/dist/db/planner/_planner-plan-artifacts-closure-sql.d.ts +1 -0
  44. package/dist/db/planner/_planner-plan-artifacts-closure-sql.js +195 -0
  45. package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.d.ts +1 -0
  46. package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.js +86 -0
  47. package/dist/db/planner/_planner-plan-artifacts.js +26 -128
  48. package/dist/db/planner/_planner-repository.d.ts +2 -1
  49. package/dist/db/planner/_planner-repository.js +3 -1
  50. package/dist/db/planner/_planner-sql.js +13 -2
  51. package/dist/db/planner/_planner-types.d.ts +23 -8
  52. package/dist/db/planner/_planner-types.js +14 -3
  53. package/dist/db/planner/index.d.ts +2 -1
  54. package/dist/db/planner/index.js +1 -0
  55. package/dist/execute/_plan-executor.d.ts +1 -1
  56. package/dist/execute/_plan-executor.js +38 -16
  57. package/dist/execute/_types.d.ts +2 -19
  58. package/dist/execute/_untag-client.d.ts +2 -2
  59. package/dist/execute/_untag-client.js +1 -42
  60. package/dist/execute/index.d.ts +1 -1
  61. package/dist/ingest/github/_manifest-kind.d.ts +7 -1
  62. package/dist/ingest/github/_manifest-kind.js +21 -6
  63. package/package.json +16 -10
  64. package/resources/sql/schema/001_schema.sql +17 -4
  65. package/dist/cli/_untag-command.d.ts +0 -1
  66. package/dist/cli/_untag-command.js +0 -57
  67. package/resources/sql/views/002_v_missing_digests.sql +0 -32
  68. package/resources/sql/views/003_v_scan_root_manifests.sql +0 -44
  69. package/resources/sql/views/004_v_digest_tag_relations.sql +0 -50
  70. package/resources/sql/views/005_v_cleanup_root_closure_members.sql +0 -101
  71. package/resources/sql/views/006_v_cleanup_blocking_overlaps.sql +0 -42
  72. package/resources/sql/views/007_v_cleanup_root_decision_readable.sql +0 -67
package/CHANGELOG.md CHANGED
@@ -7,6 +7,69 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.8] - 2026-06-03
11
+
12
+ ### Added
13
+
14
+ - Added dedicated graph-matrix GHCR scenarios and workflows to exercise shared-image, multi-arch, cosign, and
15
+ attestation cleanup cases in isolation.
16
+ - Added a local manifest-graph visualizer with browser UI for `ghcr-manager` SQLite databases, including manifest
17
+ details, zoom controls, one-hop expansion, and scan-to-scan compare mode.
18
+ - Added a separately publishable npm package, `ghcr-manager-visualizer`, plus user-facing visualizer documentation.
19
+ - Added repo-local manual visualizer demo scripts for seeding and updating GHCR packages during graph investigation.
20
+
21
+ ### Changed
22
+
23
+ - Cleanup planning was reworked around the current graph model, including direct SQL-backed tagged/untagged root
24
+ selection, graph-scoped closure walking, and refined `untag-only` vs `fully-deletable` decisions for complex
25
+ multi-arch, cosign, and attestation shapes.
26
+ - Large planner SQL bodies were split into smaller internal modules, and direct-target root selection logic was split
27
+ into smaller planner helpers.
28
+ - Orphaned digest-tag resolution now uses a direct latest-scan query instead of relying on older helper views.
29
+ - Manifest platform display now derives from descriptor data in the visualizer instead of relying on manifest-level
30
+ platform fields.
31
+ - Cross-architecture terminology is now consistently named `multi-arch` across runtime, tests, and docs.
32
+ - The root action and public CLI surface are now centered on `scan` and `cleanup` only.
33
+
34
+ ### Fixed
35
+
36
+ - Fixed digest-tag helper-edge direction and root-detection behavior so helper-tagged artifacts no longer interfere with
37
+ normal cleanup root semantics.
38
+ - Fixed shared-graph cleanup handling so selected indexes and helper-linked artifacts are deleted or retained according
39
+ to surviving real tags instead of simplistic descendant-only closure rules.
40
+
41
+ ### Removed
42
+
43
+ - The public `untag` CLI command, root-action mode, and dedicated direct-untag workflow coverage were removed. Internal
44
+ tag detachment for partial-tag cleanup matches remains part of `cleanup`.
45
+ - Several older cleanup helper views were removed after the planner rewrite moved the live logic into direct SQL query
46
+ paths.
47
+
48
+ ## [0.9.7] - 2026-05-23
49
+
50
+ ### Added
51
+
52
+ - The root action now prepares `cleanup` and `untag` CLI arguments through `tools/prepare-action-args.mjs`, keeping
53
+ printed and executed argument lists aligned.
54
+ - Cleanup planning now traverses recursively beyond `sha256-*` helper-tag manifest links as well, if deeper helper
55
+ chains ever occur.
56
+
57
+ ### Changed
58
+
59
+ - Cleanup dry-run output and GitHub step summaries were reworked to explain the plan more clearly, including a filters
60
+ table and clearer counts for tags, images, and cross-arch manifests.
61
+ - Informational manifest classification was tuned so only real multi-arch roots are labeled `multi_arch_manifest`, while
62
+ helper-tagged indexes remain `index_manifest`.
63
+ - `merge-run-artifacts` now uses a simpler current-run download flow with direct artifact download handling.
64
+ - Cleanup selected-tag audit and DB-merge metadata handling were tightened alongside the summary/output refactor.
65
+
66
+ ### Fixed
67
+
68
+ - `delete-orphaned-images` now carries orphaned `sha256-*` digest-tag targets through planner selection instead of
69
+ dropping them at the normal non-digest tag boundary.
70
+ - Fully deletable cleanup execution now deletes the planned closure package versions instead of deleting only the root
71
+ package version.
72
+
10
73
  ## [0.9.6] - 2026-05-21
11
74
 
12
75
  ### Added
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 gh-workflow
3
+ Copyright (c) 2026 Stefan Kuhn
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # ghcr-manager
2
2
 
3
- [![Release](https://img.shields.io/github/v/release/gh-workflow/ghcr-manager?style=flat-square)](https://github.com/gh-workflow/ghcr-manager/releases)
4
- [![Immutable Releases](https://img.shields.io/badge/releases-immutable-blue?labelColor=333)](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases)
5
3
  [![GitHub Marketplace](https://img.shields.io/badge/marketplace-ghcr--manager-blue?logo=github&labelColor=333&style=flat-square)](https://github.com/marketplace/actions/ghcr-manager)
6
- [![Tests](https://img.shields.io/github/actions/workflow/status/gh-workflow/ghcr-manager/.github/workflows/ci_change-validation.yml?branch=main&label=test&style=flat-square)](https://github.com/gh-workflow/ghcr-manager/actions/workflows/change-validation.yml)
4
+ [![Release](https://img.shields.io/github/v/release/ghcr-manager/ghcr-manager?style=flat-square)](https://github.com/ghcr-manager/ghcr-manager/releases)
5
+ [![Immutable Releases](https://img.shields.io/badge/releases-immutable-blue?labelColor=333)](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases)
6
+ [![Tests](https://img.shields.io/github/actions/workflow/status/ghcr-manager/ghcr-manager/.github/workflows/ci_change-validation.yml?branch=main&label=test&style=flat-square)](https://github.com/ghcr-manager/ghcr-manager/actions/workflows/ci_change-validation.yml)
7
7
 
8
8
  Inspect, review, and manage GitHub Container Registry packages.
9
9
 
@@ -12,7 +12,6 @@ Inspect, review, and manage GitHub Container Registry packages.
12
12
  - scanning one GHCR package into a SQLite database artifact
13
13
  - running cleanup with a GitHub step summary and optional DB artifact
14
14
  - previewing cleanup decisions with `dry-run` before making changes
15
- - directly removing selected tags with `untag`
16
15
 
17
16
  ## Quick Start
18
17
 
@@ -33,7 +32,7 @@ jobs:
33
32
 
34
33
  - name: Preview GHCR cleanup
35
34
  id: ghcr-manager
36
- uses: gh-workflow/ghcr-manager@0.9.6
35
+ uses: ghcr-manager/ghcr-manager@0.9.8
37
36
  with:
38
37
  command: cleanup
39
38
  token: ${{ github.token }}
@@ -55,16 +54,14 @@ After the run:
55
54
 
56
55
  ## Commands
57
56
 
58
- The action supports three commands:
57
+ The action supports two commands:
59
58
 
60
59
  - `cleanup`: Cleans using filters; use `dry-run` to preview the result
61
- - `untag`: Removes one or more tags directly
62
60
  - `scan`: Scans one package and uploads the resulting DB artifact
63
61
 
64
62
  ### Purpose of commands
65
63
 
66
64
  - `cleanup`: Normal entry point for registry maintenance
67
- - `untag`: Works directly without a full package scan
68
65
  - `scan`: For investigation and audit
69
66
 
70
67
  ## Common Usage
@@ -72,7 +69,7 @@ The action supports three commands:
72
69
  ### Preview cleanup
73
70
 
74
71
  ```yaml
75
- - uses: gh-workflow/ghcr-manager@0.9.6
72
+ - uses: ghcr-manager/ghcr-manager@0.9.8
76
73
  with:
77
74
  command: cleanup
78
75
  token: ${{ github.token }}
@@ -82,7 +79,7 @@ The action supports three commands:
82
79
  delete-tags: |
83
80
  pr-.*
84
81
  use-regex: true
85
- older-than: 30d
82
+ older-than: 30 days
86
83
  keep-n-tagged: "5"
87
84
  exclude-tags: |
88
85
  latest
@@ -93,7 +90,7 @@ The action supports three commands:
93
90
  ### Apply cleanup
94
91
 
95
92
  ```yaml
96
- - uses: gh-workflow/ghcr-manager@0.9.6
93
+ - uses: ghcr-manager/ghcr-manager@0.9.8
97
94
  with:
98
95
  command: cleanup
99
96
  token: ${{ github.token }}
@@ -109,26 +106,10 @@ If `scan-after-cleanup` is `true`, `cleanup` performs a second scan so the uploa
109
106
 
110
107
  Note: the second scan only runs if cleanup actually makes changes.
111
108
 
112
- ### Remove selected tags directly
113
-
114
- ```yaml
115
- - uses: gh-workflow/ghcr-manager@0.9.6
116
- with:
117
- command: untag
118
- token: ${{ github.token }}
119
- owner: OWNER
120
- package: PACKAGE
121
- delete-tags: |
122
- old-tag
123
- test-tag
124
- ```
125
-
126
- `untag` does not use a scan DB and does not support DB artifact upload.
127
-
128
109
  ### Scan one package
129
110
 
130
111
  ```yaml
131
- - uses: gh-workflow/ghcr-manager@0.9.6
112
+ - uses: ghcr-manager/ghcr-manager@0.9.8
132
113
  with:
133
114
  command: scan
134
115
  token: ${{ github.token }}
@@ -142,45 +123,48 @@ Note: the second scan only runs if cleanup actually makes changes.
142
123
 
143
124
  <!-- markdownlint-disable MD013 MD060 -->
144
125
 
145
- | Input | Description | Cmds | Required | Default |
146
- | ---------------------------- | ----------------------------------- | ---- | ----------- | ------------------------------ |
147
- | `command` | `scan`, `cleanup`, or `untag` | all | Yes | |
148
- | `token` | GitHub token for API calls | all | Yes | `${{ github.token }}` |
149
- | `owner` | Package owner | all | Yes | |
150
- | `package` | Package name | all | Yes | |
151
- | `db-path` | Local SQLite DB path | s,c | No | |
152
- | `upload-artifacts` | Upload DB and summary artifacts | s,c | No | `false` |
153
- | `scan-after-cleanup` | Run a second scan after cleanup | c | No | `false` |
154
- | `db-artifact-retention-days` | Override artifact retention days | s,c | No | `${{ github.retention_days }}` |
155
- | `delete-tags` | Newline-separated tags to delete | c,u | for `untag` | |
156
- | `exclude-tags` | Newline-separated tags to exclude | c | No | |
157
- | `keep-n-tagged` | Keep newest tagged roots | c | No | |
158
- | `keep-n-untagged` | Keep newest untagged roots | c | No | |
159
- | `delete-untagged` | Delete untagged roots | c | No | `false` |
160
- | `delete-ghost-images` | Delete ghost multi-arch roots | c | No | `false` |
161
- | `delete-partial-images` | Delete partial multi-arch roots | c | No | `false` |
162
- | `delete-orphaned-images` | Delete orphaned digest-derived tags | c | No | `false` |
163
- | `older-than` | Age cutoff for cleanup selectors | c | No | |
164
- | `use-regex` | Use regex for cleanup tag selectors | c | No | `false` |
165
- | `dry-run` | Show changes without mutating GHCR | c,u | No | `false` |
166
- | `log-level` | CLI log level | all | No | `info` |
126
+ | Input | Description | Cmds | Required | Default |
127
+ | ---------------------------- | ----------------------------------- | ---- | -------- | ------------------------------ |
128
+ | `command` | `scan` or `cleanup` | all | Yes | |
129
+ | `token` | GitHub token for API calls | all | Yes | `${{ github.token }}` |
130
+ | `owner` | Package owner | all | Yes | |
131
+ | `package` | Package name | all | Yes | |
132
+ | `db-path` | Local SQLite DB path | s,c | No | |
133
+ | `upload-artifacts` | Upload DB and summary artifacts | s,c | No | `false` |
134
+ | `scan-after-cleanup` | Run a second scan after cleanup | c | No | `false` |
135
+ | `db-artifact-retention-days` | Override artifact retention days | s,c | No | `${{ github.retention_days }}` |
136
+ | `delete-tags` | Newline-separated tags to delete | c | No | |
137
+ | `exclude-tags` | Newline-separated tags to exclude | c | No | |
138
+ | `keep-n-tagged` | Keep newest tagged roots | c | No | |
139
+ | `keep-n-untagged` | Keep newest untagged roots | c | No | |
140
+ | `delete-untagged` | Delete untagged roots | c | No | `false` |
141
+ | `delete-ghost-images` | Delete ghost multi-arch roots | c | No | `false` |
142
+ | `delete-partial-images` | Delete partial multi-arch roots | c | No | `false` |
143
+ | `delete-orphaned-images` | Delete orphaned digest-derived tags | c | No | `false` |
144
+ | `older-than` | Age cutoff for cleanup selectors | c | No | |
145
+ | `use-regex` | Use regex for cleanup tag selectors | c | No | `false` |
146
+ | `dry-run` | Show changes without mutating GHCR | c | No | `false` |
147
+ | `log-level` | CLI log level | all | No | `info` |
167
148
 
168
149
  <!-- markdownlint-enable MD013 MD060 -->
169
150
 
170
- `Cmds`: `s` = `scan`, `c` = `cleanup`, `u` = `untag`
151
+ `Cmds`: `s` = `scan`, `c` = `cleanup`
171
152
 
172
- Cleanup notes:
153
+ Cleanup command notes:
173
154
 
174
155
  - Tagged selector families may be combined with `delete-untagged`.
175
156
  - `exclude-tags` requires at least one tagged selector family.
176
157
  - `delete-untagged` and `keep-n-untagged` cannot be combined.
158
+ - `older-than` takes one integer plus one unit.
159
+ - Supported `older-than` units: `minutes`, `hours`, `days`, `weeks`, `months`, `years`.
160
+ - Example values: `30 days`, `2 hours`, `1 month`.
177
161
 
178
162
  ## Outputs
179
163
 
180
- | Output | Description |
181
- | ------------------- | ------------------------------------------------ |
182
- | `db-path` | SQLite DB path on the runner |
183
- | `summary-json-path` | Summary JSON file path for `cleanup` and `untag` |
164
+ | Output | Description |
165
+ | ------------------- | ------------------------------------ |
166
+ | `db-path` | SQLite DB path on the runner |
167
+ | `summary-json-path` | Summary JSON file path for `cleanup` |
184
168
 
185
169
  ## Artifacts
186
170
 
@@ -188,7 +172,6 @@ When artifacts are enabled:
188
172
 
189
173
  - `scan` always uploads one SQLite DB artifact
190
174
  - `cleanup` optionally uploads the DB artifact and a cleanup summary JSON artifact
191
- - `untag` uploads no artifacts
192
175
 
193
176
  Current naming:
194
177
 
@@ -199,12 +182,11 @@ Current naming:
199
182
 
200
183
  ## Documentation Map
201
184
 
202
- - [docs/action-usage.md](docs/action-usage.md): action commands, including `cleanup`, `scan`, and `untag`
203
- - [docs/db-merge-workflows.md](docs/db-merge-workflows.md): cleaning up multiple packages with one combined DB
204
- - [docs/schema-description.md](docs/schema-description.md): practical explanation of the SQLite schema
205
- - [docs/queries/missing-manifests-queries.md](docs/queries/missing-manifests-queries.md): SQL recipes for missing
206
- manifest references
207
- - [docs/cli-usage.md](docs/cli-usage.md): companion CLI usage
185
+ - [GitHub Action usage](docs/action-usage.md): action commands, including `cleanup` and `scan`
186
+ - [Visualizer](docs/visualizer.md): local graph inspection and scan-to-scan comparison
187
+ - [Multi-package workflows](docs/db-merge-workflows.md): cleaning up multiple packages with one combined DB
188
+ - [SQLite schema guide](docs/schema-description.md): practical explanation of the SQLite schema
189
+ - [CLI usage](docs/cli-usage.md): companion CLI usage
208
190
 
209
191
  ## Acknowledgment
210
192
 
@@ -2,5 +2,4 @@ import type { CleanupSummary } from "./_cleanup-summary.js";
2
2
  export declare function renderCleanupSummaryMarkdown(summary: CleanupSummary, options: {
3
3
  maxDirectTargetTags?: number;
4
4
  maxRootsPerSection?: number;
5
- maxTagsPerRoot?: number;
6
5
  }): string;
@@ -1,10 +1,11 @@
1
+ import { ManifestKinds } from "../core/index.js";
2
+ import { DeletePlanValidationStatuses } from "../db/index.js";
1
3
  const _DEFAULT_MAX_DIRECT_TARGET_TAGS = 100;
2
4
  const _DEFAULT_MAX_ROOTS_PER_SECTION = 100;
3
- const _DEFAULT_MAX_TAGS_PER_ROOT = 4;
5
+ const _DEFAULT_MAX_TAG_TEXT_LENGTH = 40;
4
6
  export function renderCleanupSummaryMarkdown(summary, options) {
5
7
  const maxDirectTargetTags = options.maxDirectTargetTags ?? _DEFAULT_MAX_DIRECT_TARGET_TAGS;
6
8
  const maxRootsPerSection = options.maxRootsPerSection ?? _DEFAULT_MAX_ROOTS_PER_SECTION;
7
- const maxTagsPerRoot = options.maxTagsPerRoot ?? _DEFAULT_MAX_TAGS_PER_ROOT;
8
9
  const lines = [
9
10
  "## Cleanup Summary",
10
11
  "",
@@ -12,31 +13,62 @@ export function renderCleanupSummaryMarkdown(summary, options) {
12
13
  "| --- | --- |",
13
14
  `| 📦 Package | \`${_escapeInlineCode(`${summary.owner}/${summary.packageName}`)}\` |`,
14
15
  `| ⚙️ Mode | ${summary.dryRun ? "Cleanup dry-run" : "Cleanup"} |`,
15
- `| 🏷️ Matched tags | ${summary.directTargetTags.length} |`,
16
- `| 🗑️ Fully deletable roots | ${summary.fullyDeletableRoots.length} |`,
17
- `| 🔗 Untag-only roots | ${summary.untagOnlyRoots.length} |`,
18
- `| 🛡️ Blocked roots | ${summary.blockedRoots.length} |`,
19
- `| 📄 Affected manifests | ${summary.affectedManifests.length} |`,
16
+ `| 🏷️ Selected tags | ${summary.directTargetTags.length} |`,
17
+ `| 🔖 Deleted tags | ${summary.changes.deletedTags} |`,
18
+ `| 🖼️ Deleted images | ${summary.changes.deletedImages} |`,
19
+ `| 📚 Deleted multi-arch manifests | ${summary.changes.deletedMultiArchManifests} |`,
20
+ `| 🧱 Deleted indexes | ${summary.changes.deletedIndexes} |`,
21
+ `| 📄 Deleted total | ${summary.changes.deletedTotal} |`,
22
+ `| 🔗 Tag-only updates | ${summary.untagOnlyRoots.length} |`,
23
+ `| 🛡️ Blocked items | ${summary.blockedRoots.length} |`,
20
24
  ""
21
25
  ];
22
- lines.push(..._renderJsonDetails("⚙️ Cleanup filter", summary.plannerInputs));
26
+ lines.push(..._renderPlannedDeleteBreakdown(summary));
27
+ lines.push(..._renderPlannerInputs(summary.plannerInputs));
23
28
  lines.push(..._renderDirectTargetTags(summary.directTargetTags, maxDirectTargetTags));
24
- lines.push(..._renderRootSection("🗑️ Fully deletable roots", summary.fullyDeletableRoots, maxRootsPerSection, maxTagsPerRoot));
25
- lines.push(..._renderRootSection("🔗 Untag-only roots", summary.untagOnlyRoots, maxRootsPerSection, maxTagsPerRoot));
26
- lines.push(..._renderRootSection("🛡️ Blocked roots", summary.blockedRoots, maxRootsPerSection, maxTagsPerRoot));
27
- if (!summary.dryRun && (summary.deletedPackageVersions.length > 0 || summary.untaggedTags.length > 0)) {
29
+ lines.push(..._renderRootSection("🗑️ Deleted items", summary.fullyDeletableRoots, maxRootsPerSection));
30
+ lines.push(..._renderRootSection("🔗 Tags removed only", summary.untagOnlyRoots, maxRootsPerSection));
31
+ lines.push(..._renderRootSection("🛡️ Blocked items", summary.blockedRoots, maxRootsPerSection));
32
+ if (!summary.dryRun && (summary.deletedPackageVersionCount > 0 || summary.detachedTagCount > 0)) {
28
33
  lines.push(..._renderLiveEffects(summary));
29
34
  }
30
35
  return `${lines.join("\n").trimEnd()}\n`;
31
36
  }
32
- function _renderJsonDetails(title, value) {
37
+ function _renderPlannedDeleteBreakdown(summary) {
38
+ const rows = [
39
+ { label: "Images", count: summary.changes.deletedImages },
40
+ { label: "Multi-arch manifests", count: summary.changes.deletedMultiArchManifests },
41
+ { label: "Artifact manifests", count: summary.changes.deletedArtifactManifests },
42
+ { label: "Signatures", count: summary.changes.deletedSignatures },
43
+ { label: "Attestations", count: summary.changes.deletedAttestations },
44
+ { label: "Generic indexes", count: summary.changes.deletedIndexes }
45
+ ].filter((row) => row.count > 0);
46
+ if (rows.length === 0) {
47
+ return [];
48
+ }
33
49
  return [
34
- `<details>`,
35
- `<summary>${title}</summary>`,
50
+ "<details>",
51
+ "<summary>📦 Deleted item breakdown</summary>",
36
52
  "",
37
- "```json",
38
- JSON.stringify(value, null, 2),
39
- "```",
53
+ "| Type | Count |",
54
+ "| --- | --- |",
55
+ ...rows.map((row) => `| ${row.label} | ${row.count} |`),
56
+ "",
57
+ "</details>",
58
+ ""
59
+ ];
60
+ }
61
+ function _renderPlannerInputs(plannerInputs) {
62
+ const rows = _getPlannerInputRows(plannerInputs);
63
+ const patternLines = _getPlannerPatternLines(plannerInputs);
64
+ return [
65
+ "<details>",
66
+ "<summary>⚙️ Cleanup filter</summary>",
67
+ "",
68
+ "| Filter | Value |",
69
+ "| --- | --- |",
70
+ ...(rows.length > 0 ? rows : ["| (none) | No cleanup filters recorded |"]),
71
+ ...(patternLines.length > 0 ? ["", ...patternLines] : []),
40
72
  "",
41
73
  "</details>",
42
74
  ""
@@ -47,23 +79,24 @@ function _renderDirectTargetTags(tags, maxDirectTargetTags) {
47
79
  return [];
48
80
  }
49
81
  const visibleTags = tags.slice(0, maxDirectTargetTags).map((tag) => `- \`${_escapeInlineCode(tag)}\``);
50
- const lines = ["<details>", "<summary>🏷️ Matched tags</summary>", "", ...visibleTags];
82
+ const lines = ["<details>", "<summary>🏷️ Selected tags</summary>", "", ...visibleTags];
51
83
  if (tags.length > maxDirectTargetTags) {
52
- lines.push("", `_Showing first ${maxDirectTargetTags} of ${tags.length} matched tags._`);
84
+ lines.push("", `_Showing first ${maxDirectTargetTags} of ${tags.length} selected tags._`);
53
85
  }
54
86
  lines.push("", "</details>", "");
55
87
  return lines;
56
88
  }
57
- function _renderRootSection(title, roots, maxRootsPerSection, maxTagsPerRoot) {
89
+ function _renderRootSection(title, roots, maxRootsPerSection) {
58
90
  if (roots.length === 0) {
59
91
  return [];
60
92
  }
61
93
  const lines = ["<details>", `<summary>${title}</summary>`, ""];
62
- lines.push("| Version | Digest | Tags | Reason |");
63
- lines.push("| --- | --- | --- | --- |");
94
+ lines.push("| Version | Type | Digest | Tags | Outcome |");
95
+ lines.push("| --- | --- | --- | --- | --- |");
64
96
  for (const root of roots.slice(0, maxRootsPerSection)) {
65
- lines.push(`| ${root.versionId} | \`${_escapeInlineCode(_shortDigest(root.digest))}\` | ${_escapeMarkdown(_formatTags(root, maxTagsPerRoot))} | ${_escapeMarkdown(_formatReason(root))} |`);
97
+ lines.push(`| ${root.versionId} | ${_escapeMarkdown(_describeManifestKind(root.manifestKind))} | \`${_escapeInlineCode(_shortDigest(root.digest))}\` | ${_escapeMarkdown(_formatTags(root))} | ${_escapeMarkdown(_formatReason(root))} |`);
66
98
  }
99
+ lines.push("", "_Tag lists may be truncated for table width._");
67
100
  if (roots.length > maxRootsPerSection) {
68
101
  lines.push("", `_Showing first ${maxRootsPerSection} of ${roots.length} ${title.toLowerCase()}._`);
69
102
  }
@@ -72,33 +105,32 @@ function _renderRootSection(title, roots, maxRootsPerSection, maxTagsPerRoot) {
72
105
  }
73
106
  function _renderLiveEffects(summary) {
74
107
  const lines = ["### Applied changes", ""];
75
- lines.push(`- Deleted package versions: ${summary.deletedPackageVersions.length}`);
76
- lines.push(`- Detached tags: ${summary.untaggedTags.length}`);
77
- if (summary.unsupportedUntagRoots.length > 0) {
78
- lines.push(`- Unsupported untag roots: ${summary.unsupportedUntagRoots.length}`);
79
- }
108
+ lines.push(`- Deleted package versions: ${summary.deletedPackageVersionCount}`);
109
+ lines.push(`- Detached tags: ${summary.detachedTagCount}`);
80
110
  lines.push("");
81
111
  return lines;
82
112
  }
83
- function _formatTags(root, maxTagsPerRoot) {
113
+ function _formatTags(root) {
84
114
  const tags = root.rootTags.length > 0 ? root.rootTags : root.matchedTags;
85
115
  if (tags.length === 0) {
86
116
  return "(untagged)";
87
117
  }
88
- const visible = tags.slice(0, maxTagsPerRoot);
89
- const suffix = tags.length > maxTagsPerRoot ? `, +${tags.length - maxTagsPerRoot} more` : "";
90
- return visible.join(", ") + suffix;
118
+ const joinedTags = tags.join(", ");
119
+ if (joinedTags.length <= _DEFAULT_MAX_TAG_TEXT_LENGTH) {
120
+ return joinedTags;
121
+ }
122
+ return `${joinedTags.slice(0, _DEFAULT_MAX_TAG_TEXT_LENGTH - 3)}...`;
91
123
  }
92
124
  function _formatReason(root) {
93
- if (root.validationStatus === "blocked") {
94
- const blocking = root.blockingDigest ? _shortDigest(root.blockingDigest) : "another root";
125
+ if (root.validationStatus === DeletePlanValidationStatuses.blocked) {
126
+ const blocking = root.blockingDigest ? _shortDigest(root.blockingDigest) : "another item";
95
127
  const overlap = root.overlapDigest ? ` via ${_shortDigest(root.overlapDigest)}` : "";
96
- return `Blocked by ${blocking}${overlap}`;
128
+ return `Blocked by retained item ${blocking}${overlap}`;
97
129
  }
98
- if (root.validationStatus === "untag-only") {
99
- return "Selected tags detach; root remains";
130
+ if (root.validationStatus === DeletePlanValidationStatuses.untagOnly) {
131
+ return "Remove selected tags, keep item";
100
132
  }
101
- return "Root and closure can be deleted";
133
+ return "Delete item and descendants";
102
134
  }
103
135
  function _shortDigest(value) {
104
136
  if (!value.startsWith("sha256:") || value.length <= 20) {
@@ -112,3 +144,81 @@ function _escapeInlineCode(value) {
112
144
  function _escapeMarkdown(value) {
113
145
  return value.replaceAll("|", "\\|").replaceAll("\n", " ");
114
146
  }
147
+ function _getPlannerInputRows(plannerInputs) {
148
+ const rows = [];
149
+ for (const [key, value] of Object.entries(plannerInputs)) {
150
+ rows.push(`| ${_escapeMarkdown(_plannerInputLabel(key))} | ${_escapeMarkdown(_formatPlannerInputValue(value))} |`);
151
+ }
152
+ return rows;
153
+ }
154
+ function _plannerInputLabel(key) {
155
+ switch (key) {
156
+ case "deleteTags":
157
+ return "Delete tags";
158
+ case "excludeTags":
159
+ return "Exclude tags";
160
+ case "useRegex":
161
+ return "Use regex";
162
+ case "deleteUntagged":
163
+ return "Delete untagged";
164
+ case "keepNTagged":
165
+ return "Keep newest tagged";
166
+ case "keepNUntagged":
167
+ return "Keep newest untagged";
168
+ case "olderThan":
169
+ return "Older than";
170
+ case "cutoffTimestamp":
171
+ return "Cutoff timestamp";
172
+ case "deleteGhostImages":
173
+ return "Delete ghost images";
174
+ case "deletePartialImages":
175
+ return "Delete partial images";
176
+ case "deleteOrphanedImages":
177
+ return "Delete orphaned images";
178
+ default:
179
+ return key;
180
+ }
181
+ }
182
+ function _formatPlannerInputValue(value) {
183
+ if (Array.isArray(value)) {
184
+ if (value.length === 0) {
185
+ return "(none)";
186
+ }
187
+ return value.length === 1 ? "1 pattern" : `${value.length} patterns`;
188
+ }
189
+ if (typeof value === "boolean") {
190
+ return value ? "yes" : "no";
191
+ }
192
+ return String(value);
193
+ }
194
+ function _getPlannerPatternLines(plannerInputs) {
195
+ const lines = [];
196
+ for (const [key, value] of Object.entries(plannerInputs)) {
197
+ if (!Array.isArray(value) || value.length === 0) {
198
+ continue;
199
+ }
200
+ lines.push(`- ${_plannerInputLabel(key)}:`);
201
+ for (const item of value) {
202
+ lines.push(` - \`${_escapeInlineCode(String(item))}\``);
203
+ }
204
+ }
205
+ return lines;
206
+ }
207
+ function _describeManifestKind(manifestKind) {
208
+ switch (manifestKind) {
209
+ case ManifestKinds.imageManifest:
210
+ return "image";
211
+ case ManifestKinds.multiArchManifest:
212
+ return "multi-arch";
213
+ case ManifestKinds.indexManifest:
214
+ return "index";
215
+ case ManifestKinds.signatureManifest:
216
+ return "signature";
217
+ case ManifestKinds.attestationManifest:
218
+ return "attestation";
219
+ case ManifestKinds.artifactManifest:
220
+ return "artifact";
221
+ default:
222
+ return "item";
223
+ }
224
+ }
@@ -1,23 +1,35 @@
1
- import type { DeletePlan, DeletePlanSelectionMode, DeletePlanSelectionReason } from "../db/index.js";
1
+ import type { ManifestKind } from "../core/index.js";
2
+ import type { DeletePlan, DeletePlanSelectionMode, DeletePlanSelectionReason, DeletePlanValidationReasonCode, DeletePlanValidationStatus } from "../db/index.js";
2
3
  import type { DeleteExecutionSummary } from "../execute/index.js";
3
4
  export interface CleanupSummaryRoot {
4
5
  versionId: number;
5
6
  digest: string;
6
- manifestKind?: string;
7
+ manifestKind?: ManifestKind;
7
8
  rootTags: string[];
8
9
  matchedTags: string[];
9
10
  selectionMode: DeletePlanSelectionMode;
10
11
  selectionReason: DeletePlanSelectionReason;
11
- validationStatus: "fully-deletable" | "blocked" | "untag-only";
12
- validationReasonCode: "untag-only-partial-tag-match" | "fully-deletable-no-retained-overlap" | "blocked-overlap-with-retained-root";
12
+ validationStatus: DeletePlanValidationStatus;
13
+ validationReasonCode: DeletePlanValidationReasonCode;
13
14
  validationReason: string;
14
15
  blockingVersionId?: number;
15
16
  blockingDigest?: string;
16
17
  overlapDigest?: string;
17
- overlapManifestKind?: string;
18
+ overlapManifestKind?: ManifestKind;
18
19
  }
19
20
  export interface CleanupSummaryAffectedManifest {
20
21
  digest: string;
22
+ manifestKind?: ManifestKind;
23
+ }
24
+ export interface CleanupSummaryChanges {
25
+ deletedTags: number;
26
+ deletedImages: number;
27
+ deletedIndexes: number;
28
+ deletedMultiArchManifests: number;
29
+ deletedArtifactManifests: number;
30
+ deletedAttestations: number;
31
+ deletedSignatures: number;
32
+ deletedTotal: number;
21
33
  }
22
34
  export interface CleanupSummary {
23
35
  command: "cleanup";
@@ -32,13 +44,13 @@ export interface CleanupSummary {
32
44
  untagOnlyRoots: CleanupSummaryRoot[];
33
45
  blockedRoots: CleanupSummaryRoot[];
34
46
  affectedManifests: CleanupSummaryAffectedManifest[];
35
- deletedPackageVersions: DeleteExecutionSummary["deletedPackageVersions"];
36
- untaggedTags: DeleteExecutionSummary["untaggedTags"];
37
- unsupportedUntagRoots: DeleteExecutionSummary["unsupportedUntagRoots"];
47
+ changes: CleanupSummaryChanges;
48
+ deletedPackageVersionCount: DeleteExecutionSummary["deletedPackageVersionCount"];
49
+ detachedTagCount: DeleteExecutionSummary["detachedTagCount"];
38
50
  }
39
51
  export declare function buildCleanupSummary(plan: DeletePlan, options: {
40
52
  dryRun: boolean;
41
- listRootTags: (versionId: number) => string[];
42
- listAffectedManifestDigests: (rootDigests: string[]) => string[];
53
+ rootTagsByVersionId: ReadonlyMap<number, string[]>;
54
+ changes: CleanupSummaryChanges;
43
55
  executionSummary?: DeleteExecutionSummary;
44
56
  }): CleanupSummary;