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,158 @@
|
|
|
1
|
+
export function rebuildManifestReachability(database, scanId) {
|
|
2
|
+
_refreshDigestTagEdges(database, scanId);
|
|
3
|
+
const manifestDigests = _loadManifestDigests(database, scanId);
|
|
4
|
+
const childDigestsByParent = new Map();
|
|
5
|
+
const parentDigestsByChild = new Map();
|
|
6
|
+
const neighborDigestsByDigest = new Map();
|
|
7
|
+
for (const digest of manifestDigests) {
|
|
8
|
+
childDigestsByParent.set(digest, new Set());
|
|
9
|
+
parentDigestsByChild.set(digest, new Set());
|
|
10
|
+
neighborDigestsByDigest.set(digest, new Set());
|
|
11
|
+
}
|
|
12
|
+
for (const manifestEdge of _loadManifestEdges(database, scanId)) {
|
|
13
|
+
childDigestsByParent.get(manifestEdge.parent_digest)?.add(manifestEdge.child_digest);
|
|
14
|
+
parentDigestsByChild.get(manifestEdge.child_digest)?.add(manifestEdge.parent_digest);
|
|
15
|
+
neighborDigestsByDigest.get(manifestEdge.parent_digest)?.add(manifestEdge.child_digest);
|
|
16
|
+
neighborDigestsByDigest.get(manifestEdge.child_digest)?.add(manifestEdge.parent_digest);
|
|
17
|
+
}
|
|
18
|
+
const graphIdsByDigest = _buildGraphIdsByDigest(manifestDigests, neighborDigestsByDigest);
|
|
19
|
+
const remainingChildrenCount = new Map();
|
|
20
|
+
const descendantDistancesByDigest = new Map();
|
|
21
|
+
const readyDigests = [];
|
|
22
|
+
for (const digest of manifestDigests) {
|
|
23
|
+
const childCount = childDigestsByParent.get(digest)?.size ?? 0;
|
|
24
|
+
remainingChildrenCount.set(digest, childCount);
|
|
25
|
+
if (childCount === 0) {
|
|
26
|
+
readyDigests.push(digest);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
while (readyDigests.length > 0) {
|
|
30
|
+
const digest = readyDigests.shift();
|
|
31
|
+
if (!digest) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const distances = new Map([[digest, 0]]);
|
|
35
|
+
for (const childDigest of childDigestsByParent.get(digest) ?? []) {
|
|
36
|
+
_setMinDistance(distances, childDigest, 1);
|
|
37
|
+
const childDistances = descendantDistancesByDigest.get(childDigest);
|
|
38
|
+
if (!childDistances) {
|
|
39
|
+
throw new Error(`manifest reachability build missing child results for ${childDigest}`);
|
|
40
|
+
}
|
|
41
|
+
for (const [descendantDigest, childDistance] of childDistances) {
|
|
42
|
+
if (descendantDigest === childDigest) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
_setMinDistance(distances, descendantDigest, childDistance + 1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
descendantDistancesByDigest.set(digest, distances);
|
|
49
|
+
for (const parentDigest of parentDigestsByChild.get(digest) ?? []) {
|
|
50
|
+
const nextCount = (remainingChildrenCount.get(parentDigest) ?? 0) - 1;
|
|
51
|
+
remainingChildrenCount.set(parentDigest, nextCount);
|
|
52
|
+
if (nextCount === 0) {
|
|
53
|
+
readyDigests.push(parentDigest);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (descendantDistancesByDigest.size !== manifestDigests.length) {
|
|
58
|
+
throw new Error("manifest reachability build detected a cycle in manifest_edges");
|
|
59
|
+
}
|
|
60
|
+
const insertRow = database.prepare(`
|
|
61
|
+
INSERT OR REPLACE INTO manifest_reachability(
|
|
62
|
+
scan_id,
|
|
63
|
+
ancestor_digest,
|
|
64
|
+
descendant_digest,
|
|
65
|
+
min_distance
|
|
66
|
+
)
|
|
67
|
+
VALUES(?, ?, ?, ?)
|
|
68
|
+
`);
|
|
69
|
+
const insertGraphRow = database.prepare(`
|
|
70
|
+
INSERT OR REPLACE INTO manifest_graphs(
|
|
71
|
+
scan_id,
|
|
72
|
+
digest,
|
|
73
|
+
graph_id
|
|
74
|
+
)
|
|
75
|
+
VALUES(?, ?, ?)
|
|
76
|
+
`);
|
|
77
|
+
const rebuild = database.transaction(() => {
|
|
78
|
+
database.prepare("DELETE FROM manifest_reachability WHERE scan_id = ?").run(scanId);
|
|
79
|
+
database.prepare("DELETE FROM manifest_graphs WHERE scan_id = ?").run(scanId);
|
|
80
|
+
for (const digest of manifestDigests) {
|
|
81
|
+
insertGraphRow.run(scanId, digest, graphIdsByDigest.get(digest));
|
|
82
|
+
for (const [descendantDigest, distance] of descendantDistancesByDigest.get(digest) ?? []) {
|
|
83
|
+
insertRow.run(scanId, digest, descendantDigest, distance);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
rebuild();
|
|
88
|
+
}
|
|
89
|
+
function _buildGraphIdsByDigest(manifestDigests, neighborDigestsByDigest) {
|
|
90
|
+
const graphIdsByDigest = new Map();
|
|
91
|
+
let nextGraphId = 1;
|
|
92
|
+
for (const rootDigest of manifestDigests) {
|
|
93
|
+
if (graphIdsByDigest.has(rootDigest)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const pendingDigests = [rootDigest];
|
|
97
|
+
graphIdsByDigest.set(rootDigest, nextGraphId);
|
|
98
|
+
while (pendingDigests.length > 0) {
|
|
99
|
+
const digest = pendingDigests.pop();
|
|
100
|
+
if (!digest) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
for (const neighborDigest of neighborDigestsByDigest.get(digest) ?? []) {
|
|
104
|
+
if (graphIdsByDigest.has(neighborDigest)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
graphIdsByDigest.set(neighborDigest, nextGraphId);
|
|
108
|
+
pendingDigests.push(neighborDigest);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
nextGraphId += 1;
|
|
112
|
+
}
|
|
113
|
+
return graphIdsByDigest;
|
|
114
|
+
}
|
|
115
|
+
function _refreshDigestTagEdges(database, scanId) {
|
|
116
|
+
database.prepare("DELETE FROM manifest_edges WHERE scan_id = ? AND edge_kind = 'digest-tag-referrer'").run(scanId);
|
|
117
|
+
database
|
|
118
|
+
.prepare(`
|
|
119
|
+
INSERT OR IGNORE INTO manifest_edges(scan_id, parent_digest, child_digest, edge_kind)
|
|
120
|
+
SELECT
|
|
121
|
+
t.scan_id,
|
|
122
|
+
m.digest AS parent_digest,
|
|
123
|
+
'sha256:' || SUBSTR(t.tag, 8, 64) AS child_digest,
|
|
124
|
+
'digest-tag-referrer' AS edge_kind
|
|
125
|
+
FROM tags t
|
|
126
|
+
JOIN manifests m
|
|
127
|
+
ON m.scan_id = t.scan_id
|
|
128
|
+
AND m.version_id = t.version_id
|
|
129
|
+
JOIN manifests child_manifest
|
|
130
|
+
ON child_manifest.scan_id = t.scan_id
|
|
131
|
+
AND child_manifest.digest = 'sha256:' || SUBSTR(t.tag, 8, 64)
|
|
132
|
+
WHERE t.scan_id = ?
|
|
133
|
+
AND t.is_digest_tag = 1
|
|
134
|
+
`)
|
|
135
|
+
.run(scanId);
|
|
136
|
+
}
|
|
137
|
+
function _loadManifestDigests(database, scanId) {
|
|
138
|
+
const rows = database
|
|
139
|
+
.prepare("SELECT digest FROM manifests WHERE scan_id = ? ORDER BY digest")
|
|
140
|
+
.all(scanId);
|
|
141
|
+
return rows.map((row) => row.digest);
|
|
142
|
+
}
|
|
143
|
+
function _loadManifestEdges(database, scanId) {
|
|
144
|
+
return database
|
|
145
|
+
.prepare(`
|
|
146
|
+
SELECT DISTINCT parent_digest, child_digest
|
|
147
|
+
FROM manifest_edges
|
|
148
|
+
WHERE scan_id = ?
|
|
149
|
+
ORDER BY parent_digest, child_digest
|
|
150
|
+
`)
|
|
151
|
+
.all(scanId);
|
|
152
|
+
}
|
|
153
|
+
function _setMinDistance(distances, digest, distance) {
|
|
154
|
+
const currentDistance = distances.get(digest);
|
|
155
|
+
if (currentDistance === undefined || distance < currentDistance) {
|
|
156
|
+
distances.set(digest, distance);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
import type { ManifestDescriptorRecord, ManifestEdgeRecord, ManifestRecord, PackageVersionRecord, TagRecord } from "../core/index.js";
|
|
3
|
+
export declare class ScanWriter {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(database: Database.Database);
|
|
6
|
+
startScan(owner: string, packageName: string, scanStartedAt: string, packageMetadata: {
|
|
7
|
+
rawJson: string;
|
|
8
|
+
}): void;
|
|
9
|
+
markScanCompleted(scanCompletedAt: string): void;
|
|
10
|
+
markScanFailed(scanCompletedAt: string): void;
|
|
11
|
+
insertPackageVersion(version: PackageVersionRecord): void;
|
|
12
|
+
insertPackageVersionPayload(versionId: number, rawJson: string): void;
|
|
13
|
+
insertTag(tag: TagRecord): void;
|
|
14
|
+
insertManifest(manifest: ManifestRecord): void;
|
|
15
|
+
insertManifestPayload(digest: string, rawJson: string): void;
|
|
16
|
+
insertManifestDescriptor(descriptor: ManifestDescriptorRecord): void;
|
|
17
|
+
insertManifestEdge(edge: ManifestEdgeRecord): void;
|
|
18
|
+
rebuildManifestReachability(): void;
|
|
19
|
+
getActiveScanId(): number;
|
|
20
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isDigestTag } from "../core/index.js";
|
|
3
|
+
import { resolveGitHubActionsRunUrl } from "./_github-actions-run-url.js";
|
|
4
|
+
import { rebuildManifestReachability } from "./_manifest-reachability.js";
|
|
5
|
+
export class ScanWriter {
|
|
6
|
+
#database;
|
|
7
|
+
#startScanStatement;
|
|
8
|
+
#markScanCompletedStatement;
|
|
9
|
+
#markScanFailedStatement;
|
|
10
|
+
#insertPackageVersionStatement;
|
|
11
|
+
#insertPackageVersionPayloadStatement;
|
|
12
|
+
#insertTagStatement;
|
|
13
|
+
#insertManifestStatement;
|
|
14
|
+
#insertManifestPayloadStatement;
|
|
15
|
+
#insertManifestDescriptorStatement;
|
|
16
|
+
#insertManifestEdgeStatement;
|
|
17
|
+
#activeScanId = null;
|
|
18
|
+
constructor(database) {
|
|
19
|
+
this.#database = database;
|
|
20
|
+
this.#startScanStatement = this.#database.prepare(`
|
|
21
|
+
INSERT INTO package_scans(
|
|
22
|
+
scan_uuid,
|
|
23
|
+
owner,
|
|
24
|
+
package_name,
|
|
25
|
+
package_metadata_json,
|
|
26
|
+
github_actions_run_url,
|
|
27
|
+
scan_started_at,
|
|
28
|
+
scan_completed_at,
|
|
29
|
+
status
|
|
30
|
+
)
|
|
31
|
+
VALUES(?, ?, ?, ?, ?, ?, NULL, 'running')
|
|
32
|
+
`);
|
|
33
|
+
this.#markScanCompletedStatement = this.#database.prepare(`
|
|
34
|
+
UPDATE package_scans
|
|
35
|
+
SET scan_completed_at = ?, status = 'completed'
|
|
36
|
+
WHERE scan_id = ?
|
|
37
|
+
`);
|
|
38
|
+
this.#markScanFailedStatement = this.#database.prepare(`
|
|
39
|
+
UPDATE package_scans
|
|
40
|
+
SET scan_completed_at = ?, status = 'failed'
|
|
41
|
+
WHERE scan_id = ?
|
|
42
|
+
`);
|
|
43
|
+
this.#insertPackageVersionStatement = this.#database.prepare(`
|
|
44
|
+
INSERT OR REPLACE INTO package_versions(scan_id, version_id, created_at, updated_at)
|
|
45
|
+
VALUES(@scanId, @versionId, @createdAt, @updatedAt)
|
|
46
|
+
`);
|
|
47
|
+
this.#insertPackageVersionPayloadStatement = this.#database.prepare(`
|
|
48
|
+
INSERT OR REPLACE INTO package_version_payloads(scan_id, version_id, raw_json)
|
|
49
|
+
VALUES(?, ?, ?)
|
|
50
|
+
`);
|
|
51
|
+
this.#insertTagStatement = this.#database.prepare(`
|
|
52
|
+
INSERT OR REPLACE INTO tags(scan_id, tag, version_id, is_digest_tag)
|
|
53
|
+
VALUES(@scanId, @tag, @versionId, @isDigestTag)
|
|
54
|
+
`);
|
|
55
|
+
this.#insertManifestStatement = this.#database.prepare(`
|
|
56
|
+
INSERT OR REPLACE INTO manifests(
|
|
57
|
+
scan_id,
|
|
58
|
+
version_id,
|
|
59
|
+
digest,
|
|
60
|
+
media_type,
|
|
61
|
+
artifact_type,
|
|
62
|
+
config_media_type,
|
|
63
|
+
subject_digest,
|
|
64
|
+
annotations_json,
|
|
65
|
+
manifest_kind
|
|
66
|
+
)
|
|
67
|
+
VALUES(
|
|
68
|
+
@scanId,
|
|
69
|
+
@versionId,
|
|
70
|
+
@digest,
|
|
71
|
+
@mediaType,
|
|
72
|
+
@artifactType,
|
|
73
|
+
@configMediaType,
|
|
74
|
+
@subjectDigest,
|
|
75
|
+
@annotationsJson,
|
|
76
|
+
@manifestKind
|
|
77
|
+
)
|
|
78
|
+
`);
|
|
79
|
+
this.#insertManifestPayloadStatement = this.#database.prepare(`
|
|
80
|
+
INSERT OR REPLACE INTO manifest_payloads(scan_id, digest, raw_json)
|
|
81
|
+
VALUES(?, ?, ?)
|
|
82
|
+
`);
|
|
83
|
+
this.#insertManifestDescriptorStatement = this.#database.prepare(`
|
|
84
|
+
INSERT OR REPLACE INTO manifest_descriptors(
|
|
85
|
+
scan_id,
|
|
86
|
+
parent_digest,
|
|
87
|
+
child_digest,
|
|
88
|
+
media_type,
|
|
89
|
+
artifact_type,
|
|
90
|
+
platform_os,
|
|
91
|
+
platform_architecture,
|
|
92
|
+
platform_variant
|
|
93
|
+
)
|
|
94
|
+
VALUES(
|
|
95
|
+
@scanId,
|
|
96
|
+
@parentDigest,
|
|
97
|
+
@childDigest,
|
|
98
|
+
@mediaType,
|
|
99
|
+
@artifactType,
|
|
100
|
+
@platformOs,
|
|
101
|
+
@platformArchitecture,
|
|
102
|
+
@platformVariant
|
|
103
|
+
)
|
|
104
|
+
`);
|
|
105
|
+
this.#insertManifestEdgeStatement = this.#database.prepare(`
|
|
106
|
+
INSERT OR IGNORE INTO manifest_edges(scan_id, parent_digest, child_digest, edge_kind)
|
|
107
|
+
VALUES(@scanId, @parentDigest, @childDigest, @edgeKind)
|
|
108
|
+
`);
|
|
109
|
+
}
|
|
110
|
+
startScan(owner, packageName, scanStartedAt, packageMetadata) {
|
|
111
|
+
const result = this.#startScanStatement.run(randomUUID(), owner, packageName, packageMetadata.rawJson, resolveGitHubActionsRunUrl(), scanStartedAt);
|
|
112
|
+
this.#activeScanId = Number(result.lastInsertRowid);
|
|
113
|
+
}
|
|
114
|
+
markScanCompleted(scanCompletedAt) {
|
|
115
|
+
this.#markScanCompletedStatement.run(scanCompletedAt, this.#requireScanId());
|
|
116
|
+
}
|
|
117
|
+
markScanFailed(scanCompletedAt) {
|
|
118
|
+
this.#markScanFailedStatement.run(scanCompletedAt, this.#requireScanId());
|
|
119
|
+
}
|
|
120
|
+
insertPackageVersion(version) {
|
|
121
|
+
this.#insertPackageVersionStatement.run({
|
|
122
|
+
scanId: this.#requireScanId(),
|
|
123
|
+
versionId: version.versionId,
|
|
124
|
+
createdAt: version.createdAt,
|
|
125
|
+
updatedAt: version.updatedAt
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
insertPackageVersionPayload(versionId, rawJson) {
|
|
129
|
+
this.#insertPackageVersionPayloadStatement.run(this.#requireScanId(), versionId, rawJson);
|
|
130
|
+
}
|
|
131
|
+
insertTag(tag) {
|
|
132
|
+
this.#insertTagStatement.run({
|
|
133
|
+
scanId: this.#requireScanId(),
|
|
134
|
+
...tag,
|
|
135
|
+
isDigestTag: isDigestTag(tag.tag) ? 1 : 0
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
insertManifest(manifest) {
|
|
139
|
+
this.#insertManifestStatement.run({
|
|
140
|
+
scanId: this.#requireScanId(),
|
|
141
|
+
versionId: manifest.versionId,
|
|
142
|
+
digest: manifest.digest,
|
|
143
|
+
mediaType: manifest.mediaType,
|
|
144
|
+
artifactType: manifest.artifactType ?? null,
|
|
145
|
+
configMediaType: manifest.configMediaType ?? null,
|
|
146
|
+
subjectDigest: manifest.subjectDigest ?? null,
|
|
147
|
+
annotationsJson: manifest.annotations ? JSON.stringify(manifest.annotations) : null,
|
|
148
|
+
manifestKind: manifest.manifestKind ?? null
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
insertManifestPayload(digest, rawJson) {
|
|
152
|
+
this.#insertManifestPayloadStatement.run(this.#requireScanId(), digest, rawJson);
|
|
153
|
+
}
|
|
154
|
+
insertManifestDescriptor(descriptor) {
|
|
155
|
+
this.#insertManifestDescriptorStatement.run({
|
|
156
|
+
scanId: this.#requireScanId(),
|
|
157
|
+
parentDigest: descriptor.parentDigest,
|
|
158
|
+
childDigest: descriptor.childDigest,
|
|
159
|
+
mediaType: descriptor.mediaType,
|
|
160
|
+
artifactType: descriptor.artifactType ?? null,
|
|
161
|
+
platformOs: descriptor.platform?.os ?? null,
|
|
162
|
+
platformArchitecture: descriptor.platform?.architecture ?? null,
|
|
163
|
+
platformVariant: descriptor.platform?.variant ?? null
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
insertManifestEdge(edge) {
|
|
167
|
+
this.#insertManifestEdgeStatement.run({
|
|
168
|
+
scanId: this.#requireScanId(),
|
|
169
|
+
...edge
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
rebuildManifestReachability() {
|
|
173
|
+
rebuildManifestReachability(this.#database, this.#requireScanId());
|
|
174
|
+
}
|
|
175
|
+
getActiveScanId() {
|
|
176
|
+
return this.#requireScanId();
|
|
177
|
+
}
|
|
178
|
+
#requireScanId() {
|
|
179
|
+
if (this.#activeScanId === null) {
|
|
180
|
+
throw new Error("package not initialized; call startScan(owner, packageName, scanStartedAt, packageMetadata) first");
|
|
181
|
+
}
|
|
182
|
+
return this.#activeScanId;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function initializeSchema(database) {
|
|
4
|
+
_initializeSqlDirectory(database, "schema");
|
|
5
|
+
_initializeSqlDirectory(database, "views");
|
|
6
|
+
}
|
|
7
|
+
function _initializeSqlDirectory(database, directoryName) {
|
|
8
|
+
const sqlDirectory = join(process.cwd(), "resources", "sql", directoryName);
|
|
9
|
+
if (!existsSync(sqlDirectory)) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const sqlFiles = readdirSync(sqlDirectory)
|
|
13
|
+
.filter((file) => file.endsWith(".sql"))
|
|
14
|
+
.sort();
|
|
15
|
+
for (const sqlFile of sqlFiles) {
|
|
16
|
+
const sql = readFileSync(join(sqlDirectory, sqlFile), "utf8");
|
|
17
|
+
database.exec(sql);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
export declare class SnapshotRepository {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(database: Database.Database);
|
|
5
|
+
getPackageMetadata(scanId: number): {
|
|
6
|
+
owner: string;
|
|
7
|
+
packageName: string;
|
|
8
|
+
scanCompletedAt: string;
|
|
9
|
+
};
|
|
10
|
+
countPackageVersions(scanId: number): number;
|
|
11
|
+
countTaggedVersions(scanId: number): number;
|
|
12
|
+
countTags(scanId: number): number;
|
|
13
|
+
countManifests(scanId: number): number;
|
|
14
|
+
countManifestEdges(scanId: number): number;
|
|
15
|
+
listManifestDigests(scanId: number): string[];
|
|
16
|
+
listManifestPayloads(scanId: number): Array<{
|
|
17
|
+
digest: string;
|
|
18
|
+
rawJson: string;
|
|
19
|
+
}>;
|
|
20
|
+
listPackageVersionManifestRefs(scanId: number): Array<{
|
|
21
|
+
versionId: number;
|
|
22
|
+
digest: string;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export class SnapshotRepository {
|
|
2
|
+
#database;
|
|
3
|
+
constructor(database) {
|
|
4
|
+
this.#database = database;
|
|
5
|
+
}
|
|
6
|
+
getPackageMetadata(scanId) {
|
|
7
|
+
const row = this.#database
|
|
8
|
+
.prepare(`
|
|
9
|
+
SELECT owner, package_name, scan_completed_at
|
|
10
|
+
FROM package_scans
|
|
11
|
+
WHERE scan_id = ?
|
|
12
|
+
`)
|
|
13
|
+
.get(scanId);
|
|
14
|
+
if (!row) {
|
|
15
|
+
throw new Error(`database does not contain package scan for scan_id=${scanId}`);
|
|
16
|
+
}
|
|
17
|
+
if (!row.scan_completed_at) {
|
|
18
|
+
throw new Error(`scan ${scanId} has not completed`);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
owner: row.owner,
|
|
22
|
+
packageName: row.package_name,
|
|
23
|
+
scanCompletedAt: row.scan_completed_at
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
countPackageVersions(scanId) {
|
|
27
|
+
return _count(this.#database, "SELECT COUNT(*) AS total FROM package_versions WHERE scan_id = ?", "total", scanId);
|
|
28
|
+
}
|
|
29
|
+
countTaggedVersions(scanId) {
|
|
30
|
+
return _count(this.#database, "SELECT COUNT(DISTINCT version_id) AS total FROM tags WHERE scan_id = ?", "total", scanId);
|
|
31
|
+
}
|
|
32
|
+
countTags(scanId) {
|
|
33
|
+
return _count(this.#database, "SELECT COUNT(*) AS total FROM tags WHERE scan_id = ?", "total", scanId);
|
|
34
|
+
}
|
|
35
|
+
countManifests(scanId) {
|
|
36
|
+
return _count(this.#database, "SELECT COUNT(*) AS total FROM manifests WHERE scan_id = ?", "total", scanId);
|
|
37
|
+
}
|
|
38
|
+
countManifestEdges(scanId) {
|
|
39
|
+
return _count(this.#database, "SELECT COUNT(*) AS total FROM manifest_edges WHERE scan_id = ?", "total", scanId);
|
|
40
|
+
}
|
|
41
|
+
listManifestDigests(scanId) {
|
|
42
|
+
const rows = this.#database
|
|
43
|
+
.prepare("SELECT digest FROM manifests WHERE scan_id = ? ORDER BY digest")
|
|
44
|
+
.all(scanId);
|
|
45
|
+
return rows.map((row) => row.digest);
|
|
46
|
+
}
|
|
47
|
+
listManifestPayloads(scanId) {
|
|
48
|
+
const rows = this.#database
|
|
49
|
+
.prepare(`
|
|
50
|
+
SELECT digest, raw_json
|
|
51
|
+
FROM manifest_payloads
|
|
52
|
+
WHERE scan_id = ?
|
|
53
|
+
ORDER BY digest
|
|
54
|
+
`)
|
|
55
|
+
.all(scanId);
|
|
56
|
+
return rows.map((row) => ({
|
|
57
|
+
digest: row.digest,
|
|
58
|
+
rawJson: row.raw_json
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
listPackageVersionManifestRefs(scanId) {
|
|
62
|
+
const rows = this.#database
|
|
63
|
+
.prepare(`
|
|
64
|
+
SELECT version_id, raw_json
|
|
65
|
+
FROM package_version_payloads
|
|
66
|
+
WHERE scan_id = ?
|
|
67
|
+
ORDER BY version_id
|
|
68
|
+
`)
|
|
69
|
+
.all(scanId);
|
|
70
|
+
return rows.map((row) => ({
|
|
71
|
+
versionId: row.version_id,
|
|
72
|
+
digest: _parsePackageVersionDigest(row.version_id, row.raw_json)
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function _count(database, sql, field, ...params) {
|
|
77
|
+
const row = database.prepare(sql).get(...params);
|
|
78
|
+
return row[field];
|
|
79
|
+
}
|
|
80
|
+
function _parsePackageVersionDigest(versionId, rawJson) {
|
|
81
|
+
const payload = JSON.parse(rawJson);
|
|
82
|
+
if (typeof payload.name !== "string" || payload.name.length === 0) {
|
|
83
|
+
throw new Error(`package version payload for version_id=${versionId} did not include digest name`);
|
|
84
|
+
}
|
|
85
|
+
return payload.name;
|
|
86
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function buildInClausePlaceholders(valueCount) {
|
|
2
|
+
if (valueCount <= 0) {
|
|
3
|
+
throw new Error("valueCount must be greater than 0");
|
|
4
|
+
}
|
|
5
|
+
return Array.from({ length: valueCount }, () => "?").join(", ");
|
|
6
|
+
}
|
|
7
|
+
export function buildTuplePlaceholders(rowCount, columnCount) {
|
|
8
|
+
if (rowCount <= 0) {
|
|
9
|
+
throw new Error("rowCount must be greater than 0");
|
|
10
|
+
}
|
|
11
|
+
if (columnCount <= 0) {
|
|
12
|
+
throw new Error("columnCount must be greater than 0");
|
|
13
|
+
}
|
|
14
|
+
const tuple = `(${Array.from({ length: columnCount }, () => "?").join(", ")})`;
|
|
15
|
+
return Array.from({ length: rowCount }, () => tuple).join(", ");
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
export { ScanWriter } from "./_scan-writer.js";
|
|
3
|
+
export { CleanupRunWriter } from "./_cleanup-run-writer.js";
|
|
4
|
+
export { DbMergeRepository } from "./_db-merge-repository.js";
|
|
5
|
+
export { PlannerRepository } from "./planner/index.js";
|
|
6
|
+
export { SnapshotRepository } from "./_snapshot-repository.js";
|
|
7
|
+
export type { DeletePlan, DeletePlanBlockReasonCode, DeletePlanSelectionMode, DeletePlanSelectionReason, DeletePlanValidationReasonCode, DeletePlanValidationStatus } from "./planner/index.js";
|
|
8
|
+
export { DeletePlanValidationReasonCodes, DeletePlanValidationStatuses } from "./planner/index.js";
|
|
9
|
+
export type { DbMergeSourceSummary } from "./_db-merge-repository.js";
|
|
10
|
+
export declare function openDatabase(databasePath: string): Database.Database;
|
package/dist/db/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { initializeSchema } from "./_schema.js";
|
|
3
|
+
export { ScanWriter } from "./_scan-writer.js";
|
|
4
|
+
export { CleanupRunWriter } from "./_cleanup-run-writer.js";
|
|
5
|
+
export { DbMergeRepository } from "./_db-merge-repository.js";
|
|
6
|
+
export { PlannerRepository } from "./planner/index.js";
|
|
7
|
+
export { SnapshotRepository } from "./_snapshot-repository.js";
|
|
8
|
+
export { DeletePlanValidationReasonCodes, DeletePlanValidationStatuses } from "./planner/index.js";
|
|
9
|
+
export function openDatabase(databasePath) {
|
|
10
|
+
const database = new Database(databasePath);
|
|
11
|
+
initializeSchema(database);
|
|
12
|
+
return database;
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface DirectTargetRootOptions {
|
|
2
|
+
deleteTags: string[];
|
|
3
|
+
deleteTagsRequested: boolean;
|
|
4
|
+
deleteOrphanedImages?: boolean;
|
|
5
|
+
excludeTags: string[];
|
|
6
|
+
deleteUntagged: boolean;
|
|
7
|
+
keepNTagged?: number;
|
|
8
|
+
keepNUntagged?: number;
|
|
9
|
+
useRegex?: boolean;
|
|
10
|
+
cutoffTimestamp?: string;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DirectTargetRootOptions } from "./_planner-direct-target-root-options.js";
|
|
2
|
+
import type { PlannerSql } from "./_planner-sql.js";
|
|
3
|
+
export interface DirectTargetRootTagFilters {
|
|
4
|
+
selectedTagsSql: string;
|
|
5
|
+
selectedParams: Array<number | string>;
|
|
6
|
+
}
|
|
7
|
+
export declare function buildDirectTargetRootTagFilters(sql: PlannerSql, scanId: number, options: DirectTargetRootOptions): DirectTargetRootTagFilters;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { buildTagSelectorPredicate } from "./_planner-tag-selectors.js";
|
|
2
|
+
export function buildDirectTargetRootTagFilters(sql, scanId, options) {
|
|
3
|
+
const selectedTagPredicate = options.deleteTags.length > 0
|
|
4
|
+
? buildTagSelectorPredicate(sql.database, "t.tag", options.deleteTags, options.useRegex ?? false)
|
|
5
|
+
: undefined;
|
|
6
|
+
const excludedTagPredicate = options.excludeTags.length > 0
|
|
7
|
+
? buildTagSelectorPredicate(sql.database, "xt.tag", options.excludeTags, options.useRegex ?? false)
|
|
8
|
+
: undefined;
|
|
9
|
+
const selectedTagDigestFlag = options.deleteOrphanedImages ? 1 : 0;
|
|
10
|
+
const selectedTagsSql = selectedTagPredicate
|
|
11
|
+
? `
|
|
12
|
+
SELECT DISTINCT t.version_id, t.tag
|
|
13
|
+
FROM tags t
|
|
14
|
+
WHERE t.scan_id = ?
|
|
15
|
+
AND t.is_digest_tag = ?
|
|
16
|
+
AND (${selectedTagPredicate.sql})
|
|
17
|
+
${excludedTagPredicate
|
|
18
|
+
? `
|
|
19
|
+
AND NOT EXISTS (
|
|
20
|
+
SELECT 1
|
|
21
|
+
FROM tags xt
|
|
22
|
+
WHERE xt.scan_id = t.scan_id
|
|
23
|
+
AND xt.version_id = t.version_id
|
|
24
|
+
AND xt.tag = t.tag
|
|
25
|
+
AND (${excludedTagPredicate.sql})
|
|
26
|
+
)
|
|
27
|
+
`
|
|
28
|
+
: ""}
|
|
29
|
+
`
|
|
30
|
+
: `
|
|
31
|
+
SELECT NULL AS version_id, NULL AS tag
|
|
32
|
+
WHERE 1 = 0
|
|
33
|
+
`;
|
|
34
|
+
const selectedParams = selectedTagPredicate
|
|
35
|
+
? [scanId, selectedTagDigestFlag, ...selectedTagPredicate.params, ...(excludedTagPredicate?.params ?? [])]
|
|
36
|
+
: [];
|
|
37
|
+
return {
|
|
38
|
+
selectedTagsSql,
|
|
39
|
+
selectedParams
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DirectTargetRootOptions } from "./_planner-direct-target-root-options.js";
|
|
2
|
+
export interface CombinedDirectTargetRootsQuery {
|
|
3
|
+
query: string;
|
|
4
|
+
baseParams: Array<number | string>;
|
|
5
|
+
tailParams: Array<number | string>;
|
|
6
|
+
}
|
|
7
|
+
export declare function buildCombinedDirectTargetRootsQuery(scanId: number, options: DirectTargetRootOptions, selectedTagsSql: string): CombinedDirectTargetRootsQuery;
|