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
|
@@ -7,18 +7,18 @@ export declare class SnapshotRepository {
|
|
|
7
7
|
packageName: string;
|
|
8
8
|
scanCompletedAt: string;
|
|
9
9
|
};
|
|
10
|
-
getTaggedDigests(scanId: number): Set<string>;
|
|
11
|
-
getDigestsForTags(scanId: number, tags: string[]): Set<string>;
|
|
12
|
-
getChildDigests(scanId: number, parentDigests: Iterable<string>): string[];
|
|
13
|
-
getVersionsCreatedBefore(scanId: number, cutoffTimestamp: string): Array<{
|
|
14
|
-
versionId: number;
|
|
15
|
-
digest: string;
|
|
16
|
-
}>;
|
|
17
|
-
getTaggedVersionIds(scanId: number): number[];
|
|
18
10
|
countPackageVersions(scanId: number): number;
|
|
19
11
|
countTaggedVersions(scanId: number): number;
|
|
20
12
|
countTags(scanId: number): number;
|
|
21
13
|
countManifests(scanId: number): number;
|
|
22
14
|
countManifestEdges(scanId: number): number;
|
|
23
|
-
|
|
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
24
|
}
|
|
@@ -23,50 +23,6 @@ export class SnapshotRepository {
|
|
|
23
23
|
scanCompletedAt: row.scan_completed_at
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
getTaggedDigests(scanId) {
|
|
27
|
-
return _getDigestSet(this.#database.prepare("SELECT DISTINCT digest FROM tags WHERE scan_id = ?").all(scanId), "digest");
|
|
28
|
-
}
|
|
29
|
-
getDigestsForTags(scanId, tags) {
|
|
30
|
-
if (tags.length === 0) {
|
|
31
|
-
return new Set();
|
|
32
|
-
}
|
|
33
|
-
const placeholders = tags.map(() => "?").join(", ");
|
|
34
|
-
const rows = this.#database
|
|
35
|
-
.prepare(`SELECT DISTINCT digest FROM tags WHERE scan_id = ? AND tag IN (${placeholders})`)
|
|
36
|
-
.all(scanId, ...tags);
|
|
37
|
-
return _getDigestSet(rows, "digest");
|
|
38
|
-
}
|
|
39
|
-
getChildDigests(scanId, parentDigests) {
|
|
40
|
-
const digestList = [...parentDigests];
|
|
41
|
-
if (digestList.length === 0) {
|
|
42
|
-
return [];
|
|
43
|
-
}
|
|
44
|
-
const placeholders = digestList.map(() => "?").join(", ");
|
|
45
|
-
const rows = this.#database
|
|
46
|
-
.prepare(`SELECT child_digest FROM manifest_edges WHERE scan_id = ? AND parent_digest IN (${placeholders})`)
|
|
47
|
-
.all(scanId, ...digestList);
|
|
48
|
-
return rows.map((row) => row.child_digest);
|
|
49
|
-
}
|
|
50
|
-
getVersionsCreatedBefore(scanId, cutoffTimestamp) {
|
|
51
|
-
const rows = this.#database
|
|
52
|
-
.prepare(`
|
|
53
|
-
SELECT version_id, digest
|
|
54
|
-
FROM package_versions
|
|
55
|
-
WHERE scan_id = ? AND created_at < ?
|
|
56
|
-
ORDER BY version_id
|
|
57
|
-
`)
|
|
58
|
-
.all(scanId, cutoffTimestamp);
|
|
59
|
-
return rows.map((row) => ({
|
|
60
|
-
versionId: row.version_id,
|
|
61
|
-
digest: row.digest
|
|
62
|
-
}));
|
|
63
|
-
}
|
|
64
|
-
getTaggedVersionIds(scanId) {
|
|
65
|
-
const rows = this.#database
|
|
66
|
-
.prepare("SELECT DISTINCT version_id FROM tags WHERE scan_id = ? ORDER BY version_id")
|
|
67
|
-
.all(scanId);
|
|
68
|
-
return rows.map((row) => row.version_id);
|
|
69
|
-
}
|
|
70
26
|
countPackageVersions(scanId) {
|
|
71
27
|
return _count(this.#database, "SELECT COUNT(*) AS total FROM package_versions WHERE scan_id = ?", "total", scanId);
|
|
72
28
|
}
|
|
@@ -82,17 +38,49 @@ export class SnapshotRepository {
|
|
|
82
38
|
countManifestEdges(scanId) {
|
|
83
39
|
return _count(this.#database, "SELECT COUNT(*) AS total FROM manifest_edges WHERE scan_id = ?", "total", scanId);
|
|
84
40
|
}
|
|
85
|
-
|
|
41
|
+
listManifestDigests(scanId) {
|
|
86
42
|
const rows = this.#database
|
|
87
|
-
.prepare("SELECT digest FROM
|
|
43
|
+
.prepare("SELECT digest FROM manifests WHERE scan_id = ? ORDER BY digest")
|
|
88
44
|
.all(scanId);
|
|
89
45
|
return rows.map((row) => row.digest);
|
|
90
46
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
}
|
|
94
75
|
}
|
|
95
76
|
function _count(database, sql, field, ...params) {
|
|
96
77
|
const row = database.prepare(sql).get(...params);
|
|
97
78
|
return row[field];
|
|
98
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
|
+
}
|
package/dist/db/index.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
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";
|
|
3
6
|
export { SnapshotRepository } from "./_snapshot-repository.js";
|
|
7
|
+
export type { DeletePlan } from "./planner/index.js";
|
|
8
|
+
export type { DbMergeSourceSummary } from "./_db-merge-repository.js";
|
|
4
9
|
export declare function openDatabase(databasePath: string): Database.Database;
|
package/dist/db/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
2
|
import { initializeSchema } from "./_schema.js";
|
|
3
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";
|
|
4
7
|
export { SnapshotRepository } from "./_snapshot-repository.js";
|
|
5
8
|
export function openDatabase(databasePath) {
|
|
6
9
|
const database = new Database(databasePath);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PlannerSql } from "./_planner-sql.js";
|
|
2
|
+
import { type DeletePlanRoot } from "./_planner-types.js";
|
|
3
|
+
export declare class PlannerDeleteTagRootTargets {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(sql: PlannerSql);
|
|
6
|
+
list(scanId: number, deleteTags: string[], excludeTags: string[], useRegex: boolean, keepCount?: number, cutoffTimestamp?: string): DeletePlanRoot[];
|
|
7
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { buildTagSelectorPredicate } from "./_planner-tag-selectors.js";
|
|
2
|
+
import { mapPlanRootRow } from "./_planner-types.js";
|
|
3
|
+
export class PlannerDeleteTagRootTargets {
|
|
4
|
+
#sql;
|
|
5
|
+
constructor(sql) {
|
|
6
|
+
this.#sql = sql;
|
|
7
|
+
}
|
|
8
|
+
list(scanId, deleteTags, excludeTags, useRegex, keepCount, cutoffTimestamp) {
|
|
9
|
+
const selectedTagPredicate = buildTagSelectorPredicate(this.#sql.database, "st.tag", deleteTags, useRegex);
|
|
10
|
+
const excludedTagPredicate = excludeTags.length > 0
|
|
11
|
+
? buildTagSelectorPredicate(this.#sql.database, "xt.tag", excludeTags, useRegex)
|
|
12
|
+
: undefined;
|
|
13
|
+
const excludedVersionsCte = excludedTagPredicate
|
|
14
|
+
? `
|
|
15
|
+
excluded_versions AS (
|
|
16
|
+
SELECT DISTINCT xt.version_id
|
|
17
|
+
FROM tags xt
|
|
18
|
+
WHERE xt.scan_id = ?
|
|
19
|
+
AND (${excludedTagPredicate.sql})
|
|
20
|
+
),
|
|
21
|
+
`
|
|
22
|
+
: "";
|
|
23
|
+
const excludedJoinSql = excludedTagPredicate
|
|
24
|
+
? `
|
|
25
|
+
LEFT JOIN excluded_versions ev
|
|
26
|
+
ON ev.version_id = st.version_id
|
|
27
|
+
`
|
|
28
|
+
: "";
|
|
29
|
+
const excludedWhereSql = excludedTagPredicate ? "AND ev.version_id IS NULL" : "";
|
|
30
|
+
const cutoffSql = cutoffTimestamp ? "AND pv.created_at < ?" : "";
|
|
31
|
+
const keepSql = keepCount !== undefined ? "WHERE recency_rank > ?" : "";
|
|
32
|
+
const params = [scanId, ...selectedTagPredicate.params];
|
|
33
|
+
if (excludedTagPredicate) {
|
|
34
|
+
params.push(scanId, ...excludedTagPredicate.params);
|
|
35
|
+
}
|
|
36
|
+
if (cutoffTimestamp) {
|
|
37
|
+
params.push(cutoffTimestamp);
|
|
38
|
+
}
|
|
39
|
+
params.push(scanId);
|
|
40
|
+
const tailParams = [keepCount !== undefined ? 1 : 0];
|
|
41
|
+
if (keepCount !== undefined) {
|
|
42
|
+
tailParams.push(keepCount);
|
|
43
|
+
}
|
|
44
|
+
const sql = `
|
|
45
|
+
WITH selected_tags AS (
|
|
46
|
+
SELECT st.scan_id, st.version_id, st.tag
|
|
47
|
+
FROM tags st
|
|
48
|
+
WHERE st.scan_id = ?
|
|
49
|
+
AND (${selectedTagPredicate.sql})
|
|
50
|
+
),
|
|
51
|
+
${excludedVersionsCte}
|
|
52
|
+
matched_candidate_roots AS (
|
|
53
|
+
SELECT DISTINCT
|
|
54
|
+
st.version_id AS version_id,
|
|
55
|
+
m.digest AS root_digest,
|
|
56
|
+
m.manifest_kind AS root_manifest_kind,
|
|
57
|
+
pv.created_at
|
|
58
|
+
FROM selected_tags st
|
|
59
|
+
JOIN manifests m
|
|
60
|
+
ON m.scan_id = st.scan_id
|
|
61
|
+
AND m.version_id = st.version_id
|
|
62
|
+
JOIN package_versions pv
|
|
63
|
+
ON pv.scan_id = st.scan_id
|
|
64
|
+
AND pv.version_id = st.version_id
|
|
65
|
+
${excludedJoinSql}
|
|
66
|
+
WHERE 1 = 1
|
|
67
|
+
${excludedWhereSql}
|
|
68
|
+
${cutoffSql}
|
|
69
|
+
AND NOT EXISTS (
|
|
70
|
+
SELECT 1
|
|
71
|
+
FROM manifest_reachability mr
|
|
72
|
+
WHERE mr.scan_id = st.scan_id
|
|
73
|
+
AND mr.descendant_digest = m.digest
|
|
74
|
+
AND mr.min_distance > 0
|
|
75
|
+
)
|
|
76
|
+
),
|
|
77
|
+
matched_roots AS (
|
|
78
|
+
SELECT
|
|
79
|
+
mcr.version_id,
|
|
80
|
+
mcr.root_digest,
|
|
81
|
+
mcr.root_manifest_kind,
|
|
82
|
+
mcr.created_at,
|
|
83
|
+
COUNT(t.tag) AS total_tag_count,
|
|
84
|
+
COUNT(st.tag) AS matched_tag_count
|
|
85
|
+
FROM matched_candidate_roots mcr
|
|
86
|
+
JOIN tags t
|
|
87
|
+
ON t.scan_id = ?
|
|
88
|
+
AND t.version_id = mcr.version_id
|
|
89
|
+
LEFT JOIN selected_tags st
|
|
90
|
+
ON st.scan_id = t.scan_id
|
|
91
|
+
AND st.version_id = t.version_id
|
|
92
|
+
AND st.tag = t.tag
|
|
93
|
+
GROUP BY mcr.version_id, mcr.root_digest, mcr.root_manifest_kind, mcr.created_at
|
|
94
|
+
HAVING matched_tag_count > 0
|
|
95
|
+
),
|
|
96
|
+
ranked_roots AS (
|
|
97
|
+
SELECT
|
|
98
|
+
version_id,
|
|
99
|
+
root_digest,
|
|
100
|
+
root_manifest_kind,
|
|
101
|
+
total_tag_count,
|
|
102
|
+
matched_tag_count,
|
|
103
|
+
ROW_NUMBER() OVER (
|
|
104
|
+
ORDER BY created_at DESC, version_id DESC, root_digest DESC
|
|
105
|
+
) AS recency_rank
|
|
106
|
+
FROM matched_roots
|
|
107
|
+
)
|
|
108
|
+
SELECT
|
|
109
|
+
version_id,
|
|
110
|
+
root_digest,
|
|
111
|
+
root_manifest_kind,
|
|
112
|
+
CASE
|
|
113
|
+
WHEN total_tag_count = matched_tag_count AND ? = 1
|
|
114
|
+
THEN 'keep-n-tagged-overflow'
|
|
115
|
+
WHEN total_tag_count = matched_tag_count
|
|
116
|
+
THEN 'delete-tags-all-tags-selected'
|
|
117
|
+
ELSE 'delete-tags-partial-tag-match'
|
|
118
|
+
END AS direct_target_reason,
|
|
119
|
+
CASE
|
|
120
|
+
WHEN total_tag_count = matched_tag_count
|
|
121
|
+
THEN 'delete-root'
|
|
122
|
+
ELSE 'untag-only'
|
|
123
|
+
END AS selection_mode
|
|
124
|
+
FROM ranked_roots
|
|
125
|
+
${keepSql}
|
|
126
|
+
ORDER BY root_digest
|
|
127
|
+
`;
|
|
128
|
+
return this.#sql.all(sql, [...params, ...tailParams]).map(mapPlanRootRow);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { PlannerSql } from "./_planner-sql.js";
|
|
2
|
+
export declare class PlannerDirectTargetTags {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(sql: PlannerSql);
|
|
5
|
+
listDeleteTagDirectTargetTags(scanId: number, deleteTags: string[], excludeTags: string[], useRegex: boolean, cutoffTimestamp?: string): string[];
|
|
6
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { buildTagSelectorPredicate } from "./_planner-tag-selectors.js";
|
|
2
|
+
import { mapPlanTagRows } from "./_planner-types.js";
|
|
3
|
+
export class PlannerDirectTargetTags {
|
|
4
|
+
#sql;
|
|
5
|
+
constructor(sql) {
|
|
6
|
+
this.#sql = sql;
|
|
7
|
+
}
|
|
8
|
+
listDeleteTagDirectTargetTags(scanId, deleteTags, excludeTags, useRegex, cutoffTimestamp) {
|
|
9
|
+
if (deleteTags.length === 0) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
const selectedTagPredicate = buildTagSelectorPredicate(this.#sql.database, "t.tag", deleteTags, useRegex);
|
|
13
|
+
const params = [scanId, ...selectedTagPredicate.params];
|
|
14
|
+
let excludedRootSql = "";
|
|
15
|
+
let olderThanSql = "";
|
|
16
|
+
if (excludeTags.length > 0) {
|
|
17
|
+
const excludedTagPredicate = buildTagSelectorPredicate(this.#sql.database, "xt.tag", excludeTags, useRegex);
|
|
18
|
+
excludedRootSql = `
|
|
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 (${excludedTagPredicate.sql})
|
|
25
|
+
)
|
|
26
|
+
`;
|
|
27
|
+
params.push(...excludedTagPredicate.params);
|
|
28
|
+
}
|
|
29
|
+
if (cutoffTimestamp) {
|
|
30
|
+
olderThanSql = "AND pv.created_at < ?";
|
|
31
|
+
params.push(cutoffTimestamp);
|
|
32
|
+
}
|
|
33
|
+
const sql = `
|
|
34
|
+
SELECT DISTINCT tag AS target_tag
|
|
35
|
+
FROM tags t
|
|
36
|
+
JOIN package_versions pv
|
|
37
|
+
ON pv.scan_id = t.scan_id
|
|
38
|
+
AND pv.version_id = t.version_id
|
|
39
|
+
WHERE t.scan_id = ?
|
|
40
|
+
AND (${selectedTagPredicate.sql})
|
|
41
|
+
${excludedRootSql}
|
|
42
|
+
${olderThanSql}
|
|
43
|
+
ORDER BY tag
|
|
44
|
+
`;
|
|
45
|
+
return mapPlanTagRows(this.#sql.all(sql, params));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PlannerSql } from "./_planner-sql.js";
|
|
2
|
+
import { type DeletePlanRoot } from "./_planner-types.js";
|
|
3
|
+
export declare class PlannerKeepTaggedRootTargets {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(sql: PlannerSql);
|
|
6
|
+
list(scanId: number, excludeTags: string[], useRegex: boolean, keepCount?: number, cutoffTimestamp?: string): DeletePlanRoot[];
|
|
7
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { buildTagSelectorPredicate } from "./_planner-tag-selectors.js";
|
|
2
|
+
import { mapPlanRootRow } from "./_planner-types.js";
|
|
3
|
+
export class PlannerKeepTaggedRootTargets {
|
|
4
|
+
#sql;
|
|
5
|
+
constructor(sql) {
|
|
6
|
+
this.#sql = sql;
|
|
7
|
+
}
|
|
8
|
+
list(scanId, excludeTags, useRegex, keepCount, cutoffTimestamp) {
|
|
9
|
+
const excludedTagPredicate = excludeTags.length > 0
|
|
10
|
+
? buildTagSelectorPredicate(this.#sql.database, "xt.tag", excludeTags, useRegex)
|
|
11
|
+
: undefined;
|
|
12
|
+
const excludedRootSql = excludedTagPredicate
|
|
13
|
+
? `
|
|
14
|
+
AND NOT EXISTS (
|
|
15
|
+
SELECT 1
|
|
16
|
+
FROM tags xt
|
|
17
|
+
WHERE xt.scan_id = pv.scan_id
|
|
18
|
+
AND xt.version_id = pv.version_id
|
|
19
|
+
AND (${excludedTagPredicate.sql})
|
|
20
|
+
)
|
|
21
|
+
`
|
|
22
|
+
: "";
|
|
23
|
+
const cutoffSql = cutoffTimestamp ? "AND pv.created_at < ?" : "";
|
|
24
|
+
const keepSql = keepCount !== undefined ? "WHERE recency_rank > ?" : "";
|
|
25
|
+
const params = [scanId, ...(excludedTagPredicate?.params ?? [])];
|
|
26
|
+
if (cutoffTimestamp) {
|
|
27
|
+
params.push(cutoffTimestamp);
|
|
28
|
+
}
|
|
29
|
+
if (keepCount !== undefined) {
|
|
30
|
+
params.push(keepCount);
|
|
31
|
+
}
|
|
32
|
+
const sql = `
|
|
33
|
+
WITH eligible_tagged_roots AS (
|
|
34
|
+
SELECT
|
|
35
|
+
pv.version_id AS version_id,
|
|
36
|
+
m.digest AS root_digest,
|
|
37
|
+
m.manifest_kind AS root_manifest_kind,
|
|
38
|
+
ROW_NUMBER() OVER (
|
|
39
|
+
ORDER BY pv.created_at DESC, pv.version_id DESC, m.digest DESC
|
|
40
|
+
) AS recency_rank
|
|
41
|
+
FROM package_versions pv
|
|
42
|
+
JOIN manifests m
|
|
43
|
+
ON m.scan_id = pv.scan_id
|
|
44
|
+
AND m.version_id = pv.version_id
|
|
45
|
+
WHERE pv.scan_id = ?
|
|
46
|
+
AND EXISTS (
|
|
47
|
+
SELECT 1
|
|
48
|
+
FROM tags t
|
|
49
|
+
WHERE t.scan_id = pv.scan_id
|
|
50
|
+
AND t.version_id = pv.version_id
|
|
51
|
+
)
|
|
52
|
+
${excludedRootSql}
|
|
53
|
+
AND NOT EXISTS (
|
|
54
|
+
SELECT 1
|
|
55
|
+
FROM manifest_reachability mr
|
|
56
|
+
WHERE mr.scan_id = pv.scan_id
|
|
57
|
+
AND mr.descendant_digest = m.digest
|
|
58
|
+
AND mr.min_distance > 0
|
|
59
|
+
)
|
|
60
|
+
${cutoffSql}
|
|
61
|
+
)
|
|
62
|
+
SELECT
|
|
63
|
+
version_id,
|
|
64
|
+
root_digest,
|
|
65
|
+
root_manifest_kind,
|
|
66
|
+
'keep-n-tagged-overflow' AS direct_target_reason,
|
|
67
|
+
'delete-root' AS selection_mode
|
|
68
|
+
FROM eligible_tagged_roots
|
|
69
|
+
${keepSql}
|
|
70
|
+
ORDER BY root_digest
|
|
71
|
+
`;
|
|
72
|
+
return this.#sql.all(sql, params).map(mapPlanRootRow);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DeletePlan, DeletePlanBlockedRoot, DeletePlanProtectedRoot, DeletePlanRoot, DeletePlanRootDecision, PlanArtifacts } from "./_planner-types.js";
|
|
2
|
+
export declare function buildPlanOutputs(directTargetTags: string[], directTargetRoots: DeletePlanRoot[], planArtifacts: PlanArtifacts): Pick<DeletePlan, "validationSummary" | "directTargetTags" | "directTargetRoots" | "rootDecisions" | "protectedRoots" | "closureManifests" | "blockedRoots" | "fullyDeletableRoots" | "collateralTags">;
|
|
3
|
+
export declare function buildRootDecisions(directTargetRoots: DeletePlanRoot[], planArtifacts: PlanArtifacts): DeletePlanRootDecision[];
|
|
4
|
+
export declare function buildProtectedRoots(blockedRoots: DeletePlanBlockedRoot[]): DeletePlanProtectedRoot[];
|
|
5
|
+
export declare function buildBlockedValidationReason(blockedRoot?: DeletePlanBlockedRoot): string;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export function buildPlanOutputs(directTargetTags, directTargetRoots, planArtifacts) {
|
|
2
|
+
const rootDecisions = buildRootDecisions(directTargetRoots, planArtifacts);
|
|
3
|
+
const protectedRoots = buildProtectedRoots(planArtifacts.blockedRoots);
|
|
4
|
+
const deleteRootCandidateCount = directTargetRoots.filter((root) => root.selectionMode === "delete-root").length;
|
|
5
|
+
const untagOnlyRootCount = directTargetRoots.length - deleteRootCandidateCount;
|
|
6
|
+
return {
|
|
7
|
+
validationSummary: {
|
|
8
|
+
directTargetTagCount: directTargetTags.length,
|
|
9
|
+
directTargetRootCount: directTargetRoots.length,
|
|
10
|
+
deleteRootCandidateCount,
|
|
11
|
+
untagOnlyRootCount,
|
|
12
|
+
fullyDeletableRootCount: planArtifacts.fullyDeletableRoots.length,
|
|
13
|
+
blockedDeleteRootCount: rootDecisions.filter((decision) => decision.validationStatus === "blocked").length,
|
|
14
|
+
protectedRootCount: protectedRoots.length
|
|
15
|
+
},
|
|
16
|
+
directTargetTags,
|
|
17
|
+
directTargetRoots,
|
|
18
|
+
rootDecisions,
|
|
19
|
+
protectedRoots,
|
|
20
|
+
closureManifests: planArtifacts.closureManifests,
|
|
21
|
+
blockedRoots: planArtifacts.blockedRoots,
|
|
22
|
+
fullyDeletableRoots: planArtifacts.fullyDeletableRoots,
|
|
23
|
+
collateralTags: []
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function buildRootDecisions(directTargetRoots, planArtifacts) {
|
|
27
|
+
const fullyDeletableDigests = new Set(planArtifacts.fullyDeletableRoots.map((root) => root.digest));
|
|
28
|
+
const blockedRootByDigest = new Map();
|
|
29
|
+
for (const blockedRoot of planArtifacts.blockedRoots) {
|
|
30
|
+
if (!blockedRootByDigest.has(blockedRoot.blockedDigest)) {
|
|
31
|
+
blockedRootByDigest.set(blockedRoot.blockedDigest, blockedRoot);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return directTargetRoots.map((root) => {
|
|
35
|
+
if (root.selectionMode === "untag-only") {
|
|
36
|
+
return {
|
|
37
|
+
versionId: root.versionId,
|
|
38
|
+
digest: root.digest,
|
|
39
|
+
manifestKind: root.manifestKind,
|
|
40
|
+
selectionMode: root.selectionMode,
|
|
41
|
+
selectionReason: root.reason,
|
|
42
|
+
validationStatus: "untag-only",
|
|
43
|
+
validationReasonCode: "untag-only-partial-tag-match",
|
|
44
|
+
validationReason: "matched tags cover only part of this root's tag set, so the version is retained and only those tags can be detached"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (fullyDeletableDigests.has(root.digest)) {
|
|
48
|
+
return {
|
|
49
|
+
versionId: root.versionId,
|
|
50
|
+
digest: root.digest,
|
|
51
|
+
manifestKind: root.manifestKind,
|
|
52
|
+
selectionMode: root.selectionMode,
|
|
53
|
+
selectionReason: root.reason,
|
|
54
|
+
validationStatus: "fully-deletable",
|
|
55
|
+
validationReasonCode: "fully-deletable-no-retained-overlap",
|
|
56
|
+
validationReason: "selected tags cover the whole root and its manifest closure does not overlap any retained root"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const blockedRoot = blockedRootByDigest.get(root.digest);
|
|
60
|
+
return {
|
|
61
|
+
versionId: root.versionId,
|
|
62
|
+
digest: root.digest,
|
|
63
|
+
manifestKind: root.manifestKind,
|
|
64
|
+
selectionMode: root.selectionMode,
|
|
65
|
+
selectionReason: root.reason,
|
|
66
|
+
validationStatus: "blocked",
|
|
67
|
+
validationReasonCode: "blocked-overlap-with-retained-root",
|
|
68
|
+
validationReason: buildBlockedValidationReason(blockedRoot),
|
|
69
|
+
blockingVersionId: blockedRoot?.blockingVersionId,
|
|
70
|
+
blockingDigest: blockedRoot?.blockingDigest,
|
|
71
|
+
overlapDigest: blockedRoot?.overlapDigest,
|
|
72
|
+
overlapManifestKind: blockedRoot?.overlapManifestKind
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function buildProtectedRoots(blockedRoots) {
|
|
77
|
+
const protectedRoots = new Map();
|
|
78
|
+
for (const blockedRoot of blockedRoots) {
|
|
79
|
+
const key = `${blockedRoot.blockingVersionId}:${blockedRoot.blockingDigest}`;
|
|
80
|
+
const current = protectedRoots.get(key) ?? {
|
|
81
|
+
versionId: blockedRoot.blockingVersionId,
|
|
82
|
+
digest: blockedRoot.blockingDigest,
|
|
83
|
+
blocks: []
|
|
84
|
+
};
|
|
85
|
+
current.blocks.push({
|
|
86
|
+
blockedVersionId: blockedRoot.blockedVersionId,
|
|
87
|
+
blockedDigest: blockedRoot.blockedDigest,
|
|
88
|
+
blockReasonCode: blockedRoot.reason,
|
|
89
|
+
overlapDigest: blockedRoot.overlapDigest,
|
|
90
|
+
overlapManifestKind: blockedRoot.overlapManifestKind
|
|
91
|
+
});
|
|
92
|
+
protectedRoots.set(key, current);
|
|
93
|
+
}
|
|
94
|
+
return [...protectedRoots.values()].sort((left, right) => left.digest.localeCompare(right.digest));
|
|
95
|
+
}
|
|
96
|
+
export function buildBlockedValidationReason(blockedRoot) {
|
|
97
|
+
if (!blockedRoot) {
|
|
98
|
+
return "root closure overlaps manifest members still required by a retained root";
|
|
99
|
+
}
|
|
100
|
+
return `blocked because retained root ${blockedRoot.blockingDigest} still requires shared manifest ${blockedRoot.overlapDigest}`;
|
|
101
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PlannerSql } from "./_planner-sql.js";
|
|
2
|
+
import { type DeletePlanRoot, type PlanArtifacts } from "./_planner-types.js";
|
|
3
|
+
export declare class PlannerPlanArtifacts {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(sql: PlannerSql);
|
|
6
|
+
build(scanId: number, directTargetRoots: DeletePlanRoot[]): PlanArtifacts;
|
|
7
|
+
}
|