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.
- package/CHANGELOG.md +356 -0
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +5 -0
- package/dist/cleanup-summary/_cleanup-summary-markdown.js +224 -0
- package/dist/cleanup-summary/_cleanup-summary.d.ts +56 -0
- package/dist/cleanup-summary/_cleanup-summary.js +59 -0
- package/dist/cleanup-summary/index.d.ts +2 -0
- package/dist/cleanup-summary/index.js +2 -0
- package/dist/cli/_args.d.ts +7 -0
- package/dist/cli/_args.js +41 -0
- package/dist/cli/_cleanup-command.d.ts +1 -0
- package/dist/cli/_cleanup-command.js +142 -0
- package/dist/cli/_db-merge-command.d.ts +1 -0
- package/dist/cli/_db-merge-command.js +41 -0
- package/dist/cli/_github-output.d.ts +10 -0
- package/dist/cli/_github-output.js +13 -0
- package/dist/cli/_json-output.d.ts +1 -0
- package/dist/cli/_json-output.js +11 -0
- package/dist/cli/_logger.d.ts +10 -0
- package/dist/cli/_logger.js +26 -0
- package/dist/cli/_older-than.d.ts +5 -0
- package/dist/cli/_older-than.js +42 -0
- package/dist/cli/_planner-options.d.ts +20 -0
- package/dist/cli/_planner-options.js +83 -0
- package/dist/cli/_scan-command.d.ts +1 -0
- package/dist/cli/_scan-command.js +39 -0
- package/dist/cli/_tag-selector-resolver.d.ts +3 -0
- package/dist/cli/_tag-selector-resolver.js +132 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +52 -0
- package/dist/config/_service-constants.d.ts +3 -0
- package/dist/config/_service-constants.js +3 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.js +9 -0
- package/dist/core/_digest-tag.d.ts +2 -0
- package/dist/core/_digest-tag.js +12 -0
- package/dist/core/_github-package-owner.d.ts +11 -0
- package/dist/core/_github-package-owner.js +45 -0
- package/dist/core/_http-error.d.ts +6 -0
- package/dist/core/_http-error.js +33 -0
- package/dist/core/_types.d.ts +54 -0
- package/dist/core/_types.js +8 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +4 -0
- package/dist/db/_cleanup-run-writer.d.ts +10 -0
- package/dist/db/_cleanup-run-writer.js +113 -0
- package/dist/db/_db-merge-cleanup-copy.d.ts +7 -0
- package/dist/db/_db-merge-cleanup-copy.js +172 -0
- package/dist/db/_db-merge-history.d.ts +2 -0
- package/dist/db/_db-merge-history.js +15 -0
- package/dist/db/_db-merge-repository.d.ts +8 -0
- package/dist/db/_db-merge-repository.js +95 -0
- package/dist/db/_db-merge-scan-copy.d.ts +10 -0
- package/dist/db/_db-merge-scan-copy.js +70 -0
- package/dist/db/_db-merge-types.d.ts +44 -0
- package/dist/db/_db-merge-types.js +1 -0
- package/dist/db/_github-actions-run-url.d.ts +1 -0
- package/dist/db/_github-actions-run-url.js +9 -0
- package/dist/db/_manifest-reachability.d.ts +2 -0
- package/dist/db/_manifest-reachability.js +158 -0
- package/dist/db/_scan-writer.d.ts +20 -0
- package/dist/db/_scan-writer.js +184 -0
- package/dist/db/_schema.d.ts +2 -0
- package/dist/db/_schema.js +19 -0
- package/dist/db/_snapshot-repository.d.ts +24 -0
- package/dist/db/_snapshot-repository.js +86 -0
- package/dist/db/_sql-placeholders.d.ts +2 -0
- package/dist/db/_sql-placeholders.js +16 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.js +13 -0
- package/dist/db/planner/_planner-direct-target-root-options.d.ts +11 -0
- package/dist/db/planner/_planner-direct-target-root-options.js +1 -0
- package/dist/db/planner/_planner-direct-target-root-tag-filters.d.ts +7 -0
- package/dist/db/planner/_planner-direct-target-root-tag-filters.js +41 -0
- package/dist/db/planner/_planner-direct-target-roots-combined-sql.d.ts +7 -0
- package/dist/db/planner/_planner-direct-target-roots-combined-sql.js +189 -0
- package/dist/db/planner/_planner-direct-target-roots-combined.d.ts +4 -0
- package/dist/db/planner/_planner-direct-target-roots-combined.js +10 -0
- package/dist/db/planner/_planner-direct-target-roots-tagged.d.ts +4 -0
- package/dist/db/planner/_planner-direct-target-roots-tagged.js +117 -0
- package/dist/db/planner/_planner-direct-target-roots.d.ts +8 -0
- package/dist/db/planner/_planner-direct-target-roots.js +17 -0
- package/dist/db/planner/_planner-direct-target-tags.d.ts +6 -0
- package/dist/db/planner/_planner-direct-target-tags.js +54 -0
- package/dist/db/planner/_planner-latest-scan.d.ts +10 -0
- package/dist/db/planner/_planner-latest-scan.js +20 -0
- package/dist/db/planner/_planner-output.d.ts +5 -0
- package/dist/db/planner/_planner-output.js +111 -0
- package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.d.ts +1 -0
- package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.js +110 -0
- package/dist/db/planner/_planner-plan-artifacts-closure-sql.d.ts +1 -0
- package/dist/db/planner/_planner-plan-artifacts-closure-sql.js +188 -0
- package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.d.ts +1 -0
- package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.js +86 -0
- package/dist/db/planner/_planner-plan-artifacts.d.ts +7 -0
- package/dist/db/planner/_planner-plan-artifacts.js +109 -0
- package/dist/db/planner/_planner-repository.d.ts +44 -0
- package/dist/db/planner/_planner-repository.js +105 -0
- package/dist/db/planner/_planner-sql.d.ts +12 -0
- package/dist/db/planner/_planner-sql.js +46 -0
- package/dist/db/planner/_planner-tag-selectors.d.ts +8 -0
- package/dist/db/planner/_planner-tag-selectors.js +57 -0
- package/dist/db/planner/_planner-types.d.ts +144 -0
- package/dist/db/planner/_planner-types.js +49 -0
- package/dist/db/planner/index.d.ts +3 -0
- package/dist/db/planner/index.js +2 -0
- package/dist/execute/_http.d.ts +7 -0
- package/dist/execute/_http.js +48 -0
- package/dist/execute/_manifest-detach.d.ts +4 -0
- package/dist/execute/_manifest-detach.js +31 -0
- package/dist/execute/_package-version-delete-client.d.ts +4 -0
- package/dist/execute/_package-version-delete-client.js +34 -0
- package/dist/execute/_package-version-page-client.d.ts +14 -0
- package/dist/execute/_package-version-page-client.js +64 -0
- package/dist/execute/_package-version-tag-source-client.d.ts +12 -0
- package/dist/execute/_package-version-tag-source-client.js +65 -0
- package/dist/execute/_plan-executor.d.ts +3 -0
- package/dist/execute/_plan-executor.js +69 -0
- package/dist/execute/_registry-manifest-client.d.ts +12 -0
- package/dist/execute/_registry-manifest-client.js +79 -0
- package/dist/execute/_registry-token-client.d.ts +4 -0
- package/dist/execute/_registry-token-client.js +37 -0
- package/dist/execute/_types.d.ts +34 -0
- package/dist/execute/_types.js +1 -0
- package/dist/execute/_untag-client.d.ts +2 -0
- package/dist/execute/_untag-client.js +30 -0
- package/dist/execute/index.d.ts +5 -0
- package/dist/execute/index.js +3 -0
- package/dist/ingest/github/_manifest-client.d.ts +14 -0
- package/dist/ingest/github/_manifest-client.js +108 -0
- package/dist/ingest/github/_manifest-ingest.d.ts +3 -0
- package/dist/ingest/github/_manifest-ingest.js +90 -0
- package/dist/ingest/github/_manifest-kind.d.ts +26 -0
- package/dist/ingest/github/_manifest-kind.js +65 -0
- package/dist/ingest/github/_package-metadata-load.d.ts +5 -0
- package/dist/ingest/github/_package-metadata-load.js +45 -0
- package/dist/ingest/github/_package-version-page-load.d.ts +13 -0
- package/dist/ingest/github/_package-version-page-load.js +55 -0
- package/dist/ingest/github/_packages-client.d.ts +10 -0
- package/dist/ingest/github/_packages-client.js +76 -0
- package/dist/ingest/github/_parallel-paginated-ingest.d.ts +12 -0
- package/dist/ingest/github/_parallel-paginated-ingest.js +49 -0
- package/dist/ingest/github/_registry-token-client.d.ts +6 -0
- package/dist/ingest/github/_registry-token-client.js +67 -0
- package/dist/ingest/github/_shared.d.ts +28 -0
- package/dist/ingest/github/_shared.js +70 -0
- package/dist/ingest/github/index.d.ts +11 -0
- package/dist/ingest/github/index.js +29 -0
- package/package.json +78 -0
- package/resources/sql/schema/001_schema.sql +222 -0
- 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";
|