ghcr-manager 0.9.7 → 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.
- package/CHANGELOG.md +39 -1
- package/LICENSE +1 -1
- package/README.md +37 -56
- package/dist/cleanup-summary/_cleanup-summary-markdown.js +7 -10
- package/dist/cleanup-summary/_cleanup-summary.d.ts +3 -4
- package/dist/cleanup-summary/_cleanup-summary.js +2 -3
- package/dist/cli/_cleanup-command.js +1 -1
- package/dist/cli/_tag-selector-resolver.js +29 -9
- package/dist/cli/index.js +0 -4
- package/dist/core/_types.d.ts +1 -6
- package/dist/core/_types.js +1 -1
- package/dist/db/_db-merge-scan-copy.js +3 -2
- package/dist/db/_manifest-reachability.js +47 -8
- package/dist/db/_scan-writer.js +1 -13
- package/dist/db/planner/_planner-direct-target-root-options.d.ts +11 -0
- package/dist/db/planner/_planner-direct-target-root-options.js +1 -0
- package/dist/db/planner/_planner-direct-target-root-tag-filters.d.ts +9 -0
- package/dist/db/planner/_planner-direct-target-root-tag-filters.js +42 -0
- package/dist/db/planner/_planner-direct-target-roots-combined-sql.d.ts +7 -0
- package/dist/db/planner/_planner-direct-target-roots-combined-sql.js +198 -0
- package/dist/db/planner/_planner-direct-target-roots-combined.d.ts +4 -0
- package/dist/db/planner/_planner-direct-target-roots-combined.js +10 -0
- package/dist/db/planner/_planner-direct-target-roots-tagged.d.ts +4 -0
- package/dist/db/planner/_planner-direct-target-roots-tagged.js +125 -0
- package/dist/db/planner/_planner-direct-target-roots.d.ts +2 -12
- package/dist/db/planner/_planner-direct-target-roots.js +8 -203
- package/dist/db/planner/_planner-direct-target-tags.js +3 -4
- package/dist/db/planner/_planner-output.js +28 -8
- package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.d.ts +1 -0
- package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.js +65 -0
- package/dist/db/planner/_planner-plan-artifacts-closure-sql.d.ts +1 -0
- package/dist/db/planner/_planner-plan-artifacts-closure-sql.js +195 -0
- package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.d.ts +1 -0
- package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.js +86 -0
- package/dist/db/planner/_planner-plan-artifacts.js +26 -128
- package/dist/db/planner/_planner-sql.js +13 -2
- package/dist/db/planner/_planner-types.d.ts +2 -0
- package/dist/db/planner/_planner-types.js +1 -0
- package/dist/execute/_plan-executor.js +7 -11
- package/dist/execute/_types.d.ts +2 -19
- package/dist/execute/_untag-client.d.ts +2 -2
- package/dist/execute/_untag-client.js +1 -42
- package/dist/execute/index.d.ts +1 -1
- package/dist/ingest/github/_manifest-kind.d.ts +6 -0
- package/dist/ingest/github/_manifest-kind.js +16 -2
- package/package.json +16 -10
- package/resources/sql/schema/001_schema.sql +14 -4
- package/dist/cli/_untag-command.d.ts +0 -1
- package/dist/cli/_untag-command.js +0 -58
- package/dist/db/_manifest-kind-refinement.d.ts +0 -2
- package/dist/db/_manifest-kind-refinement.js +0 -43
- package/resources/sql/views/002_v_scan_root_manifests.sql +0 -44
- package/resources/sql/views/003_v_digest_tag_relations.sql +0 -50
- package/resources/sql/views/004_v_cleanup_root_closure_members.sql +0 -101
- package/resources/sql/views/005_v_cleanup_blocking_overlaps.sql +0 -42
- package/resources/sql/views/006_v_cleanup_root_decision_readable.sql +0 -67
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,44 @@ 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
|
+
|
|
10
48
|
## [0.9.7] - 2026-05-23
|
|
11
49
|
|
|
12
50
|
### Added
|
|
@@ -20,7 +58,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
20
58
|
|
|
21
59
|
- Cleanup dry-run output and GitHub step summaries were reworked to explain the plan more clearly, including a filters
|
|
22
60
|
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 `
|
|
61
|
+
- Informational manifest classification was tuned so only real multi-arch roots are labeled `multi_arch_manifest`, while
|
|
24
62
|
helper-tagged indexes remain `index_manifest`.
|
|
25
63
|
- `merge-run-artifacts` now uses a simpler current-run download flow with direct artifact download handling.
|
|
26
64
|
- Cleanup selected-tag audit and DB-merge metadata handling were tightened alongside the summary/output refactor.
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# ghcr-manager
|
|
2
2
|
|
|
3
|
-
[](https://github.com/gh-workflow/ghcr-manager/releases)
|
|
4
|
-
[](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases)
|
|
5
3
|
[](https://github.com/marketplace/actions/ghcr-manager)
|
|
6
|
-
[](https://github.com/ghcr-manager/ghcr-manager/releases)
|
|
5
|
+
[](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases)
|
|
6
|
+
[](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:
|
|
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
|
|
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:
|
|
72
|
+
- uses: ghcr-manager/ghcr-manager@0.9.8
|
|
76
73
|
with:
|
|
77
74
|
command: cleanup
|
|
78
75
|
token: ${{ github.token }}
|
|
@@ -93,7 +90,7 @@ The action supports three commands:
|
|
|
93
90
|
### Apply cleanup
|
|
94
91
|
|
|
95
92
|
```yaml
|
|
96
|
-
- uses:
|
|
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.7
|
|
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:
|
|
112
|
+
- uses: ghcr-manager/ghcr-manager@0.9.8
|
|
132
113
|
with:
|
|
133
114
|
command: scan
|
|
134
115
|
token: ${{ github.token }}
|
|
@@ -142,32 +123,32 @@ 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
|
|
146
|
-
| ---------------------------- | ----------------------------------- | ---- |
|
|
147
|
-
| `command` | `scan
|
|
148
|
-
| `token` | GitHub token for API calls | all | Yes
|
|
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
|
|
153
|
-
| `scan-after-cleanup` | Run a second scan after cleanup | c | No
|
|
154
|
-
| `db-artifact-retention-days` | Override artifact retention days | s,c | No
|
|
155
|
-
| `delete-tags` | Newline-separated tags to delete | c
|
|
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
|
|
160
|
-
| `delete-ghost-images` | Delete ghost multi-arch roots | c | No
|
|
161
|
-
| `delete-partial-images` | Delete partial multi-arch roots | c | No
|
|
162
|
-
| `delete-orphaned-images` | Delete orphaned digest-derived tags | c | No
|
|
163
|
-
| `older-than` | Age cutoff for cleanup selectors | c | No
|
|
164
|
-
| `use-regex` | Use regex for cleanup tag selectors | c | No
|
|
165
|
-
| `dry-run` | Show changes without mutating GHCR | c
|
|
166
|
-
| `log-level` | CLI log level | all | No
|
|
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
|
|
151
|
+
`Cmds`: `s` = `scan`, `c` = `cleanup`
|
|
171
152
|
|
|
172
153
|
Cleanup command notes:
|
|
173
154
|
|
|
@@ -180,10 +161,10 @@ Cleanup command notes:
|
|
|
180
161
|
|
|
181
162
|
## Outputs
|
|
182
163
|
|
|
183
|
-
| Output | Description
|
|
184
|
-
| ------------------- |
|
|
185
|
-
| `db-path` | SQLite DB path on the runner
|
|
186
|
-
| `summary-json-path` | Summary JSON file path for `cleanup`
|
|
164
|
+
| Output | Description |
|
|
165
|
+
| ------------------- | ------------------------------------ |
|
|
166
|
+
| `db-path` | SQLite DB path on the runner |
|
|
167
|
+
| `summary-json-path` | Summary JSON file path for `cleanup` |
|
|
187
168
|
|
|
188
169
|
## Artifacts
|
|
189
170
|
|
|
@@ -191,7 +172,6 @@ When artifacts are enabled:
|
|
|
191
172
|
|
|
192
173
|
- `scan` always uploads one SQLite DB artifact
|
|
193
174
|
- `cleanup` optionally uploads the DB artifact and a cleanup summary JSON artifact
|
|
194
|
-
- `untag` uploads no artifacts
|
|
195
175
|
|
|
196
176
|
Current naming:
|
|
197
177
|
|
|
@@ -202,7 +182,8 @@ Current naming:
|
|
|
202
182
|
|
|
203
183
|
## Documentation Map
|
|
204
184
|
|
|
205
|
-
- [GitHub Action usage](docs/action-usage.md): action commands, including `cleanup
|
|
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
|
|
206
187
|
- [Multi-package workflows](docs/db-merge-workflows.md): cleaning up multiple packages with one combined DB
|
|
207
188
|
- [SQLite schema guide](docs/schema-description.md): practical explanation of the SQLite schema
|
|
208
189
|
- [CLI usage](docs/cli-usage.md): companion CLI usage
|
|
@@ -16,7 +16,7 @@ export function renderCleanupSummaryMarkdown(summary, options) {
|
|
|
16
16
|
`| 🏷️ Selected tags | ${summary.directTargetTags.length} |`,
|
|
17
17
|
`| 🔖 Deleted tags | ${summary.changes.deletedTags} |`,
|
|
18
18
|
`| 🖼️ Deleted images | ${summary.changes.deletedImages} |`,
|
|
19
|
-
`| 📚 Deleted
|
|
19
|
+
`| 📚 Deleted multi-arch manifests | ${summary.changes.deletedMultiArchManifests} |`,
|
|
20
20
|
`| 🧱 Deleted indexes | ${summary.changes.deletedIndexes} |`,
|
|
21
21
|
`| 📄 Deleted total | ${summary.changes.deletedTotal} |`,
|
|
22
22
|
`| 🔗 Tag-only updates | ${summary.untagOnlyRoots.length} |`,
|
|
@@ -29,7 +29,7 @@ export function renderCleanupSummaryMarkdown(summary, options) {
|
|
|
29
29
|
lines.push(..._renderRootSection("🗑️ Deleted items", summary.fullyDeletableRoots, maxRootsPerSection));
|
|
30
30
|
lines.push(..._renderRootSection("🔗 Tags removed only", summary.untagOnlyRoots, maxRootsPerSection));
|
|
31
31
|
lines.push(..._renderRootSection("🛡️ Blocked items", summary.blockedRoots, maxRootsPerSection));
|
|
32
|
-
if (!summary.dryRun && (summary.
|
|
32
|
+
if (!summary.dryRun && (summary.deletedPackageVersionCount > 0 || summary.detachedTagCount > 0)) {
|
|
33
33
|
lines.push(..._renderLiveEffects(summary));
|
|
34
34
|
}
|
|
35
35
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
@@ -37,7 +37,7 @@ export function renderCleanupSummaryMarkdown(summary, options) {
|
|
|
37
37
|
function _renderPlannedDeleteBreakdown(summary) {
|
|
38
38
|
const rows = [
|
|
39
39
|
{ label: "Images", count: summary.changes.deletedImages },
|
|
40
|
-
{ label: "
|
|
40
|
+
{ label: "Multi-arch manifests", count: summary.changes.deletedMultiArchManifests },
|
|
41
41
|
{ label: "Artifact manifests", count: summary.changes.deletedArtifactManifests },
|
|
42
42
|
{ label: "Signatures", count: summary.changes.deletedSignatures },
|
|
43
43
|
{ label: "Attestations", count: summary.changes.deletedAttestations },
|
|
@@ -105,11 +105,8 @@ function _renderRootSection(title, roots, maxRootsPerSection) {
|
|
|
105
105
|
}
|
|
106
106
|
function _renderLiveEffects(summary) {
|
|
107
107
|
const lines = ["### Applied changes", ""];
|
|
108
|
-
lines.push(`- Deleted package versions: ${summary.
|
|
109
|
-
lines.push(`- Detached tags: ${summary.
|
|
110
|
-
if (summary.unsupportedUntagRoots.length > 0) {
|
|
111
|
-
lines.push(`- Unsupported untag roots: ${summary.unsupportedUntagRoots.length}`);
|
|
112
|
-
}
|
|
108
|
+
lines.push(`- Deleted package versions: ${summary.deletedPackageVersionCount}`);
|
|
109
|
+
lines.push(`- Detached tags: ${summary.detachedTagCount}`);
|
|
113
110
|
lines.push("");
|
|
114
111
|
return lines;
|
|
115
112
|
}
|
|
@@ -211,8 +208,8 @@ function _describeManifestKind(manifestKind) {
|
|
|
211
208
|
switch (manifestKind) {
|
|
212
209
|
case ManifestKinds.imageManifest:
|
|
213
210
|
return "image";
|
|
214
|
-
case ManifestKinds.
|
|
215
|
-
return "
|
|
211
|
+
case ManifestKinds.multiArchManifest:
|
|
212
|
+
return "multi-arch";
|
|
216
213
|
case ManifestKinds.indexManifest:
|
|
217
214
|
return "index";
|
|
218
215
|
case ManifestKinds.signatureManifest:
|
|
@@ -25,7 +25,7 @@ export interface CleanupSummaryChanges {
|
|
|
25
25
|
deletedTags: number;
|
|
26
26
|
deletedImages: number;
|
|
27
27
|
deletedIndexes: number;
|
|
28
|
-
|
|
28
|
+
deletedMultiArchManifests: number;
|
|
29
29
|
deletedArtifactManifests: number;
|
|
30
30
|
deletedAttestations: number;
|
|
31
31
|
deletedSignatures: number;
|
|
@@ -45,9 +45,8 @@ export interface CleanupSummary {
|
|
|
45
45
|
blockedRoots: CleanupSummaryRoot[];
|
|
46
46
|
affectedManifests: CleanupSummaryAffectedManifest[];
|
|
47
47
|
changes: CleanupSummaryChanges;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
unsupportedUntagRoots: DeleteExecutionSummary["unsupportedUntagRoots"];
|
|
48
|
+
deletedPackageVersionCount: DeleteExecutionSummary["deletedPackageVersionCount"];
|
|
49
|
+
detachedTagCount: DeleteExecutionSummary["detachedTagCount"];
|
|
51
50
|
}
|
|
52
51
|
export declare function buildCleanupSummary(plan: DeletePlan, options: {
|
|
53
52
|
dryRun: boolean;
|
|
@@ -20,9 +20,8 @@ export function buildCleanupSummary(plan, options) {
|
|
|
20
20
|
blockedRoots,
|
|
21
21
|
affectedManifests,
|
|
22
22
|
changes: options.changes,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
unsupportedUntagRoots: options.executionSummary?.unsupportedUntagRoots ?? []
|
|
23
|
+
deletedPackageVersionCount: options.executionSummary?.deletedPackageVersionCount ?? 0,
|
|
24
|
+
detachedTagCount: options.executionSummary?.detachedTagCount ?? 0
|
|
26
25
|
};
|
|
27
26
|
}
|
|
28
27
|
function _mapRootDecision(decision, directTargetTagSet, rootTagsByVersionId) {
|
|
@@ -133,7 +133,7 @@ function _loadSummaryChanges(database, cleanupRunId) {
|
|
|
133
133
|
deletedTags,
|
|
134
134
|
deletedImages: countsByKind.get(ManifestKinds.imageManifest) ?? 0,
|
|
135
135
|
deletedIndexes: countsByKind.get(ManifestKinds.indexManifest) ?? 0,
|
|
136
|
-
|
|
136
|
+
deletedMultiArchManifests: countsByKind.get(ManifestKinds.multiArchManifest) ?? 0,
|
|
137
137
|
deletedArtifactManifests: countsByKind.get(ManifestKinds.artifactManifest) ?? 0,
|
|
138
138
|
deletedAttestations: countsByKind.get(ManifestKinds.attestationManifest) ?? 0,
|
|
139
139
|
deletedSignatures: countsByKind.get(ManifestKinds.signatureManifest) ?? 0,
|
|
@@ -96,16 +96,36 @@ function _listLatestBrokenIndexTags(database, owner, packageName, cutoffTimestam
|
|
|
96
96
|
function _listLatestOrphanedTags(database, owner, packageName, cutoffTimestamp) {
|
|
97
97
|
const rows = database
|
|
98
98
|
.prepare(`
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
99
|
+
WITH latest_scan AS (
|
|
100
|
+
SELECT scan_id
|
|
101
|
+
FROM v_latest_scan_per_package
|
|
102
|
+
WHERE owner = ?
|
|
103
|
+
AND package_name = ?
|
|
104
|
+
),
|
|
105
|
+
digest_tag_artifacts AS (
|
|
106
|
+
SELECT
|
|
107
|
+
t.tag,
|
|
108
|
+
t.scan_id,
|
|
109
|
+
t.version_id AS artifact_version_id,
|
|
110
|
+
'sha256:' || SUBSTR(t.tag, 8, 64) AS parent_digest
|
|
111
|
+
FROM latest_scan ls
|
|
112
|
+
JOIN tags t
|
|
113
|
+
ON t.scan_id = ls.scan_id
|
|
114
|
+
WHERE t.is_digest_tag = 1
|
|
115
|
+
)
|
|
116
|
+
SELECT DISTINCT dta.tag
|
|
117
|
+
FROM digest_tag_artifacts dta
|
|
118
|
+
JOIN package_versions pv
|
|
119
|
+
ON pv.scan_id = dta.scan_id
|
|
120
|
+
AND pv.version_id = dta.artifact_version_id
|
|
121
|
+
WHERE NOT EXISTS (
|
|
122
|
+
SELECT 1
|
|
123
|
+
FROM manifests parent
|
|
124
|
+
WHERE parent.scan_id = dta.scan_id
|
|
125
|
+
AND parent.digest = dta.parent_digest
|
|
126
|
+
)
|
|
107
127
|
AND (? IS NULL OR pv.created_at < ?)
|
|
108
|
-
ORDER BY
|
|
128
|
+
ORDER BY dta.tag
|
|
109
129
|
`)
|
|
110
130
|
.all(owner, packageName, cutoffTimestamp ?? null, cutoffTimestamp ?? null);
|
|
111
131
|
return rows.map((row) => row.tag);
|
package/dist/cli/index.js
CHANGED
|
@@ -4,7 +4,6 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { handleCleanup } from "./_cleanup-command.js";
|
|
5
5
|
import { handleDbMerge } from "./_db-merge-command.js";
|
|
6
6
|
import { handleScan } from "./_scan-command.js";
|
|
7
|
-
import { handleUntag } from "./_untag-command.js";
|
|
8
7
|
export async function main(argv) {
|
|
9
8
|
const [command, ...rest] = argv;
|
|
10
9
|
if (!command) {
|
|
@@ -18,8 +17,6 @@ export async function main(argv) {
|
|
|
18
17
|
return handleDbMerge(rest);
|
|
19
18
|
case "scan":
|
|
20
19
|
return handleScan(rest);
|
|
21
|
-
case "untag":
|
|
22
|
-
return handleUntag(rest);
|
|
23
20
|
default:
|
|
24
21
|
throw new Error(`unknown command: ${command}`);
|
|
25
22
|
}
|
|
@@ -29,7 +26,6 @@ function printUsage() {
|
|
|
29
26
|
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] [--summary-json-path <path>] --owner <org> --package <name> [--token <token>] <cleanup selectors...> [--exclude-tag <tag> ...] [--use-regex] [--older-than <interval>]
|
|
30
27
|
ghcr-manager db-merge --db <target-path> --source-db <path> [--source-db <path> ...]
|
|
31
28
|
ghcr-manager scan --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--github-output <path>] --owner <org> --package <name> --token <token>
|
|
32
|
-
ghcr-manager untag [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] [--summary-json-path <path>] --owner <org> --package <name> --token <token> --tag <tag> [--tag <tag> ...]
|
|
33
29
|
|
|
34
30
|
Cleanup selectors:
|
|
35
31
|
--delete-untagged
|
package/dist/core/_types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const ManifestKinds: {
|
|
2
2
|
readonly indexManifest: "index_manifest";
|
|
3
|
-
readonly
|
|
3
|
+
readonly multiArchManifest: "multi_arch_manifest";
|
|
4
4
|
readonly imageManifest: "image_manifest";
|
|
5
5
|
readonly artifactManifest: "artifact_manifest";
|
|
6
6
|
readonly attestationManifest: "attestation_manifest";
|
|
@@ -27,11 +27,6 @@ export interface ManifestRecord {
|
|
|
27
27
|
subjectDigest?: string;
|
|
28
28
|
annotations?: Record<string, unknown>;
|
|
29
29
|
manifestKind?: ManifestKind;
|
|
30
|
-
platform?: {
|
|
31
|
-
architecture?: string;
|
|
32
|
-
os?: string;
|
|
33
|
-
variant?: string;
|
|
34
|
-
};
|
|
35
30
|
}
|
|
36
31
|
export interface ManifestDescriptorRecord {
|
|
37
32
|
parentDigest: string;
|
package/dist/core/_types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const ManifestKinds = {
|
|
2
2
|
indexManifest: "index_manifest",
|
|
3
|
-
|
|
3
|
+
multiArchManifest: "multi_arch_manifest",
|
|
4
4
|
imageManifest: "image_manifest",
|
|
5
5
|
artifactManifest: "artifact_manifest",
|
|
6
6
|
attestationManifest: "attestation_manifest",
|
|
@@ -26,11 +26,12 @@ export class DbMergeScanCopy {
|
|
|
26
26
|
"package_versions(scan_id, version_id, created_at, updated_at)",
|
|
27
27
|
"package_version_payloads(scan_id, version_id, raw_json)",
|
|
28
28
|
"tags(scan_id, tag, version_id, is_digest_tag)",
|
|
29
|
-
"manifests(scan_id, version_id, digest, media_type, artifact_type, config_media_type, subject_digest, annotations_json,
|
|
29
|
+
"manifests(scan_id, version_id, digest, media_type, artifact_type, config_media_type, subject_digest, annotations_json, manifest_kind)",
|
|
30
30
|
"manifest_descriptors(scan_id, parent_digest, child_digest, media_type, artifact_type, platform_os, platform_architecture, platform_variant)",
|
|
31
31
|
"manifest_payloads(scan_id, digest, raw_json)",
|
|
32
32
|
"manifest_edges(scan_id, parent_digest, child_digest, edge_kind)",
|
|
33
|
-
"manifest_reachability(scan_id, ancestor_digest, descendant_digest, min_distance)"
|
|
33
|
+
"manifest_reachability(scan_id, ancestor_digest, descendant_digest, min_distance)",
|
|
34
|
+
"manifest_graphs(scan_id, digest, graph_id)"
|
|
34
35
|
];
|
|
35
36
|
for (const spec of copySpecs) {
|
|
36
37
|
const [tableName, columnList] = spec.split("(");
|
|
@@ -3,14 +3,19 @@ export function rebuildManifestReachability(database, scanId) {
|
|
|
3
3
|
const manifestDigests = _loadManifestDigests(database, scanId);
|
|
4
4
|
const childDigestsByParent = new Map();
|
|
5
5
|
const parentDigestsByChild = new Map();
|
|
6
|
+
const neighborDigestsByDigest = new Map();
|
|
6
7
|
for (const digest of manifestDigests) {
|
|
7
8
|
childDigestsByParent.set(digest, new Set());
|
|
8
9
|
parentDigestsByChild.set(digest, new Set());
|
|
10
|
+
neighborDigestsByDigest.set(digest, new Set());
|
|
9
11
|
}
|
|
10
12
|
for (const manifestEdge of _loadManifestEdges(database, scanId)) {
|
|
11
13
|
childDigestsByParent.get(manifestEdge.parent_digest)?.add(manifestEdge.child_digest);
|
|
12
14
|
parentDigestsByChild.get(manifestEdge.child_digest)?.add(manifestEdge.parent_digest);
|
|
15
|
+
neighborDigestsByDigest.get(manifestEdge.parent_digest)?.add(manifestEdge.child_digest);
|
|
16
|
+
neighborDigestsByDigest.get(manifestEdge.child_digest)?.add(manifestEdge.parent_digest);
|
|
13
17
|
}
|
|
18
|
+
const graphIdsByDigest = _buildGraphIdsByDigest(manifestDigests, neighborDigestsByDigest);
|
|
14
19
|
const remainingChildrenCount = new Map();
|
|
15
20
|
const descendantDistancesByDigest = new Map();
|
|
16
21
|
const readyDigests = [];
|
|
@@ -61,9 +66,19 @@ export function rebuildManifestReachability(database, scanId) {
|
|
|
61
66
|
)
|
|
62
67
|
VALUES(?, ?, ?, ?)
|
|
63
68
|
`);
|
|
69
|
+
const insertGraphRow = database.prepare(`
|
|
70
|
+
INSERT OR REPLACE INTO manifest_graphs(
|
|
71
|
+
scan_id,
|
|
72
|
+
digest,
|
|
73
|
+
graph_id
|
|
74
|
+
)
|
|
75
|
+
VALUES(?, ?, ?)
|
|
76
|
+
`);
|
|
64
77
|
const rebuild = database.transaction(() => {
|
|
65
78
|
database.prepare("DELETE FROM manifest_reachability WHERE scan_id = ?").run(scanId);
|
|
79
|
+
database.prepare("DELETE FROM manifest_graphs WHERE scan_id = ?").run(scanId);
|
|
66
80
|
for (const digest of manifestDigests) {
|
|
81
|
+
insertGraphRow.run(scanId, digest, graphIdsByDigest.get(digest));
|
|
67
82
|
for (const [descendantDigest, distance] of descendantDistancesByDigest.get(digest) ?? []) {
|
|
68
83
|
insertRow.run(scanId, digest, descendantDigest, distance);
|
|
69
84
|
}
|
|
@@ -71,6 +86,32 @@ export function rebuildManifestReachability(database, scanId) {
|
|
|
71
86
|
});
|
|
72
87
|
rebuild();
|
|
73
88
|
}
|
|
89
|
+
function _buildGraphIdsByDigest(manifestDigests, neighborDigestsByDigest) {
|
|
90
|
+
const graphIdsByDigest = new Map();
|
|
91
|
+
let nextGraphId = 1;
|
|
92
|
+
for (const rootDigest of manifestDigests) {
|
|
93
|
+
if (graphIdsByDigest.has(rootDigest)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const pendingDigests = [rootDigest];
|
|
97
|
+
graphIdsByDigest.set(rootDigest, nextGraphId);
|
|
98
|
+
while (pendingDigests.length > 0) {
|
|
99
|
+
const digest = pendingDigests.pop();
|
|
100
|
+
if (!digest) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
for (const neighborDigest of neighborDigestsByDigest.get(digest) ?? []) {
|
|
104
|
+
if (graphIdsByDigest.has(neighborDigest)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
graphIdsByDigest.set(neighborDigest, nextGraphId);
|
|
108
|
+
pendingDigests.push(neighborDigest);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
nextGraphId += 1;
|
|
112
|
+
}
|
|
113
|
+
return graphIdsByDigest;
|
|
114
|
+
}
|
|
74
115
|
function _refreshDigestTagEdges(database, scanId) {
|
|
75
116
|
database.prepare("DELETE FROM manifest_edges WHERE scan_id = ? AND edge_kind = 'digest-tag-referrer'").run(scanId);
|
|
76
117
|
database
|
|
@@ -78,20 +119,18 @@ function _refreshDigestTagEdges(database, scanId) {
|
|
|
78
119
|
INSERT OR IGNORE INTO manifest_edges(scan_id, parent_digest, child_digest, edge_kind)
|
|
79
120
|
SELECT
|
|
80
121
|
t.scan_id,
|
|
81
|
-
|
|
82
|
-
|
|
122
|
+
m.digest AS parent_digest,
|
|
123
|
+
'sha256:' || SUBSTR(t.tag, 8, 64) AS child_digest,
|
|
83
124
|
'digest-tag-referrer' AS edge_kind
|
|
84
125
|
FROM tags t
|
|
85
126
|
JOIN manifests m
|
|
86
127
|
ON m.scan_id = t.scan_id
|
|
87
128
|
AND m.version_id = t.version_id
|
|
88
|
-
JOIN manifests
|
|
89
|
-
ON
|
|
90
|
-
AND
|
|
129
|
+
JOIN manifests child_manifest
|
|
130
|
+
ON child_manifest.scan_id = t.scan_id
|
|
131
|
+
AND child_manifest.digest = 'sha256:' || SUBSTR(t.tag, 8, 64)
|
|
91
132
|
WHERE t.scan_id = ?
|
|
92
|
-
AND t.
|
|
93
|
-
AND LENGTH(t.tag) >= 71
|
|
94
|
-
AND SUBSTR(t.tag, 8, 64) NOT GLOB '*[^0-9A-Fa-f]*'
|
|
133
|
+
AND t.is_digest_tag = 1
|
|
95
134
|
`)
|
|
96
135
|
.run(scanId);
|
|
97
136
|
}
|
package/dist/db/_scan-writer.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { isDigestTag } from "../core/index.js";
|
|
3
3
|
import { resolveGitHubActionsRunUrl } from "./_github-actions-run-url.js";
|
|
4
|
-
import { refineManifestKinds } from "./_manifest-kind-refinement.js";
|
|
5
4
|
import { rebuildManifestReachability } from "./_manifest-reachability.js";
|
|
6
5
|
export class ScanWriter {
|
|
7
6
|
#database;
|
|
@@ -63,9 +62,6 @@ export class ScanWriter {
|
|
|
63
62
|
config_media_type,
|
|
64
63
|
subject_digest,
|
|
65
64
|
annotations_json,
|
|
66
|
-
platform_os,
|
|
67
|
-
platform_architecture,
|
|
68
|
-
platform_variant,
|
|
69
65
|
manifest_kind
|
|
70
66
|
)
|
|
71
67
|
VALUES(
|
|
@@ -77,9 +73,6 @@ export class ScanWriter {
|
|
|
77
73
|
@configMediaType,
|
|
78
74
|
@subjectDigest,
|
|
79
75
|
@annotationsJson,
|
|
80
|
-
@platformOs,
|
|
81
|
-
@platformArchitecture,
|
|
82
|
-
@platformVariant,
|
|
83
76
|
@manifestKind
|
|
84
77
|
)
|
|
85
78
|
`);
|
|
@@ -152,9 +145,6 @@ export class ScanWriter {
|
|
|
152
145
|
configMediaType: manifest.configMediaType ?? null,
|
|
153
146
|
subjectDigest: manifest.subjectDigest ?? null,
|
|
154
147
|
annotationsJson: manifest.annotations ? JSON.stringify(manifest.annotations) : null,
|
|
155
|
-
platformOs: manifest.platform?.os ?? null,
|
|
156
|
-
platformArchitecture: manifest.platform?.architecture ?? null,
|
|
157
|
-
platformVariant: manifest.platform?.variant ?? null,
|
|
158
148
|
manifestKind: manifest.manifestKind ?? null
|
|
159
149
|
});
|
|
160
150
|
}
|
|
@@ -180,9 +170,7 @@ export class ScanWriter {
|
|
|
180
170
|
});
|
|
181
171
|
}
|
|
182
172
|
rebuildManifestReachability() {
|
|
183
|
-
|
|
184
|
-
rebuildManifestReachability(this.#database, scanId);
|
|
185
|
-
refineManifestKinds(this.#database, scanId);
|
|
173
|
+
rebuildManifestReachability(this.#database, this.#requireScanId());
|
|
186
174
|
}
|
|
187
175
|
getActiveScanId() {
|
|
188
176
|
return this.#requireScanId();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface DirectTargetRootOptions {
|
|
2
|
+
deleteTags: string[];
|
|
3
|
+
deleteTagsRequested: boolean;
|
|
4
|
+
deleteOrphanedImages?: boolean;
|
|
5
|
+
excludeTags: string[];
|
|
6
|
+
deleteUntagged: boolean;
|
|
7
|
+
keepNTagged?: number;
|
|
8
|
+
keepNUntagged?: number;
|
|
9
|
+
useRegex?: boolean;
|
|
10
|
+
cutoffTimestamp?: string;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DirectTargetRootOptions } from "./_planner-direct-target-root-options.js";
|
|
2
|
+
import type { PlannerSql } from "./_planner-sql.js";
|
|
3
|
+
export interface DirectTargetRootTagFilters {
|
|
4
|
+
selectedTagsSql: string;
|
|
5
|
+
selectedParams: Array<number | string>;
|
|
6
|
+
excludedVersionsSql: string;
|
|
7
|
+
excludedParams: Array<number | string>;
|
|
8
|
+
}
|
|
9
|
+
export declare function buildDirectTargetRootTagFilters(sql: PlannerSql, scanId: number, options: DirectTargetRootOptions): DirectTargetRootTagFilters;
|