ghcr-manager 0.0.4 → 0.9.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 (137) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/README.md +166 -57
  3. package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +6 -0
  4. package/dist/cleanup-summary/_cleanup-summary-markdown.js +113 -0
  5. package/dist/cleanup-summary/_cleanup-summary.d.ts +40 -0
  6. package/dist/cleanup-summary/_cleanup-summary.js +40 -0
  7. package/dist/cleanup-summary/index.d.ts +2 -0
  8. package/dist/cleanup-summary/index.js +2 -0
  9. package/dist/cli/_args.d.ts +1 -0
  10. package/dist/cli/_args.js +3 -0
  11. package/dist/cli/_cleanup-command.d.ts +1 -0
  12. package/dist/cli/_cleanup-command.js +63 -0
  13. package/dist/cli/_db-merge-command.d.ts +1 -0
  14. package/dist/cli/_db-merge-command.js +41 -0
  15. package/dist/cli/_github-output.d.ts +10 -0
  16. package/dist/cli/_github-output.js +13 -0
  17. package/dist/cli/_logger.d.ts +2 -1
  18. package/dist/cli/_logger.js +2 -0
  19. package/dist/cli/_older-than.d.ts +5 -0
  20. package/dist/cli/_older-than.js +42 -0
  21. package/dist/cli/_planner-options.d.ts +20 -0
  22. package/dist/cli/_planner-options.js +101 -0
  23. package/dist/cli/_scan-command.js +11 -4
  24. package/dist/cli/_tag-selector-resolver.d.ts +3 -0
  25. package/dist/cli/_tag-selector-resolver.js +109 -0
  26. package/dist/cli/_untag-command.d.ts +1 -0
  27. package/dist/cli/_untag-command.js +57 -0
  28. package/dist/cli/index.js +19 -1
  29. package/dist/config/_service-constants.d.ts +3 -0
  30. package/dist/config/_service-constants.js +3 -0
  31. package/dist/{tuning → config}/index.d.ts +3 -0
  32. package/dist/{tuning → config}/index.js +3 -0
  33. package/dist/core/_github-package-owner.d.ts +11 -0
  34. package/dist/core/_github-package-owner.js +45 -0
  35. package/dist/core/_http-error.d.ts +6 -0
  36. package/dist/core/_http-error.js +33 -0
  37. package/dist/core/_types.d.ts +3 -2
  38. package/dist/core/index.d.ts +4 -1
  39. package/dist/core/index.js +2 -1
  40. package/dist/db/_cleanup-run-writer.d.ts +10 -0
  41. package/dist/db/_cleanup-run-writer.js +73 -0
  42. package/dist/db/_db-merge-cleanup-copy.d.ts +7 -0
  43. package/dist/db/_db-merge-cleanup-copy.js +122 -0
  44. package/dist/db/_db-merge-history.d.ts +2 -0
  45. package/dist/db/_db-merge-history.js +15 -0
  46. package/dist/db/_db-merge-repository.d.ts +8 -0
  47. package/dist/db/_db-merge-repository.js +95 -0
  48. package/dist/db/_db-merge-scan-copy.d.ts +10 -0
  49. package/dist/db/_db-merge-scan-copy.js +69 -0
  50. package/dist/db/_db-merge-types.d.ts +44 -0
  51. package/dist/db/_db-merge-types.js +1 -0
  52. package/dist/db/_github-actions-run-url.d.ts +1 -0
  53. package/dist/db/_github-actions-run-url.js +9 -0
  54. package/dist/db/_scan-writer.d.ts +3 -1
  55. package/dist/db/_scan-writer.js +28 -13
  56. package/dist/db/_snapshot-repository.d.ts +9 -9
  57. package/dist/db/_snapshot-repository.js +37 -49
  58. package/dist/db/_sql-placeholders.d.ts +2 -0
  59. package/dist/db/_sql-placeholders.js +16 -0
  60. package/dist/db/index.d.ts +5 -0
  61. package/dist/db/index.js +3 -0
  62. package/dist/db/planner/_planner-delete-tag-root-targets.d.ts +7 -0
  63. package/dist/db/planner/_planner-delete-tag-root-targets.js +130 -0
  64. package/dist/db/planner/_planner-direct-target-tags.d.ts +6 -0
  65. package/dist/db/planner/_planner-direct-target-tags.js +47 -0
  66. package/dist/db/planner/_planner-keep-tagged-root-targets.d.ts +7 -0
  67. package/dist/db/planner/_planner-keep-tagged-root-targets.js +74 -0
  68. package/dist/db/planner/_planner-output.d.ts +5 -0
  69. package/dist/db/planner/_planner-output.js +101 -0
  70. package/dist/db/planner/_planner-plan-artifacts.d.ts +7 -0
  71. package/dist/db/planner/_planner-plan-artifacts.js +211 -0
  72. package/dist/db/planner/_planner-repository.d.ts +34 -0
  73. package/dist/db/planner/_planner-repository.js +126 -0
  74. package/dist/db/planner/_planner-sql.d.ts +12 -0
  75. package/dist/db/planner/_planner-sql.js +35 -0
  76. package/dist/db/planner/_planner-tag-selectors.d.ts +8 -0
  77. package/dist/db/planner/_planner-tag-selectors.js +57 -0
  78. package/dist/db/planner/_planner-tagged-root-targets.d.ts +15 -0
  79. package/dist/db/planner/_planner-tagged-root-targets.js +19 -0
  80. package/dist/db/planner/_planner-tagged-targets.d.ts +8 -0
  81. package/dist/db/planner/_planner-tagged-targets.js +16 -0
  82. package/dist/db/planner/_planner-types.d.ts +135 -0
  83. package/dist/db/planner/_planner-types.js +38 -0
  84. package/dist/db/planner/_planner-untagged-targets.d.ts +9 -0
  85. package/dist/db/planner/_planner-untagged-targets.js +91 -0
  86. package/dist/db/planner/index.d.ts +2 -0
  87. package/dist/db/planner/index.js +1 -0
  88. package/dist/execute/_http.d.ts +7 -0
  89. package/dist/execute/_http.js +48 -0
  90. package/dist/execute/_manifest-detach.d.ts +4 -0
  91. package/dist/execute/_manifest-detach.js +31 -0
  92. package/dist/execute/_package-version-delete-client.d.ts +4 -0
  93. package/dist/execute/_package-version-delete-client.js +34 -0
  94. package/dist/execute/_package-version-page-client.d.ts +14 -0
  95. package/dist/execute/_package-version-page-client.js +64 -0
  96. package/dist/execute/_package-version-tag-source-client.d.ts +12 -0
  97. package/dist/execute/_package-version-tag-source-client.js +65 -0
  98. package/dist/execute/_plan-executor.d.ts +3 -0
  99. package/dist/execute/_plan-executor.js +47 -0
  100. package/dist/execute/_registry-manifest-client.d.ts +12 -0
  101. package/dist/execute/_registry-manifest-client.js +79 -0
  102. package/dist/execute/_registry-token-client.d.ts +4 -0
  103. package/dist/execute/_registry-token-client.js +37 -0
  104. package/dist/execute/_types.d.ts +51 -0
  105. package/dist/execute/_types.js +1 -0
  106. package/dist/execute/_untag-client.d.ts +2 -0
  107. package/dist/execute/_untag-client.js +71 -0
  108. package/dist/execute/index.d.ts +5 -0
  109. package/dist/execute/index.js +3 -0
  110. package/dist/ingest/github/_manifest-client.d.ts +7 -1
  111. package/dist/ingest/github/_manifest-client.js +8 -0
  112. package/dist/ingest/github/_manifest-ingest.js +39 -53
  113. package/dist/ingest/github/_manifest-kind.d.ts +20 -0
  114. package/dist/ingest/github/_manifest-kind.js +50 -0
  115. package/dist/ingest/github/_package-metadata-load.d.ts +5 -0
  116. package/dist/ingest/github/_package-metadata-load.js +45 -0
  117. package/dist/ingest/github/_package-version-page-load.d.ts +1 -1
  118. package/dist/ingest/github/_package-version-page-load.js +8 -5
  119. package/dist/ingest/github/_packages-client.d.ts +1 -1
  120. package/dist/ingest/github/_packages-client.js +21 -4
  121. package/dist/ingest/github/_parallel-paginated-ingest.d.ts +1 -0
  122. package/dist/ingest/github/_parallel-paginated-ingest.js +2 -2
  123. package/dist/ingest/github/_shared.d.ts +1 -1
  124. package/dist/ingest/github/_shared.js +2 -34
  125. package/dist/ingest/github/index.d.ts +4 -0
  126. package/dist/ingest/github/index.js +8 -5
  127. package/package.json +7 -5
  128. package/resources/sql/schema/001_schema.sql +82 -8
  129. package/resources/sql/views/001_v_latest_scan_per_package.sql +2 -2
  130. package/resources/sql/views/003_v_scan_root_manifests.sql +43 -0
  131. package/resources/sql/views/004_v_digest_derived_tag_relations.sql +51 -0
  132. package/resources/sql/views/005_v_cleanup_root_closure_members.sql +100 -0
  133. package/resources/sql/views/006_v_cleanup_blocking_overlaps.sql +42 -0
  134. package/dist/ingest/github/_paginated-ingest.d.ts +0 -11
  135. package/dist/ingest/github/_paginated-ingest.js +0 -28
  136. package/resources/sql/views/003_v_missing_digests_related_manifests.sql +0 -78
  137. package/resources/sql/views/004_v_manifests_related_manifests.sql +0 -142
@@ -0,0 +1,211 @@
1
+ import { mapBlockedRootRow, mapClosureManifestRow } from "./_planner-types.js";
2
+ export class PlannerPlanArtifacts {
3
+ #sql;
4
+ constructor(sql) {
5
+ this.#sql = sql;
6
+ }
7
+ build(scanId, directTargetRoots) {
8
+ const deleteRootCandidates = directTargetRoots.filter((root) => root.selectionMode === "delete-root");
9
+ if (deleteRootCandidates.length === 0) {
10
+ return {
11
+ closureManifests: [],
12
+ blockedRoots: [],
13
+ fullyDeletableRoots: []
14
+ };
15
+ }
16
+ return this.#withDirectTargetRootsTempTable(deleteRootCandidates, () => {
17
+ const closureManifests = this.#listClosureManifests(scanId);
18
+ const blockedRoots = this.#listBlockedRoots(scanId);
19
+ const blockedVersionIds = new Set(blockedRoots.map((root) => root.blockedVersionId));
20
+ const fullyDeletableRoots = deleteRootCandidates.filter((root) => !blockedVersionIds.has(root.versionId));
21
+ return {
22
+ closureManifests,
23
+ blockedRoots,
24
+ fullyDeletableRoots
25
+ };
26
+ });
27
+ }
28
+ #listClosureManifests(scanId) {
29
+ const sql = `
30
+ WITH direct_target_closure AS (
31
+ SELECT
32
+ dtr.root_version_id AS source_version_id,
33
+ dtr.root_digest AS source_digest,
34
+ dtr.root_version_id AS member_version_id,
35
+ dtr.root_digest AS member_digest,
36
+ dtr.root_manifest_kind AS member_manifest_kind,
37
+ 0 AS hops_from_root,
38
+ 'root' AS member_role
39
+ FROM temp_direct_target_roots dtr
40
+
41
+ UNION ALL
42
+
43
+ SELECT
44
+ dtr.root_version_id AS source_version_id,
45
+ dtr.root_digest AS source_digest,
46
+ m.version_id AS member_version_id,
47
+ m.digest AS member_digest,
48
+ m.manifest_kind AS member_manifest_kind,
49
+ mr.min_distance AS hops_from_root,
50
+ 'descendant' AS member_role
51
+ FROM temp_direct_target_roots dtr
52
+ JOIN manifest_reachability mr
53
+ ON mr.scan_id = ?
54
+ AND mr.ancestor_digest = dtr.root_digest
55
+ AND mr.min_distance > 0
56
+ JOIN manifests m
57
+ ON m.scan_id = ?
58
+ AND m.digest = mr.descendant_digest
59
+ )
60
+ SELECT
61
+ source_version_id,
62
+ source_digest,
63
+ member_version_id,
64
+ member_digest,
65
+ member_manifest_kind,
66
+ hops_from_root,
67
+ member_role
68
+ FROM direct_target_closure
69
+ ORDER BY source_digest, hops_from_root, member_digest
70
+ `;
71
+ return this.#sql.all(sql, [scanId, scanId]).map(mapClosureManifestRow);
72
+ }
73
+ #listBlockedRoots(scanId) {
74
+ const sql = `
75
+ WITH retained_roots AS (
76
+ SELECT
77
+ m.version_id AS root_version_id,
78
+ m.digest AS root_digest
79
+ FROM manifests m
80
+ WHERE m.scan_id = ?
81
+ AND NOT EXISTS (
82
+ SELECT 1
83
+ FROM manifest_reachability mr
84
+ WHERE mr.scan_id = m.scan_id
85
+ AND mr.descendant_digest = m.digest
86
+ AND mr.min_distance > 0
87
+ )
88
+ AND NOT EXISTS (
89
+ SELECT 1
90
+ FROM temp_direct_target_roots dtr
91
+ WHERE dtr.root_digest = m.digest
92
+ )
93
+ ),
94
+ direct_target_closure AS (
95
+ SELECT
96
+ dtr.root_version_id AS root_version_id,
97
+ dtr.root_digest AS root_digest,
98
+ dtr.root_manifest_kind AS member_manifest_kind,
99
+ dtr.root_digest AS member_digest,
100
+ 0 AS hops_from_root
101
+ FROM temp_direct_target_roots dtr
102
+
103
+ UNION ALL
104
+
105
+ SELECT
106
+ dtr.root_version_id AS root_version_id,
107
+ dtr.root_digest AS root_digest,
108
+ m.manifest_kind AS member_manifest_kind,
109
+ m.digest AS member_digest,
110
+ mr.min_distance AS hops_from_root
111
+ FROM temp_direct_target_roots dtr
112
+ JOIN manifest_reachability mr
113
+ ON mr.scan_id = ?
114
+ AND mr.ancestor_digest = dtr.root_digest
115
+ AND mr.min_distance > 0
116
+ JOIN manifests m
117
+ ON m.scan_id = ?
118
+ AND m.digest = mr.descendant_digest
119
+ ),
120
+ ranked_blocks AS (
121
+ SELECT
122
+ dtc.root_version_id AS blocked_version_id,
123
+ dtc.root_digest AS blocked_digest,
124
+ rr.root_version_id AS blocking_version_id,
125
+ rr.root_digest AS blocking_digest,
126
+ dtc.member_digest AS overlap_digest,
127
+ dtc.member_manifest_kind AS overlap_manifest_kind,
128
+ 'overlap-with-retained-root' AS block_reason,
129
+ ROW_NUMBER() OVER (
130
+ PARTITION BY dtc.root_digest, rr.root_digest
131
+ ORDER BY
132
+ dtc.hops_from_root,
133
+ retained_overlap.min_distance,
134
+ dtc.member_digest
135
+ ) AS rn
136
+ FROM direct_target_closure dtc
137
+ JOIN retained_roots rr
138
+ ON rr.root_digest <> dtc.root_digest
139
+ JOIN manifest_reachability retained_overlap
140
+ ON retained_overlap.scan_id = ?
141
+ AND retained_overlap.ancestor_digest = rr.root_digest
142
+ AND retained_overlap.descendant_digest = dtc.member_digest
143
+ )
144
+ SELECT
145
+ blocked_version_id,
146
+ blocked_digest,
147
+ blocking_version_id,
148
+ blocking_digest,
149
+ overlap_digest,
150
+ overlap_manifest_kind,
151
+ block_reason
152
+ FROM ranked_blocks
153
+ WHERE rn = 1
154
+ ORDER BY blocked_digest, blocking_digest, overlap_digest
155
+ `;
156
+ return this.#sql
157
+ .all(sql, [scanId, scanId, scanId, scanId])
158
+ .map(mapBlockedRootRow);
159
+ }
160
+ #withDirectTargetRootsTempTable(directTargetRoots, callback) {
161
+ this.#sql.exec(`
162
+ CREATE TEMP TABLE IF NOT EXISTS temp_direct_target_roots (
163
+ root_version_id INTEGER NOT NULL,
164
+ root_digest TEXT NOT NULL,
165
+ root_manifest_kind TEXT,
166
+ direct_target_reason TEXT NOT NULL,
167
+ selection_mode TEXT NOT NULL
168
+ )
169
+ `);
170
+ this.#sql.exec(`
171
+ CREATE INDEX IF NOT EXISTS idx_temp_direct_target_roots_digest
172
+ ON temp_direct_target_roots(root_digest)
173
+ `);
174
+ this.#sql.exec(`
175
+ CREATE INDEX IF NOT EXISTS idx_temp_direct_target_roots_version_digest
176
+ ON temp_direct_target_roots(root_version_id, root_digest)
177
+ `);
178
+ this.#sql.exec("DELETE FROM temp_direct_target_roots");
179
+ this.#insertDirectTargetRoots(directTargetRoots);
180
+ try {
181
+ return callback();
182
+ }
183
+ finally {
184
+ this.#sql.exec("DELETE FROM temp_direct_target_roots");
185
+ }
186
+ }
187
+ #insertDirectTargetRoots(directTargetRoots) {
188
+ const insertSql = `
189
+ INSERT INTO temp_direct_target_roots (
190
+ root_version_id,
191
+ root_digest,
192
+ root_manifest_kind,
193
+ direct_target_reason,
194
+ selection_mode
195
+ ) VALUES (?, ?, ?, ?, ?)
196
+ `;
197
+ this.#sql.traceSql(insertSql, ["<chunked rows omitted>"]);
198
+ const insert = this.#sql.database.prepare(insertSql);
199
+ const insertMany = this.#sql.database.transaction((roots) => {
200
+ for (const root of roots) {
201
+ insert.run(root.versionId, root.digest, root.manifestKind ?? null, root.reason, root.selectionMode);
202
+ }
203
+ });
204
+ const chunkSize = 1000;
205
+ for (let index = 0; index < directTargetRoots.length; index += chunkSize) {
206
+ const chunk = directTargetRoots.slice(index, index + chunkSize);
207
+ insertMany(chunk);
208
+ this.#sql.logger.debug(`Inserted ${chunk.length} direct target root row(s) into temp_direct_target_roots`);
209
+ }
210
+ }
211
+ }
@@ -0,0 +1,34 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { DeletePlan, PlannerLogger } from "./_planner-types.js";
3
+ export type { DeletePlan, DeletePlanBlockedRoot, DeletePlanClosureManifest, DeletePlanProtectedRoot, DeletePlanRoot, DeletePlanRootDecision } from "./_planner-types.js";
4
+ export declare class PlannerRepository {
5
+ #private;
6
+ constructor(database: Database.Database, logger?: PlannerLogger);
7
+ getDeleteUntaggedPlan(owner: string, packageName: string): DeletePlan;
8
+ getLatestCompletedScanId(owner: string, packageName: string): number;
9
+ getKeepNUntaggedPlan(owner: string, packageName: string, keepCount: number): DeletePlan;
10
+ getKeepNTaggedPlan(owner: string, packageName: string, keepCount: number): DeletePlan;
11
+ getDeleteUntaggedPlanWithCutoff(owner: string, packageName: string, options?: {
12
+ olderThan?: string;
13
+ cutoffTimestamp?: string;
14
+ }): DeletePlan;
15
+ getKeepNUntaggedPlanWithCutoff(owner: string, packageName: string, keepCount: number, options?: {
16
+ olderThan?: string;
17
+ cutoffTimestamp?: string;
18
+ }): DeletePlan;
19
+ getKeepNTaggedPlanWithCutoff(owner: string, packageName: string, keepCount: number, excludeTags: string[], options?: {
20
+ olderThan?: string;
21
+ cutoffTimestamp?: string;
22
+ }): DeletePlan;
23
+ getDeleteTagsPlan(owner: string, packageName: string, deleteTags: string[], excludeTags: string[]): DeletePlan;
24
+ getDeleteTagsPlanWithCutoff(owner: string, packageName: string, deleteTags: string[], excludeTags: string[], options?: {
25
+ deleteTagsRequested?: boolean;
26
+ deleteGhostImages?: boolean;
27
+ deletePartialImages?: boolean;
28
+ deleteOrphanedImages?: boolean;
29
+ keepNTagged?: number;
30
+ useRegex?: boolean;
31
+ olderThan?: string;
32
+ cutoffTimestamp?: string;
33
+ }): DeletePlan;
34
+ }
@@ -0,0 +1,126 @@
1
+ import { buildPlanOutputs } from "./_planner-output.js";
2
+ import { PlannerPlanArtifacts } from "./_planner-plan-artifacts.js";
3
+ import { PlannerSql } from "./_planner-sql.js";
4
+ import { PlannerTaggedTargets } from "./_planner-tagged-targets.js";
5
+ import { PlannerUntaggedTargets } from "./_planner-untagged-targets.js";
6
+ export class PlannerRepository {
7
+ #untaggedTargets;
8
+ #taggedTargets;
9
+ #planArtifacts;
10
+ constructor(database, logger) {
11
+ const sql = new PlannerSql(database, logger);
12
+ this.#untaggedTargets = new PlannerUntaggedTargets(sql);
13
+ this.#taggedTargets = new PlannerTaggedTargets(sql);
14
+ this.#planArtifacts = new PlannerPlanArtifacts(sql);
15
+ }
16
+ getDeleteUntaggedPlan(owner, packageName) {
17
+ return this.getDeleteUntaggedPlanWithCutoff(owner, packageName);
18
+ }
19
+ getLatestCompletedScanId(owner, packageName) {
20
+ return this.#untaggedTargets.getLatestCompletedScan(owner, packageName).scan_id;
21
+ }
22
+ getKeepNUntaggedPlan(owner, packageName, keepCount) {
23
+ return this.getKeepNUntaggedPlanWithCutoff(owner, packageName, keepCount);
24
+ }
25
+ getKeepNTaggedPlan(owner, packageName, keepCount) {
26
+ return this.getKeepNTaggedPlanWithCutoff(owner, packageName, keepCount, []);
27
+ }
28
+ getDeleteUntaggedPlanWithCutoff(owner, packageName, options) {
29
+ const scan = this.#untaggedTargets.getLatestCompletedScan(owner, packageName);
30
+ const directTargetRoots = this.#untaggedTargets.listDeleteUntaggedDirectTargetRoots(scan.scan_id, options?.cutoffTimestamp);
31
+ const planArtifacts = this.#planArtifacts.build(scan.scan_id, directTargetRoots);
32
+ return {
33
+ owner: scan.owner,
34
+ packageName: scan.package_name,
35
+ scanCompletedAt: scan.scan_completed_at,
36
+ plannerInputs: _buildPlannerInputs({
37
+ deleteUntagged: true,
38
+ olderThan: options?.olderThan,
39
+ cutoffTimestamp: options?.cutoffTimestamp
40
+ }),
41
+ ...buildPlanOutputs([], directTargetRoots, planArtifacts)
42
+ };
43
+ }
44
+ getKeepNUntaggedPlanWithCutoff(owner, packageName, keepCount, options) {
45
+ const scan = this.#untaggedTargets.getLatestCompletedScan(owner, packageName);
46
+ const directTargetRoots = this.#untaggedTargets.listKeepNUntaggedDirectTargetRoots(scan.scan_id, keepCount, options?.cutoffTimestamp);
47
+ const planArtifacts = this.#planArtifacts.build(scan.scan_id, directTargetRoots);
48
+ return {
49
+ owner: scan.owner,
50
+ packageName: scan.package_name,
51
+ scanCompletedAt: scan.scan_completed_at,
52
+ plannerInputs: _buildPlannerInputs({
53
+ keepNUntagged: keepCount,
54
+ olderThan: options?.olderThan,
55
+ cutoffTimestamp: options?.cutoffTimestamp
56
+ }),
57
+ ...buildPlanOutputs([], directTargetRoots, planArtifacts)
58
+ };
59
+ }
60
+ getKeepNTaggedPlanWithCutoff(owner, packageName, keepCount, excludeTags, options) {
61
+ const scan = this.#untaggedTargets.getLatestCompletedScan(owner, packageName);
62
+ const directTargetRoots = this.#taggedTargets.listTaggedDirectTargetRoots(scan.scan_id, {
63
+ deleteTags: [],
64
+ excludeTags,
65
+ keepCount,
66
+ cutoffTimestamp: options?.cutoffTimestamp
67
+ });
68
+ const planArtifacts = this.#planArtifacts.build(scan.scan_id, directTargetRoots);
69
+ return {
70
+ owner: scan.owner,
71
+ packageName: scan.package_name,
72
+ scanCompletedAt: scan.scan_completed_at,
73
+ plannerInputs: _buildPlannerInputs({
74
+ excludeTags,
75
+ keepNTagged: keepCount,
76
+ olderThan: options?.olderThan,
77
+ cutoffTimestamp: options?.cutoffTimestamp
78
+ }),
79
+ ...buildPlanOutputs([], directTargetRoots, planArtifacts)
80
+ };
81
+ }
82
+ getDeleteTagsPlan(owner, packageName, deleteTags, excludeTags) {
83
+ return this.getDeleteTagsPlanWithCutoff(owner, packageName, deleteTags, excludeTags);
84
+ }
85
+ getDeleteTagsPlanWithCutoff(owner, packageName, deleteTags, excludeTags, options) {
86
+ const scan = this.#untaggedTargets.getLatestCompletedScan(owner, packageName);
87
+ const directTargetTags = this.#taggedTargets.listDeleteTagDirectTargetTags(scan.scan_id, deleteTags, excludeTags, options?.useRegex ?? false, options?.cutoffTimestamp);
88
+ const directTargetRoots = this.#taggedTargets.listTaggedDirectTargetRoots(scan.scan_id, {
89
+ deleteTags,
90
+ deleteTagsRequested: options?.deleteTagsRequested ?? true,
91
+ excludeTags,
92
+ keepCount: options?.keepNTagged,
93
+ useRegex: options?.useRegex ?? false,
94
+ cutoffTimestamp: options?.cutoffTimestamp
95
+ });
96
+ const planArtifacts = this.#planArtifacts.build(scan.scan_id, directTargetRoots);
97
+ return {
98
+ owner: scan.owner,
99
+ packageName: scan.package_name,
100
+ scanCompletedAt: scan.scan_completed_at,
101
+ plannerInputs: _buildPlannerInputs({
102
+ deleteGhostImages: options?.deleteGhostImages || undefined,
103
+ deletePartialImages: options?.deletePartialImages || undefined,
104
+ deleteOrphanedImages: options?.deleteOrphanedImages || undefined,
105
+ deleteTags,
106
+ excludeTags,
107
+ keepNTagged: options?.keepNTagged,
108
+ useRegex: options?.useRegex || undefined,
109
+ olderThan: options?.olderThan,
110
+ cutoffTimestamp: options?.cutoffTimestamp
111
+ }),
112
+ ...buildPlanOutputs(directTargetTags, directTargetRoots, planArtifacts)
113
+ };
114
+ }
115
+ }
116
+ function _buildPlannerInputs(inputs) {
117
+ return Object.fromEntries(Object.entries(inputs).filter(([, value]) => {
118
+ if (value === undefined) {
119
+ return false;
120
+ }
121
+ if (value === false) {
122
+ return false;
123
+ }
124
+ return !(Array.isArray(value) && value.length === 0);
125
+ }));
126
+ }
@@ -0,0 +1,12 @@
1
+ import type Database from "better-sqlite3";
2
+ import { type PlannerLogger } from "./_planner-types.js";
3
+ export declare class PlannerSql {
4
+ #private;
5
+ constructor(database: Database.Database, logger?: PlannerLogger);
6
+ get database(): Database.Database;
7
+ get logger(): PlannerLogger;
8
+ exec(sql: string, params?: Array<number | string | null>): void;
9
+ get<T>(sql: string, params: Array<number | string>): T | undefined;
10
+ all<T>(sql: string, params: Array<number | string>): T[];
11
+ traceSql(sql: string, params: Array<number | string | null>): void;
12
+ }
@@ -0,0 +1,35 @@
1
+ import { silentPlannerLogger } from "./_planner-types.js";
2
+ export class PlannerSql {
3
+ #database;
4
+ #logger;
5
+ constructor(database, logger = silentPlannerLogger) {
6
+ this.#database = database;
7
+ this.#logger = logger;
8
+ }
9
+ get database() {
10
+ return this.#database;
11
+ }
12
+ get logger() {
13
+ return this.#logger;
14
+ }
15
+ exec(sql, params = []) {
16
+ this.#traceSql(sql, params);
17
+ this.#database.prepare(sql).run(...params);
18
+ }
19
+ get(sql, params) {
20
+ this.#traceSql(sql, params);
21
+ return this.#database.prepare(sql).get(...params);
22
+ }
23
+ all(sql, params) {
24
+ this.#traceSql(sql, params);
25
+ const rows = this.#database.prepare(sql).all(...params);
26
+ this.#logger.debug(`SQL returned ${rows.length} row(s)`);
27
+ return rows;
28
+ }
29
+ traceSql(sql, params) {
30
+ this.#traceSql(sql, params);
31
+ }
32
+ #traceSql(sql, params) {
33
+ this.#logger.trace(`SQL:\n${sql.trim()}\nPARAMS: ${JSON.stringify(params)}`);
34
+ }
35
+ }
@@ -0,0 +1,8 @@
1
+ import type Database from "better-sqlite3";
2
+ export interface PlannerTagSelectorPredicate {
3
+ sql: string;
4
+ params: string[];
5
+ }
6
+ export declare function buildTagSelectorPredicate(database: Database.Database, columnSql: string, selectors: string[], useRegex: boolean): PlannerTagSelectorPredicate;
7
+ export declare function wildcardSelectorToSqlLike(selector: string): string;
8
+ export declare function hasWildcard(selector: string): boolean;
@@ -0,0 +1,57 @@
1
+ export function buildTagSelectorPredicate(database, columnSql, selectors, useRegex) {
2
+ if (selectors.length === 0) {
3
+ throw new Error("selectors must not be empty");
4
+ }
5
+ if (useRegex) {
6
+ registerRegexFunction(database);
7
+ }
8
+ return {
9
+ sql: selectors
10
+ .map((selector) => {
11
+ if (useRegex) {
12
+ return `regexp(?, ${columnSql})`;
13
+ }
14
+ return hasWildcard(selector) ? `${columnSql} LIKE ? ESCAPE '\\'` : `${columnSql} = ?`;
15
+ })
16
+ .join(" OR "),
17
+ params: useRegex ? selectors : selectors.map((selector) => wildcardSelectorToSqlLike(selector))
18
+ };
19
+ }
20
+ export function wildcardSelectorToSqlLike(selector) {
21
+ if (!hasWildcard(selector)) {
22
+ return selector;
23
+ }
24
+ return selector.replaceAll(/[%_\\*?]/g, (character) => {
25
+ switch (character) {
26
+ case "%":
27
+ case "_":
28
+ case "\\":
29
+ return `\\${character}`;
30
+ case "*":
31
+ return "%";
32
+ case "?":
33
+ return "_";
34
+ default:
35
+ return character;
36
+ }
37
+ });
38
+ }
39
+ export function hasWildcard(selector) {
40
+ return selector.includes("*") || selector.includes("?");
41
+ }
42
+ function registerRegexFunction(database) {
43
+ const markedDatabase = database;
44
+ if (markedDatabase.__ghcrManagerRegexRegistered) {
45
+ return;
46
+ }
47
+ markedDatabase.__ghcrManagerRegexCache = new Map();
48
+ database.function("regexp", (pattern, value) => {
49
+ let compiled = markedDatabase.__ghcrManagerRegexCache?.get(pattern);
50
+ if (!compiled) {
51
+ compiled = new RegExp(pattern);
52
+ markedDatabase.__ghcrManagerRegexCache?.set(pattern, compiled);
53
+ }
54
+ return compiled.test(value) ? 1 : 0;
55
+ });
56
+ markedDatabase.__ghcrManagerRegexRegistered = true;
57
+ }
@@ -0,0 +1,15 @@
1
+ import { PlannerSql } from "./_planner-sql.js";
2
+ import type { DeletePlanRoot } from "./_planner-types.js";
3
+ export interface TaggedRootTargetOptions {
4
+ deleteTags: string[];
5
+ deleteTagsRequested?: boolean;
6
+ excludeTags: string[];
7
+ keepCount?: number;
8
+ useRegex?: boolean;
9
+ cutoffTimestamp?: string;
10
+ }
11
+ export declare class PlannerTaggedRootTargets {
12
+ #private;
13
+ constructor(sql: PlannerSql);
14
+ listTaggedDirectTargetRoots(scanId: number, options: TaggedRootTargetOptions): DeletePlanRoot[];
15
+ }
@@ -0,0 +1,19 @@
1
+ import { PlannerDeleteTagRootTargets } from "./_planner-delete-tag-root-targets.js";
2
+ import { PlannerKeepTaggedRootTargets } from "./_planner-keep-tagged-root-targets.js";
3
+ export class PlannerTaggedRootTargets {
4
+ #deleteTagTargets;
5
+ #keepTaggedTargets;
6
+ constructor(sql) {
7
+ this.#deleteTagTargets = new PlannerDeleteTagRootTargets(sql);
8
+ this.#keepTaggedTargets = new PlannerKeepTaggedRootTargets(sql);
9
+ }
10
+ listTaggedDirectTargetRoots(scanId, options) {
11
+ if (options.deleteTagsRequested && options.deleteTags.length === 0) {
12
+ return [];
13
+ }
14
+ if (options.deleteTags.length === 0) {
15
+ return this.#keepTaggedTargets.list(scanId, options.excludeTags, options.useRegex ?? false, options.keepCount, options.cutoffTimestamp);
16
+ }
17
+ return this.#deleteTagTargets.list(scanId, options.deleteTags, options.excludeTags, options.useRegex ?? false, options.keepCount, options.cutoffTimestamp);
18
+ }
19
+ }
@@ -0,0 +1,8 @@
1
+ import { type TaggedRootTargetOptions } from "./_planner-tagged-root-targets.js";
2
+ import { PlannerSql } from "./_planner-sql.js";
3
+ export declare class PlannerTaggedTargets {
4
+ #private;
5
+ constructor(sql: PlannerSql);
6
+ listDeleteTagDirectTargetTags(scanId: number, deleteTags: string[], excludeTags: string[], useRegex: boolean, cutoffTimestamp?: string): string[];
7
+ listTaggedDirectTargetRoots(scanId: number, options: TaggedRootTargetOptions): import("./_planner-types.js").DeletePlanRoot[];
8
+ }
@@ -0,0 +1,16 @@
1
+ import { PlannerDirectTargetTags } from "./_planner-direct-target-tags.js";
2
+ import { PlannerTaggedRootTargets } from "./_planner-tagged-root-targets.js";
3
+ export class PlannerTaggedTargets {
4
+ #directTargetTags;
5
+ #rootTargets;
6
+ constructor(sql) {
7
+ this.#directTargetTags = new PlannerDirectTargetTags(sql);
8
+ this.#rootTargets = new PlannerTaggedRootTargets(sql);
9
+ }
10
+ listDeleteTagDirectTargetTags(scanId, deleteTags, excludeTags, useRegex, cutoffTimestamp) {
11
+ return this.#directTargetTags.listDeleteTagDirectTargetTags(scanId, deleteTags, excludeTags, useRegex, cutoffTimestamp);
12
+ }
13
+ listTaggedDirectTargetRoots(scanId, options) {
14
+ return this.#rootTargets.listTaggedDirectTargetRoots(scanId, options);
15
+ }
16
+ }
@@ -0,0 +1,135 @@
1
+ interface _PlanRootRow {
2
+ version_id: number;
3
+ root_digest: string;
4
+ root_manifest_kind: string | null;
5
+ direct_target_reason: string;
6
+ selection_mode: string;
7
+ }
8
+ interface _PlanTagRow {
9
+ target_tag: string;
10
+ }
11
+ interface _ClosureManifestRow {
12
+ source_version_id: number;
13
+ source_digest: string;
14
+ member_version_id: number;
15
+ member_digest: string;
16
+ member_manifest_kind: string | null;
17
+ hops_from_root: number;
18
+ member_role: string;
19
+ }
20
+ interface _BlockedRootRow {
21
+ blocked_version_id: number;
22
+ blocked_digest: string;
23
+ blocking_version_id: number;
24
+ blocking_digest: string;
25
+ overlap_digest: string;
26
+ overlap_manifest_kind: string | null;
27
+ block_reason: string;
28
+ }
29
+ export interface PlannerLogger {
30
+ trace(message: string): void;
31
+ debug(message: string): void;
32
+ warn?(message: string): void;
33
+ }
34
+ export interface ScanRow {
35
+ scan_id: number;
36
+ owner: string;
37
+ package_name: string;
38
+ scan_completed_at: string;
39
+ }
40
+ export interface DeletePlanRoot {
41
+ versionId: number;
42
+ digest: string;
43
+ manifestKind?: string;
44
+ reason: string;
45
+ selectionMode: string;
46
+ }
47
+ export interface DeletePlanClosureManifest {
48
+ sourceVersionId: number;
49
+ sourceDigest: string;
50
+ memberVersionId: number;
51
+ memberDigest: string;
52
+ memberManifestKind?: string;
53
+ hopsFromRoot: number;
54
+ memberRole: string;
55
+ }
56
+ export interface DeletePlanBlockedRoot {
57
+ blockedVersionId: number;
58
+ blockedDigest: string;
59
+ blockingVersionId: number;
60
+ blockingDigest: string;
61
+ overlapDigest: string;
62
+ overlapManifestKind?: string;
63
+ reason: string;
64
+ }
65
+ export interface DeletePlanRootDecision {
66
+ versionId: number;
67
+ digest: string;
68
+ manifestKind?: string;
69
+ selectionMode: string;
70
+ selectionReason: string;
71
+ validationStatus: "fully-deletable" | "blocked" | "untag-only";
72
+ validationReasonCode: "untag-only-partial-tag-match" | "fully-deletable-no-retained-overlap" | "blocked-overlap-with-retained-root";
73
+ validationReason: string;
74
+ blockingVersionId?: number;
75
+ blockingDigest?: string;
76
+ overlapDigest?: string;
77
+ overlapManifestKind?: string;
78
+ }
79
+ export interface DeletePlanProtectedRoot {
80
+ versionId: number;
81
+ digest: string;
82
+ blocks: Array<{
83
+ blockedVersionId: number;
84
+ blockedDigest: string;
85
+ blockReasonCode: string;
86
+ overlapDigest: string;
87
+ overlapManifestKind?: string;
88
+ }>;
89
+ }
90
+ export interface PlanArtifacts {
91
+ closureManifests: DeletePlanClosureManifest[];
92
+ blockedRoots: DeletePlanBlockedRoot[];
93
+ fullyDeletableRoots: DeletePlanRoot[];
94
+ }
95
+ export interface DeletePlan {
96
+ owner: string;
97
+ packageName: string;
98
+ scanCompletedAt: string;
99
+ plannerInputs: {
100
+ deleteUntagged?: boolean;
101
+ deleteGhostImages?: boolean;
102
+ deletePartialImages?: boolean;
103
+ deleteOrphanedImages?: boolean;
104
+ deleteTags?: string[];
105
+ excludeTags?: string[];
106
+ keepNTagged?: number;
107
+ keepNUntagged?: number;
108
+ useRegex?: boolean;
109
+ olderThan?: string;
110
+ cutoffTimestamp?: string;
111
+ };
112
+ validationSummary: {
113
+ directTargetTagCount: number;
114
+ directTargetRootCount: number;
115
+ deleteRootCandidateCount: number;
116
+ untagOnlyRootCount: number;
117
+ fullyDeletableRootCount: number;
118
+ blockedDeleteRootCount: number;
119
+ protectedRootCount: number;
120
+ };
121
+ directTargetTags: string[];
122
+ directTargetRoots: DeletePlanRoot[];
123
+ rootDecisions: DeletePlanRootDecision[];
124
+ protectedRoots: DeletePlanProtectedRoot[];
125
+ closureManifests: DeletePlanClosureManifest[];
126
+ blockedRoots: DeletePlanBlockedRoot[];
127
+ fullyDeletableRoots: DeletePlanRoot[];
128
+ collateralTags: string[];
129
+ }
130
+ export declare const silentPlannerLogger: PlannerLogger;
131
+ export declare function mapPlanRootRow(row: _PlanRootRow): DeletePlanRoot;
132
+ export declare function mapPlanTagRows(rows: _PlanTagRow[]): string[];
133
+ export declare function mapClosureManifestRow(row: _ClosureManifestRow): DeletePlanClosureManifest;
134
+ export declare function mapBlockedRootRow(row: _BlockedRootRow): DeletePlanBlockedRoot;
135
+ export {};