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.
- package/CHANGELOG.md +31 -0
- package/README.md +9 -9
- package/dist/cleanup-summary/_cleanup-summary-markdown.js +7 -6
- package/dist/cleanup-summary/_cleanup-summary.d.ts +8 -4
- package/dist/cleanup-summary/_cleanup-summary.js +8 -4
- package/dist/cleanup-summary/index.d.ts +1 -1
- package/dist/cli/_cleanup-command.js +22 -2
- package/dist/cli/_tag-selector-resolver.js +2 -2
- package/dist/core/_digest-tag.d.ts +2 -0
- package/dist/core/_digest-tag.js +12 -0
- package/dist/core/_types.d.ts +2 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +1 -0
- package/dist/db/_cleanup-run-writer.js +69 -50
- package/dist/db/_db-merge-cleanup-copy.js +128 -80
- package/dist/db/_db-merge-scan-copy.js +1 -1
- package/dist/db/_manifest-reachability.js +25 -0
- package/dist/db/_scan-writer.js +118 -116
- package/dist/db/index.d.ts +1 -1
- package/dist/db/planner/_planner-direct-target-roots.js +2 -0
- package/dist/db/planner/_planner-direct-target-tags.js +5 -0
- package/dist/db/planner/_planner-output.d.ts +1 -1
- package/dist/db/planner/_planner-output.js +0 -11
- package/dist/db/planner/_planner-repository.d.ts +1 -1
- package/dist/db/planner/_planner-types.d.ts +12 -18
- package/dist/db/planner/index.d.ts +1 -1
- package/package.json +1 -1
- package/resources/sql/schema/001_schema.sql +20 -0
- package/resources/sql/views/003_v_scan_root_manifests.sql +1 -0
- package/resources/sql/views/{004_v_digest_derived_tag_relations.sql → 004_v_digest_tag_relations.sql} +7 -8
- package/resources/sql/views/005_v_cleanup_root_closure_members.sql +2 -1
- 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.
|
|
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-
|
|
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.
|
|
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-
|
|
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.
|
|
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-
|
|
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.
|
|
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.
|
|
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-
|
|
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 =
|
|
2
|
-
const _DEFAULT_MAX_ROOTS_PER_SECTION =
|
|
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.
|
|
16
|
-
`| 🗑️ Fully deletable roots | ${summary.
|
|
17
|
-
`| 🔗 Untag-only roots | ${summary.
|
|
18
|
-
`| 🛡️ Blocked roots | ${summary.
|
|
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:
|
|
10
|
-
selectionReason:
|
|
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
|
|
15
|
-
untagOnlyRoots
|
|
16
|
-
blockedRoots
|
|
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(
|
|
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
|
|
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
|
|
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,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
|
+
}
|
package/dist/core/_types.d.ts
CHANGED
|
@@ -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:
|
|
42
|
+
edgeKind: ManifestEdgeKind;
|
|
42
43
|
}
|
|
43
44
|
export interface PackageSnapshot {
|
|
44
45
|
packageName: string;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/core/index.js
CHANGED
|
@@ -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.#
|
|
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.#
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
}
|