ghcr-manager 0.9.4 → 0.9.6

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 (32) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +9 -9
  3. package/dist/cleanup-summary/_cleanup-summary-markdown.js +7 -6
  4. package/dist/cleanup-summary/_cleanup-summary.d.ts +8 -4
  5. package/dist/cleanup-summary/_cleanup-summary.js +8 -4
  6. package/dist/cleanup-summary/index.d.ts +1 -1
  7. package/dist/cli/_cleanup-command.js +22 -2
  8. package/dist/cli/_tag-selector-resolver.js +2 -2
  9. package/dist/core/_digest-tag.d.ts +2 -0
  10. package/dist/core/_digest-tag.js +12 -0
  11. package/dist/core/_types.d.ts +2 -1
  12. package/dist/core/index.d.ts +2 -1
  13. package/dist/core/index.js +1 -0
  14. package/dist/db/_cleanup-run-writer.js +69 -50
  15. package/dist/db/_db-merge-cleanup-copy.js +128 -80
  16. package/dist/db/_db-merge-scan-copy.js +1 -1
  17. package/dist/db/_manifest-reachability.js +25 -0
  18. package/dist/db/_scan-writer.js +118 -116
  19. package/dist/db/index.d.ts +1 -1
  20. package/dist/db/planner/_planner-direct-target-roots.js +2 -0
  21. package/dist/db/planner/_planner-direct-target-tags.js +5 -0
  22. package/dist/db/planner/_planner-output.d.ts +1 -1
  23. package/dist/db/planner/_planner-output.js +0 -11
  24. package/dist/db/planner/_planner-repository.d.ts +1 -1
  25. package/dist/db/planner/_planner-types.d.ts +12 -18
  26. package/dist/db/planner/index.d.ts +1 -1
  27. package/package.json +1 -1
  28. package/resources/sql/schema/001_schema.sql +20 -0
  29. package/resources/sql/views/003_v_scan_root_manifests.sql +1 -0
  30. package/resources/sql/views/{004_v_digest_derived_tag_relations.sql → 004_v_digest_tag_relations.sql} +7 -8
  31. package/resources/sql/views/005_v_cleanup_root_closure_members.sql +2 -1
  32. package/resources/sql/views/007_v_cleanup_root_decision_readable.sql +67 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.6] - 2026-05-21
11
+
12
+ ### Added
13
+
14
+ - Cleanup audit now persists concrete selected tags in `cleanup_selected_tags`.
15
+ - Cleanup schema docs now include a readable cleanup-decision view plus example SQL queries for audit inspection.
16
+ - GHCR digest-tag helper relations are now modeled explicitly in scan data and manifest reachability.
17
+
18
+ ### Changed
19
+
20
+ - Cleanup summary JSON now exposes derived affected manifests for fully deletable roots.
21
+ - Cleanup Markdown now reads displayed counts from the summary arrays instead of carrying duplicate count fields.
22
+ - Cleanup decision audit fields are now constrained more tightly in SQLite and TypeScript, including `selection_mode`,
23
+ `selection_reason`, and related block reason codes.
24
+ - Digest-tag helper artifacts are now classified on `tags.is_digest_tag` and excluded from normal user-facing tag
25
+ selection and output.
26
+ - Digest-tag helper terminology was simplified across code and SQL surfaces.
27
+ - Schema docs now include a table of contents and collapsible example query blocks for easier GitHub browsing.
28
+
29
+ ### Fixed
30
+
31
+ - Fixed remote action path handling for artifact upload and merge helper actions.
32
+ - Cleanup reachability now follows digest-tag helper edges recursively, matching helper-artifact cascades more closely.
33
+
34
+ ## [0.9.5] - 2026-05-21
35
+
36
+ ### Changed
37
+
38
+ - Renamed `upload-db-artifact` to `upload-artifacts`.
39
+ - Raised cleanup summary defaults to 100 matched tags and 100 roots per section.
40
+
10
41
  ## [0.9.4] - 2026-05-21
11
42
 
12
43
  ### Changed
package/README.md CHANGED
@@ -33,7 +33,7 @@ jobs:
33
33
 
34
34
  - name: Preview GHCR cleanup
35
35
  id: ghcr-manager
36
- uses: gh-workflow/ghcr-manager@0.9.4
36
+ uses: gh-workflow/ghcr-manager@0.9.6
37
37
  with:
38
38
  command: cleanup
39
39
  token: ${{ github.token }}
@@ -44,7 +44,7 @@ jobs:
44
44
  keep-n-tagged: "10"
45
45
  exclude-tags: |
46
46
  latest
47
- upload-db-artifact: true
47
+ upload-artifacts: true
48
48
  ```
49
49
 
50
50
  After the run:
@@ -72,7 +72,7 @@ The action supports three commands:
72
72
  ### Preview cleanup
73
73
 
74
74
  ```yaml
75
- - uses: gh-workflow/ghcr-manager@0.9.4
75
+ - uses: gh-workflow/ghcr-manager@0.9.6
76
76
  with:
77
77
  command: cleanup
78
78
  token: ${{ github.token }}
@@ -87,13 +87,13 @@ The action supports three commands:
87
87
  exclude-tags: |
88
88
  latest
89
89
  stable
90
- upload-db-artifact: true
90
+ upload-artifacts: true
91
91
  ```
92
92
 
93
93
  ### Apply cleanup
94
94
 
95
95
  ```yaml
96
- - uses: gh-workflow/ghcr-manager@0.9.4
96
+ - uses: gh-workflow/ghcr-manager@0.9.6
97
97
  with:
98
98
  command: cleanup
99
99
  token: ${{ github.token }}
@@ -101,7 +101,7 @@ The action supports three commands:
101
101
  package: PACKAGE
102
102
  delete-untagged: true
103
103
  keep-n-tagged: "10"
104
- upload-db-artifact: true
104
+ upload-artifacts: true
105
105
  scan-after-cleanup: true
106
106
  ```
107
107
 
@@ -112,7 +112,7 @@ Note: the second scan only runs if cleanup actually makes changes.
112
112
  ### Remove selected tags directly
113
113
 
114
114
  ```yaml
115
- - uses: gh-workflow/ghcr-manager@0.9.4
115
+ - uses: gh-workflow/ghcr-manager@0.9.6
116
116
  with:
117
117
  command: untag
118
118
  token: ${{ github.token }}
@@ -128,7 +128,7 @@ Note: the second scan only runs if cleanup actually makes changes.
128
128
  ### Scan one package
129
129
 
130
130
  ```yaml
131
- - uses: gh-workflow/ghcr-manager@0.9.4
131
+ - uses: gh-workflow/ghcr-manager@0.9.6
132
132
  with:
133
133
  command: scan
134
134
  token: ${{ github.token }}
@@ -149,7 +149,7 @@ Note: the second scan only runs if cleanup actually makes changes.
149
149
  | `owner` | Package owner | all | Yes | |
150
150
  | `package` | Package name | all | Yes | |
151
151
  | `db-path` | Local SQLite DB path | s,c | No | |
152
- | `upload-db-artifact` | Upload DB and summary artifact | s,c | No | `false` |
152
+ | `upload-artifacts` | Upload DB and summary artifacts | s,c | No | `false` |
153
153
  | `scan-after-cleanup` | Run a second scan after cleanup | c | No | `false` |
154
154
  | `db-artifact-retention-days` | Override artifact retention days | s,c | No | `${{ github.retention_days }}` |
155
155
  | `delete-tags` | Newline-separated tags to delete | c,u | for `untag` | |
@@ -1,5 +1,5 @@
1
- const _DEFAULT_MAX_DIRECT_TARGET_TAGS = 20;
2
- const _DEFAULT_MAX_ROOTS_PER_SECTION = 20;
1
+ const _DEFAULT_MAX_DIRECT_TARGET_TAGS = 100;
2
+ const _DEFAULT_MAX_ROOTS_PER_SECTION = 100;
3
3
  const _DEFAULT_MAX_TAGS_PER_ROOT = 4;
4
4
  export function renderCleanupSummaryMarkdown(summary, options) {
5
5
  const maxDirectTargetTags = options.maxDirectTargetTags ?? _DEFAULT_MAX_DIRECT_TARGET_TAGS;
@@ -12,10 +12,11 @@ export function renderCleanupSummaryMarkdown(summary, options) {
12
12
  "| --- | --- |",
13
13
  `| 📦 Package | \`${_escapeInlineCode(`${summary.owner}/${summary.packageName}`)}\` |`,
14
14
  `| ⚙️ Mode | ${summary.dryRun ? "Cleanup dry-run" : "Cleanup"} |`,
15
- `| 🏷️ Matched tags | ${summary.validationSummary.directTargetTagCount} |`,
16
- `| 🗑️ Fully deletable roots | ${summary.validationSummary.fullyDeletableRootCount} |`,
17
- `| 🔗 Untag-only roots | ${summary.validationSummary.untagOnlyRootCount} |`,
18
- `| 🛡️ Blocked roots | ${summary.validationSummary.blockedDeleteRootCount} |`,
15
+ `| 🏷️ Matched tags | ${summary.directTargetTags.length} |`,
16
+ `| 🗑️ Fully deletable roots | ${summary.fullyDeletableRoots.length} |`,
17
+ `| 🔗 Untag-only roots | ${summary.untagOnlyRoots.length} |`,
18
+ `| 🛡️ Blocked roots | ${summary.blockedRoots.length} |`,
19
+ `| 📄 Affected manifests | ${summary.affectedManifests.length} |`,
19
20
  ""
20
21
  ];
21
22
  lines.push(..._renderJsonDetails("⚙️ Cleanup filter", summary.plannerInputs));
@@ -1,4 +1,4 @@
1
- import type { DeletePlan } from "../db/index.js";
1
+ import type { DeletePlan, DeletePlanSelectionMode, DeletePlanSelectionReason } from "../db/index.js";
2
2
  import type { DeleteExecutionSummary } from "../execute/index.js";
3
3
  export interface CleanupSummaryRoot {
4
4
  versionId: number;
@@ -6,8 +6,8 @@ export interface CleanupSummaryRoot {
6
6
  manifestKind?: string;
7
7
  rootTags: string[];
8
8
  matchedTags: string[];
9
- selectionMode: string;
10
- selectionReason: string;
9
+ selectionMode: DeletePlanSelectionMode;
10
+ selectionReason: DeletePlanSelectionReason;
11
11
  validationStatus: "fully-deletable" | "blocked" | "untag-only";
12
12
  validationReasonCode: "untag-only-partial-tag-match" | "fully-deletable-no-retained-overlap" | "blocked-overlap-with-retained-root";
13
13
  validationReason: string;
@@ -16,6 +16,9 @@ export interface CleanupSummaryRoot {
16
16
  overlapDigest?: string;
17
17
  overlapManifestKind?: string;
18
18
  }
19
+ export interface CleanupSummaryAffectedManifest {
20
+ digest: string;
21
+ }
19
22
  export interface CleanupSummary {
20
23
  command: "cleanup";
21
24
  owner: string;
@@ -23,12 +26,12 @@ export interface CleanupSummary {
23
26
  scanCompletedAt: string;
24
27
  dryRun: boolean;
25
28
  plannerInputs: DeletePlan["plannerInputs"];
26
- validationSummary: DeletePlan["validationSummary"];
27
29
  directTargetTags: string[];
28
30
  collateralTags: string[];
29
31
  fullyDeletableRoots: CleanupSummaryRoot[];
30
32
  untagOnlyRoots: CleanupSummaryRoot[];
31
33
  blockedRoots: CleanupSummaryRoot[];
34
+ affectedManifests: CleanupSummaryAffectedManifest[];
32
35
  deletedPackageVersions: DeleteExecutionSummary["deletedPackageVersions"];
33
36
  untaggedTags: DeleteExecutionSummary["untaggedTags"];
34
37
  unsupportedUntagRoots: DeleteExecutionSummary["unsupportedUntagRoots"];
@@ -36,5 +39,6 @@ export interface CleanupSummary {
36
39
  export declare function buildCleanupSummary(plan: DeletePlan, options: {
37
40
  dryRun: boolean;
38
41
  listRootTags: (versionId: number) => string[];
42
+ listAffectedManifestDigests: (rootDigests: string[]) => string[];
39
43
  executionSummary?: DeleteExecutionSummary;
40
44
  }): CleanupSummary;
@@ -1,6 +1,10 @@
1
1
  export function buildCleanupSummary(plan, options) {
2
2
  const directTargetTagSet = new Set(plan.directTargetTags);
3
3
  const roots = plan.rootDecisions.map((decision) => _mapRootDecision(decision, directTargetTagSet, options.listRootTags));
4
+ const fullyDeletableRoots = roots.filter((root) => root.validationStatus === "fully-deletable");
5
+ const blockedRoots = roots.filter((root) => root.validationStatus === "blocked");
6
+ const untagOnlyRoots = roots.filter((root) => root.validationStatus === "untag-only");
7
+ const affectedManifestDigests = options.listAffectedManifestDigests(fullyDeletableRoots.map((root) => root.digest));
4
8
  return {
5
9
  command: "cleanup",
6
10
  owner: plan.owner,
@@ -8,12 +12,12 @@ export function buildCleanupSummary(plan, options) {
8
12
  scanCompletedAt: plan.scanCompletedAt,
9
13
  dryRun: options.dryRun,
10
14
  plannerInputs: plan.plannerInputs,
11
- validationSummary: plan.validationSummary,
12
15
  directTargetTags: plan.directTargetTags,
13
16
  collateralTags: plan.collateralTags,
14
- fullyDeletableRoots: roots.filter((root) => root.validationStatus === "fully-deletable"),
15
- untagOnlyRoots: roots.filter((root) => root.validationStatus === "untag-only"),
16
- blockedRoots: roots.filter((root) => root.validationStatus === "blocked"),
17
+ fullyDeletableRoots,
18
+ untagOnlyRoots,
19
+ blockedRoots,
20
+ affectedManifests: affectedManifestDigests.map((digest) => ({ digest })),
17
21
  deletedPackageVersions: options.executionSummary?.deletedPackageVersions ?? [],
18
22
  untaggedTags: options.executionSummary?.untaggedTags ?? [],
19
23
  unsupportedUntagRoots: options.executionSummary?.unsupportedUntagRoots ?? []
@@ -1,2 +1,2 @@
1
- export { buildCleanupSummary, type CleanupSummary, type CleanupSummaryRoot } from "./_cleanup-summary.js";
1
+ export { buildCleanupSummary, type CleanupSummary, type CleanupSummaryAffectedManifest, type CleanupSummaryRoot } from "./_cleanup-summary.js";
2
2
  export { renderCleanupSummaryMarkdown } from "./_cleanup-summary-markdown.js";
@@ -14,16 +14,18 @@ export async function handleCleanup(args) {
14
14
  try {
15
15
  const repository = new PlannerRepository(database, logger);
16
16
  const cleanupRunWriter = new CleanupRunWriter(database);
17
+ const scanId = repository.getLatestCompletedScanId(inputs.owner, inputs.packageName);
17
18
  logger.debug(`Starting cleanup for ${inputs.owner}/${inputs.packageName}`);
18
19
  const plan = loadDeletePlan(repository, resolveTagSelectors(database, inputs));
19
- cleanupRunWriter.persistCleanupRun(repository.getLatestCompletedScanId(inputs.owner, inputs.packageName), plan, {
20
+ cleanupRunWriter.persistCleanupRun(scanId, plan, {
20
21
  dryRun,
21
22
  cleanupStartedAt: new Date().toISOString()
22
23
  });
23
24
  if (dryRun) {
24
25
  const summary = buildCleanupSummary(plan, {
25
26
  dryRun: true,
26
- listRootTags: (versionId) => _listRootTags(database, inputs.owner, inputs.packageName, versionId)
27
+ listRootTags: (versionId) => _listRootTags(database, inputs.owner, inputs.packageName, versionId),
28
+ listAffectedManifestDigests: (rootDigests) => _listAffectedManifestDigests(database, scanId, rootDigests)
27
29
  });
28
30
  logger.debug(`Completed dry-run cleanup for ${inputs.owner}/${inputs.packageName}`);
29
31
  console.log(JSON.stringify(summary));
@@ -37,6 +39,7 @@ export async function handleCleanup(args) {
37
39
  const summary = buildCleanupSummary(plan, {
38
40
  dryRun: false,
39
41
  listRootTags: (versionId) => _listRootTags(database, inputs.owner, inputs.packageName, versionId),
42
+ listAffectedManifestDigests: (rootDigests) => _listAffectedManifestDigests(database, scanId, rootDigests),
40
43
  executionSummary
41
44
  });
42
45
  logger.debug(`Completed cleanup for ${inputs.owner}/${inputs.packageName}`);
@@ -47,6 +50,22 @@ export async function handleCleanup(args) {
47
50
  database.close();
48
51
  }
49
52
  }
53
+ function _listAffectedManifestDigests(database, scanId, rootDigests) {
54
+ if (rootDigests.length === 0) {
55
+ return [];
56
+ }
57
+ const placeholders = rootDigests.map(() => "?").join(", ");
58
+ const rows = database
59
+ .prepare(`
60
+ SELECT DISTINCT descendant_digest AS digest
61
+ FROM manifest_reachability
62
+ WHERE scan_id = ?
63
+ AND ancestor_digest IN (${placeholders})
64
+ ORDER BY descendant_digest
65
+ `)
66
+ .all(scanId, ...rootDigests);
67
+ return rows.map((row) => row.digest);
68
+ }
50
69
  function _listRootTags(database, owner, packageName, versionId) {
51
70
  const rows = database
52
71
  .prepare(`
@@ -56,6 +75,7 @@ function _listRootTags(database, owner, packageName, versionId) {
56
75
  WHERE latest_scan.owner = ?
57
76
  AND latest_scan.package_name = ?
58
77
  AND tags.version_id = ?
78
+ AND tags.is_digest_tag = 0
59
79
  ORDER BY tags.tag
60
80
  `)
61
81
  .all(owner, packageName, versionId);
@@ -77,7 +77,7 @@ function _listLatestBrokenIndexTags(database, owner, packageName, cutoffTimestam
77
77
  return rows.map((row) => row.tag);
78
78
  }
79
79
  // Some OCI tooling publishes companion artifacts such as signatures or attestations under
80
- // digest-derived tags in the same repository, for example `sha256-<digest>.sig`, while the
80
+ // digest tags in the same repository, for example `sha256-<digest>.sig`, while the
81
81
  // actual relationship is the artifact's subject/referrer link to the parent digest.
82
82
  //
83
83
  // Public references:
@@ -94,7 +94,7 @@ function _listLatestOrphanedTags(database, owner, packageName, cutoffTimestamp)
94
94
  const rows = database
95
95
  .prepare(`
96
96
  SELECT DISTINCT dtr.tag
97
- FROM v_digest_derived_tag_relations dtr
97
+ FROM v_digest_tag_relations dtr
98
98
  INNER JOIN package_versions pv
99
99
  ON pv.scan_id = dtr.scan_id
100
100
  AND pv.version_id = dtr.artifact_version_id
@@ -0,0 +1,2 @@
1
+ export declare function isDigestTag(tag: string): boolean;
2
+ export declare function digestFromDigestTag(tag: string): string | null;
@@ -0,0 +1,12 @@
1
+ export function isDigestTag(tag) {
2
+ return tag.startsWith("sha256-") && tag.length >= 71 && !_containsNonHex(tag.slice(7, 71));
3
+ }
4
+ export function digestFromDigestTag(tag) {
5
+ if (!isDigestTag(tag)) {
6
+ return null;
7
+ }
8
+ return `sha256:${tag.slice(7, 71)}`;
9
+ }
10
+ function _containsNonHex(value) {
11
+ return /[^0-9A-Fa-f]/.test(value);
12
+ }
@@ -1,4 +1,5 @@
1
1
  export type ManifestKind = "image_index" | "image_manifest" | "artifact_manifest" | "attestation_manifest" | "signature_manifest";
2
+ export type ManifestEdgeKind = "image-child" | "referrer" | "digest-tag-referrer";
2
3
  export interface PackageVersionRecord {
3
4
  versionId: number;
4
5
  createdAt: string;
@@ -38,7 +39,7 @@ export interface ManifestDescriptorRecord {
38
39
  export interface ManifestEdgeRecord {
39
40
  parentDigest: string;
40
41
  childDigest: string;
41
- edgeKind: "image-child" | "referrer";
42
+ edgeKind: ManifestEdgeKind;
42
43
  }
43
44
  export interface PackageSnapshot {
44
45
  packageName: string;
@@ -1,4 +1,5 @@
1
- export type { ManifestEdgeRecord, ManifestDescriptorRecord, ManifestKind, ManifestRecord, PackageSnapshot, PackageVersionRecord, TagRecord } from "./_types.js";
1
+ export type { ManifestEdgeKind, ManifestEdgeRecord, ManifestDescriptorRecord, ManifestKind, ManifestRecord, PackageSnapshot, PackageVersionRecord, TagRecord } from "./_types.js";
2
2
  export type { HttpErrorResponse } from "./_http-error.js";
3
3
  export { buildHttpErrorMessage } from "./_http-error.js";
4
4
  export { getOwnerURIComponent } from "./_github-package-owner.js";
5
+ export { digestFromDigestTag, isDigestTag } from "./_digest-tag.js";
@@ -1,2 +1,3 @@
1
1
  export { buildHttpErrorMessage } from "./_http-error.js";
2
2
  export { getOwnerURIComponent } from "./_github-package-owner.js";
3
+ export { digestFromDigestTag, isDigestTag } from "./_digest-tag.js";
@@ -2,72 +2,91 @@ import { randomUUID } from "node:crypto";
2
2
  import { resolveGitHubActionsRunUrl } from "./_github-actions-run-url.js";
3
3
  export class CleanupRunWriter {
4
4
  #database;
5
+ #insertSelectedTagStatement;
6
+ #insertRootDecisionStatement;
7
+ #insertProtectedRootBlockStatement;
8
+ #insertCleanupRunStatement;
5
9
  constructor(database) {
6
10
  this.#database = database;
11
+ this.#insertSelectedTagStatement = this.#database.prepare(`
12
+ INSERT INTO cleanup_selected_tags(
13
+ cleanup_run_id,
14
+ scan_id,
15
+ tag
16
+ )
17
+ VALUES(?, ?, ?)
18
+ `);
19
+ this.#insertRootDecisionStatement = this.#database.prepare(`
20
+ INSERT INTO cleanup_root_decisions(
21
+ cleanup_run_id,
22
+ scan_id,
23
+ digest,
24
+ selection_mode,
25
+ selection_reason,
26
+ validation_status,
27
+ validation_reason_code,
28
+ validation_reason,
29
+ blocking_digest,
30
+ overlap_digest
31
+ )
32
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
33
+ `);
34
+ this.#insertProtectedRootBlockStatement = this.#database.prepare(`
35
+ INSERT INTO cleanup_protected_root_blocks(
36
+ cleanup_run_id,
37
+ scan_id,
38
+ protected_digest,
39
+ blocked_digest,
40
+ block_reason_code,
41
+ overlap_digest
42
+ )
43
+ VALUES(?, ?, ?, ?, ?, ?)
44
+ `);
45
+ this.#insertCleanupRunStatement = this.#database.prepare(`
46
+ INSERT INTO cleanup_runs(
47
+ scan_id,
48
+ cleanup_uuid,
49
+ cleanup_started_at,
50
+ github_actions_run_url,
51
+ dry_run,
52
+ planner_inputs_json,
53
+ direct_target_tag_count,
54
+ direct_target_root_count,
55
+ delete_root_candidate_count,
56
+ untag_only_root_count,
57
+ fully_deletable_root_count,
58
+ blocked_delete_root_count,
59
+ protected_root_count
60
+ )
61
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
62
+ `);
7
63
  }
8
64
  persistCleanupRun(scanId, plan, options) {
9
65
  return this.#database.transaction(() => {
10
66
  const cleanupRunId = this.#insertCleanupRun(scanId, plan, options);
67
+ for (const tag of plan.directTargetTags) {
68
+ this.#insertSelectedTagStatement.run(cleanupRunId, scanId, tag);
69
+ }
11
70
  for (const rootDecision of plan.rootDecisions) {
12
- this.#database
13
- .prepare(`
14
- INSERT INTO cleanup_root_decisions(
15
- cleanup_run_id,
16
- scan_id,
17
- digest,
18
- selection_mode,
19
- selection_reason,
20
- validation_status,
21
- validation_reason_code,
22
- validation_reason,
23
- blocking_digest,
24
- overlap_digest
25
- )
26
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
27
- `)
28
- .run(cleanupRunId, scanId, rootDecision.digest, rootDecision.selectionMode, rootDecision.selectionReason, rootDecision.validationStatus, rootDecision.validationReasonCode, rootDecision.validationReason, rootDecision.blockingDigest ?? null, rootDecision.overlapDigest ?? null);
71
+ this.#insertRootDecisionStatement.run(cleanupRunId, scanId, rootDecision.digest, rootDecision.selectionMode, rootDecision.selectionReason, rootDecision.validationStatus, rootDecision.validationReasonCode, rootDecision.validationReason, rootDecision.blockingDigest ?? null, rootDecision.overlapDigest ?? null);
29
72
  }
30
73
  for (const protectedRoot of plan.protectedRoots) {
31
74
  for (const block of protectedRoot.blocks) {
32
- this.#database
33
- .prepare(`
34
- INSERT INTO cleanup_protected_root_blocks(
35
- cleanup_run_id,
36
- scan_id,
37
- protected_digest,
38
- blocked_digest,
39
- block_reason_code,
40
- overlap_digest
41
- )
42
- VALUES(?, ?, ?, ?, ?, ?)
43
- `)
44
- .run(cleanupRunId, scanId, protectedRoot.digest, block.blockedDigest, block.blockReasonCode, block.overlapDigest);
75
+ this.#insertProtectedRootBlockStatement.run(cleanupRunId, scanId, protectedRoot.digest, block.blockedDigest, block.blockReasonCode, block.overlapDigest);
45
76
  }
46
77
  }
47
78
  return cleanupRunId;
48
79
  })();
49
80
  }
50
81
  #insertCleanupRun(scanId, plan, options) {
51
- const result = this.#database
52
- .prepare(`
53
- INSERT INTO cleanup_runs(
54
- scan_id,
55
- cleanup_uuid,
56
- cleanup_started_at,
57
- github_actions_run_url,
58
- dry_run,
59
- planner_inputs_json,
60
- direct_target_tag_count,
61
- direct_target_root_count,
62
- delete_root_candidate_count,
63
- untag_only_root_count,
64
- fully_deletable_root_count,
65
- blocked_delete_root_count,
66
- protected_root_count
67
- )
68
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
69
- `)
70
- .run(scanId, randomUUID(), options.cleanupStartedAt, resolveGitHubActionsRunUrl(), options.dryRun ? 1 : 0, JSON.stringify(plan.plannerInputs), plan.validationSummary.directTargetTagCount, plan.validationSummary.directTargetRootCount, plan.validationSummary.deleteRootCandidateCount, plan.validationSummary.untagOnlyRootCount, plan.validationSummary.fullyDeletableRootCount, plan.validationSummary.blockedDeleteRootCount, plan.validationSummary.protectedRootCount);
82
+ const directTargetTagCount = plan.directTargetTags.length;
83
+ const directTargetRootCount = plan.directTargetRoots.length;
84
+ const deleteRootCandidateCount = plan.directTargetRoots.filter((root) => root.selectionMode === "delete-root").length;
85
+ const untagOnlyRootCount = plan.rootDecisions.filter((decision) => decision.validationStatus === "untag-only").length;
86
+ const fullyDeletableRootCount = plan.fullyDeletableRoots.length;
87
+ const blockedDeleteRootCount = plan.rootDecisions.filter((decision) => decision.validationStatus === "blocked").length;
88
+ const protectedRootCount = plan.protectedRoots.length;
89
+ 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);
71
90
  return Number(result.lastInsertRowid);
72
91
  }
73
92
  }