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.
Files changed (48) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +14 -13
  3. package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +0 -1
  4. package/dist/cleanup-summary/_cleanup-summary-markdown.js +146 -33
  5. package/dist/cleanup-summary/_cleanup-summary.d.ts +20 -7
  6. package/dist/cleanup-summary/_cleanup-summary.js +24 -8
  7. package/dist/cleanup-summary/index.d.ts +1 -1
  8. package/dist/cli/_cleanup-command.js +82 -23
  9. package/dist/cli/_json-output.d.ts +1 -0
  10. package/dist/cli/_json-output.js +11 -0
  11. package/dist/cli/_tag-selector-resolver.js +7 -4
  12. package/dist/cli/_untag-command.js +2 -1
  13. package/dist/cli/index.js +2 -2
  14. package/dist/core/_types.d.ts +9 -1
  15. package/dist/core/_types.js +8 -1
  16. package/dist/core/index.d.ts +1 -0
  17. package/dist/core/index.js +1 -0
  18. package/dist/db/_cleanup-run-writer.d.ts +1 -1
  19. package/dist/db/_cleanup-run-writer.js +28 -7
  20. package/dist/db/_db-merge-cleanup-copy.js +4 -2
  21. package/dist/db/_manifest-kind-refinement.d.ts +2 -0
  22. package/dist/db/_manifest-kind-refinement.js +43 -0
  23. package/dist/db/_scan-writer.js +4 -1
  24. package/dist/db/index.d.ts +2 -1
  25. package/dist/db/index.js +1 -0
  26. package/dist/db/planner/_planner-direct-target-roots.d.ts +1 -0
  27. package/dist/db/planner/_planner-direct-target-roots.js +23 -12
  28. package/dist/db/planner/_planner-direct-target-tags.d.ts +1 -1
  29. package/dist/db/planner/_planner-direct-target-tags.js +4 -2
  30. package/dist/db/planner/_planner-output.js +7 -6
  31. package/dist/db/planner/_planner-repository.d.ts +2 -1
  32. package/dist/db/planner/_planner-repository.js +3 -1
  33. package/dist/db/planner/_planner-types.d.ts +21 -8
  34. package/dist/db/planner/_planner-types.js +13 -3
  35. package/dist/db/planner/index.d.ts +2 -1
  36. package/dist/db/planner/index.js +1 -0
  37. package/dist/execute/_plan-executor.d.ts +1 -1
  38. package/dist/execute/_plan-executor.js +35 -9
  39. package/dist/ingest/github/_manifest-kind.d.ts +1 -1
  40. package/dist/ingest/github/_manifest-kind.js +6 -5
  41. package/package.json +1 -1
  42. package/resources/sql/schema/001_schema.sql +4 -1
  43. package/resources/sql/views/002_v_missing_digests.sql +0 -32
  44. /package/resources/sql/views/{003_v_scan_root_manifests.sql → 002_v_scan_root_manifests.sql} +0 -0
  45. /package/resources/sql/views/{004_v_digest_tag_relations.sql → 003_v_digest_tag_relations.sql} +0 -0
  46. /package/resources/sql/views/{005_v_cleanup_root_closure_members.sql → 004_v_cleanup_root_closure_members.sql} +0 -0
  47. /package/resources/sql/views/{006_v_cleanup_blocking_overlaps.sql → 005_v_cleanup_blocking_overlaps.sql} +0 -0
  48. /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, "all-missing");
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, "some-missing");
24
+ return _listLatestBrokenIndexTags(database, owner, packageName, cutoffTimestamp, BrokenIndexModes.someMissing);
21
25
  }
22
26
  function _listLatestBrokenIndexTags(database, owner, packageName, cutoffTimestamp, mode) {
23
- const havingClause = mode === "all-missing"
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
- console.log(JSON.stringify(summary));
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
@@ -1,4 +1,12 @@
1
- export type ManifestKind = "image_index" | "image_manifest" | "artifact_manifest" | "attestation_manifest" | "signature_manifest";
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;
@@ -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
+ };
@@ -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";
@@ -1,3 +1,4 @@
1
+ export { ManifestKinds } from "./_types.js";
1
2
  export { buildHttpErrorMessage } from "./_http-error.js";
2
3
  export { getOwnerURIComponent } from "./_github-package-owner.js";
3
4
  export { digestFromDigestTag, isDigestTag } from "./_digest-tag.js";
@@ -1,5 +1,5 @@
1
1
  import type Database from "better-sqlite3";
2
- import type { DeletePlan } from "./planner/index.js";
2
+ import { type DeletePlan } from "./planner/index.js";
3
3
  export declare class CleanupRunWriter {
4
4
  #private;
5
5
  constructor(database: Database.Database);
@@ -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 === "untag-only").length;
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 === "blocked").length;
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,2 @@
1
+ import type Database from "better-sqlite3";
2
+ export declare function refineManifestKinds(database: Database.Database, scanId: number): void;
@@ -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
+ }
@@ -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
- rebuildManifestReachability(this.#database, this.#requireScanId());
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();
@@ -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);
@@ -3,6 +3,7 @@ import { type DeletePlanRoot } from "./_planner-types.js";
3
3
  export interface DirectTargetRootOptions {
4
4
  deleteTags: string[];
5
5
  deleteTagsRequested: boolean;
6
+ deleteOrphanedImages?: boolean;
6
7
  excludeTags: string[];
7
8
  deleteUntagged: boolean;
8
9
  keepNTagged?: number;
@@ -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
- SELECT DISTINCT t.version_id, t.tag
23
- FROM tags t
24
- WHERE t.scan_id = ?
25
- AND t.is_digest_tag = 0
26
- AND (${selectedTagPredicate.sql})
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
- SELECT NULL AS version_id, NULL AS tag
30
- WHERE 1 = 0
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
- rc.tag_count AS total_tag_count,
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 rc.is_tagged = 1
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 = 0
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: "untag-only",
32
- validationReasonCode: "untag-only-partial-tag-match",
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: "fully-deletable",
44
- validationReasonCode: "fully-deletable-no-retained-overlap",
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: "blocked",
56
- validationReasonCode: "blocked-overlap-with-retained-root",
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?: string;
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?: string;
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?: string;
77
+ overlapManifestKind?: ManifestKind;
66
78
  reason: DeletePlanBlockReasonCode;
67
79
  }
68
80
  export interface DeletePlanRootDecision {
69
81
  versionId: number;
70
82
  digest: string;
71
- manifestKind?: string;
83
+ manifestKind?: ManifestKind;
72
84
  selectionMode: DeletePlanSelectionMode;
73
85
  selectionReason: DeletePlanSelectionReason;
74
- validationStatus: "fully-deletable" | "blocked" | "untag-only";
75
- validationReasonCode: "untag-only-partial-tag-match" | "fully-deletable-no-retained-overlap" | "blocked-overlap-with-retained-root";
86
+ validationStatus: DeletePlanValidationStatus;
87
+ validationReasonCode: DeletePlanValidationReasonCode;
76
88
  validationReason: string;
77
89
  blockingVersionId?: number;
78
90
  blockingDigest?: string;
79
91
  overlapDigest?: string;
80
- overlapManifestKind?: string;
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?: string;
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 type { DeletePlan, DeletePlanBlockReasonCode, DeletePlanBlockedRoot, DeletePlanClosureManifest, DeletePlanProtectedRoot, DeletePlanRoot, DeletePlanRootDecision, DeletePlanSelectionMode, DeletePlanSelectionReason } from "./_planner-repository.js";
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";
@@ -1 +1,2 @@
1
1
  export { PlannerRepository } from "./_planner-repository.js";
2
+ export { DeletePlanValidationReasonCodes, DeletePlanValidationStatuses } from "./_planner-repository.js";
@@ -1,3 +1,3 @@
1
- import type { DeletePlan } from "../db/index.js";
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>;