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.

Potentially problematic release.


This version of ghcr-manager might be problematic. Click here for more details.

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
@@ -2,8 +2,8 @@ interface _PlanRootRow {
2
2
  version_id: number;
3
3
  root_digest: string;
4
4
  root_manifest_kind: string | null;
5
- direct_target_reason: string;
6
- selection_mode: string;
5
+ direct_target_reason: DeletePlanSelectionReason;
6
+ selection_mode: DeletePlanSelectionMode;
7
7
  }
8
8
  interface _PlanTagRow {
9
9
  target_tag: string;
@@ -24,7 +24,7 @@ interface _BlockedRootRow {
24
24
  blocking_digest: string;
25
25
  overlap_digest: string;
26
26
  overlap_manifest_kind: string | null;
27
- block_reason: string;
27
+ block_reason: DeletePlanBlockReasonCode;
28
28
  }
29
29
  export interface PlannerLogger {
30
30
  trace(message: string): void;
@@ -37,12 +37,15 @@ export interface ScanRow {
37
37
  package_name: string;
38
38
  scan_completed_at: string;
39
39
  }
40
+ export type DeletePlanSelectionMode = "delete-root" | "untag-only";
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
+ export type DeletePlanBlockReasonCode = "overlap-with-retained-root";
40
43
  export interface DeletePlanRoot {
41
44
  versionId: number;
42
45
  digest: string;
43
46
  manifestKind?: string;
44
- reason: string;
45
- selectionMode: string;
47
+ reason: DeletePlanSelectionReason;
48
+ selectionMode: DeletePlanSelectionMode;
46
49
  }
47
50
  export interface DeletePlanClosureManifest {
48
51
  sourceVersionId: number;
@@ -60,14 +63,14 @@ export interface DeletePlanBlockedRoot {
60
63
  blockingDigest: string;
61
64
  overlapDigest: string;
62
65
  overlapManifestKind?: string;
63
- reason: string;
66
+ reason: DeletePlanBlockReasonCode;
64
67
  }
65
68
  export interface DeletePlanRootDecision {
66
69
  versionId: number;
67
70
  digest: string;
68
71
  manifestKind?: string;
69
- selectionMode: string;
70
- selectionReason: string;
72
+ selectionMode: DeletePlanSelectionMode;
73
+ selectionReason: DeletePlanSelectionReason;
71
74
  validationStatus: "fully-deletable" | "blocked" | "untag-only";
72
75
  validationReasonCode: "untag-only-partial-tag-match" | "fully-deletable-no-retained-overlap" | "blocked-overlap-with-retained-root";
73
76
  validationReason: string;
@@ -82,7 +85,7 @@ export interface DeletePlanProtectedRoot {
82
85
  blocks: Array<{
83
86
  blockedVersionId: number;
84
87
  blockedDigest: string;
85
- blockReasonCode: string;
88
+ blockReasonCode: DeletePlanBlockReasonCode;
86
89
  overlapDigest: string;
87
90
  overlapManifestKind?: string;
88
91
  }>;
@@ -109,15 +112,6 @@ export interface DeletePlan {
109
112
  olderThan?: string;
110
113
  cutoffTimestamp?: string;
111
114
  };
112
- validationSummary: {
113
- directTargetTagCount: number;
114
- directTargetRootCount: number;
115
- deleteRootCandidateCount: number;
116
- untagOnlyRootCount: number;
117
- fullyDeletableRootCount: number;
118
- blockedDeleteRootCount: number;
119
- protectedRootCount: number;
120
- };
121
115
  directTargetTags: string[];
122
116
  directTargetRoots: DeletePlanRoot[];
123
117
  rootDecisions: DeletePlanRootDecision[];
@@ -1,2 +1,2 @@
1
1
  export { PlannerRepository } from "./_planner-repository.js";
2
- export type { DeletePlan, DeletePlanBlockedRoot, DeletePlanClosureManifest, DeletePlanProtectedRoot, DeletePlanRoot, DeletePlanRootDecision } from "./_planner-repository.js";
2
+ export type { DeletePlan, DeletePlanBlockReasonCode, DeletePlanBlockedRoot, DeletePlanClosureManifest, DeletePlanProtectedRoot, DeletePlanRoot, DeletePlanRootDecision, DeletePlanSelectionMode, DeletePlanSelectionReason } from "./_planner-repository.js";
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "type": "git",
10
10
  "url": "https://github.com/gh-workflow/ghcr-manager"
11
11
  },
12
- "version": "0.9.4",
12
+ "version": "0.9.6",
13
13
  "type": "module",
14
14
  "engines": {
15
15
  "node": ">=20.0.0"
@@ -34,6 +34,8 @@ CREATE TABLE IF NOT EXISTS tags (
34
34
  scan_id INTEGER NOT NULL,
35
35
  tag TEXT NOT NULL,
36
36
  version_id INTEGER NOT NULL,
37
+ is_digest_tag INTEGER NOT NULL,
38
+ CHECK(is_digest_tag IN (0, 1)),
37
39
  PRIMARY KEY(scan_id, tag),
38
40
  FOREIGN KEY(scan_id, version_id) REFERENCES package_versions(scan_id, version_id)
39
41
  );
@@ -89,6 +91,7 @@ CREATE TABLE IF NOT EXISTS manifest_edges (
89
91
  parent_digest TEXT NOT NULL,
90
92
  child_digest TEXT NOT NULL,
91
93
  edge_kind TEXT NOT NULL,
94
+ CHECK(edge_kind IN ('image-child', 'referrer', 'digest-tag-referrer')),
92
95
  PRIMARY KEY(scan_id, parent_digest, child_digest, edge_kind),
93
96
  FOREIGN KEY(scan_id, parent_digest) REFERENCES manifests(scan_id, digest),
94
97
  FOREIGN KEY(scan_id, child_digest) REFERENCES manifests(scan_id, digest)
@@ -137,6 +140,14 @@ CREATE TABLE IF NOT EXISTS cleanup_root_decisions (
137
140
  blocking_digest TEXT,
138
141
  overlap_digest TEXT,
139
142
  PRIMARY KEY(cleanup_run_id, digest),
143
+ CHECK(selection_mode IN ('delete-root', 'untag-only')),
144
+ CHECK(selection_reason IN (
145
+ 'delete-tags-all-tags-selected',
146
+ 'delete-tags-partial-tag-match',
147
+ 'delete-untagged',
148
+ 'keep-n-tagged-overflow',
149
+ 'keep-n-untagged-overflow'
150
+ )),
140
151
  CHECK(validation_status IN ('fully-deletable', 'blocked', 'untag-only')),
141
152
  CHECK(validation_reason_code IN (
142
153
  'untag-only-partial-tag-match',
@@ -149,6 +160,15 @@ CREATE TABLE IF NOT EXISTS cleanup_root_decisions (
149
160
  FOREIGN KEY(scan_id, overlap_digest) REFERENCES manifests(scan_id, digest)
150
161
  );
151
162
 
163
+ CREATE TABLE IF NOT EXISTS cleanup_selected_tags (
164
+ cleanup_run_id INTEGER NOT NULL,
165
+ scan_id INTEGER NOT NULL,
166
+ tag TEXT NOT NULL,
167
+ PRIMARY KEY(cleanup_run_id, tag),
168
+ FOREIGN KEY(cleanup_run_id, scan_id) REFERENCES cleanup_runs(cleanup_run_id, scan_id),
169
+ FOREIGN KEY(scan_id, tag) REFERENCES tags(scan_id, tag)
170
+ );
171
+
152
172
  CREATE TABLE IF NOT EXISTS cleanup_protected_root_blocks (
153
173
  cleanup_run_id INTEGER NOT NULL,
154
174
  scan_id INTEGER NOT NULL,
@@ -31,6 +31,7 @@ JOIN package_scans ps
31
31
  LEFT JOIN tags t
32
32
  ON t.scan_id = m.scan_id
33
33
  AND t.version_id = m.version_id
34
+ AND t.is_digest_tag = 0
34
35
  GROUP BY
35
36
  ps.scan_id,
36
37
  ps.owner,
@@ -1,6 +1,6 @@
1
- DROP VIEW IF EXISTS v_digest_derived_tag_relations;
1
+ DROP VIEW IF EXISTS v_digest_tag_relations;
2
2
 
3
- CREATE VIEW v_digest_derived_tag_relations AS
3
+ CREATE VIEW v_digest_tag_relations AS
4
4
  WITH digest_like_tags AS (
5
5
  SELECT
6
6
  lsp.scan_id,
@@ -11,7 +11,7 @@ WITH digest_like_tags AS (
11
11
  m.digest AS artifact_digest,
12
12
  m.manifest_kind AS artifact_manifest_kind,
13
13
  m.subject_digest AS artifact_subject_digest,
14
- 'sha256:' || SUBSTR(t.tag, 8, 64) AS inferred_parent_digest,
14
+ 'sha256:' || SUBSTR(t.tag, 8, 64) AS parent_digest,
15
15
  SUBSTR(t.tag, 72) AS tag_suffix
16
16
  FROM tags t
17
17
  JOIN v_latest_scan_per_package lsp
@@ -32,15 +32,14 @@ SELECT
32
32
  dlt.artifact_digest,
33
33
  dlt.artifact_manifest_kind,
34
34
  dlt.artifact_subject_digest,
35
- dlt.inferred_parent_digest,
35
+ dlt.parent_digest,
36
36
  dlt.tag_suffix,
37
37
  pm.version_id AS parent_version_id,
38
- pm.digest AS parent_digest,
39
38
  pm.manifest_kind AS parent_manifest_kind,
40
39
  CASE
41
- WHEN dlt.artifact_subject_digest = dlt.inferred_parent_digest THEN 1
40
+ WHEN dlt.artifact_subject_digest = dlt.parent_digest THEN 1
42
41
  ELSE 0
43
- END AS subject_matches_inferred_parent,
42
+ END AS subject_matches_parent,
44
43
  CASE
45
44
  WHEN pm.digest IS NULL THEN 0
46
45
  ELSE 1
@@ -48,4 +47,4 @@ SELECT
48
47
  FROM digest_like_tags dlt
49
48
  LEFT JOIN manifests pm
50
49
  ON pm.scan_id = dlt.scan_id
51
- AND pm.digest = dlt.inferred_parent_digest;
50
+ AND pm.digest = dlt.parent_digest;
@@ -97,4 +97,5 @@ SELECT
97
97
  FROM closure_members cm
98
98
  LEFT JOIN tags tag
99
99
  ON tag.scan_id = cm.scan_id
100
- AND tag.version_id = cm.member_version_id;
100
+ AND tag.version_id = cm.member_version_id
101
+ AND tag.is_digest_tag = 0;
@@ -0,0 +1,67 @@
1
+ DROP VIEW IF EXISTS v_cleanup_root_decision_readable;
2
+
3
+ CREATE VIEW v_cleanup_root_decision_readable AS
4
+ WITH selected_tag_summary AS (
5
+ SELECT
6
+ selected.cleanup_run_id,
7
+ tag.version_id AS root_version_id,
8
+ COUNT(*) AS selected_tag_count,
9
+ GROUP_CONCAT(selected.tag, ', ') AS selected_tags
10
+ FROM cleanup_selected_tags selected
11
+ JOIN tags tag
12
+ ON tag.scan_id = selected.scan_id
13
+ AND tag.tag = selected.tag
14
+ GROUP BY
15
+ selected.cleanup_run_id,
16
+ tag.version_id
17
+ )
18
+ SELECT
19
+ run.cleanup_run_id,
20
+ run.scan_id,
21
+ scan.owner,
22
+ scan.package_name,
23
+ decision.digest AS root_digest,
24
+ root_manifest.version_id AS root_version_id,
25
+ root_manifest.manifest_kind AS root_manifest_kind,
26
+ decision.selection_mode,
27
+ CASE decision.selection_mode
28
+ WHEN 'delete-root' THEN 'delete root'
29
+ WHEN 'untag-only' THEN 'detach selected tags only'
30
+ END AS selection_mode_label,
31
+ decision.selection_reason,
32
+ CASE decision.selection_reason
33
+ WHEN 'delete-tags-all-tags-selected' THEN 'all tags on this root were selected'
34
+ WHEN 'delete-tags-partial-tag-match' THEN 'only part of this root''s tag set was selected'
35
+ WHEN 'delete-untagged' THEN 'root was selected because it is untagged'
36
+ WHEN 'keep-n-tagged-overflow' THEN 'root fell outside the tagged keep window'
37
+ WHEN 'keep-n-untagged-overflow' THEN 'root fell outside the untagged keep window'
38
+ END AS selection_reason_label,
39
+ decision.validation_status,
40
+ CASE decision.validation_status
41
+ WHEN 'fully-deletable' THEN 'root and closure can be deleted'
42
+ WHEN 'untag-only' THEN 'only selected tags can be detached'
43
+ WHEN 'blocked' THEN 'root deletion is blocked'
44
+ END AS validation_status_label,
45
+ decision.validation_reason_code,
46
+ CASE decision.validation_reason_code
47
+ WHEN 'fully-deletable-no-retained-overlap' THEN 'no retained root overlaps this closure'
48
+ WHEN 'untag-only-partial-tag-match' THEN 'matched tags cover only part of the root tag set'
49
+ WHEN 'blocked-overlap-with-retained-root' THEN 'a retained root still requires an overlapping manifest'
50
+ END AS validation_reason_code_label,
51
+ decision.validation_reason,
52
+ decision.blocking_digest,
53
+ decision.overlap_digest,
54
+ COALESCE(tag_summary.selected_tag_count, 0) AS selected_tag_count,
55
+ tag_summary.selected_tags
56
+ FROM cleanup_root_decisions decision
57
+ JOIN cleanup_runs run
58
+ ON run.cleanup_run_id = decision.cleanup_run_id
59
+ AND run.scan_id = decision.scan_id
60
+ JOIN package_scans scan
61
+ ON scan.scan_id = run.scan_id
62
+ JOIN manifests root_manifest
63
+ ON root_manifest.scan_id = decision.scan_id
64
+ AND root_manifest.digest = decision.digest
65
+ LEFT JOIN selected_tag_summary tag_summary
66
+ ON tag_summary.cleanup_run_id = decision.cleanup_run_id
67
+ AND tag_summary.root_version_id = root_manifest.version_id;