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.
- package/CHANGELOG.md +50 -1
- package/README.md +166 -57
- package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +6 -0
- package/dist/cleanup-summary/_cleanup-summary-markdown.js +113 -0
- package/dist/cleanup-summary/_cleanup-summary.d.ts +40 -0
- package/dist/cleanup-summary/_cleanup-summary.js +40 -0
- package/dist/cleanup-summary/index.d.ts +2 -0
- package/dist/cleanup-summary/index.js +2 -0
- package/dist/cli/_args.d.ts +1 -0
- package/dist/cli/_args.js +3 -0
- package/dist/cli/_cleanup-command.d.ts +1 -0
- package/dist/cli/_cleanup-command.js +63 -0
- package/dist/cli/_db-merge-command.d.ts +1 -0
- package/dist/cli/_db-merge-command.js +41 -0
- package/dist/cli/_github-output.d.ts +10 -0
- package/dist/cli/_github-output.js +13 -0
- package/dist/cli/_logger.d.ts +2 -1
- package/dist/cli/_logger.js +2 -0
- package/dist/cli/_older-than.d.ts +5 -0
- package/dist/cli/_older-than.js +42 -0
- package/dist/cli/_planner-options.d.ts +20 -0
- package/dist/cli/_planner-options.js +101 -0
- package/dist/cli/_scan-command.js +11 -4
- package/dist/cli/_tag-selector-resolver.d.ts +3 -0
- package/dist/cli/_tag-selector-resolver.js +109 -0
- package/dist/cli/_untag-command.d.ts +1 -0
- package/dist/cli/_untag-command.js +57 -0
- package/dist/cli/index.js +19 -1
- package/dist/config/_service-constants.d.ts +3 -0
- package/dist/config/_service-constants.js +3 -0
- package/dist/{tuning → config}/index.d.ts +3 -0
- package/dist/{tuning → config}/index.js +3 -0
- package/dist/core/_github-package-owner.d.ts +11 -0
- package/dist/core/_github-package-owner.js +45 -0
- package/dist/core/_http-error.d.ts +6 -0
- package/dist/core/_http-error.js +33 -0
- package/dist/core/_types.d.ts +3 -2
- package/dist/core/index.d.ts +4 -1
- package/dist/core/index.js +2 -1
- package/dist/db/_cleanup-run-writer.d.ts +10 -0
- package/dist/db/_cleanup-run-writer.js +73 -0
- package/dist/db/_db-merge-cleanup-copy.d.ts +7 -0
- package/dist/db/_db-merge-cleanup-copy.js +122 -0
- package/dist/db/_db-merge-history.d.ts +2 -0
- package/dist/db/_db-merge-history.js +15 -0
- package/dist/db/_db-merge-repository.d.ts +8 -0
- package/dist/db/_db-merge-repository.js +95 -0
- package/dist/db/_db-merge-scan-copy.d.ts +10 -0
- package/dist/db/_db-merge-scan-copy.js +69 -0
- package/dist/db/_db-merge-types.d.ts +44 -0
- package/dist/db/_db-merge-types.js +1 -0
- package/dist/db/_github-actions-run-url.d.ts +1 -0
- package/dist/db/_github-actions-run-url.js +9 -0
- package/dist/db/_scan-writer.d.ts +3 -1
- package/dist/db/_scan-writer.js +28 -13
- package/dist/db/_snapshot-repository.d.ts +9 -9
- package/dist/db/_snapshot-repository.js +37 -49
- package/dist/db/_sql-placeholders.d.ts +2 -0
- package/dist/db/_sql-placeholders.js +16 -0
- package/dist/db/index.d.ts +5 -0
- package/dist/db/index.js +3 -0
- package/dist/db/planner/_planner-delete-tag-root-targets.d.ts +7 -0
- package/dist/db/planner/_planner-delete-tag-root-targets.js +130 -0
- package/dist/db/planner/_planner-direct-target-tags.d.ts +6 -0
- package/dist/db/planner/_planner-direct-target-tags.js +47 -0
- package/dist/db/planner/_planner-keep-tagged-root-targets.d.ts +7 -0
- package/dist/db/planner/_planner-keep-tagged-root-targets.js +74 -0
- package/dist/db/planner/_planner-output.d.ts +5 -0
- package/dist/db/planner/_planner-output.js +101 -0
- package/dist/db/planner/_planner-plan-artifacts.d.ts +7 -0
- package/dist/db/planner/_planner-plan-artifacts.js +211 -0
- package/dist/db/planner/_planner-repository.d.ts +34 -0
- package/dist/db/planner/_planner-repository.js +126 -0
- package/dist/db/planner/_planner-sql.d.ts +12 -0
- package/dist/db/planner/_planner-sql.js +35 -0
- package/dist/db/planner/_planner-tag-selectors.d.ts +8 -0
- package/dist/db/planner/_planner-tag-selectors.js +57 -0
- package/dist/db/planner/_planner-tagged-root-targets.d.ts +15 -0
- package/dist/db/planner/_planner-tagged-root-targets.js +19 -0
- package/dist/db/planner/_planner-tagged-targets.d.ts +8 -0
- package/dist/db/planner/_planner-tagged-targets.js +16 -0
- package/dist/db/planner/_planner-types.d.ts +135 -0
- package/dist/db/planner/_planner-types.js +38 -0
- package/dist/db/planner/_planner-untagged-targets.d.ts +9 -0
- package/dist/db/planner/_planner-untagged-targets.js +91 -0
- package/dist/db/planner/index.d.ts +2 -0
- package/dist/db/planner/index.js +1 -0
- package/dist/execute/_http.d.ts +7 -0
- package/dist/execute/_http.js +48 -0
- package/dist/execute/_manifest-detach.d.ts +4 -0
- package/dist/execute/_manifest-detach.js +31 -0
- package/dist/execute/_package-version-delete-client.d.ts +4 -0
- package/dist/execute/_package-version-delete-client.js +34 -0
- package/dist/execute/_package-version-page-client.d.ts +14 -0
- package/dist/execute/_package-version-page-client.js +64 -0
- package/dist/execute/_package-version-tag-source-client.d.ts +12 -0
- package/dist/execute/_package-version-tag-source-client.js +65 -0
- package/dist/execute/_plan-executor.d.ts +3 -0
- package/dist/execute/_plan-executor.js +47 -0
- package/dist/execute/_registry-manifest-client.d.ts +12 -0
- package/dist/execute/_registry-manifest-client.js +79 -0
- package/dist/execute/_registry-token-client.d.ts +4 -0
- package/dist/execute/_registry-token-client.js +37 -0
- package/dist/execute/_types.d.ts +51 -0
- package/dist/execute/_types.js +1 -0
- package/dist/execute/_untag-client.d.ts +2 -0
- package/dist/execute/_untag-client.js +71 -0
- package/dist/execute/index.d.ts +5 -0
- package/dist/execute/index.js +3 -0
- package/dist/ingest/github/_manifest-client.d.ts +7 -1
- package/dist/ingest/github/_manifest-client.js +8 -0
- package/dist/ingest/github/_manifest-ingest.js +39 -53
- package/dist/ingest/github/_manifest-kind.d.ts +20 -0
- package/dist/ingest/github/_manifest-kind.js +50 -0
- package/dist/ingest/github/_package-metadata-load.d.ts +5 -0
- package/dist/ingest/github/_package-metadata-load.js +45 -0
- package/dist/ingest/github/_package-version-page-load.d.ts +1 -1
- package/dist/ingest/github/_package-version-page-load.js +8 -5
- package/dist/ingest/github/_packages-client.d.ts +1 -1
- package/dist/ingest/github/_packages-client.js +21 -4
- package/dist/ingest/github/_parallel-paginated-ingest.d.ts +1 -0
- package/dist/ingest/github/_parallel-paginated-ingest.js +2 -2
- package/dist/ingest/github/_shared.d.ts +1 -1
- package/dist/ingest/github/_shared.js +2 -34
- package/dist/ingest/github/index.d.ts +4 -0
- package/dist/ingest/github/index.js +8 -5
- package/package.json +7 -5
- package/resources/sql/schema/001_schema.sql +82 -8
- package/resources/sql/views/001_v_latest_scan_per_package.sql +2 -2
- package/resources/sql/views/003_v_scan_root_manifests.sql +43 -0
- package/resources/sql/views/004_v_digest_derived_tag_relations.sql +51 -0
- package/resources/sql/views/005_v_cleanup_root_closure_members.sql +100 -0
- package/resources/sql/views/006_v_cleanup_blocking_overlaps.sql +42 -0
- package/dist/ingest/github/_paginated-ingest.d.ts +0 -11
- package/dist/ingest/github/_paginated-ingest.js +0 -28
- package/resources/sql/views/003_v_missing_digests_related_manifests.sql +0 -78
- package/resources/sql/views/004_v_manifests_related_manifests.sql +0 -142
package/dist/core/_types.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/core/index.js
CHANGED
|
@@ -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,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
|
-
|
|
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;
|
package/dist/db/_scan-writer.js
CHANGED
|
@@ -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
|
-
|
|
10
|
+
startScan(owner, packageName, scanStartedAt, packageMetadata) {
|
|
10
11
|
const result = this.#database
|
|
11
12
|
.prepare(`
|
|
12
|
-
INSERT INTO package_scans(
|
|
13
|
-
|
|
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,
|
|
40
|
-
VALUES(@scanId, @versionId, @
|
|
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,
|
|
62
|
-
VALUES(@scanId, @tag, @
|
|
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
|
|
187
|
+
throw new Error("package not initialized; call startScan(owner, packageName, scanStartedAt, packageMetadata) first");
|
|
173
188
|
}
|
|
174
189
|
return this.#activeScanId;
|
|
175
190
|
}
|