ghcr-manager 0.9.7 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/LICENSE +1 -1
  3. package/README.md +37 -56
  4. package/dist/cleanup-summary/_cleanup-summary-markdown.js +7 -10
  5. package/dist/cleanup-summary/_cleanup-summary.d.ts +3 -4
  6. package/dist/cleanup-summary/_cleanup-summary.js +2 -3
  7. package/dist/cli/_cleanup-command.js +1 -1
  8. package/dist/cli/_tag-selector-resolver.js +29 -9
  9. package/dist/cli/index.js +0 -4
  10. package/dist/core/_types.d.ts +1 -6
  11. package/dist/core/_types.js +1 -1
  12. package/dist/db/_db-merge-scan-copy.js +3 -2
  13. package/dist/db/_manifest-reachability.js +47 -8
  14. package/dist/db/_scan-writer.js +1 -13
  15. package/dist/db/planner/_planner-direct-target-root-options.d.ts +11 -0
  16. package/dist/db/planner/_planner-direct-target-root-options.js +1 -0
  17. package/dist/db/planner/_planner-direct-target-root-tag-filters.d.ts +9 -0
  18. package/dist/db/planner/_planner-direct-target-root-tag-filters.js +42 -0
  19. package/dist/db/planner/_planner-direct-target-roots-combined-sql.d.ts +7 -0
  20. package/dist/db/planner/_planner-direct-target-roots-combined-sql.js +198 -0
  21. package/dist/db/planner/_planner-direct-target-roots-combined.d.ts +4 -0
  22. package/dist/db/planner/_planner-direct-target-roots-combined.js +10 -0
  23. package/dist/db/planner/_planner-direct-target-roots-tagged.d.ts +4 -0
  24. package/dist/db/planner/_planner-direct-target-roots-tagged.js +125 -0
  25. package/dist/db/planner/_planner-direct-target-roots.d.ts +2 -12
  26. package/dist/db/planner/_planner-direct-target-roots.js +8 -203
  27. package/dist/db/planner/_planner-direct-target-tags.js +3 -4
  28. package/dist/db/planner/_planner-output.js +28 -8
  29. package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.d.ts +1 -0
  30. package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.js +65 -0
  31. package/dist/db/planner/_planner-plan-artifacts-closure-sql.d.ts +1 -0
  32. package/dist/db/planner/_planner-plan-artifacts-closure-sql.js +195 -0
  33. package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.d.ts +1 -0
  34. package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.js +86 -0
  35. package/dist/db/planner/_planner-plan-artifacts.js +26 -128
  36. package/dist/db/planner/_planner-sql.js +13 -2
  37. package/dist/db/planner/_planner-types.d.ts +2 -0
  38. package/dist/db/planner/_planner-types.js +1 -0
  39. package/dist/execute/_plan-executor.js +7 -11
  40. package/dist/execute/_types.d.ts +2 -19
  41. package/dist/execute/_untag-client.d.ts +2 -2
  42. package/dist/execute/_untag-client.js +1 -42
  43. package/dist/execute/index.d.ts +1 -1
  44. package/dist/ingest/github/_manifest-kind.d.ts +6 -0
  45. package/dist/ingest/github/_manifest-kind.js +16 -2
  46. package/package.json +16 -10
  47. package/resources/sql/schema/001_schema.sql +14 -4
  48. package/dist/cli/_untag-command.d.ts +0 -1
  49. package/dist/cli/_untag-command.js +0 -58
  50. package/dist/db/_manifest-kind-refinement.d.ts +0 -2
  51. package/dist/db/_manifest-kind-refinement.js +0 -43
  52. package/resources/sql/views/002_v_scan_root_manifests.sql +0 -44
  53. package/resources/sql/views/003_v_digest_tag_relations.sql +0 -50
  54. package/resources/sql/views/004_v_cleanup_root_closure_members.sql +0 -101
  55. package/resources/sql/views/005_v_cleanup_blocking_overlaps.sql +0 -42
  56. package/resources/sql/views/006_v_cleanup_root_decision_readable.sql +0 -67
@@ -0,0 +1,42 @@
1
+ import { buildTagSelectorPredicate } from "./_planner-tag-selectors.js";
2
+ export function buildDirectTargetRootTagFilters(sql, scanId, options) {
3
+ const selectedTagPredicate = options.deleteTags.length > 0
4
+ ? buildTagSelectorPredicate(sql.database, "t.tag", options.deleteTags, options.useRegex ?? false)
5
+ : undefined;
6
+ const excludedTagPredicate = options.excludeTags.length > 0
7
+ ? buildTagSelectorPredicate(sql.database, "xt.tag", options.excludeTags, options.useRegex ?? false)
8
+ : undefined;
9
+ const selectedTagDigestFlag = options.deleteOrphanedImages ? 1 : 0;
10
+ const selectedTagsSql = selectedTagPredicate
11
+ ? `
12
+ SELECT DISTINCT t.version_id, t.tag
13
+ FROM tags t
14
+ WHERE t.scan_id = ?
15
+ AND t.is_digest_tag = ?
16
+ AND (${selectedTagPredicate.sql})
17
+ `
18
+ : `
19
+ SELECT NULL AS version_id, NULL AS tag
20
+ WHERE 1 = 0
21
+ `;
22
+ const selectedParams = selectedTagPredicate ? [scanId, selectedTagDigestFlag, ...selectedTagPredicate.params] : [];
23
+ const excludedVersionsSql = excludedTagPredicate
24
+ ? `
25
+ SELECT DISTINCT xt.version_id
26
+ FROM tags xt
27
+ WHERE xt.scan_id = ?
28
+ AND xt.is_digest_tag = 0
29
+ AND (${excludedTagPredicate.sql})
30
+ `
31
+ : `
32
+ SELECT NULL AS version_id
33
+ WHERE 1 = 0
34
+ `;
35
+ const excludedParams = excludedTagPredicate ? [scanId, ...excludedTagPredicate.params] : [];
36
+ return {
37
+ selectedTagsSql,
38
+ selectedParams,
39
+ excludedVersionsSql,
40
+ excludedParams
41
+ };
42
+ }
@@ -0,0 +1,7 @@
1
+ import type { DirectTargetRootOptions } from "./_planner-direct-target-root-options.js";
2
+ export interface CombinedDirectTargetRootsQuery {
3
+ query: string;
4
+ baseParams: Array<number | string>;
5
+ tailParams: Array<number | string>;
6
+ }
7
+ export declare function buildCombinedDirectTargetRootsQuery(scanId: number, options: DirectTargetRootOptions, selectedTagsSql: string, excludedVersionsSql: string): CombinedDirectTargetRootsQuery;
@@ -0,0 +1,198 @@
1
+ export function buildCombinedDirectTargetRootsQuery(scanId, options, selectedTagsSql, excludedVersionsSql) {
2
+ const baseParams = [scanId];
3
+ const cutoffSql = options.cutoffTimestamp ? "AND created_at < ?" : "";
4
+ if (options.cutoffTimestamp) {
5
+ baseParams.push(options.cutoffTimestamp);
6
+ }
7
+ const taggedBranchEnabled = options.deleteTagsRequested || options.keepNTagged !== undefined ? 1 : 0;
8
+ const deleteTagsRequested = options.deleteTagsRequested ? 1 : 0;
9
+ const deleteOrphanedImages = options.deleteOrphanedImages ? 1 : 0;
10
+ const keepNTaggedActive = options.keepNTagged !== undefined ? 1 : 0;
11
+ const deleteUntagged = options.deleteUntagged ? 1 : 0;
12
+ const keepNUntaggedActive = options.keepNUntagged !== undefined ? 1 : 0;
13
+ const tailParams = [
14
+ deleteOrphanedImages,
15
+ deleteOrphanedImages,
16
+ taggedBranchEnabled,
17
+ deleteTagsRequested,
18
+ deleteTagsRequested,
19
+ keepNTaggedActive,
20
+ deleteTagsRequested,
21
+ keepNTaggedActive,
22
+ options.keepNTagged ?? 0,
23
+ deleteUntagged,
24
+ keepNUntaggedActive,
25
+ deleteUntagged,
26
+ deleteUntagged,
27
+ keepNUntaggedActive,
28
+ options.keepNUntagged ?? 0
29
+ ];
30
+ const query = `
31
+ WITH base_manifests AS (
32
+ SELECT
33
+ m.version_id,
34
+ m.digest AS root_digest,
35
+ m.manifest_kind AS root_manifest_kind,
36
+ pv.created_at
37
+ FROM manifests m
38
+ JOIN package_versions pv
39
+ ON pv.scan_id = m.scan_id
40
+ AND pv.version_id = m.version_id
41
+ WHERE m.scan_id = ?
42
+ ${cutoffSql}
43
+ ),
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
+ root_candidates AS (
60
+ SELECT
61
+ bm.version_id,
62
+ bm.root_digest,
63
+ bm.root_manifest_kind,
64
+ 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
68
+ 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
+ ),
74
+ selected_tags AS (
75
+ ${selectedTagsSql}
76
+ ),
77
+ excluded_versions AS (
78
+ ${excludedVersionsSql}
79
+ ),
80
+ matched_tag_counts AS (
81
+ SELECT
82
+ st.version_id,
83
+ COUNT(DISTINCT st.tag) AS matched_tag_count
84
+ FROM selected_tags st
85
+ GROUP BY st.version_id
86
+ ),
87
+ eligible_tagged_roots AS (
88
+ SELECT
89
+ rc.version_id,
90
+ rc.root_digest,
91
+ rc.root_manifest_kind,
92
+ rc.created_at,
93
+ CASE
94
+ WHEN ? = 1 AND rc.tag_count = 0 AND COALESCE(mtc.matched_tag_count, 0) > 0
95
+ THEN COALESCE(mtc.matched_tag_count, 0)
96
+ ELSE rc.tag_count
97
+ END AS total_tag_count,
98
+ COALESCE(mtc.matched_tag_count, 0) AS matched_tag_count
99
+ FROM root_candidates rc
100
+ LEFT JOIN matched_tag_counts mtc
101
+ ON mtc.version_id = rc.version_id
102
+ LEFT JOIN excluded_versions ev
103
+ ON ev.version_id = rc.version_id
104
+ WHERE (
105
+ rc.is_tagged = 1
106
+ OR (? = 1 AND COALESCE(mtc.matched_tag_count, 0) > 0)
107
+ )
108
+ AND ev.version_id IS NULL
109
+ AND ? = 1
110
+ ),
111
+ ranked_tagged_roots AS (
112
+ SELECT
113
+ version_id,
114
+ root_digest,
115
+ root_manifest_kind,
116
+ total_tag_count,
117
+ matched_tag_count,
118
+ ROW_NUMBER() OVER (
119
+ ORDER BY created_at DESC, version_id DESC, root_digest DESC
120
+ ) AS recency_rank
121
+ FROM eligible_tagged_roots
122
+ WHERE ? = 0
123
+ OR matched_tag_count > 0
124
+ ),
125
+ final_tagged_targets AS (
126
+ SELECT
127
+ version_id,
128
+ root_digest,
129
+ root_manifest_kind,
130
+ CASE
131
+ WHEN ? = 0
132
+ THEN 'keep-n-tagged-overflow'
133
+ WHEN ? = 1 AND total_tag_count = matched_tag_count
134
+ THEN 'keep-n-tagged-overflow'
135
+ WHEN total_tag_count = matched_tag_count
136
+ THEN 'delete-tags-all-tags-selected'
137
+ ELSE 'delete-tags-partial-tag-match'
138
+ END AS direct_target_reason,
139
+ CASE
140
+ WHEN ? = 0
141
+ THEN 'delete-root'
142
+ WHEN total_tag_count = matched_tag_count
143
+ THEN 'delete-root'
144
+ ELSE 'untag-only'
145
+ END AS selection_mode
146
+ FROM ranked_tagged_roots
147
+ WHERE ? = 0
148
+ OR recency_rank > ?
149
+ ),
150
+ ranked_untagged_roots AS (
151
+ SELECT
152
+ rc.version_id,
153
+ rc.root_digest,
154
+ rc.root_manifest_kind,
155
+ ROW_NUMBER() OVER (
156
+ ORDER BY rc.created_at DESC, rc.version_id DESC, rc.root_digest DESC
157
+ ) AS recency_rank
158
+ FROM root_candidates rc
159
+ WHERE rc.is_tagged = 0
160
+ AND rc.has_ancestor = 0
161
+ AND (? = 1 OR ? = 1)
162
+ ),
163
+ final_untagged_targets AS (
164
+ SELECT
165
+ version_id,
166
+ root_digest,
167
+ root_manifest_kind,
168
+ CASE
169
+ WHEN ? = 1
170
+ THEN 'delete-untagged'
171
+ ELSE 'keep-n-untagged-overflow'
172
+ END AS direct_target_reason,
173
+ 'delete-root' AS selection_mode
174
+ FROM ranked_untagged_roots
175
+ WHERE ? = 1
176
+ OR (? = 1 AND recency_rank > ?)
177
+ )
178
+ SELECT
179
+ version_id,
180
+ root_digest,
181
+ root_manifest_kind,
182
+ direct_target_reason,
183
+ selection_mode
184
+ FROM final_tagged_targets
185
+
186
+ UNION ALL
187
+
188
+ SELECT
189
+ version_id,
190
+ root_digest,
191
+ root_manifest_kind,
192
+ direct_target_reason,
193
+ selection_mode
194
+ FROM final_untagged_targets
195
+ ORDER BY root_digest
196
+ `;
197
+ return { query, baseParams, tailParams };
198
+ }
@@ -0,0 +1,4 @@
1
+ import type { DirectTargetRootOptions } from "./_planner-direct-target-root-options.js";
2
+ import type { PlannerSql } from "./_planner-sql.js";
3
+ import { type DeletePlanRoot } from "./_planner-types.js";
4
+ export declare function listCombinedDirectTargetRoots(sql: PlannerSql, scanId: number, options: DirectTargetRootOptions): DeletePlanRoot[];
@@ -0,0 +1,10 @@
1
+ import { buildDirectTargetRootTagFilters } from "./_planner-direct-target-root-tag-filters.js";
2
+ import { buildCombinedDirectTargetRootsQuery } from "./_planner-direct-target-roots-combined-sql.js";
3
+ import { mapPlanRootRow } from "./_planner-types.js";
4
+ export function listCombinedDirectTargetRoots(sql, scanId, options) {
5
+ const { selectedTagsSql, selectedParams, excludedVersionsSql, excludedParams } = buildDirectTargetRootTagFilters(sql, scanId, options);
6
+ const { query, baseParams, tailParams } = buildCombinedDirectTargetRootsQuery(scanId, options, selectedTagsSql, excludedVersionsSql);
7
+ return sql
8
+ .all(query, [...baseParams, scanId, scanId, ...selectedParams, ...excludedParams, ...tailParams])
9
+ .map(mapPlanRootRow);
10
+ }
@@ -0,0 +1,4 @@
1
+ import { PlannerSql } from "./_planner-sql.js";
2
+ import { type DeletePlanRoot } from "./_planner-types.js";
3
+ import type { DirectTargetRootOptions } from "./_planner-direct-target-root-options.js";
4
+ export declare function listTaggedOnlyDirectTargetRoots(sql: PlannerSql, scanId: number, options: DirectTargetRootOptions): DeletePlanRoot[];
@@ -0,0 +1,125 @@
1
+ import { buildTagSelectorPredicate } from "./_planner-tag-selectors.js";
2
+ import { mapPlanRootRow } from "./_planner-types.js";
3
+ export function listTaggedOnlyDirectTargetRoots(sql, scanId, options) {
4
+ const selectedTagPredicate = options.deleteTags.length > 0
5
+ ? buildTagSelectorPredicate(sql.database, "t.tag", options.deleteTags, options.useRegex ?? false)
6
+ : undefined;
7
+ const excludedTagPredicate = options.excludeTags.length > 0
8
+ ? buildTagSelectorPredicate(sql.database, "xt.tag", options.excludeTags, options.useRegex ?? false)
9
+ : undefined;
10
+ const params = [];
11
+ const cutoffSql = options.cutoffTimestamp ? "AND pv.created_at < ?" : "";
12
+ if (options.cutoffTimestamp) {
13
+ params.push(options.cutoffTimestamp);
14
+ }
15
+ const selectedTagDigestFlag = options.deleteOrphanedImages ? 1 : 0;
16
+ const selectedTagsSql = selectedTagPredicate
17
+ ? `
18
+ SELECT DISTINCT t.version_id, t.tag
19
+ FROM tags t
20
+ WHERE t.scan_id = ?
21
+ AND t.is_digest_tag = ?
22
+ AND (${selectedTagPredicate.sql})
23
+ `
24
+ : `
25
+ SELECT NULL AS version_id, NULL AS tag
26
+ WHERE 1 = 0
27
+ `;
28
+ if (selectedTagPredicate) {
29
+ params.push(scanId, selectedTagDigestFlag, ...selectedTagPredicate.params);
30
+ }
31
+ const excludedVersionsSql = excludedTagPredicate
32
+ ? `
33
+ SELECT DISTINCT xt.version_id
34
+ FROM tags xt
35
+ WHERE xt.scan_id = ?
36
+ AND xt.is_digest_tag = 0
37
+ AND (${excludedTagPredicate.sql})
38
+ `
39
+ : `
40
+ SELECT NULL AS version_id
41
+ WHERE 1 = 0
42
+ `;
43
+ if (excludedTagPredicate) {
44
+ params.push(scanId, ...excludedTagPredicate.params);
45
+ }
46
+ const deleteOrphanedImages = options.deleteOrphanedImages ? 1 : 0;
47
+ const query = `
48
+ WITH selected_tags AS (
49
+ ${selectedTagsSql}
50
+ ),
51
+ excluded_versions AS (
52
+ ${excludedVersionsSql}
53
+ ),
54
+ matched_tag_counts AS (
55
+ SELECT
56
+ st.version_id,
57
+ COUNT(DISTINCT st.tag) AS matched_tag_count
58
+ FROM selected_tags st
59
+ GROUP BY st.version_id
60
+ ),
61
+ tagged_versions AS (
62
+ SELECT
63
+ m.version_id,
64
+ m.digest AS root_digest,
65
+ m.manifest_kind AS root_manifest_kind,
66
+ COUNT(t.tag) AS total_tag_count
67
+ FROM manifests m
68
+ JOIN package_versions pv
69
+ ON pv.scan_id = m.scan_id
70
+ AND pv.version_id = m.version_id
71
+ LEFT JOIN tags t
72
+ ON t.scan_id = m.scan_id
73
+ AND t.version_id = m.version_id
74
+ AND t.is_digest_tag = 0
75
+ WHERE m.scan_id = ?
76
+ ${cutoffSql}
77
+ GROUP BY
78
+ m.version_id,
79
+ m.digest,
80
+ m.manifest_kind
81
+ ),
82
+ eligible_tagged_roots AS (
83
+ SELECT
84
+ tv.version_id,
85
+ tv.root_digest,
86
+ tv.root_manifest_kind,
87
+ CASE
88
+ WHEN ? = 1 AND tv.total_tag_count = 0 AND COALESCE(mtc.matched_tag_count, 0) > 0
89
+ THEN COALESCE(mtc.matched_tag_count, 0)
90
+ ELSE tv.total_tag_count
91
+ END AS total_tag_count,
92
+ COALESCE(mtc.matched_tag_count, 0) AS matched_tag_count
93
+ FROM tagged_versions tv
94
+ LEFT JOIN matched_tag_counts mtc
95
+ ON mtc.version_id = tv.version_id
96
+ LEFT JOIN excluded_versions ev
97
+ ON ev.version_id = tv.version_id
98
+ WHERE (
99
+ tv.total_tag_count > 0
100
+ OR (? = 1 AND COALESCE(mtc.matched_tag_count, 0) > 0)
101
+ )
102
+ AND ev.version_id IS NULL
103
+ AND matched_tag_count > 0
104
+ )
105
+ SELECT
106
+ version_id,
107
+ root_digest,
108
+ root_manifest_kind,
109
+ CASE
110
+ WHEN total_tag_count = matched_tag_count
111
+ THEN 'delete-tags-all-tags-selected'
112
+ ELSE 'delete-tags-partial-tag-match'
113
+ END AS direct_target_reason,
114
+ CASE
115
+ WHEN total_tag_count = matched_tag_count
116
+ THEN 'delete-root'
117
+ ELSE 'untag-only'
118
+ END AS selection_mode
119
+ FROM eligible_tagged_roots
120
+ ORDER BY root_digest
121
+ `;
122
+ return sql
123
+ .all(query, [...params, scanId, deleteOrphanedImages, deleteOrphanedImages])
124
+ .map(mapPlanRootRow);
125
+ }
@@ -1,16 +1,6 @@
1
1
  import { PlannerSql } from "./_planner-sql.js";
2
- import { type DeletePlanRoot } from "./_planner-types.js";
3
- export interface DirectTargetRootOptions {
4
- deleteTags: string[];
5
- deleteTagsRequested: boolean;
6
- deleteOrphanedImages?: boolean;
7
- excludeTags: string[];
8
- deleteUntagged: boolean;
9
- keepNTagged?: number;
10
- keepNUntagged?: number;
11
- useRegex?: boolean;
12
- cutoffTimestamp?: string;
13
- }
2
+ import type { DeletePlanRoot } from "./_planner-types.js";
3
+ import type { DirectTargetRootOptions } from "./_planner-direct-target-root-options.js";
14
4
  export declare class PlannerDirectTargetRoots {
15
5
  #private;
16
6
  constructor(sql: PlannerSql);
@@ -1,212 +1,17 @@
1
- import { buildTagSelectorPredicate } from "./_planner-tag-selectors.js";
2
- import { mapPlanRootRow } from "./_planner-types.js";
1
+ import { listCombinedDirectTargetRoots } from "./_planner-direct-target-roots-combined.js";
2
+ import { listTaggedOnlyDirectTargetRoots } from "./_planner-direct-target-roots-tagged.js";
3
3
  export class PlannerDirectTargetRoots {
4
4
  #sql;
5
5
  constructor(sql) {
6
6
  this.#sql = sql;
7
7
  }
8
8
  list(scanId, options) {
9
- const selectedTagPredicate = options.deleteTags.length > 0
10
- ? buildTagSelectorPredicate(this.#sql.database, "t.tag", options.deleteTags, options.useRegex ?? false)
11
- : undefined;
12
- const excludedTagPredicate = options.excludeTags.length > 0
13
- ? buildTagSelectorPredicate(this.#sql.database, "xt.tag", options.excludeTags, options.useRegex ?? false)
14
- : undefined;
15
- const params = [scanId];
16
- const cutoffSql = options.cutoffTimestamp ? "AND created_at < ?" : "";
17
- if (options.cutoffTimestamp) {
18
- params.push(options.cutoffTimestamp);
9
+ if (options.deleteTagsRequested &&
10
+ options.keepNTagged === undefined &&
11
+ !options.deleteUntagged &&
12
+ options.keepNUntagged === undefined) {
13
+ return listTaggedOnlyDirectTargetRoots(this.#sql, scanId, options);
19
14
  }
20
- const selectedTagDigestFlag = options.deleteOrphanedImages ? 1 : 0;
21
- const selectedTagsSql = selectedTagPredicate
22
- ? `
23
- SELECT DISTINCT t.version_id, t.tag
24
- FROM tags t
25
- WHERE t.scan_id = ?
26
- AND t.is_digest_tag = ?
27
- AND (${selectedTagPredicate.sql})
28
- `
29
- : `
30
- SELECT NULL AS version_id, NULL AS tag
31
- WHERE 1 = 0
32
- `;
33
- if (selectedTagPredicate) {
34
- params.push(scanId, selectedTagDigestFlag, ...selectedTagPredicate.params);
35
- }
36
- const excludedVersionsSql = excludedTagPredicate
37
- ? `
38
- SELECT DISTINCT xt.version_id
39
- FROM tags xt
40
- WHERE xt.scan_id = ?
41
- AND xt.is_digest_tag = 0
42
- AND (${excludedTagPredicate.sql})
43
- `
44
- : `
45
- SELECT NULL AS version_id
46
- WHERE 1 = 0
47
- `;
48
- if (excludedTagPredicate) {
49
- params.push(scanId, ...excludedTagPredicate.params);
50
- }
51
- const taggedBranchEnabled = options.deleteTagsRequested || options.keepNTagged !== undefined ? 1 : 0;
52
- const deleteTagsRequested = options.deleteTagsRequested ? 1 : 0;
53
- const deleteOrphanedImages = options.deleteOrphanedImages ? 1 : 0;
54
- const keepNTaggedActive = options.keepNTagged !== undefined ? 1 : 0;
55
- const deleteUntagged = options.deleteUntagged ? 1 : 0;
56
- const keepNUntaggedActive = options.keepNUntagged !== undefined ? 1 : 0;
57
- const paramsTail = [
58
- deleteOrphanedImages,
59
- deleteOrphanedImages,
60
- taggedBranchEnabled,
61
- deleteTagsRequested,
62
- deleteTagsRequested,
63
- keepNTaggedActive,
64
- deleteTagsRequested,
65
- keepNTaggedActive,
66
- options.keepNTagged ?? 0,
67
- deleteUntagged,
68
- keepNUntaggedActive,
69
- deleteUntagged,
70
- deleteUntagged,
71
- keepNUntaggedActive,
72
- options.keepNUntagged ?? 0
73
- ];
74
- const sql = `
75
- WITH root_candidates AS (
76
- SELECT
77
- root_version_id AS version_id,
78
- root_digest,
79
- root_manifest_kind,
80
- created_at,
81
- tag_count,
82
- is_tagged
83
- FROM v_scan_root_manifests
84
- WHERE scan_id = ?
85
- AND has_ancestor = 0
86
- ${cutoffSql}
87
- ),
88
- selected_tags AS (
89
- ${selectedTagsSql}
90
- ),
91
- excluded_versions AS (
92
- ${excludedVersionsSql}
93
- ),
94
- matched_tag_counts AS (
95
- SELECT
96
- st.version_id,
97
- COUNT(DISTINCT st.tag) AS matched_tag_count
98
- FROM selected_tags st
99
- GROUP BY st.version_id
100
- ),
101
- eligible_tagged_roots AS (
102
- SELECT
103
- rc.version_id,
104
- rc.root_digest,
105
- rc.root_manifest_kind,
106
- rc.created_at,
107
- CASE
108
- WHEN ? = 1 AND rc.tag_count = 0 AND COALESCE(mtc.matched_tag_count, 0) > 0
109
- THEN COALESCE(mtc.matched_tag_count, 0)
110
- ELSE rc.tag_count
111
- END AS total_tag_count,
112
- COALESCE(mtc.matched_tag_count, 0) AS matched_tag_count
113
- FROM root_candidates rc
114
- LEFT JOIN matched_tag_counts mtc
115
- ON mtc.version_id = rc.version_id
116
- LEFT JOIN excluded_versions ev
117
- ON ev.version_id = rc.version_id
118
- WHERE (
119
- rc.is_tagged = 1
120
- OR (? = 1 AND COALESCE(mtc.matched_tag_count, 0) > 0)
121
- )
122
- AND ev.version_id IS NULL
123
- AND ? = 1
124
- ),
125
- ranked_tagged_roots AS (
126
- SELECT
127
- version_id,
128
- root_digest,
129
- root_manifest_kind,
130
- total_tag_count,
131
- matched_tag_count,
132
- ROW_NUMBER() OVER (
133
- ORDER BY created_at DESC, version_id DESC, root_digest DESC
134
- ) AS recency_rank
135
- FROM eligible_tagged_roots
136
- WHERE ? = 0
137
- OR matched_tag_count > 0
138
- ),
139
- final_tagged_targets AS (
140
- SELECT
141
- version_id,
142
- root_digest,
143
- root_manifest_kind,
144
- CASE
145
- WHEN ? = 0
146
- THEN 'keep-n-tagged-overflow'
147
- WHEN ? = 1 AND total_tag_count = matched_tag_count
148
- THEN 'keep-n-tagged-overflow'
149
- WHEN total_tag_count = matched_tag_count
150
- THEN 'delete-tags-all-tags-selected'
151
- ELSE 'delete-tags-partial-tag-match'
152
- END AS direct_target_reason,
153
- CASE
154
- WHEN ? = 0
155
- THEN 'delete-root'
156
- WHEN total_tag_count = matched_tag_count
157
- THEN 'delete-root'
158
- ELSE 'untag-only'
159
- END AS selection_mode
160
- FROM ranked_tagged_roots
161
- WHERE ? = 0
162
- OR recency_rank > ?
163
- ),
164
- ranked_untagged_roots AS (
165
- SELECT
166
- rc.version_id,
167
- rc.root_digest,
168
- rc.root_manifest_kind,
169
- ROW_NUMBER() OVER (
170
- ORDER BY rc.created_at DESC, rc.version_id DESC, rc.root_digest DESC
171
- ) AS recency_rank
172
- FROM root_candidates rc
173
- WHERE rc.is_tagged = 0
174
- AND (? = 1 OR ? = 1)
175
- ),
176
- final_untagged_targets AS (
177
- SELECT
178
- version_id,
179
- root_digest,
180
- root_manifest_kind,
181
- CASE
182
- WHEN ? = 1
183
- THEN 'delete-untagged'
184
- ELSE 'keep-n-untagged-overflow'
185
- END AS direct_target_reason,
186
- 'delete-root' AS selection_mode
187
- FROM ranked_untagged_roots
188
- WHERE ? = 1
189
- OR (? = 1 AND recency_rank > ?)
190
- )
191
- SELECT
192
- version_id,
193
- root_digest,
194
- root_manifest_kind,
195
- direct_target_reason,
196
- selection_mode
197
- FROM final_tagged_targets
198
-
199
- UNION ALL
200
-
201
- SELECT
202
- version_id,
203
- root_digest,
204
- root_manifest_kind,
205
- direct_target_reason,
206
- selection_mode
207
- FROM final_untagged_targets
208
- ORDER BY root_digest
209
- `;
210
- return this.#sql.all(sql, [...params, ...paramsTail]).map(mapPlanRootRow);
15
+ return listCombinedDirectTargetRoots(this.#sql, scanId, options);
211
16
  }
212
17
  }
@@ -38,12 +38,11 @@ export class PlannerDirectTargetTags {
38
38
  JOIN package_versions pv
39
39
  ON pv.scan_id = t.scan_id
40
40
  AND pv.version_id = t.version_id
41
- JOIN v_scan_root_manifests roots
42
- ON roots.scan_id = t.scan_id
43
- AND roots.root_version_id = t.version_id
41
+ JOIN manifests m
42
+ ON m.scan_id = pv.scan_id
43
+ AND m.version_id = pv.version_id
44
44
  WHERE t.scan_id = ?
45
45
  AND t.is_digest_tag = ?
46
- AND roots.has_ancestor = 0
47
46
  AND (${selectedTagPredicate.sql})
48
47
  ${excludedRootSql}
49
48
  ${olderThanSql}