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