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
@@ -1,22 +1,23 @@
1
+ export type ManifestKind = "image_index" | "image_manifest" | "artifact_manifest" | "attestation_manifest" | "signature_manifest";
1
2
  export interface PackageVersionRecord {
2
3
  versionId: number;
3
- digest: string;
4
4
  createdAt: string;
5
5
  updatedAt: string;
6
6
  metadata?: Record<string, unknown>;
7
7
  }
8
8
  export interface TagRecord {
9
9
  tag: string;
10
- digest: string;
11
10
  versionId: number;
12
11
  }
13
12
  export interface ManifestRecord {
13
+ versionId: number;
14
14
  digest: string;
15
15
  mediaType: string;
16
16
  artifactType?: string;
17
17
  configMediaType?: string;
18
18
  subjectDigest?: string;
19
19
  annotations?: Record<string, unknown>;
20
+ manifestKind?: ManifestKind;
20
21
  platform?: {
21
22
  architecture?: string;
22
23
  os?: string;
@@ -1 +1,4 @@
1
- export type { ManifestEdgeRecord, ManifestDescriptorRecord, ManifestRecord, PackageSnapshot, PackageVersionRecord, TagRecord } from "./_types.js";
1
+ export type { ManifestEdgeRecord, ManifestDescriptorRecord, ManifestKind, ManifestRecord, PackageSnapshot, PackageVersionRecord, TagRecord } from "./_types.js";
2
+ export type { HttpErrorResponse } from "./_http-error.js";
3
+ export { buildHttpErrorMessage } from "./_http-error.js";
4
+ export { getOwnerURIComponent } from "./_github-package-owner.js";
@@ -1 +1,2 @@
1
- export {};
1
+ export { buildHttpErrorMessage } from "./_http-error.js";
2
+ export { getOwnerURIComponent } from "./_github-package-owner.js";
@@ -0,0 +1,10 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { DeletePlan } from "./planner/index.js";
3
+ export declare class CleanupRunWriter {
4
+ #private;
5
+ constructor(database: Database.Database);
6
+ persistCleanupRun(scanId: number, plan: DeletePlan, options: {
7
+ dryRun: boolean;
8
+ cleanupStartedAt: string;
9
+ }): number;
10
+ }
@@ -0,0 +1,73 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { resolveGitHubActionsRunUrl } from "./_github-actions-run-url.js";
3
+ export class CleanupRunWriter {
4
+ #database;
5
+ constructor(database) {
6
+ this.#database = database;
7
+ }
8
+ persistCleanupRun(scanId, plan, options) {
9
+ return this.#database.transaction(() => {
10
+ const cleanupRunId = this.#insertCleanupRun(scanId, plan, options);
11
+ for (const rootDecision of plan.rootDecisions) {
12
+ this.#database
13
+ .prepare(`
14
+ INSERT INTO cleanup_root_decisions(
15
+ cleanup_run_id,
16
+ scan_id,
17
+ digest,
18
+ selection_mode,
19
+ selection_reason,
20
+ validation_status,
21
+ validation_reason_code,
22
+ validation_reason,
23
+ blocking_digest,
24
+ overlap_digest
25
+ )
26
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
27
+ `)
28
+ .run(cleanupRunId, scanId, rootDecision.digest, rootDecision.selectionMode, rootDecision.selectionReason, rootDecision.validationStatus, rootDecision.validationReasonCode, rootDecision.validationReason, rootDecision.blockingDigest ?? null, rootDecision.overlapDigest ?? null);
29
+ }
30
+ for (const protectedRoot of plan.protectedRoots) {
31
+ for (const block of protectedRoot.blocks) {
32
+ this.#database
33
+ .prepare(`
34
+ INSERT INTO cleanup_protected_root_blocks(
35
+ cleanup_run_id,
36
+ scan_id,
37
+ protected_digest,
38
+ blocked_digest,
39
+ block_reason_code,
40
+ overlap_digest
41
+ )
42
+ VALUES(?, ?, ?, ?, ?, ?)
43
+ `)
44
+ .run(cleanupRunId, scanId, protectedRoot.digest, block.blockedDigest, block.blockReasonCode, block.overlapDigest);
45
+ }
46
+ }
47
+ return cleanupRunId;
48
+ })();
49
+ }
50
+ #insertCleanupRun(scanId, plan, options) {
51
+ const result = this.#database
52
+ .prepare(`
53
+ INSERT INTO cleanup_runs(
54
+ scan_id,
55
+ cleanup_uuid,
56
+ cleanup_started_at,
57
+ github_actions_run_url,
58
+ dry_run,
59
+ planner_inputs_json,
60
+ direct_target_tag_count,
61
+ direct_target_root_count,
62
+ delete_root_candidate_count,
63
+ untag_only_root_count,
64
+ fully_deletable_root_count,
65
+ blocked_delete_root_count,
66
+ protected_root_count
67
+ )
68
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
69
+ `)
70
+ .run(scanId, randomUUID(), options.cleanupStartedAt, resolveGitHubActionsRunUrl(), options.dryRun ? 1 : 0, JSON.stringify(plan.plannerInputs), plan.validationSummary.directTargetTagCount, plan.validationSummary.directTargetRootCount, plan.validationSummary.deleteRootCandidateCount, plan.validationSummary.untagOnlyRootCount, plan.validationSummary.fullyDeletableRootCount, plan.validationSummary.blockedDeleteRootCount, plan.validationSummary.protectedRootCount);
71
+ return Number(result.lastInsertRowid);
72
+ }
73
+ }
@@ -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,122 @@
1
+ export class DbMergeCleanupCopy {
2
+ #database;
3
+ constructor(database) {
4
+ this.#database = database;
5
+ }
6
+ copyCleanupRuns(attachName, sourceScanId, targetScanId, existingCleanupUuids) {
7
+ const rows = this.#database
8
+ .prepare(`
9
+ SELECT
10
+ cleanup_run_id,
11
+ cleanup_uuid,
12
+ cleanup_started_at,
13
+ github_actions_run_url,
14
+ dry_run,
15
+ planner_inputs_json,
16
+ direct_target_tag_count,
17
+ direct_target_root_count,
18
+ delete_root_candidate_count,
19
+ untag_only_root_count,
20
+ fully_deletable_root_count,
21
+ blocked_delete_root_count,
22
+ protected_root_count
23
+ FROM ${attachName}.cleanup_runs
24
+ WHERE scan_id = ?
25
+ ORDER BY cleanup_run_id
26
+ `)
27
+ .all(sourceScanId);
28
+ const knownCleanupUuids = new Set(existingCleanupUuids);
29
+ let importedCleanupRunCount = 0;
30
+ for (const row of rows) {
31
+ if (knownCleanupUuids.has(row.cleanup_uuid)) {
32
+ continue;
33
+ }
34
+ const cleanupRunId = Number(this.#database
35
+ .prepare(`
36
+ INSERT INTO cleanup_runs(
37
+ scan_id,
38
+ cleanup_uuid,
39
+ cleanup_started_at,
40
+ github_actions_run_url,
41
+ dry_run,
42
+ planner_inputs_json,
43
+ direct_target_tag_count,
44
+ direct_target_root_count,
45
+ delete_root_candidate_count,
46
+ untag_only_root_count,
47
+ fully_deletable_root_count,
48
+ blocked_delete_root_count,
49
+ protected_root_count
50
+ )
51
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
52
+ `)
53
+ .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);
54
+ this.#database
55
+ .prepare(`
56
+ INSERT INTO cleanup_root_decisions(
57
+ cleanup_run_id,
58
+ scan_id,
59
+ digest,
60
+ selection_mode,
61
+ selection_reason,
62
+ validation_status,
63
+ validation_reason_code,
64
+ validation_reason,
65
+ blocking_digest,
66
+ overlap_digest
67
+ )
68
+ SELECT
69
+ ?,
70
+ ?,
71
+ digest,
72
+ selection_mode,
73
+ selection_reason,
74
+ validation_status,
75
+ validation_reason_code,
76
+ validation_reason,
77
+ blocking_digest,
78
+ overlap_digest
79
+ FROM ${attachName}.cleanup_root_decisions
80
+ WHERE cleanup_run_id = ?
81
+ AND scan_id = ?
82
+ `)
83
+ .run(cleanupRunId, targetScanId, row.cleanup_run_id, sourceScanId);
84
+ this.#database
85
+ .prepare(`
86
+ INSERT INTO cleanup_protected_root_blocks(
87
+ cleanup_run_id,
88
+ scan_id,
89
+ protected_digest,
90
+ blocked_digest,
91
+ block_reason_code,
92
+ overlap_digest
93
+ )
94
+ SELECT
95
+ ?,
96
+ ?,
97
+ protected_digest,
98
+ blocked_digest,
99
+ block_reason_code,
100
+ overlap_digest
101
+ FROM ${attachName}.cleanup_protected_root_blocks
102
+ WHERE cleanup_run_id = ?
103
+ AND scan_id = ?
104
+ `)
105
+ .run(cleanupRunId, targetScanId, row.cleanup_run_id, sourceScanId);
106
+ knownCleanupUuids.add(row.cleanup_uuid);
107
+ importedCleanupRunCount += 1;
108
+ }
109
+ return importedCleanupRunCount;
110
+ }
111
+ listCleanupUuids(tableName, scanId) {
112
+ const rows = this.#database
113
+ .prepare(`
114
+ SELECT cleanup_uuid
115
+ FROM ${tableName}
116
+ WHERE scan_id = ?
117
+ ORDER BY cleanup_run_id
118
+ `)
119
+ .all(scanId);
120
+ return rows.map((row) => row.cleanup_uuid);
121
+ }
122
+ }
@@ -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,69 @@
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)",
29
+ "manifests(scan_id, version_id, digest, media_type, artifact_type, config_media_type, subject_digest, annotations_json, platform_os, platform_architecture, platform_variant, 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
+ ];
35
+ for (const spec of copySpecs) {
36
+ const [tableName, columnList] = spec.split("(");
37
+ const trimmedColumnList = columnList.slice(0, -1);
38
+ const columns = trimmedColumnList.split(", ").slice(1);
39
+ this.#database
40
+ .prepare(`
41
+ INSERT INTO ${tableName}(${trimmedColumnList})
42
+ SELECT ?, ${columns.join(", ")}
43
+ FROM ${attachName}.${tableName}
44
+ WHERE scan_id = ?
45
+ `)
46
+ .run(targetScanId, sourceScanId);
47
+ }
48
+ }
49
+ assertMatchingScanMetadata(sourceScan, targetScan, sourceDatabasePath) {
50
+ const scanFields = [
51
+ "scan_uuid",
52
+ "owner",
53
+ "package_name",
54
+ "package_metadata_json",
55
+ "github_actions_run_url",
56
+ "scan_started_at",
57
+ "scan_completed_at",
58
+ "status"
59
+ ];
60
+ for (const field of scanFields) {
61
+ if (sourceScan[field] !== targetScan[field]) {
62
+ throw new Error(`scan metadata mismatch for scan_uuid ${sourceScan.scan_uuid} while merging ${sourceDatabasePath}`);
63
+ }
64
+ }
65
+ }
66
+ quoteDatabasePath(value) {
67
+ return `'${value.replaceAll("'", "''")}'`;
68
+ }
69
+ }
@@ -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
+ }
@@ -3,7 +3,9 @@ import type { ManifestDescriptorRecord, ManifestEdgeRecord, ManifestRecord, Pack
3
3
  export declare class ScanWriter {
4
4
  #private;
5
5
  constructor(database: Database.Database);
6
- resetScan(owner: string, packageName: string, scanStartedAt: string): void;
6
+ startScan(owner: string, packageName: string, scanStartedAt: string, packageMetadata: {
7
+ rawJson: string;
8
+ }): void;
7
9
  markScanCompleted(scanCompletedAt: string): void;
8
10
  markScanFailed(scanCompletedAt: string): void;
9
11
  insertPackageVersion(version: PackageVersionRecord): void;
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { resolveGitHubActionsRunUrl } from "./_github-actions-run-url.js";
2
3
  import { rebuildManifestReachability } from "./_manifest-reachability.js";
3
4
  export class ScanWriter {
4
5
  #database;
@@ -6,13 +7,22 @@ export class ScanWriter {
6
7
  constructor(database) {
7
8
  this.#database = database;
8
9
  }
9
- resetScan(owner, packageName, scanStartedAt) {
10
+ startScan(owner, packageName, scanStartedAt, packageMetadata) {
10
11
  const result = this.#database
11
12
  .prepare(`
12
- INSERT INTO package_scans(scan_uuid, owner, package_name, scan_started_at, scan_completed_at, status)
13
- VALUES(?, ?, ?, ?, NULL, 'running')
13
+ INSERT INTO package_scans(
14
+ scan_uuid,
15
+ owner,
16
+ package_name,
17
+ package_metadata_json,
18
+ github_actions_run_url,
19
+ scan_started_at,
20
+ scan_completed_at,
21
+ status
22
+ )
23
+ VALUES(?, ?, ?, ?, ?, ?, NULL, 'running')
14
24
  `)
15
- .run(randomUUID(), owner, packageName, scanStartedAt);
25
+ .run(randomUUID(), owner, packageName, packageMetadata.rawJson, resolveGitHubActionsRunUrl(), scanStartedAt);
16
26
  this.#activeScanId = Number(result.lastInsertRowid);
17
27
  }
18
28
  markScanCompleted(scanCompletedAt) {
@@ -36,13 +46,12 @@ export class ScanWriter {
36
46
  insertPackageVersion(version) {
37
47
  this.#database
38
48
  .prepare(`
39
- INSERT OR REPLACE INTO package_versions(scan_id, version_id, digest, created_at, updated_at)
40
- VALUES(@scanId, @versionId, @digest, @createdAt, @updatedAt)
49
+ INSERT OR REPLACE INTO package_versions(scan_id, version_id, created_at, updated_at)
50
+ VALUES(@scanId, @versionId, @createdAt, @updatedAt)
41
51
  `)
42
52
  .run({
43
53
  scanId: this.#requireScanId(),
44
54
  versionId: version.versionId,
45
- digest: version.digest,
46
55
  createdAt: version.createdAt,
47
56
  updatedAt: version.updatedAt
48
57
  });
@@ -58,8 +67,8 @@ export class ScanWriter {
58
67
  insertTag(tag) {
59
68
  this.#database
60
69
  .prepare(`
61
- INSERT OR REPLACE INTO tags(scan_id, tag, digest, version_id)
62
- VALUES(@scanId, @tag, @digest, @versionId)
70
+ INSERT OR REPLACE INTO tags(scan_id, tag, version_id)
71
+ VALUES(@scanId, @tag, @versionId)
63
72
  `)
64
73
  .run({
65
74
  scanId: this.#requireScanId(),
@@ -71,6 +80,7 @@ export class ScanWriter {
71
80
  .prepare(`
72
81
  INSERT OR REPLACE INTO manifests(
73
82
  scan_id,
83
+ version_id,
74
84
  digest,
75
85
  media_type,
76
86
  artifact_type,
@@ -79,10 +89,12 @@ export class ScanWriter {
79
89
  annotations_json,
80
90
  platform_os,
81
91
  platform_architecture,
82
- platform_variant
92
+ platform_variant,
93
+ manifest_kind
83
94
  )
84
95
  VALUES(
85
96
  @scanId,
97
+ @versionId,
86
98
  @digest,
87
99
  @mediaType,
88
100
  @artifactType,
@@ -91,11 +103,13 @@ export class ScanWriter {
91
103
  @annotationsJson,
92
104
  @platformOs,
93
105
  @platformArchitecture,
94
- @platformVariant
106
+ @platformVariant,
107
+ @manifestKind
95
108
  )
96
109
  `)
97
110
  .run({
98
111
  scanId: this.#requireScanId(),
112
+ versionId: manifest.versionId,
99
113
  digest: manifest.digest,
100
114
  mediaType: manifest.mediaType,
101
115
  artifactType: manifest.artifactType ?? null,
@@ -104,7 +118,8 @@ export class ScanWriter {
104
118
  annotationsJson: manifest.annotations ? JSON.stringify(manifest.annotations) : null,
105
119
  platformOs: manifest.platform?.os ?? null,
106
120
  platformArchitecture: manifest.platform?.architecture ?? null,
107
- platformVariant: manifest.platform?.variant ?? null
121
+ platformVariant: manifest.platform?.variant ?? null,
122
+ manifestKind: manifest.manifestKind ?? null
108
123
  });
109
124
  }
110
125
  insertManifestPayload(digest, rawJson) {
@@ -169,7 +184,7 @@ export class ScanWriter {
169
184
  }
170
185
  #requireScanId() {
171
186
  if (this.#activeScanId === null) {
172
- throw new Error("package not initialized; call resetScan(owner, packageName, scanStartedAt) first");
187
+ throw new Error("package not initialized; call startScan(owner, packageName, scanStartedAt, packageMetadata) first");
173
188
  }
174
189
  return this.#activeScanId;
175
190
  }