ghcr-manager 0.9.9 → 1.0.0

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 CHANGED
@@ -7,6 +7,42 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [v1.0.0] - 2026-06-05
11
+
12
+ `v1.0.0` is the first stable `v1.x` release of `ghcr-manager`.
13
+
14
+ This milestone reflects the current project shape after repeated live testing, cleanup-planner tuning, visualizer
15
+ refinement, and a full documentation pass.
16
+
17
+ ### Added
18
+
19
+ - Added one merged graph-scenario SQLite database to the GitHub release assets as
20
+ `ghcr-manager-release-scenarios.sqlite`, so users can explore real before/after cleanup cases immediately in the
21
+ visualizer.
22
+ - Added dedicated user docs for live test scenarios, package setup, and the workflow-to-visualizer path.
23
+
24
+ ### Changed
25
+
26
+ - The release workflow now builds and attaches the merged scenario DB as part of the GitHub release publish flow.
27
+ - Visualizer docs are now centered on one canonical `visualizer/README.md`, with richer screenshots and a release-asset
28
+ quick-demo path.
29
+ - README and companion docs now explain the stable action/CLI/visualizer workflows more directly, including permission
30
+ guidance for dry-runs versus live cleanup and explicit Node.js 24 requirements for local npm installs.
31
+ - Visualizer node labeling was clarified so manifest metadata is easier to interpret during graph inspection.
32
+
33
+ ## [0.9.10] - 2026-06-04
34
+
35
+ ### Changed
36
+
37
+ - Cleanup planning for large package databases is much faster while preserving the existing cleanup behavior.
38
+ - On a large validation package with more than 100k manifests, dry-run planning and summary generation dropped from more
39
+ than 20 minutes to roughly 15 seconds.
40
+
41
+ ### Fixed
42
+
43
+ - Internal GHCR validation workflows now avoid GitHub's "cannot delete the last tagged version" failure mode during
44
+ temporary-tag cleanup.
45
+
10
46
  ## [0.9.9] - 2026-05-30
11
47
 
12
48
  No additional user-facing changes were introduced beyond `0.9.8`.
package/README.md CHANGED
@@ -5,13 +5,19 @@
5
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
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
- Inspect, review, and manage GitHub Container Registry packages.
8
+ Inspect, analyze, and manage large GitHub Container Registry packages.
9
9
 
10
10
  `ghcr-manager` is a GitHub Action for:
11
11
 
12
- - scanning one GHCR package into a SQLite database artifact
13
- - running cleanup with a GitHub step summary and optional DB artifact
14
- - previewing cleanup decisions with `dry-run` before making changes
12
+ - scan GHCR packages: into SQLite database artifacts
13
+ - cleanup packages: with a GitHub step summary and optional DB artifact
14
+ - preview cleanup: with `dry-run` before making changes
15
+ - visualize graphs: and their changes with the
16
+ [visualizer](https://github.com/ghcr-manager/ghcr-manager/blob/main/visualizer/README.md)
17
+
18
+ ![Example compare view: red-bordered manifests are present in the older scan and removed in the newer one.](https://raw.githubusercontent.com/ghcr-manager/ghcr-manager/main/docs/images/visualizer/graph-2images-cosign--wide.png "Example compare view: red-bordered manifests are present in the older scan and removed in the newer one.")
19
+
20
+ _Example graph compare view: red-bordered manifests are present in the older scan and removed in the newer one._
15
21
 
16
22
  ## Quick Start
17
23
 
@@ -32,7 +38,7 @@ jobs:
32
38
 
33
39
  - name: Preview GHCR cleanup
34
40
  id: ghcr-manager
35
- uses: ghcr-manager/ghcr-manager@0.9.9
41
+ uses: ghcr-manager/ghcr-manager@v1.0.0
36
42
  with:
37
43
  command: cleanup
38
44
  token: ${{ github.token }}
@@ -46,6 +52,12 @@ jobs:
46
52
  upload-artifacts: true
47
53
  ```
48
54
 
55
+ > Permission notes:
56
+ >
57
+ > - `scan` and `cleanup` dry-runs need `packages: read`
58
+ > - live `cleanup` that mutates GHCR needs `packages: write`
59
+ > - artifact upload needs `actions: write`
60
+
49
61
  After the run:
50
62
 
51
63
  1. Open the GitHub step summary for the action run.
@@ -69,7 +81,7 @@ The action supports two commands:
69
81
  ### Preview cleanup
70
82
 
71
83
  ```yaml
72
- - uses: ghcr-manager/ghcr-manager@0.9.9
84
+ - uses: ghcr-manager/ghcr-manager@v1.0.0
73
85
  with:
74
86
  command: cleanup
75
87
  token: ${{ github.token }}
@@ -90,7 +102,7 @@ The action supports two commands:
90
102
  ### Apply cleanup
91
103
 
92
104
  ```yaml
93
- - uses: ghcr-manager/ghcr-manager@0.9.9
105
+ - uses: ghcr-manager/ghcr-manager@v1.0.0
94
106
  with:
95
107
  command: cleanup
96
108
  token: ${{ github.token }}
@@ -106,10 +118,13 @@ If `scan-after-cleanup` is `true`, `cleanup` performs a second scan so the uploa
106
118
 
107
119
  Note: the second scan only runs if cleanup actually makes changes.
108
120
 
121
+ Live cleanup permission note: change the workflow permission from `packages: read` to `packages: write` before turning
122
+ off `dry-run`.
123
+
109
124
  ### Scan one package
110
125
 
111
126
  ```yaml
112
- - uses: ghcr-manager/ghcr-manager@0.9.9
127
+ - uses: ghcr-manager/ghcr-manager@v1.0.0
113
128
  with:
114
129
  command: scan
115
130
  token: ${{ github.token }}
@@ -182,13 +197,46 @@ Current naming:
182
197
 
183
198
  ## Documentation Map
184
199
 
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
200
+ - [GitHub Action usage](https://github.com/ghcr-manager/ghcr-manager/blob/main/docs/action-usage.md): action commands,
201
+ including `cleanup` and `scan`
202
+ - [Visualizer](https://github.com/ghcr-manager/ghcr-manager/blob/main/visualizer/README.md): local graph inspection and
203
+ scan-to-scan comparison
204
+ - [Multi-package workflows](https://github.com/ghcr-manager/ghcr-manager/blob/main/docs/db-merge-workflows.md): cleaning
205
+ up multiple packages with one combined DB
206
+ - [SQLite schema guide](https://github.com/ghcr-manager/ghcr-manager/blob/main/docs/schema-description.md): practical
207
+ explanation of the SQLite schema
208
+ - [CLI usage](https://github.com/ghcr-manager/ghcr-manager/blob/main/docs/cli-usage.md): companion CLI usage
209
+
210
+ ## Explore A Real Scenario DB
211
+
212
+ The release assets also include one merged SQLite DB from `ghcr-manager`'s live scenario workflows. You can use it as a
213
+ quick visualizer demo and as a compact way to inspect dozens of real cleanup and graph cases.
214
+
215
+ ```sh
216
+ curl -LO https://github.com/ghcr-manager/ghcr-manager/releases/latest/download/ghcr-manager-release-scenarios.sqlite
217
+ npx ghcr-manager-visualizer --db ./ghcr-manager-release-scenarios.sqlite
218
+ ```
219
+
220
+ For a first look in the visualizer, start with:
221
+
222
+ - owner: `ghcr-manager-test`
223
+ - package: select one with `2images` or `2multiarch` in the name
224
+ - tag search: `image` or `multiarch`
225
+
226
+ For more details, see
227
+ [visualizer/README.md](https://github.com/ghcr-manager/ghcr-manager/blob/main/visualizer/README.md).
228
+
229
+ ## Project
230
+
231
+ Main project and issue tracker:
232
+
233
+ - Repository: <https://github.com/ghcr-manager/ghcr-manager>
234
+ - Issues: <https://github.com/ghcr-manager/ghcr-manager/issues>
190
235
 
191
- ## Acknowledgment
236
+ ## Similar Tools
192
237
 
193
- This project was influenced by [dataaxiom/ghcr-cleanup-action](https://github.com/dataaxiom/ghcr-cleanup-action), with a
194
- similar problem focus and a different implementation approach.
238
+ - [ghcr-manager/ghcr-untag-action](https://github.com/ghcr-manager/ghcr-untag-action): focused tag removal without the
239
+ broader scan and cleanup workflow.
240
+ - [dataaxiom/ghcr-cleanup-action](https://github.com/dataaxiom/ghcr-cleanup-action): another GHCR cleanup action with a
241
+ similar problem focus.
242
+ - [mkoepf/ghcrctl](https://github.com/mkoepf/ghcrctl): CLI tooling for working with GHCR packages and graph deletions.
@@ -4,6 +4,7 @@ export function buildCombinedDirectTargetRootsQuery(scanId, options, selectedTag
4
4
  if (options.cutoffTimestamp) {
5
5
  baseParams.push(options.cutoffTimestamp);
6
6
  }
7
+ baseParams.push(scanId, scanId);
7
8
  const taggedBranchEnabled = options.deleteTagsRequested || options.keepNTagged !== undefined ? 1 : 0;
8
9
  const deleteTagsRequested = options.deleteTagsRequested ? 1 : 0;
9
10
  const deleteOrphanedImages = options.deleteOrphanedImages ? 1 : 0;
@@ -41,35 +42,31 @@ export function buildCombinedDirectTargetRootsQuery(scanId, options, selectedTag
41
42
  WHERE m.scan_id = ?
42
43
  ${cutoffSql}
43
44
  ),
44
- tag_counts AS (
45
- SELECT
46
- t.version_id,
47
- COUNT(t.tag) AS tag_count
48
- FROM tags t
49
- WHERE t.scan_id = ?
50
- AND t.is_digest_tag = 0
51
- GROUP BY t.version_id
52
- ),
53
- parented_digests AS (
54
- SELECT DISTINCT me.child_digest
55
- FROM manifest_edges me
56
- WHERE me.scan_id = ?
57
- AND me.edge_kind != 'digest-tag-referrer'
58
- ),
59
45
  root_candidates AS (
60
46
  SELECT
61
47
  bm.version_id,
62
48
  bm.root_digest,
63
49
  bm.root_manifest_kind,
64
50
  bm.created_at,
65
- COALESCE(tc.tag_count, 0) AS tag_count,
66
- CASE WHEN COALESCE(tc.tag_count, 0) > 0 THEN 1 ELSE 0 END AS is_tagged,
67
- CASE WHEN pd.child_digest IS NULL THEN 0 ELSE 1 END AS has_ancestor
51
+ (
52
+ SELECT COUNT(*)
53
+ FROM tags t
54
+ WHERE t.scan_id = ?
55
+ AND t.version_id = bm.version_id
56
+ AND t.is_digest_tag = 0
57
+ ) AS tag_count,
58
+ CASE
59
+ WHEN EXISTS (
60
+ SELECT 1
61
+ FROM manifest_edges me
62
+ WHERE me.scan_id = ?
63
+ AND me.child_digest = bm.root_digest
64
+ AND me.edge_kind != 'digest-tag-referrer'
65
+ )
66
+ THEN 1
67
+ ELSE 0
68
+ END AS has_ancestor
68
69
  FROM base_manifests bm
69
- LEFT JOIN tag_counts tc
70
- ON tc.version_id = bm.version_id
71
- LEFT JOIN parented_digests pd
72
- ON pd.child_digest = bm.root_digest
73
70
  ),
74
71
  selected_tags AS (
75
72
  ${selectedTagsSql}
@@ -102,7 +99,7 @@ export function buildCombinedDirectTargetRootsQuery(scanId, options, selectedTag
102
99
  LEFT JOIN excluded_versions ev
103
100
  ON ev.version_id = rc.version_id
104
101
  WHERE (
105
- rc.is_tagged = 1
102
+ rc.tag_count > 0
106
103
  OR (? = 1 AND COALESCE(mtc.matched_tag_count, 0) > 0)
107
104
  )
108
105
  AND ev.version_id IS NULL
@@ -156,7 +153,7 @@ export function buildCombinedDirectTargetRootsQuery(scanId, options, selectedTag
156
153
  ORDER BY rc.created_at DESC, rc.version_id DESC, rc.root_digest DESC
157
154
  ) AS recency_rank
158
155
  FROM root_candidates rc
159
- WHERE rc.is_tagged = 0
156
+ WHERE rc.tag_count = 0
160
157
  AND rc.has_ancestor = 0
161
158
  AND (? = 1 OR ? = 1)
162
159
  ),
@@ -5,6 +5,6 @@ export function listCombinedDirectTargetRoots(sql, scanId, options) {
5
5
  const { selectedTagsSql, selectedParams, excludedVersionsSql, excludedParams } = buildDirectTargetRootTagFilters(sql, scanId, options);
6
6
  const { query, baseParams, tailParams } = buildCombinedDirectTargetRootsQuery(scanId, options, selectedTagsSql, excludedVersionsSql);
7
7
  return sql
8
- .all(query, [...baseParams, scanId, scanId, ...selectedParams, ...excludedParams, ...tailParams])
8
+ .all(query, [...baseParams, ...selectedParams, ...excludedParams, ...tailParams])
9
9
  .map(mapPlanRootRow);
10
10
  }
@@ -1 +1 @@
1
- export declare const _LIST_BLOCKED_ROOTS_SQL = "\n WITH selected_graphs AS (\n SELECT DISTINCT\n manifest_graphs.graph_id\n FROM temp_direct_target_roots dtr\n CROSS JOIN manifest_graphs\n WHERE manifest_graphs.scan_id = ?\n AND manifest_graphs.digest = dtr.root_digest\n ),\n retained_tagged_manifests AS (\n SELECT\n m.version_id AS tagged_version_id,\n m.digest AS tagged_digest\n FROM selected_graphs\n CROSS JOIN manifest_graphs\n CROSS JOIN manifests m\n JOIN tags t\n ON t.scan_id = m.scan_id\n AND t.version_id = m.version_id\n AND t.is_digest_tag = 0\n WHERE manifest_graphs.scan_id = m.scan_id\n AND selected_graphs.graph_id = manifest_graphs.graph_id\n AND manifest_graphs.digest = m.digest\n AND m.scan_id = ?\n AND NOT EXISTS (\n SELECT 1\n FROM temp_direct_target_roots dtr\n WHERE dtr.root_digest = m.digest\n )\n ),\n ranked_blocks AS (\n SELECT\n dtr.root_version_id AS blocked_version_id,\n dtr.root_digest AS blocked_digest,\n retained.tagged_version_id AS blocking_version_id,\n retained.tagged_digest AS blocking_digest,\n dtr.root_digest AS overlap_digest,\n dtr.root_manifest_kind AS overlap_manifest_kind,\n 'overlap-with-retained-root' AS block_reason,\n ROW_NUMBER() OVER (\n PARTITION BY dtr.root_digest, retained.tagged_digest\n ORDER BY\n retained_overlap.min_distance,\n dtr.root_digest\n ) AS rn\n FROM temp_direct_target_roots dtr\n JOIN retained_tagged_manifests retained\n ON retained.tagged_digest <> dtr.root_digest\n JOIN manifest_reachability retained_overlap\n ON retained_overlap.scan_id = ?\n AND retained_overlap.ancestor_digest = retained.tagged_digest\n AND retained_overlap.descendant_digest = dtr.root_digest\n )\n SELECT\n blocked_version_id,\n blocked_digest,\n blocking_version_id,\n blocking_digest,\n overlap_digest,\n overlap_manifest_kind,\n block_reason\n FROM ranked_blocks\n WHERE rn = 1\n ORDER BY blocked_digest, blocking_digest, overlap_digest\n";
1
+ export declare const _LIST_BLOCKED_ROOTS_SQL = "\n WITH selected_root_graphs AS (\n SELECT\n dtr.root_version_id,\n dtr.root_digest,\n dtr.root_manifest_kind,\n manifest_graphs.graph_id\n FROM temp_direct_target_roots dtr\n CROSS JOIN manifest_graphs\n WHERE manifest_graphs.scan_id = ?\n AND manifest_graphs.digest = dtr.root_digest\n ),\n selected_graphs AS (\n SELECT DISTINCT\n selected_root_graphs.graph_id\n FROM selected_root_graphs\n ),\n retained_tagged_manifests AS (\n SELECT DISTINCT\n manifest_graphs.graph_id,\n m.version_id AS tagged_version_id,\n m.digest AS tagged_digest\n FROM selected_graphs\n CROSS JOIN manifest_graphs\n CROSS JOIN manifests m\n JOIN tags t\n ON t.scan_id = m.scan_id\n AND t.version_id = m.version_id\n AND t.is_digest_tag = 0\n WHERE manifest_graphs.scan_id = m.scan_id\n AND selected_graphs.graph_id = manifest_graphs.graph_id\n AND manifest_graphs.digest = m.digest\n AND m.scan_id = ?\n AND NOT EXISTS (\n SELECT 1\n FROM temp_direct_target_roots dtr\n WHERE dtr.root_digest = m.digest\n )\n ),\n ranked_blocks AS (\n SELECT\n root_graph.root_version_id AS blocked_version_id,\n root_graph.root_digest AS blocked_digest,\n retained.tagged_version_id AS blocking_version_id,\n retained.tagged_digest AS blocking_digest,\n root_graph.root_digest AS overlap_digest,\n root_graph.root_manifest_kind AS overlap_manifest_kind,\n 'overlap-with-retained-root' AS block_reason,\n ROW_NUMBER() OVER (\n PARTITION BY root_graph.root_digest, retained.tagged_digest\n ORDER BY\n retained_overlap.min_distance,\n root_graph.root_digest\n ) AS rn\n FROM selected_root_graphs root_graph\n JOIN retained_tagged_manifests retained\n ON retained.graph_id = root_graph.graph_id\n AND retained.tagged_digest <> root_graph.root_digest\n JOIN manifest_reachability retained_overlap\n ON retained_overlap.scan_id = ?\n AND retained_overlap.ancestor_digest = retained.tagged_digest\n AND retained_overlap.descendant_digest = root_graph.root_digest\n )\n SELECT\n blocked_version_id,\n blocked_digest,\n blocking_version_id,\n blocking_digest,\n overlap_digest,\n overlap_manifest_kind,\n block_reason\n FROM ranked_blocks\n WHERE rn = 1\n ORDER BY blocked_digest, blocking_digest, overlap_digest\n";
@@ -1,14 +1,23 @@
1
1
  export const _LIST_BLOCKED_ROOTS_SQL = `
2
- WITH selected_graphs AS (
3
- SELECT DISTINCT
2
+ WITH selected_root_graphs AS (
3
+ SELECT
4
+ dtr.root_version_id,
5
+ dtr.root_digest,
6
+ dtr.root_manifest_kind,
4
7
  manifest_graphs.graph_id
5
8
  FROM temp_direct_target_roots dtr
6
9
  CROSS JOIN manifest_graphs
7
10
  WHERE manifest_graphs.scan_id = ?
8
11
  AND manifest_graphs.digest = dtr.root_digest
9
12
  ),
13
+ selected_graphs AS (
14
+ SELECT DISTINCT
15
+ selected_root_graphs.graph_id
16
+ FROM selected_root_graphs
17
+ ),
10
18
  retained_tagged_manifests AS (
11
- SELECT
19
+ SELECT DISTINCT
20
+ manifest_graphs.graph_id,
12
21
  m.version_id AS tagged_version_id,
13
22
  m.digest AS tagged_digest
14
23
  FROM selected_graphs
@@ -30,26 +39,27 @@ export const _LIST_BLOCKED_ROOTS_SQL = `
30
39
  ),
31
40
  ranked_blocks AS (
32
41
  SELECT
33
- dtr.root_version_id AS blocked_version_id,
34
- dtr.root_digest AS blocked_digest,
42
+ root_graph.root_version_id AS blocked_version_id,
43
+ root_graph.root_digest AS blocked_digest,
35
44
  retained.tagged_version_id AS blocking_version_id,
36
45
  retained.tagged_digest AS blocking_digest,
37
- dtr.root_digest AS overlap_digest,
38
- dtr.root_manifest_kind AS overlap_manifest_kind,
46
+ root_graph.root_digest AS overlap_digest,
47
+ root_graph.root_manifest_kind AS overlap_manifest_kind,
39
48
  'overlap-with-retained-root' AS block_reason,
40
49
  ROW_NUMBER() OVER (
41
- PARTITION BY dtr.root_digest, retained.tagged_digest
50
+ PARTITION BY root_graph.root_digest, retained.tagged_digest
42
51
  ORDER BY
43
52
  retained_overlap.min_distance,
44
- dtr.root_digest
53
+ root_graph.root_digest
45
54
  ) AS rn
46
- FROM temp_direct_target_roots dtr
55
+ FROM selected_root_graphs root_graph
47
56
  JOIN retained_tagged_manifests retained
48
- ON retained.tagged_digest <> dtr.root_digest
57
+ ON retained.graph_id = root_graph.graph_id
58
+ AND retained.tagged_digest <> root_graph.root_digest
49
59
  JOIN manifest_reachability retained_overlap
50
60
  ON retained_overlap.scan_id = ?
51
61
  AND retained_overlap.ancestor_digest = retained.tagged_digest
52
- AND retained_overlap.descendant_digest = dtr.root_digest
62
+ AND retained_overlap.descendant_digest = root_graph.root_digest
53
63
  )
54
64
  SELECT
55
65
  blocked_version_id,
@@ -1 +1 @@
1
- export declare const _LIST_CLOSURE_MANIFESTS_SQL = "\n WITH selected_graphs AS (\n SELECT DISTINCT\n manifest_graphs.graph_id\n FROM temp_direct_target_roots dtr\n CROSS JOIN manifest_graphs\n WHERE manifest_graphs.scan_id = ?\n AND manifest_graphs.digest = dtr.root_digest\n ),\n retained_tagged_manifests AS (\n SELECT DISTINCT\n m.version_id,\n m.digest\n FROM selected_graphs\n CROSS JOIN manifest_graphs\n CROSS JOIN manifests m\n JOIN tags t\n ON t.scan_id = m.scan_id\n AND t.version_id = m.version_id\n AND t.is_digest_tag = 0\n WHERE manifest_graphs.scan_id = m.scan_id\n AND selected_graphs.graph_id = manifest_graphs.graph_id\n AND manifest_graphs.digest = m.digest\n AND m.scan_id = ?\n AND NOT EXISTS (\n SELECT 1\n FROM temp_direct_target_roots dtr\n WHERE dtr.root_digest = m.digest\n )\n ),\n retained_manifests AS (\n SELECT\n retained.version_id,\n retained.digest\n FROM retained_tagged_manifests retained\n\n UNION\n\n SELECT\n m.version_id,\n m.digest\n FROM retained_tagged_manifests retained\n CROSS JOIN manifest_reachability mr\n CROSS JOIN manifests m\n WHERE mr.scan_id = ?\n AND mr.ancestor_digest = retained.digest\n AND mr.min_distance > 0\n AND m.scan_id = ?\n AND m.digest = mr.descendant_digest\n ),\n direct_target_closure AS (\n SELECT\n dtr.root_version_id AS source_version_id,\n dtr.root_digest AS source_digest,\n dtr.root_version_id AS member_version_id,\n dtr.root_digest AS member_digest,\n dtr.root_manifest_kind AS member_manifest_kind,\n 0 AS hops_from_root\n FROM temp_direct_target_roots dtr\n\n UNION ALL\n\n SELECT\n dtr.root_version_id AS source_version_id,\n dtr.root_digest AS source_digest,\n m.version_id AS member_version_id,\n m.digest AS member_digest,\n m.manifest_kind AS member_manifest_kind,\n mr.min_distance AS hops_from_root\n FROM temp_direct_target_roots dtr\n CROSS JOIN manifest_reachability mr\n CROSS JOIN manifests m\n WHERE mr.scan_id = ?\n AND mr.ancestor_digest = dtr.root_digest\n AND mr.min_distance > 0\n AND m.scan_id = ?\n AND m.digest = mr.descendant_digest\n ),\n closure_seed AS (\n SELECT\n dtc.source_version_id,\n dtc.source_digest,\n dtc.member_digest,\n dtc.hops_from_root\n FROM direct_target_closure dtc\n WHERE dtc.hops_from_root = 0\n OR NOT EXISTS (\n SELECT 1\n FROM retained_manifests retained\n WHERE retained.digest = dtc.member_digest\n )\n ),\n undirected_edges AS (\n SELECT\n me.parent_digest AS source_digest,\n me.child_digest AS target_digest\n FROM selected_graphs\n CROSS JOIN manifest_graphs parent_graph\n CROSS JOIN manifest_edges me INDEXED BY idx_manifest_edges_scan_parent\n WHERE parent_graph.scan_id = me.scan_id\n AND selected_graphs.graph_id = parent_graph.graph_id\n AND parent_graph.digest = me.parent_digest\n AND me.scan_id = ?\n\n UNION\n\n SELECT\n me.child_digest AS source_digest,\n me.parent_digest AS target_digest\n FROM selected_graphs\n CROSS JOIN manifest_graphs child_graph\n CROSS JOIN manifest_edges me INDEXED BY idx_manifest_edges_scan_child\n WHERE child_graph.scan_id = me.scan_id\n AND selected_graphs.graph_id = child_graph.graph_id\n AND child_graph.digest = me.child_digest\n AND me.scan_id = ?\n ),\n delete_component_members AS (\n SELECT\n seed.source_version_id,\n seed.source_digest,\n seed.member_digest\n FROM closure_seed seed\n\n UNION\n\n SELECT\n walk.source_version_id,\n walk.source_digest,\n m.digest AS member_digest\n FROM delete_component_members walk\n JOIN undirected_edges edge\n ON edge.source_digest = walk.member_digest\n JOIN manifests m\n ON m.scan_id = ?\n AND m.digest = edge.target_digest\n WHERE NOT EXISTS (\n SELECT 1\n FROM retained_manifests retained\n WHERE retained.digest = m.digest\n )\n ),\n source_seed_hops AS (\n SELECT\n seed.source_digest,\n MAX(seed.hops_from_root) AS max_seed_hops\n FROM closure_seed seed\n GROUP BY seed.source_digest\n ),\n descendant_hops AS (\n SELECT\n dtc.source_digest,\n dtc.member_digest,\n MIN(dtc.hops_from_root) AS min_hops_from_root\n FROM direct_target_closure dtc\n WHERE dtc.hops_from_root > 0\n GROUP BY dtc.source_digest, dtc.member_digest\n )\n SELECT\n walk.source_version_id,\n walk.source_digest,\n MIN(member_manifest.version_id) AS member_version_id,\n walk.member_digest,\n MIN(member_manifest.manifest_kind) AS member_manifest_kind,\n CASE\n WHEN walk.member_digest = walk.source_digest\n THEN 0\n WHEN descendant_hops.min_hops_from_root IS NOT NULL\n THEN descendant_hops.min_hops_from_root\n ELSE source_seed_hops.max_seed_hops + 1\n END AS hops_from_root,\n CASE\n WHEN walk.member_digest = walk.source_digest\n THEN 'root'\n WHEN descendant_hops.min_hops_from_root IS NOT NULL\n THEN 'descendant'\n ELSE 'connected'\n END AS member_role\n FROM delete_component_members walk\n JOIN manifests member_manifest\n ON member_manifest.scan_id = ?\n AND member_manifest.digest = walk.member_digest\n JOIN source_seed_hops\n ON source_seed_hops.source_digest = walk.source_digest\n LEFT JOIN descendant_hops\n ON descendant_hops.source_digest = walk.source_digest\n AND descendant_hops.member_digest = walk.member_digest\n GROUP BY\n walk.source_version_id,\n walk.source_digest,\n walk.member_digest,\n descendant_hops.min_hops_from_root,\n source_seed_hops.max_seed_hops\n ORDER BY walk.source_digest, hops_from_root, walk.member_digest\n";
1
+ export declare const _LIST_CLOSURE_MANIFESTS_SQL = "\n WITH selected_graphs AS (\n SELECT DISTINCT\n manifest_graphs.graph_id\n FROM temp_direct_target_roots dtr\n CROSS JOIN manifest_graphs\n WHERE manifest_graphs.scan_id = ?\n AND manifest_graphs.digest = dtr.root_digest\n ),\n retained_tagged_manifests AS (\n SELECT DISTINCT\n m.digest\n FROM selected_graphs\n CROSS JOIN manifest_graphs\n CROSS JOIN manifests m\n JOIN tags t\n ON t.scan_id = m.scan_id\n AND t.version_id = m.version_id\n AND t.is_digest_tag = 0\n WHERE manifest_graphs.scan_id = m.scan_id\n AND selected_graphs.graph_id = manifest_graphs.graph_id\n AND manifest_graphs.digest = m.digest\n AND m.scan_id = ?\n AND NOT EXISTS (\n SELECT 1\n FROM temp_direct_target_roots dtr\n WHERE dtr.root_digest = m.digest\n )\n ),\n retained_manifests AS (\n SELECT\n retained.digest\n FROM retained_tagged_manifests retained\n\n UNION\n\n SELECT\n mr.descendant_digest AS digest\n FROM retained_tagged_manifests retained\n CROSS JOIN manifest_reachability mr\n WHERE mr.scan_id = ?\n AND mr.ancestor_digest = retained.digest\n AND mr.min_distance > 0\n ),\n direct_target_closure AS (\n SELECT\n dtr.root_digest AS source_digest,\n dtr.root_digest AS member_digest,\n 0 AS hops_from_root\n FROM temp_direct_target_roots dtr\n\n UNION ALL\n\n SELECT\n dtr.root_digest AS source_digest,\n mr.descendant_digest AS member_digest,\n mr.min_distance AS hops_from_root\n FROM temp_direct_target_roots dtr\n CROSS JOIN manifest_reachability mr\n WHERE mr.scan_id = ?\n AND mr.ancestor_digest = dtr.root_digest\n AND mr.min_distance > 0\n ),\n closure_seed AS (\n SELECT\n dtc.source_digest,\n dtc.member_digest,\n dtc.hops_from_root\n FROM direct_target_closure dtc\n WHERE dtc.hops_from_root = 0\n OR NOT EXISTS (\n SELECT 1\n FROM retained_manifests retained\n WHERE retained.digest = dtc.member_digest\n )\n ),\n undirected_edges AS (\n SELECT\n me.parent_digest AS source_digest,\n me.child_digest AS target_digest\n FROM selected_graphs\n CROSS JOIN manifest_graphs parent_graph\n CROSS JOIN manifest_edges me INDEXED BY idx_manifest_edges_scan_parent\n WHERE parent_graph.scan_id = me.scan_id\n AND selected_graphs.graph_id = parent_graph.graph_id\n AND parent_graph.digest = me.parent_digest\n AND me.scan_id = ?\n AND NOT EXISTS (\n SELECT 1\n FROM retained_manifests retained\n WHERE retained.digest = me.parent_digest\n OR retained.digest = me.child_digest\n )\n\n UNION\n\n SELECT\n me.child_digest AS source_digest,\n me.parent_digest AS target_digest\n FROM selected_graphs\n CROSS JOIN manifest_graphs child_graph\n CROSS JOIN manifest_edges me INDEXED BY idx_manifest_edges_scan_child\n WHERE child_graph.scan_id = me.scan_id\n AND selected_graphs.graph_id = child_graph.graph_id\n AND child_graph.digest = me.child_digest\n AND me.scan_id = ?\n AND NOT EXISTS (\n SELECT 1\n FROM retained_manifests retained\n WHERE retained.digest = me.parent_digest\n OR retained.digest = me.child_digest\n )\n ),\n delete_component_members AS (\n SELECT\n seed.source_digest,\n seed.member_digest\n FROM closure_seed seed\n\n UNION\n\n SELECT\n walk.source_digest,\n edge.target_digest AS member_digest\n FROM delete_component_members walk\n JOIN undirected_edges edge\n ON edge.source_digest = walk.member_digest\n WHERE NOT EXISTS (\n SELECT 1\n FROM retained_manifests retained\n WHERE retained.digest = edge.target_digest\n )\n ),\n source_seed_hops AS (\n SELECT\n seed.source_digest,\n MAX(seed.hops_from_root) AS max_seed_hops\n FROM closure_seed seed\n GROUP BY seed.source_digest\n ),\n descendant_hops AS (\n SELECT\n dtc.source_digest,\n dtc.member_digest,\n MIN(dtc.hops_from_root) AS min_hops_from_root\n FROM direct_target_closure dtc\n WHERE dtc.hops_from_root > 0\n GROUP BY dtc.source_digest, dtc.member_digest\n )\n SELECT\n dtr.root_version_id AS source_version_id,\n walk.source_digest,\n MIN(member_manifest.version_id) AS member_version_id,\n walk.member_digest,\n MIN(member_manifest.manifest_kind) AS member_manifest_kind,\n CASE\n WHEN walk.member_digest = walk.source_digest\n THEN 0\n WHEN descendant_hops.min_hops_from_root IS NOT NULL\n THEN descendant_hops.min_hops_from_root\n ELSE source_seed_hops.max_seed_hops + 1\n END AS hops_from_root,\n CASE\n WHEN walk.member_digest = walk.source_digest\n THEN 'root'\n WHEN descendant_hops.min_hops_from_root IS NOT NULL\n THEN 'descendant'\n ELSE 'connected'\n END AS member_role\n FROM delete_component_members walk\n JOIN temp_direct_target_roots dtr\n ON dtr.root_digest = walk.source_digest\n JOIN manifests member_manifest\n ON member_manifest.scan_id = ?\n AND member_manifest.digest = walk.member_digest\n JOIN source_seed_hops\n ON source_seed_hops.source_digest = walk.source_digest\n LEFT JOIN descendant_hops\n ON descendant_hops.source_digest = walk.source_digest\n AND descendant_hops.member_digest = walk.member_digest\n GROUP BY\n dtr.root_version_id,\n walk.source_digest,\n walk.member_digest,\n descendant_hops.min_hops_from_root,\n source_seed_hops.max_seed_hops\n ORDER BY walk.source_digest, hops_from_root, walk.member_digest\n";
@@ -9,7 +9,6 @@ export const _LIST_CLOSURE_MANIFESTS_SQL = `
9
9
  ),
10
10
  retained_tagged_manifests AS (
11
11
  SELECT DISTINCT
12
- m.version_id,
13
12
  m.digest
14
13
  FROM selected_graphs
15
14
  CROSS JOIN manifest_graphs
@@ -30,55 +29,40 @@ export const _LIST_CLOSURE_MANIFESTS_SQL = `
30
29
  ),
31
30
  retained_manifests AS (
32
31
  SELECT
33
- retained.version_id,
34
32
  retained.digest
35
33
  FROM retained_tagged_manifests retained
36
34
 
37
35
  UNION
38
36
 
39
37
  SELECT
40
- m.version_id,
41
- m.digest
38
+ mr.descendant_digest AS digest
42
39
  FROM retained_tagged_manifests retained
43
40
  CROSS JOIN manifest_reachability mr
44
- CROSS JOIN manifests m
45
41
  WHERE mr.scan_id = ?
46
42
  AND mr.ancestor_digest = retained.digest
47
43
  AND mr.min_distance > 0
48
- AND m.scan_id = ?
49
- AND m.digest = mr.descendant_digest
50
44
  ),
51
45
  direct_target_closure AS (
52
46
  SELECT
53
- dtr.root_version_id AS source_version_id,
54
47
  dtr.root_digest AS source_digest,
55
- dtr.root_version_id AS member_version_id,
56
48
  dtr.root_digest AS member_digest,
57
- dtr.root_manifest_kind AS member_manifest_kind,
58
49
  0 AS hops_from_root
59
50
  FROM temp_direct_target_roots dtr
60
51
 
61
52
  UNION ALL
62
53
 
63
54
  SELECT
64
- dtr.root_version_id AS source_version_id,
65
55
  dtr.root_digest AS source_digest,
66
- m.version_id AS member_version_id,
67
- m.digest AS member_digest,
68
- m.manifest_kind AS member_manifest_kind,
56
+ mr.descendant_digest AS member_digest,
69
57
  mr.min_distance AS hops_from_root
70
58
  FROM temp_direct_target_roots dtr
71
59
  CROSS JOIN manifest_reachability mr
72
- CROSS JOIN manifests m
73
60
  WHERE mr.scan_id = ?
74
61
  AND mr.ancestor_digest = dtr.root_digest
75
62
  AND mr.min_distance > 0
76
- AND m.scan_id = ?
77
- AND m.digest = mr.descendant_digest
78
63
  ),
79
64
  closure_seed AS (
80
65
  SELECT
81
- dtc.source_version_id,
82
66
  dtc.source_digest,
83
67
  dtc.member_digest,
84
68
  dtc.hops_from_root
@@ -101,6 +85,12 @@ export const _LIST_CLOSURE_MANIFESTS_SQL = `
101
85
  AND selected_graphs.graph_id = parent_graph.graph_id
102
86
  AND parent_graph.digest = me.parent_digest
103
87
  AND me.scan_id = ?
88
+ AND NOT EXISTS (
89
+ SELECT 1
90
+ FROM retained_manifests retained
91
+ WHERE retained.digest = me.parent_digest
92
+ OR retained.digest = me.child_digest
93
+ )
104
94
 
105
95
  UNION
106
96
 
@@ -114,10 +104,15 @@ export const _LIST_CLOSURE_MANIFESTS_SQL = `
114
104
  AND selected_graphs.graph_id = child_graph.graph_id
115
105
  AND child_graph.digest = me.child_digest
116
106
  AND me.scan_id = ?
107
+ AND NOT EXISTS (
108
+ SELECT 1
109
+ FROM retained_manifests retained
110
+ WHERE retained.digest = me.parent_digest
111
+ OR retained.digest = me.child_digest
112
+ )
117
113
  ),
118
114
  delete_component_members AS (
119
115
  SELECT
120
- seed.source_version_id,
121
116
  seed.source_digest,
122
117
  seed.member_digest
123
118
  FROM closure_seed seed
@@ -125,19 +120,15 @@ export const _LIST_CLOSURE_MANIFESTS_SQL = `
125
120
  UNION
126
121
 
127
122
  SELECT
128
- walk.source_version_id,
129
123
  walk.source_digest,
130
- m.digest AS member_digest
124
+ edge.target_digest AS member_digest
131
125
  FROM delete_component_members walk
132
126
  JOIN undirected_edges edge
133
127
  ON edge.source_digest = walk.member_digest
134
- JOIN manifests m
135
- ON m.scan_id = ?
136
- AND m.digest = edge.target_digest
137
128
  WHERE NOT EXISTS (
138
129
  SELECT 1
139
130
  FROM retained_manifests retained
140
- WHERE retained.digest = m.digest
131
+ WHERE retained.digest = edge.target_digest
141
132
  )
142
133
  ),
143
134
  source_seed_hops AS (
@@ -157,7 +148,7 @@ export const _LIST_CLOSURE_MANIFESTS_SQL = `
157
148
  GROUP BY dtc.source_digest, dtc.member_digest
158
149
  )
159
150
  SELECT
160
- walk.source_version_id,
151
+ dtr.root_version_id AS source_version_id,
161
152
  walk.source_digest,
162
153
  MIN(member_manifest.version_id) AS member_version_id,
163
154
  walk.member_digest,
@@ -177,6 +168,8 @@ export const _LIST_CLOSURE_MANIFESTS_SQL = `
177
168
  ELSE 'connected'
178
169
  END AS member_role
179
170
  FROM delete_component_members walk
171
+ JOIN temp_direct_target_roots dtr
172
+ ON dtr.root_digest = walk.source_digest
180
173
  JOIN manifests member_manifest
181
174
  ON member_manifest.scan_id = ?
182
175
  AND member_manifest.digest = walk.member_digest
@@ -186,7 +179,7 @@ export const _LIST_CLOSURE_MANIFESTS_SQL = `
186
179
  ON descendant_hops.source_digest = walk.source_digest
187
180
  AND descendant_hops.member_digest = walk.member_digest
188
181
  GROUP BY
189
- walk.source_version_id,
182
+ dtr.root_version_id,
190
183
  walk.source_digest,
191
184
  walk.member_digest,
192
185
  descendant_hops.min_hops_from_root,
@@ -47,7 +47,7 @@ export class PlannerPlanArtifacts {
47
47
  }
48
48
  #listClosureManifests(scanId) {
49
49
  return this.#sql
50
- .all(_LIST_CLOSURE_MANIFESTS_SQL, [scanId, scanId, scanId, scanId, scanId, scanId, scanId, scanId, scanId, scanId])
50
+ .all(_LIST_CLOSURE_MANIFESTS_SQL, [scanId, scanId, scanId, scanId, scanId, scanId, scanId])
51
51
  .map(mapClosureManifestRow);
52
52
  }
53
53
  #listBlockedRoots(scanId) {
package/package.json CHANGED
@@ -9,7 +9,8 @@
9
9
  "type": "git",
10
10
  "url": "https://github.com/ghcr-manager/ghcr-manager"
11
11
  },
12
- "version": "0.9.9",
12
+ "license": "MIT",
13
+ "version": "v1.0.0",
13
14
  "type": "module",
14
15
  "workspaces": [
15
16
  "visualizer"
@@ -204,10 +204,16 @@ CREATE INDEX IF NOT EXISTS idx_cleanup_runs_scan_id ON cleanup_runs(scan_id);
204
204
  CREATE INDEX IF NOT EXISTS idx_cleanup_protected_root_blocks_run_blocked
205
205
  ON cleanup_protected_root_blocks(cleanup_run_id, blocked_digest);
206
206
  CREATE INDEX IF NOT EXISTS idx_tags_scan_version ON tags(scan_id, version_id);
207
+ CREATE INDEX IF NOT EXISTS idx_tags_scan_version_nondigest
208
+ ON tags(scan_id, version_id)
209
+ WHERE is_digest_tag = 0;
207
210
  CREATE INDEX IF NOT EXISTS idx_manifest_descriptors_scan_child ON manifest_descriptors(scan_id, child_digest);
208
211
  CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_parent ON manifest_edges(scan_id, parent_digest);
209
212
  CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_child ON manifest_edges(scan_id, child_digest);
210
213
  CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_child_kind ON manifest_edges(scan_id, child_digest, edge_kind);
214
+ CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_child_nondigest_referrer
215
+ ON manifest_edges(scan_id, child_digest)
216
+ WHERE edge_kind != 'digest-tag-referrer';
211
217
  CREATE INDEX IF NOT EXISTS idx_manifest_reachability_scan_descendant
212
218
  ON manifest_reachability(scan_id, descendant_digest);
213
219
  CREATE INDEX IF NOT EXISTS idx_manifest_reachability_scan_descendant_distance