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,113 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { resolveGitHubActionsRunUrl } from "./_github-actions-run-url.js";
3
+ import { DeletePlanValidationStatuses } from "./planner/index.js";
4
+ export class CleanupRunWriter {
5
+ #database;
6
+ #insertSelectedTagStatement;
7
+ #updateSelectedTagsDeletedStatement;
8
+ #insertRootDecisionStatement;
9
+ #insertProtectedRootBlockStatement;
10
+ #insertCleanupRunStatement;
11
+ constructor(database) {
12
+ this.#database = database;
13
+ this.#insertSelectedTagStatement = this.#database.prepare(`
14
+ INSERT INTO cleanup_selected_tags(
15
+ cleanup_run_id,
16
+ scan_id,
17
+ tag,
18
+ is_deleted
19
+ )
20
+ VALUES(?, ?, ?, 0)
21
+ `);
22
+ this.#updateSelectedTagsDeletedStatement = this.#database.prepare(`
23
+ UPDATE cleanup_selected_tags
24
+ SET is_deleted = 1
25
+ FROM cleanup_root_decisions decision
26
+ JOIN manifests manifest
27
+ ON manifest.scan_id = decision.scan_id
28
+ AND manifest.digest = decision.digest
29
+ JOIN tags
30
+ ON tags.scan_id = manifest.scan_id
31
+ AND tags.version_id = manifest.version_id
32
+ WHERE cleanup_selected_tags.cleanup_run_id = ?
33
+ AND cleanup_selected_tags.scan_id = ?
34
+ AND decision.cleanup_run_id = cleanup_selected_tags.cleanup_run_id
35
+ AND decision.scan_id = cleanup_selected_tags.scan_id
36
+ AND decision.validation_status != 'blocked'
37
+ AND tags.tag = cleanup_selected_tags.tag
38
+ `);
39
+ this.#insertRootDecisionStatement = this.#database.prepare(`
40
+ INSERT INTO cleanup_root_decisions(
41
+ cleanup_run_id,
42
+ scan_id,
43
+ digest,
44
+ selection_mode,
45
+ selection_reason,
46
+ validation_status,
47
+ validation_reason_code,
48
+ validation_reason,
49
+ blocking_digest,
50
+ overlap_digest
51
+ )
52
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
53
+ `);
54
+ this.#insertProtectedRootBlockStatement = this.#database.prepare(`
55
+ INSERT INTO cleanup_protected_root_blocks(
56
+ cleanup_run_id,
57
+ scan_id,
58
+ protected_digest,
59
+ blocked_digest,
60
+ block_reason_code,
61
+ overlap_digest
62
+ )
63
+ VALUES(?, ?, ?, ?, ?, ?)
64
+ `);
65
+ this.#insertCleanupRunStatement = this.#database.prepare(`
66
+ INSERT INTO cleanup_runs(
67
+ scan_id,
68
+ cleanup_uuid,
69
+ cleanup_started_at,
70
+ github_actions_run_url,
71
+ dry_run,
72
+ planner_inputs_json,
73
+ direct_target_tag_count,
74
+ direct_target_root_count,
75
+ delete_root_candidate_count,
76
+ untag_only_root_count,
77
+ fully_deletable_root_count,
78
+ blocked_delete_root_count,
79
+ protected_root_count
80
+ )
81
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
82
+ `);
83
+ }
84
+ persistCleanupRun(scanId, plan, options) {
85
+ return this.#database.transaction(() => {
86
+ const cleanupRunId = this.#insertCleanupRun(scanId, plan, options);
87
+ for (const rootDecision of plan.rootDecisions) {
88
+ this.#insertRootDecisionStatement.run(cleanupRunId, scanId, rootDecision.digest, rootDecision.selectionMode, rootDecision.selectionReason, rootDecision.validationStatus, rootDecision.validationReasonCode, rootDecision.validationReason, rootDecision.blockingDigest ?? null, rootDecision.overlapDigest ?? null);
89
+ }
90
+ for (const protectedRoot of plan.protectedRoots) {
91
+ for (const block of protectedRoot.blocks) {
92
+ this.#insertProtectedRootBlockStatement.run(cleanupRunId, scanId, protectedRoot.digest, block.blockedDigest, block.blockReasonCode, block.overlapDigest);
93
+ }
94
+ }
95
+ for (const tag of plan.directTargetTags) {
96
+ this.#insertSelectedTagStatement.run(cleanupRunId, scanId, tag);
97
+ }
98
+ this.#updateSelectedTagsDeletedStatement.run(cleanupRunId, scanId);
99
+ return cleanupRunId;
100
+ })();
101
+ }
102
+ #insertCleanupRun(scanId, plan, options) {
103
+ const directTargetTagCount = plan.directTargetTags.length;
104
+ const directTargetRootCount = plan.directTargetRoots.length;
105
+ const deleteRootCandidateCount = plan.directTargetRoots.filter((root) => root.selectionMode === "delete-root").length;
106
+ const untagOnlyRootCount = plan.rootDecisions.filter((decision) => decision.validationStatus === DeletePlanValidationStatuses.untagOnly).length;
107
+ const fullyDeletableRootCount = plan.fullyDeletableRoots.length;
108
+ const blockedDeleteRootCount = plan.rootDecisions.filter((decision) => decision.validationStatus === DeletePlanValidationStatuses.blocked).length;
109
+ const protectedRootCount = plan.protectedRoots.length;
110
+ const result = this.#insertCleanupRunStatement.run(scanId, randomUUID(), options.cleanupStartedAt, resolveGitHubActionsRunUrl(), options.dryRun ? 1 : 0, JSON.stringify(plan.plannerInputs), directTargetTagCount, directTargetRootCount, deleteRootCandidateCount, untagOnlyRootCount, fullyDeletableRootCount, blockedDeleteRootCount, protectedRootCount);
111
+ return Number(result.lastInsertRowid);
112
+ }
113
+ }
@@ -0,0 +1,7 @@
1
+ import type Database from "better-sqlite3";
2
+ export declare class DbMergeCleanupCopy {
3
+ #private;
4
+ constructor(database: Database.Database);
5
+ copyCleanupRuns(attachName: string, sourceScanId: number, targetScanId: number, existingCleanupUuids: string[]): number;
6
+ listCleanupUuids(tableName: string, scanId: number): string[];
7
+ }
@@ -0,0 +1,172 @@
1
+ export class DbMergeCleanupCopy {
2
+ #database;
3
+ #insertCleanupRunStatement;
4
+ #copyRootDecisionsStatementByAttachName = new Map();
5
+ #copySelectedTagsStatementByAttachName = new Map();
6
+ #copyProtectedRootBlocksStatementByAttachName = new Map();
7
+ #listCleanupUuidsStatementByTableName = new Map();
8
+ constructor(database) {
9
+ this.#database = database;
10
+ this.#insertCleanupRunStatement = this.#database.prepare(`
11
+ INSERT INTO cleanup_runs(
12
+ scan_id,
13
+ cleanup_uuid,
14
+ cleanup_started_at,
15
+ github_actions_run_url,
16
+ dry_run,
17
+ planner_inputs_json,
18
+ direct_target_tag_count,
19
+ direct_target_root_count,
20
+ delete_root_candidate_count,
21
+ untag_only_root_count,
22
+ fully_deletable_root_count,
23
+ blocked_delete_root_count,
24
+ protected_root_count
25
+ )
26
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
27
+ `);
28
+ }
29
+ copyCleanupRuns(attachName, sourceScanId, targetScanId, existingCleanupUuids) {
30
+ const rows = this.#database
31
+ .prepare(`
32
+ SELECT
33
+ cleanup_run_id,
34
+ cleanup_uuid,
35
+ cleanup_started_at,
36
+ github_actions_run_url,
37
+ dry_run,
38
+ planner_inputs_json,
39
+ direct_target_tag_count,
40
+ direct_target_root_count,
41
+ delete_root_candidate_count,
42
+ untag_only_root_count,
43
+ fully_deletable_root_count,
44
+ blocked_delete_root_count,
45
+ protected_root_count
46
+ FROM ${attachName}.cleanup_runs
47
+ WHERE scan_id = ?
48
+ ORDER BY cleanup_run_id
49
+ `)
50
+ .all(sourceScanId);
51
+ const knownCleanupUuids = new Set(existingCleanupUuids);
52
+ let importedCleanupRunCount = 0;
53
+ for (const row of rows) {
54
+ if (knownCleanupUuids.has(row.cleanup_uuid)) {
55
+ continue;
56
+ }
57
+ const cleanupRunId = Number(this.#insertCleanupRunStatement.run(targetScanId, row.cleanup_uuid, row.cleanup_started_at, row.github_actions_run_url, row.dry_run, row.planner_inputs_json, row.direct_target_tag_count, row.direct_target_root_count, row.delete_root_candidate_count, row.untag_only_root_count, row.fully_deletable_root_count, row.blocked_delete_root_count, row.protected_root_count).lastInsertRowid);
58
+ this.#copyRootDecisionsStatement(attachName).run(cleanupRunId, targetScanId, row.cleanup_run_id, sourceScanId);
59
+ this.#copySelectedTagsStatement(attachName).run(cleanupRunId, targetScanId, row.cleanup_run_id, sourceScanId);
60
+ this.#copyProtectedRootBlocksStatement(attachName).run(cleanupRunId, targetScanId, row.cleanup_run_id, sourceScanId);
61
+ knownCleanupUuids.add(row.cleanup_uuid);
62
+ importedCleanupRunCount += 1;
63
+ }
64
+ return importedCleanupRunCount;
65
+ }
66
+ listCleanupUuids(tableName, scanId) {
67
+ const rows = this.#listCleanupUuidsStatement(tableName).all(scanId);
68
+ return rows.map((row) => row.cleanup_uuid);
69
+ }
70
+ #copyRootDecisionsStatement(attachName) {
71
+ const cached = this.#copyRootDecisionsStatementByAttachName.get(attachName);
72
+ if (cached) {
73
+ return cached;
74
+ }
75
+ const statement = this.#database.prepare(`
76
+ INSERT INTO cleanup_root_decisions(
77
+ cleanup_run_id,
78
+ scan_id,
79
+ digest,
80
+ selection_mode,
81
+ selection_reason,
82
+ validation_status,
83
+ validation_reason_code,
84
+ validation_reason,
85
+ blocking_digest,
86
+ overlap_digest
87
+ )
88
+ SELECT
89
+ ?,
90
+ ?,
91
+ digest,
92
+ selection_mode,
93
+ selection_reason,
94
+ validation_status,
95
+ validation_reason_code,
96
+ validation_reason,
97
+ blocking_digest,
98
+ overlap_digest
99
+ FROM ${attachName}.cleanup_root_decisions
100
+ WHERE cleanup_run_id = ?
101
+ AND scan_id = ?
102
+ `);
103
+ this.#copyRootDecisionsStatementByAttachName.set(attachName, statement);
104
+ return statement;
105
+ }
106
+ #copySelectedTagsStatement(attachName) {
107
+ const cached = this.#copySelectedTagsStatementByAttachName.get(attachName);
108
+ if (cached) {
109
+ return cached;
110
+ }
111
+ const statement = this.#database.prepare(`
112
+ INSERT INTO cleanup_selected_tags(
113
+ cleanup_run_id,
114
+ scan_id,
115
+ tag,
116
+ is_deleted
117
+ )
118
+ SELECT
119
+ ?,
120
+ ?,
121
+ tag,
122
+ is_deleted
123
+ FROM ${attachName}.cleanup_selected_tags
124
+ WHERE cleanup_run_id = ?
125
+ AND scan_id = ?
126
+ `);
127
+ this.#copySelectedTagsStatementByAttachName.set(attachName, statement);
128
+ return statement;
129
+ }
130
+ #copyProtectedRootBlocksStatement(attachName) {
131
+ const cached = this.#copyProtectedRootBlocksStatementByAttachName.get(attachName);
132
+ if (cached) {
133
+ return cached;
134
+ }
135
+ const statement = this.#database.prepare(`
136
+ INSERT INTO cleanup_protected_root_blocks(
137
+ cleanup_run_id,
138
+ scan_id,
139
+ protected_digest,
140
+ blocked_digest,
141
+ block_reason_code,
142
+ overlap_digest
143
+ )
144
+ SELECT
145
+ ?,
146
+ ?,
147
+ protected_digest,
148
+ blocked_digest,
149
+ block_reason_code,
150
+ overlap_digest
151
+ FROM ${attachName}.cleanup_protected_root_blocks
152
+ WHERE cleanup_run_id = ?
153
+ AND scan_id = ?
154
+ `);
155
+ this.#copyProtectedRootBlocksStatementByAttachName.set(attachName, statement);
156
+ return statement;
157
+ }
158
+ #listCleanupUuidsStatement(tableName) {
159
+ const cached = this.#listCleanupUuidsStatementByTableName.get(tableName);
160
+ if (cached) {
161
+ return cached;
162
+ }
163
+ const statement = this.#database.prepare(`
164
+ SELECT cleanup_uuid
165
+ FROM ${tableName}
166
+ WHERE scan_id = ?
167
+ ORDER BY cleanup_run_id
168
+ `);
169
+ this.#listCleanupUuidsStatementByTableName.set(tableName, statement);
170
+ return statement;
171
+ }
172
+ }
@@ -0,0 +1,2 @@
1
+ export type CleanupHistoryRelation = "source-ahead" | "target-ahead" | "diverged";
2
+ export declare function resolveCleanupHistoryRelation(sourceCleanupUuids: string[], targetCleanupUuids: string[]): CleanupHistoryRelation;
@@ -0,0 +1,15 @@
1
+ export function resolveCleanupHistoryRelation(sourceCleanupUuids, targetCleanupUuids) {
2
+ if (_isPrefix(targetCleanupUuids, sourceCleanupUuids)) {
3
+ return "source-ahead";
4
+ }
5
+ if (_isPrefix(sourceCleanupUuids, targetCleanupUuids)) {
6
+ return "target-ahead";
7
+ }
8
+ return "diverged";
9
+ }
10
+ function _isPrefix(prefixCandidate, history) {
11
+ if (prefixCandidate.length > history.length) {
12
+ return false;
13
+ }
14
+ return prefixCandidate.every((cleanupUuid, index) => cleanupUuid === history[index]);
15
+ }
@@ -0,0 +1,8 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { DbMergeSourceSummary } from "./_db-merge-types.js";
3
+ export type { DbMergeSourceSummary } from "./_db-merge-types.js";
4
+ export declare class DbMergeRepository {
5
+ #private;
6
+ constructor(database: Database.Database);
7
+ mergeSourceDatabase(sourceDatabasePath: string): DbMergeSourceSummary;
8
+ }
@@ -0,0 +1,95 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { DbMergeCleanupCopy } from "./_db-merge-cleanup-copy.js";
3
+ import { resolveCleanupHistoryRelation } from "./_db-merge-history.js";
4
+ import { DbMergeScanCopy } from "./_db-merge-scan-copy.js";
5
+ export class DbMergeRepository {
6
+ #database;
7
+ #cleanupCopy;
8
+ #scanCopy;
9
+ constructor(database) {
10
+ this.#database = database;
11
+ this.#cleanupCopy = new DbMergeCleanupCopy(database);
12
+ this.#scanCopy = new DbMergeScanCopy(database);
13
+ }
14
+ mergeSourceDatabase(sourceDatabasePath) {
15
+ const targetPath = realpathSync(this.#database.name);
16
+ const sourcePath = realpathSync(sourceDatabasePath);
17
+ if (targetPath === sourcePath) {
18
+ throw new Error(`source DB matches target DB: ${sourceDatabasePath}`);
19
+ }
20
+ const attachName = "merge_source";
21
+ const quotedAttachName = `"${attachName}"`;
22
+ this.#database.exec(`ATTACH DATABASE ${this.#scanCopy.quoteDatabasePath(sourcePath)} AS ${quotedAttachName}`);
23
+ try {
24
+ return this.#database.transaction(() => this.#mergeAttachedSource(attachName, sourceDatabasePath))();
25
+ }
26
+ finally {
27
+ this.#database.exec(`DETACH DATABASE ${quotedAttachName}`);
28
+ }
29
+ }
30
+ #mergeAttachedSource(attachName, sourceDatabasePath) {
31
+ const sourceScans = this.#database
32
+ .prepare(`
33
+ SELECT
34
+ scan_id,
35
+ scan_uuid,
36
+ owner,
37
+ package_name,
38
+ package_metadata_json,
39
+ github_actions_run_url,
40
+ scan_started_at,
41
+ scan_completed_at,
42
+ status
43
+ FROM ${attachName}.package_scans
44
+ ORDER BY scan_started_at, scan_uuid
45
+ `)
46
+ .all();
47
+ const summary = {
48
+ sourceDatabasePath,
49
+ importedScanCount: 0,
50
+ skippedScanCount: 0,
51
+ importedCleanupRunCount: 0,
52
+ skippedCleanupRunCount: 0
53
+ };
54
+ for (const sourceScan of sourceScans) {
55
+ const targetScan = this.#database
56
+ .prepare(`
57
+ SELECT
58
+ scan_id,
59
+ scan_uuid,
60
+ owner,
61
+ package_name,
62
+ package_metadata_json,
63
+ github_actions_run_url,
64
+ scan_started_at,
65
+ scan_completed_at,
66
+ status
67
+ FROM package_scans
68
+ WHERE scan_uuid = ?
69
+ `)
70
+ .get(sourceScan.scan_uuid);
71
+ if (!targetScan) {
72
+ const targetScanId = this.#scanCopy.insertScan(sourceScan);
73
+ this.#scanCopy.copyScanRows(attachName, sourceScan.scan_id, targetScanId);
74
+ summary.importedScanCount += 1;
75
+ summary.importedCleanupRunCount += this.#cleanupCopy.copyCleanupRuns(attachName, sourceScan.scan_id, targetScanId, []);
76
+ continue;
77
+ }
78
+ this.#scanCopy.assertMatchingScanMetadata(sourceScan, targetScan, sourceDatabasePath);
79
+ const sourceCleanupUuids = this.#cleanupCopy.listCleanupUuids(`${attachName}.cleanup_runs`, sourceScan.scan_id);
80
+ const targetCleanupUuids = this.#cleanupCopy.listCleanupUuids("cleanup_runs", targetScan.scan_id);
81
+ const historyRelation = resolveCleanupHistoryRelation(sourceCleanupUuids, targetCleanupUuids);
82
+ if (historyRelation === "source-ahead") {
83
+ summary.importedCleanupRunCount += this.#cleanupCopy.copyCleanupRuns(attachName, sourceScan.scan_id, targetScan.scan_id, targetCleanupUuids);
84
+ }
85
+ else if (historyRelation === "target-ahead") {
86
+ summary.skippedCleanupRunCount += sourceCleanupUuids.length;
87
+ }
88
+ else {
89
+ throw new Error(`cleanup history diverged for scan_uuid ${sourceScan.scan_uuid} while merging ${sourceDatabasePath}`);
90
+ }
91
+ summary.skippedScanCount += 1;
92
+ }
93
+ return summary;
94
+ }
95
+ }
@@ -0,0 +1,10 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { SourceScanRow, TargetScanRow } from "./_db-merge-types.js";
3
+ export declare class DbMergeScanCopy {
4
+ #private;
5
+ constructor(database: Database.Database);
6
+ insertScan(sourceScan: SourceScanRow): number;
7
+ copyScanRows(attachName: string, sourceScanId: number, targetScanId: number): void;
8
+ assertMatchingScanMetadata(sourceScan: SourceScanRow, targetScan: TargetScanRow, sourceDatabasePath: string): void;
9
+ quoteDatabasePath(value: string): string;
10
+ }
@@ -0,0 +1,70 @@
1
+ export class DbMergeScanCopy {
2
+ #database;
3
+ constructor(database) {
4
+ this.#database = database;
5
+ }
6
+ insertScan(sourceScan) {
7
+ const result = this.#database
8
+ .prepare(`
9
+ INSERT INTO package_scans(
10
+ scan_uuid,
11
+ owner,
12
+ package_name,
13
+ package_metadata_json,
14
+ github_actions_run_url,
15
+ scan_started_at,
16
+ scan_completed_at,
17
+ status
18
+ )
19
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?)
20
+ `)
21
+ .run(sourceScan.scan_uuid, sourceScan.owner, sourceScan.package_name, sourceScan.package_metadata_json, sourceScan.github_actions_run_url, sourceScan.scan_started_at, sourceScan.scan_completed_at, sourceScan.status);
22
+ return Number(result.lastInsertRowid);
23
+ }
24
+ copyScanRows(attachName, sourceScanId, targetScanId) {
25
+ const copySpecs = [
26
+ "package_versions(scan_id, version_id, created_at, updated_at)",
27
+ "package_version_payloads(scan_id, version_id, raw_json)",
28
+ "tags(scan_id, tag, version_id, is_digest_tag)",
29
+ "manifests(scan_id, version_id, digest, media_type, artifact_type, config_media_type, subject_digest, annotations_json, manifest_kind)",
30
+ "manifest_descriptors(scan_id, parent_digest, child_digest, media_type, artifact_type, platform_os, platform_architecture, platform_variant)",
31
+ "manifest_payloads(scan_id, digest, raw_json)",
32
+ "manifest_edges(scan_id, parent_digest, child_digest, edge_kind)",
33
+ "manifest_reachability(scan_id, ancestor_digest, descendant_digest, min_distance)",
34
+ "manifest_graphs(scan_id, digest, graph_id)"
35
+ ];
36
+ for (const spec of copySpecs) {
37
+ const [tableName, columnList] = spec.split("(");
38
+ const trimmedColumnList = columnList.slice(0, -1);
39
+ const columns = trimmedColumnList.split(", ").slice(1);
40
+ this.#database
41
+ .prepare(`
42
+ INSERT INTO ${tableName}(${trimmedColumnList})
43
+ SELECT ?, ${columns.join(", ")}
44
+ FROM ${attachName}.${tableName}
45
+ WHERE scan_id = ?
46
+ `)
47
+ .run(targetScanId, sourceScanId);
48
+ }
49
+ }
50
+ assertMatchingScanMetadata(sourceScan, targetScan, sourceDatabasePath) {
51
+ const scanFields = [
52
+ "scan_uuid",
53
+ "owner",
54
+ "package_name",
55
+ "package_metadata_json",
56
+ "github_actions_run_url",
57
+ "scan_started_at",
58
+ "scan_completed_at",
59
+ "status"
60
+ ];
61
+ for (const field of scanFields) {
62
+ if (sourceScan[field] !== targetScan[field]) {
63
+ throw new Error(`scan metadata mismatch for scan_uuid ${sourceScan.scan_uuid} while merging ${sourceDatabasePath}`);
64
+ }
65
+ }
66
+ }
67
+ quoteDatabasePath(value) {
68
+ return `'${value.replaceAll("'", "''")}'`;
69
+ }
70
+ }
@@ -0,0 +1,44 @@
1
+ export interface SourceScanRow {
2
+ scan_id: number;
3
+ scan_uuid: string;
4
+ owner: string;
5
+ package_name: string;
6
+ package_metadata_json: string | null;
7
+ github_actions_run_url: string | null;
8
+ scan_started_at: string;
9
+ scan_completed_at: string | null;
10
+ status: string;
11
+ }
12
+ export interface TargetScanRow {
13
+ scan_id: number;
14
+ scan_uuid: string;
15
+ owner: string;
16
+ package_name: string;
17
+ package_metadata_json: string | null;
18
+ github_actions_run_url: string | null;
19
+ scan_started_at: string;
20
+ scan_completed_at: string | null;
21
+ status: string;
22
+ }
23
+ export interface CleanupRunRow {
24
+ cleanup_run_id: number;
25
+ cleanup_uuid: string;
26
+ cleanup_started_at: string;
27
+ github_actions_run_url: string | null;
28
+ dry_run: number;
29
+ planner_inputs_json: string;
30
+ direct_target_tag_count: number;
31
+ direct_target_root_count: number;
32
+ delete_root_candidate_count: number;
33
+ untag_only_root_count: number;
34
+ fully_deletable_root_count: number;
35
+ blocked_delete_root_count: number;
36
+ protected_root_count: number;
37
+ }
38
+ export interface DbMergeSourceSummary {
39
+ sourceDatabasePath: string;
40
+ importedScanCount: number;
41
+ skippedScanCount: number;
42
+ importedCleanupRunCount: number;
43
+ skippedCleanupRunCount: number;
44
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function resolveGitHubActionsRunUrl(env?: NodeJS.ProcessEnv): string | null;
@@ -0,0 +1,9 @@
1
+ export function resolveGitHubActionsRunUrl(env = process.env) {
2
+ const serverUrl = env.GITHUB_SERVER_URL;
3
+ const repository = env.GITHUB_REPOSITORY;
4
+ const runId = env.GITHUB_RUN_ID;
5
+ if (!serverUrl || !repository || !runId) {
6
+ return null;
7
+ }
8
+ return `${serverUrl}/${repository}/actions/runs/${runId}`;
9
+ }
@@ -0,0 +1,2 @@
1
+ import type Database from "better-sqlite3";
2
+ export declare function rebuildManifestReachability(database: Database.Database, scanId: number): void;