ghcr-manager 0.9.6 → 0.9.7
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 +25 -0
- package/README.md +14 -13
- package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +0 -1
- package/dist/cleanup-summary/_cleanup-summary-markdown.js +146 -33
- package/dist/cleanup-summary/_cleanup-summary.d.ts +20 -7
- package/dist/cleanup-summary/_cleanup-summary.js +24 -8
- package/dist/cleanup-summary/index.d.ts +1 -1
- package/dist/cli/_cleanup-command.js +82 -23
- package/dist/cli/_json-output.d.ts +1 -0
- package/dist/cli/_json-output.js +11 -0
- package/dist/cli/_tag-selector-resolver.js +7 -4
- package/dist/cli/_untag-command.js +2 -1
- package/dist/cli/index.js +2 -2
- package/dist/core/_types.d.ts +9 -1
- package/dist/core/_types.js +8 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/db/_cleanup-run-writer.d.ts +1 -1
- package/dist/db/_cleanup-run-writer.js +28 -7
- package/dist/db/_db-merge-cleanup-copy.js +4 -2
- package/dist/db/_manifest-kind-refinement.d.ts +2 -0
- package/dist/db/_manifest-kind-refinement.js +43 -0
- package/dist/db/_scan-writer.js +4 -1
- package/dist/db/index.d.ts +2 -1
- package/dist/db/index.js +1 -0
- package/dist/db/planner/_planner-direct-target-roots.d.ts +1 -0
- package/dist/db/planner/_planner-direct-target-roots.js +23 -12
- package/dist/db/planner/_planner-direct-target-tags.d.ts +1 -1
- package/dist/db/planner/_planner-direct-target-tags.js +4 -2
- package/dist/db/planner/_planner-output.js +7 -6
- package/dist/db/planner/_planner-repository.d.ts +2 -1
- package/dist/db/planner/_planner-repository.js +3 -1
- package/dist/db/planner/_planner-types.d.ts +21 -8
- package/dist/db/planner/_planner-types.js +13 -3
- package/dist/db/planner/index.d.ts +2 -1
- package/dist/db/planner/index.js +1 -0
- package/dist/execute/_plan-executor.d.ts +1 -1
- package/dist/execute/_plan-executor.js +35 -9
- package/dist/ingest/github/_manifest-kind.d.ts +1 -1
- package/dist/ingest/github/_manifest-kind.js +6 -5
- package/package.json +1 -1
- package/resources/sql/schema/001_schema.sql +4 -1
- package/resources/sql/views/002_v_missing_digests.sql +0 -32
- /package/resources/sql/views/{003_v_scan_root_manifests.sql → 002_v_scan_root_manifests.sql} +0 -0
- /package/resources/sql/views/{004_v_digest_tag_relations.sql → 003_v_digest_tag_relations.sql} +0 -0
- /package/resources/sql/views/{005_v_cleanup_root_closure_members.sql → 004_v_cleanup_root_closure_members.sql} +0 -0
- /package/resources/sql/views/{006_v_cleanup_blocking_overlaps.sql → 005_v_cleanup_blocking_overlaps.sql} +0 -0
- /package/resources/sql/views/{007_v_cleanup_root_decision_readable.sql → 006_v_cleanup_root_decision_readable.sql} +0 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
const BrokenIndexModes = {
|
|
2
|
+
allMissing: "all-missing",
|
|
3
|
+
someMissing: "some-missing"
|
|
4
|
+
};
|
|
1
5
|
export function resolveTagSelectors(database, inputs) {
|
|
2
6
|
if (!inputs.deleteGhostImages && !inputs.deletePartialImages && !inputs.deleteOrphanedImages) {
|
|
3
7
|
return inputs;
|
|
@@ -14,13 +18,13 @@ export function resolveTagSelectors(database, inputs) {
|
|
|
14
18
|
};
|
|
15
19
|
}
|
|
16
20
|
function _listLatestGhostTags(database, owner, packageName, cutoffTimestamp) {
|
|
17
|
-
return _listLatestBrokenIndexTags(database, owner, packageName, cutoffTimestamp,
|
|
21
|
+
return _listLatestBrokenIndexTags(database, owner, packageName, cutoffTimestamp, BrokenIndexModes.allMissing);
|
|
18
22
|
}
|
|
19
23
|
function _listLatestPartialTags(database, owner, packageName, cutoffTimestamp) {
|
|
20
|
-
return _listLatestBrokenIndexTags(database, owner, packageName, cutoffTimestamp,
|
|
24
|
+
return _listLatestBrokenIndexTags(database, owner, packageName, cutoffTimestamp, BrokenIndexModes.someMissing);
|
|
21
25
|
}
|
|
22
26
|
function _listLatestBrokenIndexTags(database, owner, packageName, cutoffTimestamp, mode) {
|
|
23
|
-
const havingClause = mode ===
|
|
27
|
+
const havingClause = mode === BrokenIndexModes.allMissing
|
|
24
28
|
? "COUNT(*) > 0 AND COUNT(child.digest) = 0"
|
|
25
29
|
: "COUNT(child.digest) > 0 AND COUNT(child.digest) < COUNT(*)";
|
|
26
30
|
const rows = database
|
|
@@ -30,7 +34,6 @@ function _listLatestBrokenIndexTags(database, owner, packageName, cutoffTimestam
|
|
|
30
34
|
FROM v_latest_scan_per_package
|
|
31
35
|
WHERE owner = ?
|
|
32
36
|
AND package_name = ?
|
|
33
|
-
LIMIT 1
|
|
34
37
|
),
|
|
35
38
|
ghost_roots AS (
|
|
36
39
|
SELECT
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { listPackageVersionTagSources, untagRootTags } from "../execute/index.js";
|
|
2
2
|
import { collectRepeatedOption, hasFlag, requireOption, resolveLogLevel, resolveToken } from "./_args.js";
|
|
3
|
+
import { writeJsonOutput } from "./_json-output.js";
|
|
3
4
|
import { createLogger } from "./_logger.js";
|
|
4
5
|
export async function handleUntag(args) {
|
|
5
6
|
const owner = requireOption(args, "--owner");
|
|
@@ -35,7 +36,7 @@ export async function handleUntag(args) {
|
|
|
35
36
|
roots,
|
|
36
37
|
untaggedTags
|
|
37
38
|
};
|
|
38
|
-
|
|
39
|
+
writeJsonOutput(args, "--summary-json-path", summary);
|
|
39
40
|
return 0;
|
|
40
41
|
}
|
|
41
42
|
function _groupTagSources(tagSources) {
|
package/dist/cli/index.js
CHANGED
|
@@ -26,10 +26,10 @@ export async function main(argv) {
|
|
|
26
26
|
}
|
|
27
27
|
function printUsage() {
|
|
28
28
|
console.error(`Usage:
|
|
29
|
-
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> [--token <token>] <cleanup selectors...> [--exclude-tag <tag> ...] [--use-regex] [--older-than <interval>]
|
|
29
|
+
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] [--summary-json-path <path>] --owner <org> --package <name> [--token <token>] <cleanup selectors...> [--exclude-tag <tag> ...] [--use-regex] [--older-than <interval>]
|
|
30
30
|
ghcr-manager db-merge --db <target-path> --source-db <path> [--source-db <path> ...]
|
|
31
31
|
ghcr-manager scan --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--github-output <path>] --owner <org> --package <name> --token <token>
|
|
32
|
-
ghcr-manager untag [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> --token <token> --tag <tag> [--tag <tag> ...]
|
|
32
|
+
ghcr-manager untag [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] [--summary-json-path <path>] --owner <org> --package <name> --token <token> --tag <tag> [--tag <tag> ...]
|
|
33
33
|
|
|
34
34
|
Cleanup selectors:
|
|
35
35
|
--delete-untagged
|
package/dist/core/_types.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare const ManifestKinds: {
|
|
2
|
+
readonly indexManifest: "index_manifest";
|
|
3
|
+
readonly crossArchManifest: "cross_arch_manifest";
|
|
4
|
+
readonly imageManifest: "image_manifest";
|
|
5
|
+
readonly artifactManifest: "artifact_manifest";
|
|
6
|
+
readonly attestationManifest: "attestation_manifest";
|
|
7
|
+
readonly signatureManifest: "signature_manifest";
|
|
8
|
+
};
|
|
9
|
+
export type ManifestKind = (typeof ManifestKinds)[keyof typeof ManifestKinds];
|
|
2
10
|
export type ManifestEdgeKind = "image-child" | "referrer" | "digest-tag-referrer";
|
|
3
11
|
export interface PackageVersionRecord {
|
|
4
12
|
versionId: number;
|
package/dist/core/_types.js
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export const ManifestKinds = {
|
|
2
|
+
indexManifest: "index_manifest",
|
|
3
|
+
crossArchManifest: "cross_arch_manifest",
|
|
4
|
+
imageManifest: "image_manifest",
|
|
5
|
+
artifactManifest: "artifact_manifest",
|
|
6
|
+
attestationManifest: "attestation_manifest",
|
|
7
|
+
signatureManifest: "signature_manifest"
|
|
8
|
+
};
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type { ManifestEdgeKind, ManifestEdgeRecord, ManifestDescriptorRecord, ManifestKind, ManifestRecord, PackageSnapshot, PackageVersionRecord, TagRecord } from "./_types.js";
|
|
2
|
+
export { ManifestKinds } from "./_types.js";
|
|
2
3
|
export type { HttpErrorResponse } from "./_http-error.js";
|
|
3
4
|
export { buildHttpErrorMessage } from "./_http-error.js";
|
|
4
5
|
export { getOwnerURIComponent } from "./_github-package-owner.js";
|
package/dist/core/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { resolveGitHubActionsRunUrl } from "./_github-actions-run-url.js";
|
|
3
|
+
import { DeletePlanValidationStatuses } from "./planner/index.js";
|
|
3
4
|
export class CleanupRunWriter {
|
|
4
5
|
#database;
|
|
5
6
|
#insertSelectedTagStatement;
|
|
7
|
+
#updateSelectedTagsDeletedStatement;
|
|
6
8
|
#insertRootDecisionStatement;
|
|
7
9
|
#insertProtectedRootBlockStatement;
|
|
8
10
|
#insertCleanupRunStatement;
|
|
@@ -12,9 +14,27 @@ export class CleanupRunWriter {
|
|
|
12
14
|
INSERT INTO cleanup_selected_tags(
|
|
13
15
|
cleanup_run_id,
|
|
14
16
|
scan_id,
|
|
15
|
-
tag
|
|
17
|
+
tag,
|
|
18
|
+
is_deleted
|
|
16
19
|
)
|
|
17
|
-
VALUES(?, ?,
|
|
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
|
|
18
38
|
`);
|
|
19
39
|
this.#insertRootDecisionStatement = this.#database.prepare(`
|
|
20
40
|
INSERT INTO cleanup_root_decisions(
|
|
@@ -64,9 +84,6 @@ export class CleanupRunWriter {
|
|
|
64
84
|
persistCleanupRun(scanId, plan, options) {
|
|
65
85
|
return this.#database.transaction(() => {
|
|
66
86
|
const cleanupRunId = this.#insertCleanupRun(scanId, plan, options);
|
|
67
|
-
for (const tag of plan.directTargetTags) {
|
|
68
|
-
this.#insertSelectedTagStatement.run(cleanupRunId, scanId, tag);
|
|
69
|
-
}
|
|
70
87
|
for (const rootDecision of plan.rootDecisions) {
|
|
71
88
|
this.#insertRootDecisionStatement.run(cleanupRunId, scanId, rootDecision.digest, rootDecision.selectionMode, rootDecision.selectionReason, rootDecision.validationStatus, rootDecision.validationReasonCode, rootDecision.validationReason, rootDecision.blockingDigest ?? null, rootDecision.overlapDigest ?? null);
|
|
72
89
|
}
|
|
@@ -75,6 +92,10 @@ export class CleanupRunWriter {
|
|
|
75
92
|
this.#insertProtectedRootBlockStatement.run(cleanupRunId, scanId, protectedRoot.digest, block.blockedDigest, block.blockReasonCode, block.overlapDigest);
|
|
76
93
|
}
|
|
77
94
|
}
|
|
95
|
+
for (const tag of plan.directTargetTags) {
|
|
96
|
+
this.#insertSelectedTagStatement.run(cleanupRunId, scanId, tag);
|
|
97
|
+
}
|
|
98
|
+
this.#updateSelectedTagsDeletedStatement.run(cleanupRunId, scanId);
|
|
78
99
|
return cleanupRunId;
|
|
79
100
|
})();
|
|
80
101
|
}
|
|
@@ -82,9 +103,9 @@ export class CleanupRunWriter {
|
|
|
82
103
|
const directTargetTagCount = plan.directTargetTags.length;
|
|
83
104
|
const directTargetRootCount = plan.directTargetRoots.length;
|
|
84
105
|
const deleteRootCandidateCount = plan.directTargetRoots.filter((root) => root.selectionMode === "delete-root").length;
|
|
85
|
-
const untagOnlyRootCount = plan.rootDecisions.filter((decision) => decision.validationStatus ===
|
|
106
|
+
const untagOnlyRootCount = plan.rootDecisions.filter((decision) => decision.validationStatus === DeletePlanValidationStatuses.untagOnly).length;
|
|
86
107
|
const fullyDeletableRootCount = plan.fullyDeletableRoots.length;
|
|
87
|
-
const blockedDeleteRootCount = plan.rootDecisions.filter((decision) => decision.validationStatus ===
|
|
108
|
+
const blockedDeleteRootCount = plan.rootDecisions.filter((decision) => decision.validationStatus === DeletePlanValidationStatuses.blocked).length;
|
|
88
109
|
const protectedRootCount = plan.protectedRoots.length;
|
|
89
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);
|
|
90
111
|
return Number(result.lastInsertRowid);
|
|
@@ -112,12 +112,14 @@ export class DbMergeCleanupCopy {
|
|
|
112
112
|
INSERT INTO cleanup_selected_tags(
|
|
113
113
|
cleanup_run_id,
|
|
114
114
|
scan_id,
|
|
115
|
-
tag
|
|
115
|
+
tag,
|
|
116
|
+
is_deleted
|
|
116
117
|
)
|
|
117
118
|
SELECT
|
|
118
119
|
?,
|
|
119
120
|
?,
|
|
120
|
-
tag
|
|
121
|
+
tag,
|
|
122
|
+
is_deleted
|
|
121
123
|
FROM ${attachName}.cleanup_selected_tags
|
|
122
124
|
WHERE cleanup_run_id = ?
|
|
123
125
|
AND scan_id = ?
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ManifestKinds } from "../core/index.js";
|
|
2
|
+
const _refineManifestKindsStatementByDatabase = new WeakMap();
|
|
3
|
+
export function refineManifestKinds(database, scanId) {
|
|
4
|
+
_refineManifestKindsStatement(database).run(ManifestKinds.crossArchManifest, scanId, ManifestKinds.indexManifest);
|
|
5
|
+
}
|
|
6
|
+
function _refineManifestKindsStatement(database) {
|
|
7
|
+
const cached = _refineManifestKindsStatementByDatabase.get(database);
|
|
8
|
+
if (cached) {
|
|
9
|
+
return cached;
|
|
10
|
+
}
|
|
11
|
+
const statement = database.prepare(`
|
|
12
|
+
UPDATE manifests AS parent
|
|
13
|
+
SET manifest_kind = ?
|
|
14
|
+
WHERE parent.scan_id = ?
|
|
15
|
+
AND parent.manifest_kind = ?
|
|
16
|
+
AND parent.media_type IN (
|
|
17
|
+
'application/vnd.oci.image.index.v1+json',
|
|
18
|
+
'application/vnd.docker.distribution.manifest.list.v2+json'
|
|
19
|
+
)
|
|
20
|
+
AND NOT EXISTS (
|
|
21
|
+
SELECT 1
|
|
22
|
+
FROM tags helper_tag
|
|
23
|
+
WHERE helper_tag.scan_id = parent.scan_id
|
|
24
|
+
AND helper_tag.version_id = parent.version_id
|
|
25
|
+
AND helper_tag.is_digest_tag = 1
|
|
26
|
+
)
|
|
27
|
+
AND EXISTS (
|
|
28
|
+
SELECT 1
|
|
29
|
+
FROM manifest_descriptors descriptor
|
|
30
|
+
JOIN manifests child
|
|
31
|
+
ON child.scan_id = descriptor.scan_id
|
|
32
|
+
AND child.digest = descriptor.child_digest
|
|
33
|
+
WHERE descriptor.scan_id = parent.scan_id
|
|
34
|
+
AND descriptor.parent_digest = parent.digest
|
|
35
|
+
AND child.media_type IN (
|
|
36
|
+
'application/vnd.oci.image.manifest.v1+json',
|
|
37
|
+
'application/vnd.docker.distribution.manifest.v2+json'
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
`);
|
|
41
|
+
_refineManifestKindsStatementByDatabase.set(database, statement);
|
|
42
|
+
return statement;
|
|
43
|
+
}
|
package/dist/db/_scan-writer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { isDigestTag } from "../core/index.js";
|
|
3
3
|
import { resolveGitHubActionsRunUrl } from "./_github-actions-run-url.js";
|
|
4
|
+
import { refineManifestKinds } from "./_manifest-kind-refinement.js";
|
|
4
5
|
import { rebuildManifestReachability } from "./_manifest-reachability.js";
|
|
5
6
|
export class ScanWriter {
|
|
6
7
|
#database;
|
|
@@ -179,7 +180,9 @@ export class ScanWriter {
|
|
|
179
180
|
});
|
|
180
181
|
}
|
|
181
182
|
rebuildManifestReachability() {
|
|
182
|
-
|
|
183
|
+
const scanId = this.#requireScanId();
|
|
184
|
+
rebuildManifestReachability(this.#database, scanId);
|
|
185
|
+
refineManifestKinds(this.#database, scanId);
|
|
183
186
|
}
|
|
184
187
|
getActiveScanId() {
|
|
185
188
|
return this.#requireScanId();
|
package/dist/db/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { CleanupRunWriter } from "./_cleanup-run-writer.js";
|
|
|
4
4
|
export { DbMergeRepository } from "./_db-merge-repository.js";
|
|
5
5
|
export { PlannerRepository } from "./planner/index.js";
|
|
6
6
|
export { SnapshotRepository } from "./_snapshot-repository.js";
|
|
7
|
-
export type { DeletePlan, DeletePlanBlockReasonCode, DeletePlanSelectionMode, DeletePlanSelectionReason } from "./planner/index.js";
|
|
7
|
+
export type { DeletePlan, DeletePlanBlockReasonCode, DeletePlanSelectionMode, DeletePlanSelectionReason, DeletePlanValidationReasonCode, DeletePlanValidationStatus } from "./planner/index.js";
|
|
8
|
+
export { DeletePlanValidationReasonCodes, DeletePlanValidationStatuses } from "./planner/index.js";
|
|
8
9
|
export type { DbMergeSourceSummary } from "./_db-merge-repository.js";
|
|
9
10
|
export declare function openDatabase(databasePath: string): Database.Database;
|
package/dist/db/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { CleanupRunWriter } from "./_cleanup-run-writer.js";
|
|
|
5
5
|
export { DbMergeRepository } from "./_db-merge-repository.js";
|
|
6
6
|
export { PlannerRepository } from "./planner/index.js";
|
|
7
7
|
export { SnapshotRepository } from "./_snapshot-repository.js";
|
|
8
|
+
export { DeletePlanValidationReasonCodes, DeletePlanValidationStatuses } from "./planner/index.js";
|
|
8
9
|
export function openDatabase(databasePath) {
|
|
9
10
|
const database = new Database(databasePath);
|
|
10
11
|
initializeSchema(database);
|
|
@@ -17,20 +17,21 @@ export class PlannerDirectTargetRoots {
|
|
|
17
17
|
if (options.cutoffTimestamp) {
|
|
18
18
|
params.push(options.cutoffTimestamp);
|
|
19
19
|
}
|
|
20
|
+
const selectedTagDigestFlag = options.deleteOrphanedImages ? 1 : 0;
|
|
20
21
|
const selectedTagsSql = selectedTagPredicate
|
|
21
22
|
? `
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
SELECT DISTINCT t.version_id, t.tag
|
|
24
|
+
FROM tags t
|
|
25
|
+
WHERE t.scan_id = ?
|
|
26
|
+
AND t.is_digest_tag = ?
|
|
27
|
+
AND (${selectedTagPredicate.sql})
|
|
28
|
+
`
|
|
28
29
|
: `
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
SELECT NULL AS version_id, NULL AS tag
|
|
31
|
+
WHERE 1 = 0
|
|
32
|
+
`;
|
|
32
33
|
if (selectedTagPredicate) {
|
|
33
|
-
params.push(scanId, ...selectedTagPredicate.params);
|
|
34
|
+
params.push(scanId, selectedTagDigestFlag, ...selectedTagPredicate.params);
|
|
34
35
|
}
|
|
35
36
|
const excludedVersionsSql = excludedTagPredicate
|
|
36
37
|
? `
|
|
@@ -49,10 +50,13 @@ export class PlannerDirectTargetRoots {
|
|
|
49
50
|
}
|
|
50
51
|
const taggedBranchEnabled = options.deleteTagsRequested || options.keepNTagged !== undefined ? 1 : 0;
|
|
51
52
|
const deleteTagsRequested = options.deleteTagsRequested ? 1 : 0;
|
|
53
|
+
const deleteOrphanedImages = options.deleteOrphanedImages ? 1 : 0;
|
|
52
54
|
const keepNTaggedActive = options.keepNTagged !== undefined ? 1 : 0;
|
|
53
55
|
const deleteUntagged = options.deleteUntagged ? 1 : 0;
|
|
54
56
|
const keepNUntaggedActive = options.keepNUntagged !== undefined ? 1 : 0;
|
|
55
57
|
const paramsTail = [
|
|
58
|
+
deleteOrphanedImages,
|
|
59
|
+
deleteOrphanedImages,
|
|
56
60
|
taggedBranchEnabled,
|
|
57
61
|
deleteTagsRequested,
|
|
58
62
|
deleteTagsRequested,
|
|
@@ -100,14 +104,21 @@ export class PlannerDirectTargetRoots {
|
|
|
100
104
|
rc.root_digest,
|
|
101
105
|
rc.root_manifest_kind,
|
|
102
106
|
rc.created_at,
|
|
103
|
-
|
|
107
|
+
CASE
|
|
108
|
+
WHEN ? = 1 AND rc.tag_count = 0 AND COALESCE(mtc.matched_tag_count, 0) > 0
|
|
109
|
+
THEN COALESCE(mtc.matched_tag_count, 0)
|
|
110
|
+
ELSE rc.tag_count
|
|
111
|
+
END AS total_tag_count,
|
|
104
112
|
COALESCE(mtc.matched_tag_count, 0) AS matched_tag_count
|
|
105
113
|
FROM root_candidates rc
|
|
106
114
|
LEFT JOIN matched_tag_counts mtc
|
|
107
115
|
ON mtc.version_id = rc.version_id
|
|
108
116
|
LEFT JOIN excluded_versions ev
|
|
109
117
|
ON ev.version_id = rc.version_id
|
|
110
|
-
WHERE
|
|
118
|
+
WHERE (
|
|
119
|
+
rc.is_tagged = 1
|
|
120
|
+
OR (? = 1 AND COALESCE(mtc.matched_tag_count, 0) > 0)
|
|
121
|
+
)
|
|
111
122
|
AND ev.version_id IS NULL
|
|
112
123
|
AND ? = 1
|
|
113
124
|
),
|
|
@@ -2,5 +2,5 @@ import { PlannerSql } from "./_planner-sql.js";
|
|
|
2
2
|
export declare class PlannerDirectTargetTags {
|
|
3
3
|
#private;
|
|
4
4
|
constructor(sql: PlannerSql);
|
|
5
|
-
listDeleteTagDirectTargetTags(scanId: number, deleteTags: string[], excludeTags: string[], useRegex: boolean, cutoffTimestamp?: string): string[];
|
|
5
|
+
listDeleteTagDirectTargetTags(scanId: number, deleteTags: string[], excludeTags: string[], useRegex: boolean, deleteOrphanedImages: boolean, cutoffTimestamp?: string): string[];
|
|
6
6
|
}
|
|
@@ -5,7 +5,7 @@ export class PlannerDirectTargetTags {
|
|
|
5
5
|
constructor(sql) {
|
|
6
6
|
this.#sql = sql;
|
|
7
7
|
}
|
|
8
|
-
listDeleteTagDirectTargetTags(scanId, deleteTags, excludeTags, useRegex, cutoffTimestamp) {
|
|
8
|
+
listDeleteTagDirectTargetTags(scanId, deleteTags, excludeTags, useRegex, deleteOrphanedImages, cutoffTimestamp) {
|
|
9
9
|
if (deleteTags.length === 0) {
|
|
10
10
|
return [];
|
|
11
11
|
}
|
|
@@ -30,6 +30,8 @@ export class PlannerDirectTargetTags {
|
|
|
30
30
|
olderThanSql = "AND pv.created_at < ?";
|
|
31
31
|
params.push(cutoffTimestamp);
|
|
32
32
|
}
|
|
33
|
+
const digestTagFlag = deleteOrphanedImages ? 1 : 0;
|
|
34
|
+
params.splice(1, 0, digestTagFlag);
|
|
33
35
|
const sql = `
|
|
34
36
|
SELECT DISTINCT tag AS target_tag
|
|
35
37
|
FROM tags t
|
|
@@ -40,7 +42,7 @@ export class PlannerDirectTargetTags {
|
|
|
40
42
|
ON roots.scan_id = t.scan_id
|
|
41
43
|
AND roots.root_version_id = t.version_id
|
|
42
44
|
WHERE t.scan_id = ?
|
|
43
|
-
AND t.is_digest_tag =
|
|
45
|
+
AND t.is_digest_tag = ?
|
|
44
46
|
AND roots.has_ancestor = 0
|
|
45
47
|
AND (${selectedTagPredicate.sql})
|
|
46
48
|
${excludedRootSql}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DeletePlanValidationReasonCodes, DeletePlanValidationStatuses } from "./_planner-types.js";
|
|
1
2
|
export function buildPlanOutputs(directTargetTags, directTargetRoots, planArtifacts) {
|
|
2
3
|
const rootDecisions = buildRootDecisions(directTargetRoots, planArtifacts);
|
|
3
4
|
const protectedRoots = buildProtectedRoots(planArtifacts.blockedRoots);
|
|
@@ -28,8 +29,8 @@ export function buildRootDecisions(directTargetRoots, planArtifacts) {
|
|
|
28
29
|
manifestKind: root.manifestKind,
|
|
29
30
|
selectionMode: root.selectionMode,
|
|
30
31
|
selectionReason: root.reason,
|
|
31
|
-
validationStatus:
|
|
32
|
-
validationReasonCode:
|
|
32
|
+
validationStatus: DeletePlanValidationStatuses.untagOnly,
|
|
33
|
+
validationReasonCode: DeletePlanValidationReasonCodes.untagOnlyPartialTagMatch,
|
|
33
34
|
validationReason: "matched tags cover only part of this root's tag set, so the version is retained and only those tags can be detached"
|
|
34
35
|
};
|
|
35
36
|
}
|
|
@@ -40,8 +41,8 @@ export function buildRootDecisions(directTargetRoots, planArtifacts) {
|
|
|
40
41
|
manifestKind: root.manifestKind,
|
|
41
42
|
selectionMode: root.selectionMode,
|
|
42
43
|
selectionReason: root.reason,
|
|
43
|
-
validationStatus:
|
|
44
|
-
validationReasonCode:
|
|
44
|
+
validationStatus: DeletePlanValidationStatuses.fullyDeletable,
|
|
45
|
+
validationReasonCode: DeletePlanValidationReasonCodes.fullyDeletableNoRetainedOverlap,
|
|
45
46
|
validationReason: "selected tags cover the whole root and its manifest closure does not overlap any retained root"
|
|
46
47
|
};
|
|
47
48
|
}
|
|
@@ -52,8 +53,8 @@ export function buildRootDecisions(directTargetRoots, planArtifacts) {
|
|
|
52
53
|
manifestKind: root.manifestKind,
|
|
53
54
|
selectionMode: root.selectionMode,
|
|
54
55
|
selectionReason: root.reason,
|
|
55
|
-
validationStatus:
|
|
56
|
-
validationReasonCode:
|
|
56
|
+
validationStatus: DeletePlanValidationStatuses.blocked,
|
|
57
|
+
validationReasonCode: DeletePlanValidationReasonCodes.blockedOverlapWithRetainedRoot,
|
|
57
58
|
validationReason: buildBlockedValidationReason(blockedRoot),
|
|
58
59
|
blockingVersionId: blockedRoot?.blockingVersionId,
|
|
59
60
|
blockingDigest: blockedRoot?.blockingDigest,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type Database from "better-sqlite3";
|
|
2
2
|
import type { DeletePlan, PlannerLogger } from "./_planner-types.js";
|
|
3
|
-
export type { DeletePlan, DeletePlanBlockReasonCode, DeletePlanBlockedRoot, DeletePlanClosureManifest, DeletePlanProtectedRoot, DeletePlanRoot, DeletePlanRootDecision, DeletePlanSelectionMode, DeletePlanSelectionReason } from "./_planner-types.js";
|
|
3
|
+
export type { DeletePlan, DeletePlanBlockReasonCode, DeletePlanBlockedRoot, DeletePlanClosureManifest, DeletePlanProtectedRoot, DeletePlanRoot, DeletePlanRootDecision, DeletePlanSelectionMode, DeletePlanSelectionReason, DeletePlanValidationReasonCode, DeletePlanValidationStatus } from "./_planner-types.js";
|
|
4
|
+
export { DeletePlanValidationReasonCodes, DeletePlanValidationStatuses } from "./_planner-types.js";
|
|
4
5
|
export declare class PlannerRepository {
|
|
5
6
|
#private;
|
|
6
7
|
constructor(database: Database.Database, logger?: PlannerLogger);
|
|
@@ -4,6 +4,7 @@ import { PlannerLatestScan } from "./_planner-latest-scan.js";
|
|
|
4
4
|
import { buildPlanOutputs } from "./_planner-output.js";
|
|
5
5
|
import { PlannerPlanArtifacts } from "./_planner-plan-artifacts.js";
|
|
6
6
|
import { PlannerSql } from "./_planner-sql.js";
|
|
7
|
+
export { DeletePlanValidationReasonCodes, DeletePlanValidationStatuses } from "./_planner-types.js";
|
|
7
8
|
export class PlannerRepository {
|
|
8
9
|
#latestScan;
|
|
9
10
|
#directTargetTags;
|
|
@@ -60,10 +61,11 @@ export class PlannerRepository {
|
|
|
60
61
|
const scan = this.#latestScan.get(owner, packageName);
|
|
61
62
|
const deleteTags = options?.deleteTags ?? [];
|
|
62
63
|
const excludeTags = options?.excludeTags ?? [];
|
|
63
|
-
const directTargetTags = this.#directTargetTags.listDeleteTagDirectTargetTags(scan.scan_id, deleteTags, excludeTags, options?.useRegex ?? false, options?.cutoffTimestamp);
|
|
64
|
+
const directTargetTags = this.#directTargetTags.listDeleteTagDirectTargetTags(scan.scan_id, deleteTags, excludeTags, options?.useRegex ?? false, options?.deleteOrphanedImages ?? false, options?.cutoffTimestamp);
|
|
64
65
|
const directTargetRoots = this.#directTargetRoots.list(scan.scan_id, {
|
|
65
66
|
deleteTags,
|
|
66
67
|
deleteTagsRequested: options?.deleteTagsRequested ?? false,
|
|
68
|
+
deleteOrphanedImages: options?.deleteOrphanedImages ?? false,
|
|
67
69
|
excludeTags,
|
|
68
70
|
deleteUntagged: options?.deleteUntagged ?? false,
|
|
69
71
|
keepNTagged: options?.keepNTagged,
|
|
@@ -40,10 +40,22 @@ export interface ScanRow {
|
|
|
40
40
|
export type DeletePlanSelectionMode = "delete-root" | "untag-only";
|
|
41
41
|
export type DeletePlanSelectionReason = "delete-tags-all-tags-selected" | "delete-tags-partial-tag-match" | "delete-untagged" | "keep-n-tagged-overflow" | "keep-n-untagged-overflow";
|
|
42
42
|
export type DeletePlanBlockReasonCode = "overlap-with-retained-root";
|
|
43
|
+
export declare const DeletePlanValidationStatuses: {
|
|
44
|
+
readonly fullyDeletable: "fully-deletable";
|
|
45
|
+
readonly blocked: "blocked";
|
|
46
|
+
readonly untagOnly: "untag-only";
|
|
47
|
+
};
|
|
48
|
+
export type DeletePlanValidationStatus = (typeof DeletePlanValidationStatuses)[keyof typeof DeletePlanValidationStatuses];
|
|
49
|
+
export declare const DeletePlanValidationReasonCodes: {
|
|
50
|
+
readonly untagOnlyPartialTagMatch: "untag-only-partial-tag-match";
|
|
51
|
+
readonly fullyDeletableNoRetainedOverlap: "fully-deletable-no-retained-overlap";
|
|
52
|
+
readonly blockedOverlapWithRetainedRoot: "blocked-overlap-with-retained-root";
|
|
53
|
+
};
|
|
54
|
+
export type DeletePlanValidationReasonCode = (typeof DeletePlanValidationReasonCodes)[keyof typeof DeletePlanValidationReasonCodes];
|
|
43
55
|
export interface DeletePlanRoot {
|
|
44
56
|
versionId: number;
|
|
45
57
|
digest: string;
|
|
46
|
-
manifestKind?:
|
|
58
|
+
manifestKind?: ManifestKind;
|
|
47
59
|
reason: DeletePlanSelectionReason;
|
|
48
60
|
selectionMode: DeletePlanSelectionMode;
|
|
49
61
|
}
|
|
@@ -52,7 +64,7 @@ export interface DeletePlanClosureManifest {
|
|
|
52
64
|
sourceDigest: string;
|
|
53
65
|
memberVersionId: number;
|
|
54
66
|
memberDigest: string;
|
|
55
|
-
memberManifestKind?:
|
|
67
|
+
memberManifestKind?: ManifestKind;
|
|
56
68
|
hopsFromRoot: number;
|
|
57
69
|
memberRole: string;
|
|
58
70
|
}
|
|
@@ -62,22 +74,22 @@ export interface DeletePlanBlockedRoot {
|
|
|
62
74
|
blockingVersionId: number;
|
|
63
75
|
blockingDigest: string;
|
|
64
76
|
overlapDigest: string;
|
|
65
|
-
overlapManifestKind?:
|
|
77
|
+
overlapManifestKind?: ManifestKind;
|
|
66
78
|
reason: DeletePlanBlockReasonCode;
|
|
67
79
|
}
|
|
68
80
|
export interface DeletePlanRootDecision {
|
|
69
81
|
versionId: number;
|
|
70
82
|
digest: string;
|
|
71
|
-
manifestKind?:
|
|
83
|
+
manifestKind?: ManifestKind;
|
|
72
84
|
selectionMode: DeletePlanSelectionMode;
|
|
73
85
|
selectionReason: DeletePlanSelectionReason;
|
|
74
|
-
validationStatus:
|
|
75
|
-
validationReasonCode:
|
|
86
|
+
validationStatus: DeletePlanValidationStatus;
|
|
87
|
+
validationReasonCode: DeletePlanValidationReasonCode;
|
|
76
88
|
validationReason: string;
|
|
77
89
|
blockingVersionId?: number;
|
|
78
90
|
blockingDigest?: string;
|
|
79
91
|
overlapDigest?: string;
|
|
80
|
-
overlapManifestKind?:
|
|
92
|
+
overlapManifestKind?: ManifestKind;
|
|
81
93
|
}
|
|
82
94
|
export interface DeletePlanProtectedRoot {
|
|
83
95
|
versionId: number;
|
|
@@ -87,7 +99,7 @@ export interface DeletePlanProtectedRoot {
|
|
|
87
99
|
blockedDigest: string;
|
|
88
100
|
blockReasonCode: DeletePlanBlockReasonCode;
|
|
89
101
|
overlapDigest: string;
|
|
90
|
-
overlapManifestKind?:
|
|
102
|
+
overlapManifestKind?: ManifestKind;
|
|
91
103
|
}>;
|
|
92
104
|
}
|
|
93
105
|
export interface PlanArtifacts {
|
|
@@ -126,4 +138,5 @@ export declare function mapPlanRootRow(row: _PlanRootRow): DeletePlanRoot;
|
|
|
126
138
|
export declare function mapPlanTagRows(rows: _PlanTagRow[]): string[];
|
|
127
139
|
export declare function mapClosureManifestRow(row: _ClosureManifestRow): DeletePlanClosureManifest;
|
|
128
140
|
export declare function mapBlockedRootRow(row: _BlockedRootRow): DeletePlanBlockedRoot;
|
|
141
|
+
import type { ManifestKind } from "../../core/index.js";
|
|
129
142
|
export {};
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
export const DeletePlanValidationStatuses = {
|
|
2
|
+
fullyDeletable: "fully-deletable",
|
|
3
|
+
blocked: "blocked",
|
|
4
|
+
untagOnly: "untag-only"
|
|
5
|
+
};
|
|
6
|
+
export const DeletePlanValidationReasonCodes = {
|
|
7
|
+
untagOnlyPartialTagMatch: "untag-only-partial-tag-match",
|
|
8
|
+
fullyDeletableNoRetainedOverlap: "fully-deletable-no-retained-overlap",
|
|
9
|
+
blockedOverlapWithRetainedRoot: "blocked-overlap-with-retained-root"
|
|
10
|
+
};
|
|
1
11
|
export const silentPlannerLogger = {
|
|
2
12
|
trace() { },
|
|
3
13
|
debug() { }
|
|
@@ -6,7 +16,7 @@ export function mapPlanRootRow(row) {
|
|
|
6
16
|
return {
|
|
7
17
|
versionId: row.version_id,
|
|
8
18
|
digest: row.root_digest,
|
|
9
|
-
manifestKind: row.root_manifest_kind ?? undefined,
|
|
19
|
+
manifestKind: (row.root_manifest_kind ?? undefined),
|
|
10
20
|
reason: row.direct_target_reason,
|
|
11
21
|
selectionMode: row.selection_mode
|
|
12
22
|
};
|
|
@@ -20,7 +30,7 @@ export function mapClosureManifestRow(row) {
|
|
|
20
30
|
sourceDigest: row.source_digest,
|
|
21
31
|
memberVersionId: row.member_version_id,
|
|
22
32
|
memberDigest: row.member_digest,
|
|
23
|
-
memberManifestKind: row.member_manifest_kind ?? undefined,
|
|
33
|
+
memberManifestKind: (row.member_manifest_kind ?? undefined),
|
|
24
34
|
hopsFromRoot: row.hops_from_root,
|
|
25
35
|
memberRole: row.member_role
|
|
26
36
|
};
|
|
@@ -32,7 +42,7 @@ export function mapBlockedRootRow(row) {
|
|
|
32
42
|
blockingVersionId: row.blocking_version_id,
|
|
33
43
|
blockingDigest: row.blocking_digest,
|
|
34
44
|
overlapDigest: row.overlap_digest,
|
|
35
|
-
overlapManifestKind: row.overlap_manifest_kind ?? undefined,
|
|
45
|
+
overlapManifestKind: (row.overlap_manifest_kind ?? undefined),
|
|
36
46
|
reason: row.block_reason
|
|
37
47
|
};
|
|
38
48
|
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { PlannerRepository } from "./_planner-repository.js";
|
|
2
|
-
export
|
|
2
|
+
export { DeletePlanValidationReasonCodes, DeletePlanValidationStatuses } from "./_planner-repository.js";
|
|
3
|
+
export type { DeletePlan, DeletePlanBlockReasonCode, DeletePlanBlockedRoot, DeletePlanClosureManifest, DeletePlanProtectedRoot, DeletePlanRoot, DeletePlanRootDecision, DeletePlanSelectionMode, DeletePlanSelectionReason, DeletePlanValidationReasonCode, DeletePlanValidationStatus } from "./_planner-repository.js";
|
package/dist/db/planner/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DeletePlan } from "../db/index.js";
|
|
2
2
|
import { type DeleteExecutionOptions, type DeleteExecutionSummary } from "./_types.js";
|
|
3
3
|
export declare function executeDeletePlan(plan: DeletePlan, options: DeleteExecutionOptions): Promise<DeleteExecutionSummary>;
|