ghcr-cleanup-manager 1.1.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.
Files changed (152) hide show
  1. package/CHANGELOG.md +356 -0
  2. package/LICENSE +21 -0
  3. package/README.md +262 -0
  4. package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +5 -0
  5. package/dist/cleanup-summary/_cleanup-summary-markdown.js +224 -0
  6. package/dist/cleanup-summary/_cleanup-summary.d.ts +56 -0
  7. package/dist/cleanup-summary/_cleanup-summary.js +59 -0
  8. package/dist/cleanup-summary/index.d.ts +2 -0
  9. package/dist/cleanup-summary/index.js +2 -0
  10. package/dist/cli/_args.d.ts +7 -0
  11. package/dist/cli/_args.js +41 -0
  12. package/dist/cli/_cleanup-command.d.ts +1 -0
  13. package/dist/cli/_cleanup-command.js +142 -0
  14. package/dist/cli/_db-merge-command.d.ts +1 -0
  15. package/dist/cli/_db-merge-command.js +41 -0
  16. package/dist/cli/_github-output.d.ts +10 -0
  17. package/dist/cli/_github-output.js +13 -0
  18. package/dist/cli/_json-output.d.ts +1 -0
  19. package/dist/cli/_json-output.js +11 -0
  20. package/dist/cli/_logger.d.ts +10 -0
  21. package/dist/cli/_logger.js +26 -0
  22. package/dist/cli/_older-than.d.ts +5 -0
  23. package/dist/cli/_older-than.js +42 -0
  24. package/dist/cli/_planner-options.d.ts +20 -0
  25. package/dist/cli/_planner-options.js +83 -0
  26. package/dist/cli/_scan-command.d.ts +1 -0
  27. package/dist/cli/_scan-command.js +39 -0
  28. package/dist/cli/_tag-selector-resolver.d.ts +3 -0
  29. package/dist/cli/_tag-selector-resolver.js +132 -0
  30. package/dist/cli/index.d.ts +2 -0
  31. package/dist/cli/index.js +52 -0
  32. package/dist/config/_service-constants.d.ts +3 -0
  33. package/dist/config/_service-constants.js +3 -0
  34. package/dist/config/index.d.ts +9 -0
  35. package/dist/config/index.js +9 -0
  36. package/dist/core/_digest-tag.d.ts +2 -0
  37. package/dist/core/_digest-tag.js +12 -0
  38. package/dist/core/_github-package-owner.d.ts +11 -0
  39. package/dist/core/_github-package-owner.js +45 -0
  40. package/dist/core/_http-error.d.ts +6 -0
  41. package/dist/core/_http-error.js +33 -0
  42. package/dist/core/_types.d.ts +54 -0
  43. package/dist/core/_types.js +8 -0
  44. package/dist/core/index.d.ts +6 -0
  45. package/dist/core/index.js +4 -0
  46. package/dist/db/_cleanup-run-writer.d.ts +10 -0
  47. package/dist/db/_cleanup-run-writer.js +113 -0
  48. package/dist/db/_db-merge-cleanup-copy.d.ts +7 -0
  49. package/dist/db/_db-merge-cleanup-copy.js +172 -0
  50. package/dist/db/_db-merge-history.d.ts +2 -0
  51. package/dist/db/_db-merge-history.js +15 -0
  52. package/dist/db/_db-merge-repository.d.ts +8 -0
  53. package/dist/db/_db-merge-repository.js +95 -0
  54. package/dist/db/_db-merge-scan-copy.d.ts +10 -0
  55. package/dist/db/_db-merge-scan-copy.js +70 -0
  56. package/dist/db/_db-merge-types.d.ts +44 -0
  57. package/dist/db/_db-merge-types.js +1 -0
  58. package/dist/db/_github-actions-run-url.d.ts +1 -0
  59. package/dist/db/_github-actions-run-url.js +9 -0
  60. package/dist/db/_manifest-reachability.d.ts +2 -0
  61. package/dist/db/_manifest-reachability.js +158 -0
  62. package/dist/db/_scan-writer.d.ts +20 -0
  63. package/dist/db/_scan-writer.js +184 -0
  64. package/dist/db/_schema.d.ts +2 -0
  65. package/dist/db/_schema.js +19 -0
  66. package/dist/db/_snapshot-repository.d.ts +24 -0
  67. package/dist/db/_snapshot-repository.js +86 -0
  68. package/dist/db/_sql-placeholders.d.ts +2 -0
  69. package/dist/db/_sql-placeholders.js +16 -0
  70. package/dist/db/index.d.ts +10 -0
  71. package/dist/db/index.js +13 -0
  72. package/dist/db/planner/_planner-direct-target-root-options.d.ts +11 -0
  73. package/dist/db/planner/_planner-direct-target-root-options.js +1 -0
  74. package/dist/db/planner/_planner-direct-target-root-tag-filters.d.ts +7 -0
  75. package/dist/db/planner/_planner-direct-target-root-tag-filters.js +41 -0
  76. package/dist/db/planner/_planner-direct-target-roots-combined-sql.d.ts +7 -0
  77. package/dist/db/planner/_planner-direct-target-roots-combined-sql.js +189 -0
  78. package/dist/db/planner/_planner-direct-target-roots-combined.d.ts +4 -0
  79. package/dist/db/planner/_planner-direct-target-roots-combined.js +10 -0
  80. package/dist/db/planner/_planner-direct-target-roots-tagged.d.ts +4 -0
  81. package/dist/db/planner/_planner-direct-target-roots-tagged.js +117 -0
  82. package/dist/db/planner/_planner-direct-target-roots.d.ts +8 -0
  83. package/dist/db/planner/_planner-direct-target-roots.js +17 -0
  84. package/dist/db/planner/_planner-direct-target-tags.d.ts +6 -0
  85. package/dist/db/planner/_planner-direct-target-tags.js +54 -0
  86. package/dist/db/planner/_planner-latest-scan.d.ts +10 -0
  87. package/dist/db/planner/_planner-latest-scan.js +20 -0
  88. package/dist/db/planner/_planner-output.d.ts +5 -0
  89. package/dist/db/planner/_planner-output.js +111 -0
  90. package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.d.ts +1 -0
  91. package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.js +110 -0
  92. package/dist/db/planner/_planner-plan-artifacts-closure-sql.d.ts +1 -0
  93. package/dist/db/planner/_planner-plan-artifacts-closure-sql.js +188 -0
  94. package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.d.ts +1 -0
  95. package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.js +86 -0
  96. package/dist/db/planner/_planner-plan-artifacts.d.ts +7 -0
  97. package/dist/db/planner/_planner-plan-artifacts.js +109 -0
  98. package/dist/db/planner/_planner-repository.d.ts +44 -0
  99. package/dist/db/planner/_planner-repository.js +105 -0
  100. package/dist/db/planner/_planner-sql.d.ts +12 -0
  101. package/dist/db/planner/_planner-sql.js +46 -0
  102. package/dist/db/planner/_planner-tag-selectors.d.ts +8 -0
  103. package/dist/db/planner/_planner-tag-selectors.js +57 -0
  104. package/dist/db/planner/_planner-types.d.ts +144 -0
  105. package/dist/db/planner/_planner-types.js +49 -0
  106. package/dist/db/planner/index.d.ts +3 -0
  107. package/dist/db/planner/index.js +2 -0
  108. package/dist/execute/_http.d.ts +7 -0
  109. package/dist/execute/_http.js +48 -0
  110. package/dist/execute/_manifest-detach.d.ts +4 -0
  111. package/dist/execute/_manifest-detach.js +31 -0
  112. package/dist/execute/_package-version-delete-client.d.ts +4 -0
  113. package/dist/execute/_package-version-delete-client.js +34 -0
  114. package/dist/execute/_package-version-page-client.d.ts +14 -0
  115. package/dist/execute/_package-version-page-client.js +64 -0
  116. package/dist/execute/_package-version-tag-source-client.d.ts +12 -0
  117. package/dist/execute/_package-version-tag-source-client.js +65 -0
  118. package/dist/execute/_plan-executor.d.ts +3 -0
  119. package/dist/execute/_plan-executor.js +69 -0
  120. package/dist/execute/_registry-manifest-client.d.ts +12 -0
  121. package/dist/execute/_registry-manifest-client.js +79 -0
  122. package/dist/execute/_registry-token-client.d.ts +4 -0
  123. package/dist/execute/_registry-token-client.js +37 -0
  124. package/dist/execute/_types.d.ts +34 -0
  125. package/dist/execute/_types.js +1 -0
  126. package/dist/execute/_untag-client.d.ts +2 -0
  127. package/dist/execute/_untag-client.js +30 -0
  128. package/dist/execute/index.d.ts +5 -0
  129. package/dist/execute/index.js +3 -0
  130. package/dist/ingest/github/_manifest-client.d.ts +14 -0
  131. package/dist/ingest/github/_manifest-client.js +108 -0
  132. package/dist/ingest/github/_manifest-ingest.d.ts +3 -0
  133. package/dist/ingest/github/_manifest-ingest.js +90 -0
  134. package/dist/ingest/github/_manifest-kind.d.ts +26 -0
  135. package/dist/ingest/github/_manifest-kind.js +65 -0
  136. package/dist/ingest/github/_package-metadata-load.d.ts +5 -0
  137. package/dist/ingest/github/_package-metadata-load.js +45 -0
  138. package/dist/ingest/github/_package-version-page-load.d.ts +13 -0
  139. package/dist/ingest/github/_package-version-page-load.js +55 -0
  140. package/dist/ingest/github/_packages-client.d.ts +10 -0
  141. package/dist/ingest/github/_packages-client.js +76 -0
  142. package/dist/ingest/github/_parallel-paginated-ingest.d.ts +12 -0
  143. package/dist/ingest/github/_parallel-paginated-ingest.js +49 -0
  144. package/dist/ingest/github/_registry-token-client.d.ts +6 -0
  145. package/dist/ingest/github/_registry-token-client.js +67 -0
  146. package/dist/ingest/github/_shared.d.ts +28 -0
  147. package/dist/ingest/github/_shared.js +70 -0
  148. package/dist/ingest/github/index.d.ts +11 -0
  149. package/dist/ingest/github/index.js +29 -0
  150. package/package.json +78 -0
  151. package/resources/sql/schema/001_schema.sql +222 -0
  152. package/resources/sql/views/001_v_latest_scan_per_package.sql +27 -0
@@ -0,0 +1,189 @@
1
+ export function buildCombinedDirectTargetRootsQuery(scanId, options, selectedTagsSql) {
2
+ const baseParams = [scanId];
3
+ const cutoffSql = options.cutoffTimestamp ? "AND created_at < ?" : "";
4
+ if (options.cutoffTimestamp) {
5
+ baseParams.push(options.cutoffTimestamp);
6
+ }
7
+ baseParams.push(scanId, scanId);
8
+ const taggedBranchEnabled = options.deleteTagsRequested || options.keepNTagged !== undefined ? 1 : 0;
9
+ const deleteTagsRequested = options.deleteTagsRequested ? 1 : 0;
10
+ const deleteOrphanedImages = options.deleteOrphanedImages ? 1 : 0;
11
+ const keepNTaggedActive = options.keepNTagged !== undefined ? 1 : 0;
12
+ const deleteUntagged = options.deleteUntagged ? 1 : 0;
13
+ const keepNUntaggedActive = options.keepNUntagged !== undefined ? 1 : 0;
14
+ const tailParams = [
15
+ deleteOrphanedImages,
16
+ deleteOrphanedImages,
17
+ taggedBranchEnabled,
18
+ deleteTagsRequested,
19
+ deleteTagsRequested,
20
+ keepNTaggedActive,
21
+ deleteTagsRequested,
22
+ keepNTaggedActive,
23
+ options.keepNTagged ?? 0,
24
+ deleteUntagged,
25
+ keepNUntaggedActive,
26
+ deleteUntagged,
27
+ deleteUntagged,
28
+ keepNUntaggedActive,
29
+ options.keepNUntagged ?? 0
30
+ ];
31
+ const query = `
32
+ WITH base_manifests AS (
33
+ SELECT
34
+ m.version_id,
35
+ m.digest AS root_digest,
36
+ m.manifest_kind AS root_manifest_kind,
37
+ pv.created_at
38
+ FROM manifests m
39
+ JOIN package_versions pv
40
+ ON pv.scan_id = m.scan_id
41
+ AND pv.version_id = m.version_id
42
+ WHERE m.scan_id = ?
43
+ ${cutoffSql}
44
+ ),
45
+ root_candidates AS (
46
+ SELECT
47
+ bm.version_id,
48
+ bm.root_digest,
49
+ bm.root_manifest_kind,
50
+ bm.created_at,
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
69
+ FROM base_manifests bm
70
+ ),
71
+ selected_tags AS (
72
+ ${selectedTagsSql}
73
+ ),
74
+ matched_tag_counts AS (
75
+ SELECT
76
+ st.version_id,
77
+ COUNT(DISTINCT st.tag) AS matched_tag_count
78
+ FROM selected_tags st
79
+ GROUP BY st.version_id
80
+ ),
81
+ eligible_tagged_roots AS (
82
+ SELECT
83
+ rc.version_id,
84
+ rc.root_digest,
85
+ rc.root_manifest_kind,
86
+ rc.created_at,
87
+ CASE
88
+ WHEN ? = 1 AND rc.tag_count = 0 AND COALESCE(mtc.matched_tag_count, 0) > 0
89
+ THEN COALESCE(mtc.matched_tag_count, 0)
90
+ ELSE rc.tag_count
91
+ END AS total_tag_count,
92
+ COALESCE(mtc.matched_tag_count, 0) AS matched_tag_count
93
+ FROM root_candidates rc
94
+ LEFT JOIN matched_tag_counts mtc
95
+ ON mtc.version_id = rc.version_id
96
+ WHERE (
97
+ rc.tag_count > 0
98
+ OR (? = 1 AND COALESCE(mtc.matched_tag_count, 0) > 0)
99
+ )
100
+ AND ? = 1
101
+ ),
102
+ ranked_tagged_roots AS (
103
+ SELECT
104
+ version_id,
105
+ root_digest,
106
+ root_manifest_kind,
107
+ total_tag_count,
108
+ matched_tag_count,
109
+ ROW_NUMBER() OVER (
110
+ ORDER BY created_at DESC, version_id DESC, root_digest DESC
111
+ ) AS recency_rank
112
+ FROM eligible_tagged_roots
113
+ WHERE ? = 0
114
+ OR matched_tag_count > 0
115
+ ),
116
+ final_tagged_targets AS (
117
+ SELECT
118
+ version_id,
119
+ root_digest,
120
+ root_manifest_kind,
121
+ CASE
122
+ WHEN ? = 0
123
+ THEN 'keep-n-tagged-overflow'
124
+ WHEN ? = 1 AND total_tag_count = matched_tag_count
125
+ THEN 'keep-n-tagged-overflow'
126
+ WHEN total_tag_count = matched_tag_count
127
+ THEN 'delete-tags-all-tags-selected'
128
+ ELSE 'delete-tags-partial-tag-match'
129
+ END AS direct_target_reason,
130
+ CASE
131
+ WHEN ? = 0
132
+ THEN 'delete-root'
133
+ WHEN total_tag_count = matched_tag_count
134
+ THEN 'delete-root'
135
+ ELSE 'untag-only'
136
+ END AS selection_mode
137
+ FROM ranked_tagged_roots
138
+ WHERE ? = 0
139
+ OR recency_rank > ?
140
+ ),
141
+ ranked_untagged_roots AS (
142
+ SELECT
143
+ rc.version_id,
144
+ rc.root_digest,
145
+ rc.root_manifest_kind,
146
+ ROW_NUMBER() OVER (
147
+ ORDER BY rc.created_at DESC, rc.version_id DESC, rc.root_digest DESC
148
+ ) AS recency_rank
149
+ FROM root_candidates rc
150
+ WHERE rc.tag_count = 0
151
+ AND rc.has_ancestor = 0
152
+ AND (? = 1 OR ? = 1)
153
+ ),
154
+ final_untagged_targets AS (
155
+ SELECT
156
+ version_id,
157
+ root_digest,
158
+ root_manifest_kind,
159
+ CASE
160
+ WHEN ? = 1
161
+ THEN 'delete-untagged'
162
+ ELSE 'keep-n-untagged-overflow'
163
+ END AS direct_target_reason,
164
+ 'delete-root' AS selection_mode
165
+ FROM ranked_untagged_roots
166
+ WHERE ? = 1
167
+ OR (? = 1 AND recency_rank > ?)
168
+ )
169
+ SELECT
170
+ version_id,
171
+ root_digest,
172
+ root_manifest_kind,
173
+ direct_target_reason,
174
+ selection_mode
175
+ FROM final_tagged_targets
176
+
177
+ UNION ALL
178
+
179
+ SELECT
180
+ version_id,
181
+ root_digest,
182
+ root_manifest_kind,
183
+ direct_target_reason,
184
+ selection_mode
185
+ FROM final_untagged_targets
186
+ ORDER BY root_digest
187
+ `;
188
+ return { query, baseParams, tailParams };
189
+ }
@@ -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 } = buildDirectTargetRootTagFilters(sql, scanId, options);
6
+ const { query, baseParams, tailParams } = buildCombinedDirectTargetRootsQuery(scanId, options, selectedTagsSql);
7
+ return sql
8
+ .all(query, [...baseParams, ...selectedParams, ...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,117 @@
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
+ const selectedTagDigestFlag = options.deleteOrphanedImages ? 1 : 0;
13
+ const selectedTagsSql = selectedTagPredicate
14
+ ? `
15
+ SELECT DISTINCT t.version_id, t.tag
16
+ FROM tags t
17
+ WHERE t.scan_id = ?
18
+ AND t.is_digest_tag = ?
19
+ AND (${selectedTagPredicate.sql})
20
+ ${excludedTagPredicate
21
+ ? `
22
+ AND NOT EXISTS (
23
+ SELECT 1
24
+ FROM tags xt
25
+ WHERE xt.scan_id = t.scan_id
26
+ AND xt.version_id = t.version_id
27
+ AND xt.tag = t.tag
28
+ AND (${excludedTagPredicate.sql})
29
+ )
30
+ `
31
+ : ""}
32
+ `
33
+ : `
34
+ SELECT NULL AS version_id, NULL AS tag
35
+ WHERE 1 = 0
36
+ `;
37
+ if (selectedTagPredicate) {
38
+ params.push(scanId, selectedTagDigestFlag, ...selectedTagPredicate.params, ...(excludedTagPredicate?.params ?? []));
39
+ }
40
+ params.push(scanId);
41
+ if (options.cutoffTimestamp) {
42
+ params.push(options.cutoffTimestamp);
43
+ }
44
+ const deleteOrphanedImages = options.deleteOrphanedImages ? 1 : 0;
45
+ const query = `
46
+ WITH selected_tags AS (
47
+ ${selectedTagsSql}
48
+ ),
49
+ matched_tag_counts AS (
50
+ SELECT
51
+ st.version_id,
52
+ COUNT(DISTINCT st.tag) AS matched_tag_count
53
+ FROM selected_tags st
54
+ GROUP BY st.version_id
55
+ ),
56
+ tagged_versions AS (
57
+ SELECT
58
+ m.version_id,
59
+ m.digest AS root_digest,
60
+ m.manifest_kind AS root_manifest_kind,
61
+ COUNT(t.tag) AS total_tag_count
62
+ FROM manifests m
63
+ JOIN package_versions pv
64
+ ON pv.scan_id = m.scan_id
65
+ AND pv.version_id = m.version_id
66
+ LEFT JOIN tags t
67
+ ON t.scan_id = m.scan_id
68
+ AND t.version_id = m.version_id
69
+ AND t.is_digest_tag = 0
70
+ WHERE m.scan_id = ?
71
+ ${cutoffSql}
72
+ GROUP BY
73
+ m.version_id,
74
+ m.digest,
75
+ m.manifest_kind
76
+ ),
77
+ eligible_tagged_roots AS (
78
+ SELECT
79
+ tv.version_id,
80
+ tv.root_digest,
81
+ tv.root_manifest_kind,
82
+ CASE
83
+ WHEN ? = 1 AND tv.total_tag_count = 0 AND COALESCE(mtc.matched_tag_count, 0) > 0
84
+ THEN COALESCE(mtc.matched_tag_count, 0)
85
+ ELSE tv.total_tag_count
86
+ END AS total_tag_count,
87
+ COALESCE(mtc.matched_tag_count, 0) AS matched_tag_count
88
+ FROM tagged_versions tv
89
+ LEFT JOIN matched_tag_counts mtc
90
+ ON mtc.version_id = tv.version_id
91
+ WHERE (
92
+ tv.total_tag_count > 0
93
+ OR (? = 1 AND COALESCE(mtc.matched_tag_count, 0) > 0)
94
+ )
95
+ AND matched_tag_count > 0
96
+ )
97
+ SELECT
98
+ version_id,
99
+ root_digest,
100
+ root_manifest_kind,
101
+ CASE
102
+ WHEN total_tag_count = matched_tag_count
103
+ THEN 'delete-tags-all-tags-selected'
104
+ ELSE 'delete-tags-partial-tag-match'
105
+ END AS direct_target_reason,
106
+ CASE
107
+ WHEN total_tag_count = matched_tag_count
108
+ THEN 'delete-root'
109
+ ELSE 'untag-only'
110
+ END AS selection_mode
111
+ FROM eligible_tagged_roots
112
+ ORDER BY root_digest
113
+ `;
114
+ return sql
115
+ .all(query, [...params, deleteOrphanedImages, deleteOrphanedImages])
116
+ .map(mapPlanRootRow);
117
+ }
@@ -0,0 +1,8 @@
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 class PlannerDirectTargetRoots {
5
+ #private;
6
+ constructor(sql: PlannerSql);
7
+ list(scanId: number, options: DirectTargetRootOptions): DeletePlanRoot[];
8
+ }
@@ -0,0 +1,17 @@
1
+ import { listCombinedDirectTargetRoots } from "./_planner-direct-target-roots-combined.js";
2
+ import { listTaggedOnlyDirectTargetRoots } from "./_planner-direct-target-roots-tagged.js";
3
+ export class PlannerDirectTargetRoots {
4
+ #sql;
5
+ constructor(sql) {
6
+ this.#sql = sql;
7
+ }
8
+ list(scanId, options) {
9
+ if (options.deleteTagsRequested &&
10
+ options.keepNTagged === undefined &&
11
+ !options.deleteUntagged &&
12
+ options.keepNUntagged === undefined) {
13
+ return listTaggedOnlyDirectTargetRoots(this.#sql, scanId, options);
14
+ }
15
+ return listCombinedDirectTargetRoots(this.#sql, scanId, options);
16
+ }
17
+ }
@@ -0,0 +1,6 @@
1
+ import { PlannerSql } from "./_planner-sql.js";
2
+ export declare class PlannerDirectTargetTags {
3
+ #private;
4
+ constructor(sql: PlannerSql);
5
+ listDeleteTagDirectTargetTags(scanId: number, deleteTags: string[], excludeTags: string[], useRegex: boolean, deleteOrphanedImages: boolean, cutoffTimestamp?: string): string[];
6
+ }
@@ -0,0 +1,54 @@
1
+ import { buildTagSelectorPredicate } from "./_planner-tag-selectors.js";
2
+ import { mapPlanTagRows } from "./_planner-types.js";
3
+ export class PlannerDirectTargetTags {
4
+ #sql;
5
+ constructor(sql) {
6
+ this.#sql = sql;
7
+ }
8
+ listDeleteTagDirectTargetTags(scanId, deleteTags, excludeTags, useRegex, deleteOrphanedImages, cutoffTimestamp) {
9
+ if (deleteTags.length === 0) {
10
+ return [];
11
+ }
12
+ const selectedTagPredicate = buildTagSelectorPredicate(this.#sql.database, "t.tag", deleteTags, useRegex);
13
+ const params = [scanId, ...selectedTagPredicate.params];
14
+ let excludedTagSql = "";
15
+ let olderThanSql = "";
16
+ if (excludeTags.length > 0) {
17
+ const excludedTagPredicate = buildTagSelectorPredicate(this.#sql.database, "xt.tag", excludeTags, useRegex);
18
+ excludedTagSql = `
19
+ AND NOT EXISTS (
20
+ SELECT 1
21
+ FROM tags xt
22
+ WHERE xt.scan_id = t.scan_id
23
+ AND xt.version_id = t.version_id
24
+ AND xt.tag = t.tag
25
+ AND (${excludedTagPredicate.sql})
26
+ )
27
+ `;
28
+ params.push(...excludedTagPredicate.params);
29
+ }
30
+ if (cutoffTimestamp) {
31
+ olderThanSql = "AND pv.created_at < ?";
32
+ params.push(cutoffTimestamp);
33
+ }
34
+ const digestTagFlag = deleteOrphanedImages ? 1 : 0;
35
+ params.splice(1, 0, digestTagFlag);
36
+ const sql = `
37
+ SELECT DISTINCT tag AS target_tag
38
+ FROM tags t
39
+ JOIN package_versions pv
40
+ ON pv.scan_id = t.scan_id
41
+ AND pv.version_id = t.version_id
42
+ JOIN manifests m
43
+ ON m.scan_id = pv.scan_id
44
+ AND m.version_id = pv.version_id
45
+ WHERE t.scan_id = ?
46
+ AND t.is_digest_tag = ?
47
+ AND (${selectedTagPredicate.sql})
48
+ ${excludedTagSql}
49
+ ${olderThanSql}
50
+ ORDER BY tag
51
+ `;
52
+ return mapPlanTagRows(this.#sql.all(sql, params));
53
+ }
54
+ }
@@ -0,0 +1,10 @@
1
+ import type { ScanRow } from "./_planner-types.js";
2
+ interface LatestScanSql {
3
+ get<T>(sql: string, params: Array<number | string>): T | undefined;
4
+ }
5
+ export declare class PlannerLatestScan {
6
+ #private;
7
+ constructor(sql: LatestScanSql);
8
+ get(owner: string, packageName: string): ScanRow;
9
+ }
10
+ export {};
@@ -0,0 +1,20 @@
1
+ export class PlannerLatestScan {
2
+ #sql;
3
+ constructor(sql) {
4
+ this.#sql = sql;
5
+ }
6
+ get(owner, packageName) {
7
+ const sql = `
8
+ SELECT scan_id, owner, package_name, scan_completed_at
9
+ FROM v_latest_scan_per_package
10
+ WHERE owner = ?
11
+ AND package_name = ?
12
+ LIMIT 1
13
+ `;
14
+ const row = this.#sql.get(sql, [owner, packageName]);
15
+ if (!row) {
16
+ throw new Error(`database does not contain completed package scan for ${owner}/${packageName}`);
17
+ }
18
+ return row;
19
+ }
20
+ }
@@ -0,0 +1,5 @@
1
+ import type { DeletePlan, DeletePlanBlockedRoot, DeletePlanProtectedRoot, DeletePlanRoot, DeletePlanRootDecision, PlanArtifacts } from "./_planner-types.js";
2
+ export declare function buildPlanOutputs(directTargetTags: string[], directTargetRoots: DeletePlanRoot[], planArtifacts: PlanArtifacts): Pick<DeletePlan, "directTargetTags" | "directTargetRoots" | "rootDecisions" | "protectedRoots" | "closureManifests" | "blockedRoots" | "fullyDeletableRoots" | "collateralTags">;
3
+ export declare function buildRootDecisions(directTargetRoots: DeletePlanRoot[], planArtifacts: PlanArtifacts): DeletePlanRootDecision[];
4
+ export declare function buildProtectedRoots(blockedRoots: DeletePlanBlockedRoot[]): DeletePlanProtectedRoot[];
5
+ export declare function buildBlockedValidationReason(blockedRoot?: DeletePlanBlockedRoot): string;
@@ -0,0 +1,111 @@
1
+ import { DeletePlanValidationReasonCodes, DeletePlanValidationStatuses } from "./_planner-types.js";
2
+ export function buildPlanOutputs(directTargetTags, directTargetRoots, planArtifacts) {
3
+ const rootDecisions = buildRootDecisions(directTargetRoots, planArtifacts);
4
+ const fullyDeletableDigests = new Set(rootDecisions
5
+ .filter((decision) => decision.validationStatus === DeletePlanValidationStatuses.fullyDeletable)
6
+ .map((decision) => decision.digest));
7
+ const blockedDigests = new Set(rootDecisions
8
+ .filter((decision) => decision.validationStatus === DeletePlanValidationStatuses.blocked)
9
+ .map((decision) => decision.digest));
10
+ const blockedRoots = planArtifacts.blockedRoots.filter((blockedRoot) => blockedDigests.has(blockedRoot.blockedDigest));
11
+ const protectedRoots = buildProtectedRoots(blockedRoots);
12
+ return {
13
+ directTargetTags,
14
+ directTargetRoots,
15
+ rootDecisions,
16
+ protectedRoots,
17
+ closureManifests: planArtifacts.closureManifests,
18
+ blockedRoots,
19
+ fullyDeletableRoots: planArtifacts.fullyDeletableRoots.filter((root) => fullyDeletableDigests.has(root.digest)),
20
+ collateralTags: []
21
+ };
22
+ }
23
+ export function buildRootDecisions(directTargetRoots, planArtifacts) {
24
+ const fullyDeletableDigests = new Set(planArtifacts.fullyDeletableRoots.map((root) => root.digest));
25
+ const supportedUntagOnlyRootDigests = planArtifacts.supportedUntagOnlyRootDigests;
26
+ const blockedRootByDigest = new Map();
27
+ for (const blockedRoot of planArtifacts.blockedRoots) {
28
+ if (!blockedRootByDigest.has(blockedRoot.blockedDigest)) {
29
+ blockedRootByDigest.set(blockedRoot.blockedDigest, blockedRoot);
30
+ }
31
+ }
32
+ return directTargetRoots.map((root) => {
33
+ const blockedRoot = blockedRootByDigest.get(root.digest);
34
+ if (_isUntagOnly(root, blockedRoot, supportedUntagOnlyRootDigests)) {
35
+ return {
36
+ versionId: root.versionId,
37
+ digest: root.digest,
38
+ manifestKind: root.manifestKind,
39
+ selectionMode: "untag-only",
40
+ selectionReason: root.reason,
41
+ validationStatus: DeletePlanValidationStatuses.untagOnly,
42
+ validationReasonCode: root.selectionMode === "untag-only"
43
+ ? DeletePlanValidationReasonCodes.untagOnlyPartialTagMatch
44
+ : DeletePlanValidationReasonCodes.untagOnlyRetainedManifest,
45
+ validationReason: root.selectionMode === "untag-only"
46
+ ? "matched tags cover only part of this root's tag set, so the version is retained and only those tags can be detached"
47
+ : "selected tags can be detached, but the manifest itself must remain because surviving tags still need it"
48
+ };
49
+ }
50
+ if (fullyDeletableDigests.has(root.digest)) {
51
+ return {
52
+ versionId: root.versionId,
53
+ digest: root.digest,
54
+ manifestKind: root.manifestKind,
55
+ selectionMode: root.selectionMode,
56
+ selectionReason: root.reason,
57
+ validationStatus: DeletePlanValidationStatuses.fullyDeletable,
58
+ validationReasonCode: DeletePlanValidationReasonCodes.fullyDeletableNoRetainedOverlap,
59
+ validationReason: "selected tags cover the whole root and its manifest closure does not overlap any retained root"
60
+ };
61
+ }
62
+ return {
63
+ versionId: root.versionId,
64
+ digest: root.digest,
65
+ manifestKind: root.manifestKind,
66
+ selectionMode: root.selectionMode,
67
+ selectionReason: root.reason,
68
+ validationStatus: DeletePlanValidationStatuses.blocked,
69
+ validationReasonCode: DeletePlanValidationReasonCodes.blockedOverlapWithRetainedRoot,
70
+ validationReason: buildBlockedValidationReason(blockedRoot),
71
+ blockingVersionId: blockedRoot?.blockingVersionId,
72
+ blockingDigest: blockedRoot?.blockingDigest,
73
+ overlapDigest: blockedRoot?.overlapDigest,
74
+ overlapManifestKind: blockedRoot?.overlapManifestKind
75
+ };
76
+ });
77
+ }
78
+ export function buildProtectedRoots(blockedRoots) {
79
+ const protectedRoots = new Map();
80
+ for (const blockedRoot of blockedRoots) {
81
+ const key = `${blockedRoot.blockingVersionId}:${blockedRoot.blockingDigest}`;
82
+ const current = protectedRoots.get(key) ?? {
83
+ versionId: blockedRoot.blockingVersionId,
84
+ digest: blockedRoot.blockingDigest,
85
+ blocks: []
86
+ };
87
+ current.blocks.push({
88
+ blockedVersionId: blockedRoot.blockedVersionId,
89
+ blockedDigest: blockedRoot.blockedDigest,
90
+ blockReasonCode: blockedRoot.reason,
91
+ overlapDigest: blockedRoot.overlapDigest,
92
+ overlapManifestKind: blockedRoot.overlapManifestKind
93
+ });
94
+ protectedRoots.set(key, current);
95
+ }
96
+ return [...protectedRoots.values()].sort((left, right) => left.digest.localeCompare(right.digest));
97
+ }
98
+ export function buildBlockedValidationReason(blockedRoot) {
99
+ if (!blockedRoot) {
100
+ return "root closure overlaps manifest members still required by a retained root";
101
+ }
102
+ return `blocked because retained root ${blockedRoot.blockingDigest} still requires shared manifest ${blockedRoot.overlapDigest}`;
103
+ }
104
+ function _isUntagOnly(root, blockedRoot, supportedUntagOnlyRootDigests) {
105
+ if (root.selectionMode === "untag-only") {
106
+ return true;
107
+ }
108
+ return (root.reason === "delete-tags-all-tags-selected" &&
109
+ ((blockedRoot !== undefined && blockedRoot.overlapDigest === root.digest) ||
110
+ supportedUntagOnlyRootDigests.has(root.digest)));
111
+ }
@@ -0,0 +1 @@
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 protected_manifests AS (\n SELECT\n retained.graph_id,\n retained.tagged_version_id AS blocking_version_id,\n retained.tagged_digest AS blocking_digest,\n retained.tagged_digest AS digest,\n 0 AS min_distance\n FROM retained_tagged_manifests retained\n\n UNION\n\n SELECT\n retained.graph_id,\n retained.tagged_version_id AS blocking_version_id,\n retained.tagged_digest AS blocking_digest,\n retained_overlap.descendant_digest AS digest,\n retained_overlap.min_distance\n FROM retained_tagged_manifests retained\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.min_distance > 0\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 COALESCE(retained_overlap.descendant_digest, helper_overlap.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 COALESCE(retained_overlap.min_distance, helper_overlap.min_distance),\n COALESCE(retained_overlap.descendant_digest, helper_overlap.digest),\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 LEFT 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 AND retained_overlap.min_distance > 0\n LEFT JOIN manifest_edges helper_edge INDEXED BY idx_manifest_edges_scan_parent\n ON helper_edge.scan_id = ?\n AND helper_edge.parent_digest = root_graph.root_digest\n AND helper_edge.edge_kind = 'digest-tag-referrer'\n LEFT JOIN protected_manifests helper_overlap\n ON helper_overlap.graph_id = root_graph.graph_id\n AND helper_overlap.blocking_digest = retained.tagged_digest\n AND helper_overlap.digest = helper_edge.child_digest\n WHERE retained_overlap.descendant_digest IS NOT NULL\n OR helper_overlap.digest IS NOT NULL\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";