ghcr-manager 0.9.6 → 0.9.7
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.
- package/CHANGELOG.md +25 -0
- package/README.md +14 -13
- package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +0 -1
- package/dist/cleanup-summary/_cleanup-summary-markdown.js +146 -33
- package/dist/cleanup-summary/_cleanup-summary.d.ts +20 -7
- package/dist/cleanup-summary/_cleanup-summary.js +24 -8
- package/dist/cleanup-summary/index.d.ts +1 -1
- package/dist/cli/_cleanup-command.js +82 -23
- package/dist/cli/_json-output.d.ts +1 -0
- package/dist/cli/_json-output.js +11 -0
- package/dist/cli/_tag-selector-resolver.js +7 -4
- package/dist/cli/_untag-command.js +2 -1
- package/dist/cli/index.js +2 -2
- package/dist/core/_types.d.ts +9 -1
- package/dist/core/_types.js +8 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/db/_cleanup-run-writer.d.ts +1 -1
- package/dist/db/_cleanup-run-writer.js +28 -7
- package/dist/db/_db-merge-cleanup-copy.js +4 -2
- package/dist/db/_manifest-kind-refinement.d.ts +2 -0
- package/dist/db/_manifest-kind-refinement.js +43 -0
- package/dist/db/_scan-writer.js +4 -1
- package/dist/db/index.d.ts +2 -1
- package/dist/db/index.js +1 -0
- package/dist/db/planner/_planner-direct-target-roots.d.ts +1 -0
- package/dist/db/planner/_planner-direct-target-roots.js +23 -12
- package/dist/db/planner/_planner-direct-target-tags.d.ts +1 -1
- package/dist/db/planner/_planner-direct-target-tags.js +4 -2
- package/dist/db/planner/_planner-output.js +7 -6
- package/dist/db/planner/_planner-repository.d.ts +2 -1
- package/dist/db/planner/_planner-repository.js +3 -1
- package/dist/db/planner/_planner-types.d.ts +21 -8
- package/dist/db/planner/_planner-types.js +13 -3
- package/dist/db/planner/index.d.ts +2 -1
- package/dist/db/planner/index.js +1 -0
- package/dist/execute/_plan-executor.d.ts +1 -1
- package/dist/execute/_plan-executor.js +35 -9
- package/dist/ingest/github/_manifest-kind.d.ts +1 -1
- package/dist/ingest/github/_manifest-kind.js +6 -5
- package/package.json +1 -1
- package/resources/sql/schema/001_schema.sql +4 -1
- package/resources/sql/views/002_v_missing_digests.sql +0 -32
- /package/resources/sql/views/{003_v_scan_root_manifests.sql → 002_v_scan_root_manifests.sql} +0 -0
- /package/resources/sql/views/{004_v_digest_tag_relations.sql → 003_v_digest_tag_relations.sql} +0 -0
- /package/resources/sql/views/{005_v_cleanup_root_closure_members.sql → 004_v_cleanup_root_closure_members.sql} +0 -0
- /package/resources/sql/views/{006_v_cleanup_blocking_overlaps.sql → 005_v_cleanup_blocking_overlaps.sql} +0 -0
- /package/resources/sql/views/{007_v_cleanup_root_decision_readable.sql → 006_v_cleanup_root_decision_readable.sql} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@ 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.7] - 2026-05-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- The root action now prepares `cleanup` and `untag` CLI arguments through `tools/prepare-action-args.mjs`, keeping
|
|
15
|
+
printed and executed argument lists aligned.
|
|
16
|
+
- Cleanup planning now traverses recursively beyond `sha256-*` helper-tag manifest links as well, if deeper helper
|
|
17
|
+
chains ever occur.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Cleanup dry-run output and GitHub step summaries were reworked to explain the plan more clearly, including a filters
|
|
22
|
+
table and clearer counts for tags, images, and cross-arch manifests.
|
|
23
|
+
- Informational manifest classification was tuned so only real multi-arch roots are labeled `cross_arch_manifest`, while
|
|
24
|
+
helper-tagged indexes remain `index_manifest`.
|
|
25
|
+
- `merge-run-artifacts` now uses a simpler current-run download flow with direct artifact download handling.
|
|
26
|
+
- Cleanup selected-tag audit and DB-merge metadata handling were tightened alongside the summary/output refactor.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- `delete-orphaned-images` now carries orphaned `sha256-*` digest-tag targets through planner selection instead of
|
|
31
|
+
dropping them at the normal non-digest tag boundary.
|
|
32
|
+
- Fully deletable cleanup execution now deletes the planned closure package versions instead of deleting only the root
|
|
33
|
+
package version.
|
|
34
|
+
|
|
10
35
|
## [0.9.6] - 2026-05-21
|
|
11
36
|
|
|
12
37
|
### Added
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ jobs:
|
|
|
33
33
|
|
|
34
34
|
- name: Preview GHCR cleanup
|
|
35
35
|
id: ghcr-manager
|
|
36
|
-
uses: gh-workflow/ghcr-manager@0.9.
|
|
36
|
+
uses: gh-workflow/ghcr-manager@0.9.7
|
|
37
37
|
with:
|
|
38
38
|
command: cleanup
|
|
39
39
|
token: ${{ github.token }}
|
|
@@ -72,7 +72,7 @@ The action supports three commands:
|
|
|
72
72
|
### Preview cleanup
|
|
73
73
|
|
|
74
74
|
```yaml
|
|
75
|
-
- uses: gh-workflow/ghcr-manager@0.9.
|
|
75
|
+
- uses: gh-workflow/ghcr-manager@0.9.7
|
|
76
76
|
with:
|
|
77
77
|
command: cleanup
|
|
78
78
|
token: ${{ github.token }}
|
|
@@ -82,7 +82,7 @@ The action supports three commands:
|
|
|
82
82
|
delete-tags: |
|
|
83
83
|
pr-.*
|
|
84
84
|
use-regex: true
|
|
85
|
-
older-than:
|
|
85
|
+
older-than: 30 days
|
|
86
86
|
keep-n-tagged: "5"
|
|
87
87
|
exclude-tags: |
|
|
88
88
|
latest
|
|
@@ -93,7 +93,7 @@ The action supports three commands:
|
|
|
93
93
|
### Apply cleanup
|
|
94
94
|
|
|
95
95
|
```yaml
|
|
96
|
-
- uses: gh-workflow/ghcr-manager@0.9.
|
|
96
|
+
- uses: gh-workflow/ghcr-manager@0.9.7
|
|
97
97
|
with:
|
|
98
98
|
command: cleanup
|
|
99
99
|
token: ${{ github.token }}
|
|
@@ -112,7 +112,7 @@ Note: the second scan only runs if cleanup actually makes changes.
|
|
|
112
112
|
### Remove selected tags directly
|
|
113
113
|
|
|
114
114
|
```yaml
|
|
115
|
-
- uses: gh-workflow/ghcr-manager@0.9.
|
|
115
|
+
- uses: gh-workflow/ghcr-manager@0.9.7
|
|
116
116
|
with:
|
|
117
117
|
command: untag
|
|
118
118
|
token: ${{ github.token }}
|
|
@@ -128,7 +128,7 @@ Note: the second scan only runs if cleanup actually makes changes.
|
|
|
128
128
|
### Scan one package
|
|
129
129
|
|
|
130
130
|
```yaml
|
|
131
|
-
- uses: gh-workflow/ghcr-manager@0.9.
|
|
131
|
+
- uses: gh-workflow/ghcr-manager@0.9.7
|
|
132
132
|
with:
|
|
133
133
|
command: scan
|
|
134
134
|
token: ${{ github.token }}
|
|
@@ -169,11 +169,14 @@ Note: the second scan only runs if cleanup actually makes changes.
|
|
|
169
169
|
|
|
170
170
|
`Cmds`: `s` = `scan`, `c` = `cleanup`, `u` = `untag`
|
|
171
171
|
|
|
172
|
-
Cleanup notes:
|
|
172
|
+
Cleanup command notes:
|
|
173
173
|
|
|
174
174
|
- Tagged selector families may be combined with `delete-untagged`.
|
|
175
175
|
- `exclude-tags` requires at least one tagged selector family.
|
|
176
176
|
- `delete-untagged` and `keep-n-untagged` cannot be combined.
|
|
177
|
+
- `older-than` takes one integer plus one unit.
|
|
178
|
+
- Supported `older-than` units: `minutes`, `hours`, `days`, `weeks`, `months`, `years`.
|
|
179
|
+
- Example values: `30 days`, `2 hours`, `1 month`.
|
|
177
180
|
|
|
178
181
|
## Outputs
|
|
179
182
|
|
|
@@ -199,12 +202,10 @@ Current naming:
|
|
|
199
202
|
|
|
200
203
|
## Documentation Map
|
|
201
204
|
|
|
202
|
-
- [
|
|
203
|
-
- [
|
|
204
|
-
- [
|
|
205
|
-
- [
|
|
206
|
-
manifest references
|
|
207
|
-
- [docs/cli-usage.md](docs/cli-usage.md): companion CLI usage
|
|
205
|
+
- [GitHub Action usage](docs/action-usage.md): action commands, including `cleanup`, `scan`, and `untag`
|
|
206
|
+
- [Multi-package workflows](docs/db-merge-workflows.md): cleaning up multiple packages with one combined DB
|
|
207
|
+
- [SQLite schema guide](docs/schema-description.md): practical explanation of the SQLite schema
|
|
208
|
+
- [CLI usage](docs/cli-usage.md): companion CLI usage
|
|
208
209
|
|
|
209
210
|
## Acknowledgment
|
|
210
211
|
|
|
@@ -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
|
|
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
|
-
`| 🏷️
|
|
16
|
-
`|
|
|
17
|
-
`|
|
|
18
|
-
`|
|
|
19
|
-
`|
|
|
16
|
+
`| 🏷️ Selected tags | ${summary.directTargetTags.length} |`,
|
|
17
|
+
`| 🔖 Deleted tags | ${summary.changes.deletedTags} |`,
|
|
18
|
+
`| 🖼️ Deleted images | ${summary.changes.deletedImages} |`,
|
|
19
|
+
`| 📚 Deleted cross-arch manifests | ${summary.changes.deletedCrossArchManifests} |`,
|
|
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(...
|
|
26
|
+
lines.push(..._renderPlannedDeleteBreakdown(summary));
|
|
27
|
+
lines.push(..._renderPlannerInputs(summary.plannerInputs));
|
|
23
28
|
lines.push(..._renderDirectTargetTags(summary.directTargetTags, maxDirectTargetTags));
|
|
24
|
-
lines.push(..._renderRootSection("🗑️
|
|
25
|
-
lines.push(..._renderRootSection("🔗
|
|
26
|
-
lines.push(..._renderRootSection("🛡️ Blocked
|
|
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));
|
|
27
32
|
if (!summary.dryRun && (summary.deletedPackageVersions.length > 0 || summary.untaggedTags.length > 0)) {
|
|
28
33
|
lines.push(..._renderLiveEffects(summary));
|
|
29
34
|
}
|
|
30
35
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
31
36
|
}
|
|
32
|
-
function
|
|
37
|
+
function _renderPlannedDeleteBreakdown(summary) {
|
|
38
|
+
const rows = [
|
|
39
|
+
{ label: "Images", count: summary.changes.deletedImages },
|
|
40
|
+
{ label: "Cross-arch manifests", count: summary.changes.deletedCrossArchManifests },
|
|
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
|
+
}
|
|
49
|
+
return [
|
|
50
|
+
"<details>",
|
|
51
|
+
"<summary>📦 Deleted item breakdown</summary>",
|
|
52
|
+
"",
|
|
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);
|
|
33
64
|
return [
|
|
34
|
-
|
|
35
|
-
|
|
65
|
+
"<details>",
|
|
66
|
+
"<summary>⚙️ Cleanup filter</summary>",
|
|
36
67
|
"",
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
"
|
|
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>🏷️
|
|
82
|
+
const lines = ["<details>", "<summary>🏷️ Selected tags</summary>", "", ...visibleTags];
|
|
51
83
|
if (tags.length > maxDirectTargetTags) {
|
|
52
|
-
lines.push("", `_Showing first ${maxDirectTargetTags} of ${tags.length}
|
|
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
|
|
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 |
|
|
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
|
|
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
|
}
|
|
@@ -80,25 +113,27 @@ function _renderLiveEffects(summary) {
|
|
|
80
113
|
lines.push("");
|
|
81
114
|
return lines;
|
|
82
115
|
}
|
|
83
|
-
function _formatTags(root
|
|
116
|
+
function _formatTags(root) {
|
|
84
117
|
const tags = root.rootTags.length > 0 ? root.rootTags : root.matchedTags;
|
|
85
118
|
if (tags.length === 0) {
|
|
86
119
|
return "(untagged)";
|
|
87
120
|
}
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
121
|
+
const joinedTags = tags.join(", ");
|
|
122
|
+
if (joinedTags.length <= _DEFAULT_MAX_TAG_TEXT_LENGTH) {
|
|
123
|
+
return joinedTags;
|
|
124
|
+
}
|
|
125
|
+
return `${joinedTags.slice(0, _DEFAULT_MAX_TAG_TEXT_LENGTH - 3)}...`;
|
|
91
126
|
}
|
|
92
127
|
function _formatReason(root) {
|
|
93
|
-
if (root.validationStatus ===
|
|
94
|
-
const blocking = root.blockingDigest ? _shortDigest(root.blockingDigest) : "another
|
|
128
|
+
if (root.validationStatus === DeletePlanValidationStatuses.blocked) {
|
|
129
|
+
const blocking = root.blockingDigest ? _shortDigest(root.blockingDigest) : "another item";
|
|
95
130
|
const overlap = root.overlapDigest ? ` via ${_shortDigest(root.overlapDigest)}` : "";
|
|
96
|
-
return `Blocked by ${blocking}${overlap}`;
|
|
131
|
+
return `Blocked by retained item ${blocking}${overlap}`;
|
|
97
132
|
}
|
|
98
|
-
if (root.validationStatus ===
|
|
99
|
-
return "
|
|
133
|
+
if (root.validationStatus === DeletePlanValidationStatuses.untagOnly) {
|
|
134
|
+
return "Remove selected tags, keep item";
|
|
100
135
|
}
|
|
101
|
-
return "
|
|
136
|
+
return "Delete item and descendants";
|
|
102
137
|
}
|
|
103
138
|
function _shortDigest(value) {
|
|
104
139
|
if (!value.startsWith("sha256:") || value.length <= 20) {
|
|
@@ -112,3 +147,81 @@ function _escapeInlineCode(value) {
|
|
|
112
147
|
function _escapeMarkdown(value) {
|
|
113
148
|
return value.replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
114
149
|
}
|
|
150
|
+
function _getPlannerInputRows(plannerInputs) {
|
|
151
|
+
const rows = [];
|
|
152
|
+
for (const [key, value] of Object.entries(plannerInputs)) {
|
|
153
|
+
rows.push(`| ${_escapeMarkdown(_plannerInputLabel(key))} | ${_escapeMarkdown(_formatPlannerInputValue(value))} |`);
|
|
154
|
+
}
|
|
155
|
+
return rows;
|
|
156
|
+
}
|
|
157
|
+
function _plannerInputLabel(key) {
|
|
158
|
+
switch (key) {
|
|
159
|
+
case "deleteTags":
|
|
160
|
+
return "Delete tags";
|
|
161
|
+
case "excludeTags":
|
|
162
|
+
return "Exclude tags";
|
|
163
|
+
case "useRegex":
|
|
164
|
+
return "Use regex";
|
|
165
|
+
case "deleteUntagged":
|
|
166
|
+
return "Delete untagged";
|
|
167
|
+
case "keepNTagged":
|
|
168
|
+
return "Keep newest tagged";
|
|
169
|
+
case "keepNUntagged":
|
|
170
|
+
return "Keep newest untagged";
|
|
171
|
+
case "olderThan":
|
|
172
|
+
return "Older than";
|
|
173
|
+
case "cutoffTimestamp":
|
|
174
|
+
return "Cutoff timestamp";
|
|
175
|
+
case "deleteGhostImages":
|
|
176
|
+
return "Delete ghost images";
|
|
177
|
+
case "deletePartialImages":
|
|
178
|
+
return "Delete partial images";
|
|
179
|
+
case "deleteOrphanedImages":
|
|
180
|
+
return "Delete orphaned images";
|
|
181
|
+
default:
|
|
182
|
+
return key;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function _formatPlannerInputValue(value) {
|
|
186
|
+
if (Array.isArray(value)) {
|
|
187
|
+
if (value.length === 0) {
|
|
188
|
+
return "(none)";
|
|
189
|
+
}
|
|
190
|
+
return value.length === 1 ? "1 pattern" : `${value.length} patterns`;
|
|
191
|
+
}
|
|
192
|
+
if (typeof value === "boolean") {
|
|
193
|
+
return value ? "yes" : "no";
|
|
194
|
+
}
|
|
195
|
+
return String(value);
|
|
196
|
+
}
|
|
197
|
+
function _getPlannerPatternLines(plannerInputs) {
|
|
198
|
+
const lines = [];
|
|
199
|
+
for (const [key, value] of Object.entries(plannerInputs)) {
|
|
200
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
lines.push(`- ${_plannerInputLabel(key)}:`);
|
|
204
|
+
for (const item of value) {
|
|
205
|
+
lines.push(` - \`${_escapeInlineCode(String(item))}\``);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return lines;
|
|
209
|
+
}
|
|
210
|
+
function _describeManifestKind(manifestKind) {
|
|
211
|
+
switch (manifestKind) {
|
|
212
|
+
case ManifestKinds.imageManifest:
|
|
213
|
+
return "image";
|
|
214
|
+
case ManifestKinds.crossArchManifest:
|
|
215
|
+
return "cross-arch";
|
|
216
|
+
case ManifestKinds.indexManifest:
|
|
217
|
+
return "index";
|
|
218
|
+
case ManifestKinds.signatureManifest:
|
|
219
|
+
return "signature";
|
|
220
|
+
case ManifestKinds.attestationManifest:
|
|
221
|
+
return "attestation";
|
|
222
|
+
case ManifestKinds.artifactManifest:
|
|
223
|
+
return "artifact";
|
|
224
|
+
default:
|
|
225
|
+
return "item";
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -1,23 +1,35 @@
|
|
|
1
|
-
import type {
|
|
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?:
|
|
7
|
+
manifestKind?: ManifestKind;
|
|
7
8
|
rootTags: string[];
|
|
8
9
|
matchedTags: string[];
|
|
9
10
|
selectionMode: DeletePlanSelectionMode;
|
|
10
11
|
selectionReason: DeletePlanSelectionReason;
|
|
11
|
-
validationStatus:
|
|
12
|
-
validationReasonCode:
|
|
12
|
+
validationStatus: DeletePlanValidationStatus;
|
|
13
|
+
validationReasonCode: DeletePlanValidationReasonCode;
|
|
13
14
|
validationReason: string;
|
|
14
15
|
blockingVersionId?: number;
|
|
15
16
|
blockingDigest?: string;
|
|
16
17
|
overlapDigest?: string;
|
|
17
|
-
overlapManifestKind?:
|
|
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
|
+
deletedCrossArchManifests: 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,14 @@ export interface CleanupSummary {
|
|
|
32
44
|
untagOnlyRoots: CleanupSummaryRoot[];
|
|
33
45
|
blockedRoots: CleanupSummaryRoot[];
|
|
34
46
|
affectedManifests: CleanupSummaryAffectedManifest[];
|
|
47
|
+
changes: CleanupSummaryChanges;
|
|
35
48
|
deletedPackageVersions: DeleteExecutionSummary["deletedPackageVersions"];
|
|
36
49
|
untaggedTags: DeleteExecutionSummary["untaggedTags"];
|
|
37
50
|
unsupportedUntagRoots: DeleteExecutionSummary["unsupportedUntagRoots"];
|
|
38
51
|
}
|
|
39
52
|
export declare function buildCleanupSummary(plan: DeletePlan, options: {
|
|
40
53
|
dryRun: boolean;
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
rootTagsByVersionId: ReadonlyMap<number, string[]>;
|
|
55
|
+
changes: CleanupSummaryChanges;
|
|
43
56
|
executionSummary?: DeleteExecutionSummary;
|
|
44
57
|
}): CleanupSummary;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { DeletePlanValidationStatuses } from "../db/index.js";
|
|
1
2
|
export function buildCleanupSummary(plan, options) {
|
|
2
3
|
const directTargetTagSet = new Set(plan.directTargetTags);
|
|
3
|
-
const roots = plan.rootDecisions.map((decision) => _mapRootDecision(decision, directTargetTagSet, options.
|
|
4
|
-
const fullyDeletableRoots = roots.filter((root) => root.validationStatus ===
|
|
5
|
-
const blockedRoots = roots.filter((root) => root.validationStatus ===
|
|
6
|
-
const untagOnlyRoots = roots.filter((root) => root.validationStatus ===
|
|
7
|
-
const
|
|
4
|
+
const roots = plan.rootDecisions.map((decision) => _mapRootDecision(decision, directTargetTagSet, options.rootTagsByVersionId));
|
|
5
|
+
const fullyDeletableRoots = roots.filter((root) => root.validationStatus === DeletePlanValidationStatuses.fullyDeletable);
|
|
6
|
+
const blockedRoots = roots.filter((root) => root.validationStatus === DeletePlanValidationStatuses.blocked);
|
|
7
|
+
const untagOnlyRoots = roots.filter((root) => root.validationStatus === DeletePlanValidationStatuses.untagOnly);
|
|
8
|
+
const affectedManifests = _listAffectedManifests(plan, fullyDeletableRoots.map((root) => root.digest));
|
|
8
9
|
return {
|
|
9
10
|
command: "cleanup",
|
|
10
11
|
owner: plan.owner,
|
|
@@ -17,14 +18,15 @@ export function buildCleanupSummary(plan, options) {
|
|
|
17
18
|
fullyDeletableRoots,
|
|
18
19
|
untagOnlyRoots,
|
|
19
20
|
blockedRoots,
|
|
20
|
-
affectedManifests
|
|
21
|
+
affectedManifests,
|
|
22
|
+
changes: options.changes,
|
|
21
23
|
deletedPackageVersions: options.executionSummary?.deletedPackageVersions ?? [],
|
|
22
24
|
untaggedTags: options.executionSummary?.untaggedTags ?? [],
|
|
23
25
|
unsupportedUntagRoots: options.executionSummary?.unsupportedUntagRoots ?? []
|
|
24
26
|
};
|
|
25
27
|
}
|
|
26
|
-
function _mapRootDecision(decision, directTargetTagSet,
|
|
27
|
-
const rootTags =
|
|
28
|
+
function _mapRootDecision(decision, directTargetTagSet, rootTagsByVersionId) {
|
|
29
|
+
const rootTags = rootTagsByVersionId.get(decision.versionId) ?? [];
|
|
28
30
|
return {
|
|
29
31
|
versionId: decision.versionId,
|
|
30
32
|
digest: decision.digest,
|
|
@@ -42,3 +44,17 @@ function _mapRootDecision(decision, directTargetTagSet, listRootTags) {
|
|
|
42
44
|
overlapManifestKind: decision.overlapManifestKind
|
|
43
45
|
};
|
|
44
46
|
}
|
|
47
|
+
function _listAffectedManifests(plan, fullyDeletableRootDigests) {
|
|
48
|
+
const fullyDeletableRootDigestSet = new Set(fullyDeletableRootDigests);
|
|
49
|
+
const manifestsByDigest = new Map();
|
|
50
|
+
for (const manifest of plan.closureManifests) {
|
|
51
|
+
if (!fullyDeletableRootDigestSet.has(manifest.sourceDigest)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
manifestsByDigest.set(manifest.memberDigest, {
|
|
55
|
+
digest: manifest.memberDigest,
|
|
56
|
+
manifestKind: manifest.memberManifestKind
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return [...manifestsByDigest.values()].sort((left, right) => left.digest.localeCompare(right.digest));
|
|
60
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { buildCleanupSummary, type CleanupSummary, type CleanupSummaryAffectedManifest, type CleanupSummaryRoot } from "./_cleanup-summary.js";
|
|
1
|
+
export { buildCleanupSummary, type CleanupSummary, type CleanupSummaryAffectedManifest, type CleanupSummaryChanges, type CleanupSummaryRoot } from "./_cleanup-summary.js";
|
|
2
2
|
export { renderCleanupSummaryMarkdown } from "./_cleanup-summary-markdown.js";
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { ManifestKinds } from "../core/index.js";
|
|
1
2
|
import { buildCleanupSummary } from "../cleanup-summary/index.js";
|
|
2
3
|
import { CleanupRunWriter, openDatabase, PlannerRepository } from "../db/index.js";
|
|
3
4
|
import { executeDeletePlan } from "../execute/index.js";
|
|
4
5
|
import { hasFlag, resolveLogLevel, resolveToken } from "./_args.js";
|
|
6
|
+
import { writeJsonOutput } from "./_json-output.js";
|
|
5
7
|
import { createLogger } from "./_logger.js";
|
|
6
8
|
import { loadDeletePlan, resolvePlanCommandInputs } from "./_planner-options.js";
|
|
7
9
|
import { resolveTagSelectors } from "./_tag-selector-resolver.js";
|
|
@@ -17,18 +19,19 @@ export async function handleCleanup(args) {
|
|
|
17
19
|
const scanId = repository.getLatestCompletedScanId(inputs.owner, inputs.packageName);
|
|
18
20
|
logger.debug(`Starting cleanup for ${inputs.owner}/${inputs.packageName}`);
|
|
19
21
|
const plan = loadDeletePlan(repository, resolveTagSelectors(database, inputs));
|
|
20
|
-
|
|
22
|
+
const rootTagsByVersionId = _loadRootTagsByVersionId(database, inputs.owner, inputs.packageName, plan.rootDecisions.map((decision) => decision.versionId));
|
|
23
|
+
const cleanupRunId = cleanupRunWriter.persistCleanupRun(scanId, plan, {
|
|
21
24
|
dryRun,
|
|
22
25
|
cleanupStartedAt: new Date().toISOString()
|
|
23
26
|
});
|
|
24
27
|
if (dryRun) {
|
|
25
28
|
const summary = buildCleanupSummary(plan, {
|
|
26
29
|
dryRun: true,
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
rootTagsByVersionId,
|
|
31
|
+
changes: _loadSummaryChanges(database, cleanupRunId)
|
|
29
32
|
});
|
|
30
33
|
logger.debug(`Completed dry-run cleanup for ${inputs.owner}/${inputs.packageName}`);
|
|
31
|
-
|
|
34
|
+
writeJsonOutput(args, "--summary-json-path", summary);
|
|
32
35
|
return 0;
|
|
33
36
|
}
|
|
34
37
|
const executionSummary = await executeDeletePlan(plan, {
|
|
@@ -38,34 +41,18 @@ export async function handleCleanup(args) {
|
|
|
38
41
|
});
|
|
39
42
|
const summary = buildCleanupSummary(plan, {
|
|
40
43
|
dryRun: false,
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
rootTagsByVersionId,
|
|
45
|
+
changes: _loadSummaryChanges(database, cleanupRunId),
|
|
43
46
|
executionSummary
|
|
44
47
|
});
|
|
45
48
|
logger.debug(`Completed cleanup for ${inputs.owner}/${inputs.packageName}`);
|
|
46
|
-
|
|
49
|
+
writeJsonOutput(args, "--summary-json-path", summary);
|
|
47
50
|
return 0;
|
|
48
51
|
}
|
|
49
52
|
finally {
|
|
50
53
|
database.close();
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
|
-
function _listAffectedManifestDigests(database, scanId, rootDigests) {
|
|
54
|
-
if (rootDigests.length === 0) {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
const placeholders = rootDigests.map(() => "?").join(", ");
|
|
58
|
-
const rows = database
|
|
59
|
-
.prepare(`
|
|
60
|
-
SELECT DISTINCT descendant_digest AS digest
|
|
61
|
-
FROM manifest_reachability
|
|
62
|
-
WHERE scan_id = ?
|
|
63
|
-
AND ancestor_digest IN (${placeholders})
|
|
64
|
-
ORDER BY descendant_digest
|
|
65
|
-
`)
|
|
66
|
-
.all(scanId, ...rootDigests);
|
|
67
|
-
return rows.map((row) => row.digest);
|
|
68
|
-
}
|
|
69
56
|
function _listRootTags(database, owner, packageName, versionId) {
|
|
70
57
|
const rows = database
|
|
71
58
|
.prepare(`
|
|
@@ -81,3 +68,75 @@ function _listRootTags(database, owner, packageName, versionId) {
|
|
|
81
68
|
.all(owner, packageName, versionId);
|
|
82
69
|
return rows.map((row) => row.tag);
|
|
83
70
|
}
|
|
71
|
+
function _loadRootTagsByVersionId(database, owner, packageName, versionIds) {
|
|
72
|
+
const requestedVersionIds = new Set(versionIds);
|
|
73
|
+
const tagsByVersionId = new Map();
|
|
74
|
+
for (const versionId of requestedVersionIds) {
|
|
75
|
+
tagsByVersionId.set(versionId, []);
|
|
76
|
+
}
|
|
77
|
+
if (requestedVersionIds.size === 0) {
|
|
78
|
+
return tagsByVersionId;
|
|
79
|
+
}
|
|
80
|
+
const rows = database
|
|
81
|
+
.prepare(`
|
|
82
|
+
SELECT tags.version_id, tags.tag
|
|
83
|
+
FROM tags
|
|
84
|
+
INNER JOIN v_latest_scan_per_package latest_scan ON latest_scan.scan_id = tags.scan_id
|
|
85
|
+
WHERE latest_scan.owner = ?
|
|
86
|
+
AND latest_scan.package_name = ?
|
|
87
|
+
AND tags.is_digest_tag = 0
|
|
88
|
+
ORDER BY tags.version_id, tags.tag
|
|
89
|
+
`)
|
|
90
|
+
.all(owner, packageName);
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
if (!requestedVersionIds.has(row.version_id)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
tagsByVersionId.get(row.version_id)?.push(row.tag);
|
|
96
|
+
}
|
|
97
|
+
return tagsByVersionId;
|
|
98
|
+
}
|
|
99
|
+
function _loadSummaryChanges(database, cleanupRunId) {
|
|
100
|
+
const deletedTags = database
|
|
101
|
+
.prepare(`
|
|
102
|
+
SELECT COUNT(*) AS count
|
|
103
|
+
FROM cleanup_selected_tags
|
|
104
|
+
WHERE cleanup_run_id = ?
|
|
105
|
+
AND is_deleted = 1
|
|
106
|
+
`)
|
|
107
|
+
.get(cleanupRunId).count;
|
|
108
|
+
const manifestCounts = database
|
|
109
|
+
.prepare(`
|
|
110
|
+
WITH fully_deletable_manifests AS (
|
|
111
|
+
SELECT DISTINCT
|
|
112
|
+
reachable.descendant_digest AS digest,
|
|
113
|
+
manifest.manifest_kind
|
|
114
|
+
FROM cleanup_root_decisions decision
|
|
115
|
+
JOIN manifest_reachability reachable
|
|
116
|
+
ON reachable.scan_id = decision.scan_id
|
|
117
|
+
AND reachable.ancestor_digest = decision.digest
|
|
118
|
+
JOIN manifests manifest
|
|
119
|
+
ON manifest.scan_id = reachable.scan_id
|
|
120
|
+
AND manifest.digest = reachable.descendant_digest
|
|
121
|
+
WHERE decision.cleanup_run_id = ?
|
|
122
|
+
AND decision.validation_status = 'fully-deletable'
|
|
123
|
+
)
|
|
124
|
+
SELECT
|
|
125
|
+
manifest_kind,
|
|
126
|
+
COUNT(*) AS count
|
|
127
|
+
FROM fully_deletable_manifests
|
|
128
|
+
GROUP BY manifest_kind
|
|
129
|
+
`)
|
|
130
|
+
.all(cleanupRunId);
|
|
131
|
+
const countsByKind = new Map(manifestCounts.map((row) => [row.manifest_kind ?? "", row.count]));
|
|
132
|
+
return {
|
|
133
|
+
deletedTags,
|
|
134
|
+
deletedImages: countsByKind.get(ManifestKinds.imageManifest) ?? 0,
|
|
135
|
+
deletedIndexes: countsByKind.get(ManifestKinds.indexManifest) ?? 0,
|
|
136
|
+
deletedCrossArchManifests: countsByKind.get(ManifestKinds.crossArchManifest) ?? 0,
|
|
137
|
+
deletedArtifactManifests: countsByKind.get(ManifestKinds.artifactManifest) ?? 0,
|
|
138
|
+
deletedAttestations: countsByKind.get(ManifestKinds.attestationManifest) ?? 0,
|
|
139
|
+
deletedSignatures: countsByKind.get(ManifestKinds.signatureManifest) ?? 0,
|
|
140
|
+
deletedTotal: manifestCounts.reduce((total, row) => total + row.count, 0)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function writeJsonOutput(args: string[], optionName: string, payload: unknown): void;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { findOption } from "./_args.js";
|
|
3
|
+
export function writeJsonOutput(args, optionName, payload) {
|
|
4
|
+
const json = JSON.stringify(payload);
|
|
5
|
+
const outputPath = findOption(args, optionName);
|
|
6
|
+
if (outputPath) {
|
|
7
|
+
writeFileSync(outputPath, `${json}\n`, "utf8");
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
console.log(json);
|
|
11
|
+
}
|