ghcr-manager 0.9.2 → 0.9.4
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 +24 -0
- package/README.md +15 -9
- package/dist/cli/_planner-options.js +12 -30
- package/dist/cli/index.js +16 -8
- package/dist/db/planner/_planner-direct-target-roots.d.ts +17 -0
- package/dist/db/planner/_planner-direct-target-roots.js +199 -0
- package/dist/db/planner/_planner-latest-scan.d.ts +10 -0
- package/dist/db/planner/_planner-latest-scan.js +20 -0
- package/dist/db/planner/_planner-repository.d.ts +14 -5
- package/dist/db/planner/_planner-repository.js +42 -65
- package/package.json +1 -1
- package/dist/db/planner/_planner-delete-tag-root-targets.d.ts +0 -7
- package/dist/db/planner/_planner-delete-tag-root-targets.js +0 -130
- package/dist/db/planner/_planner-keep-tagged-root-targets.d.ts +0 -7
- package/dist/db/planner/_planner-keep-tagged-root-targets.js +0 -74
- package/dist/db/planner/_planner-tagged-root-targets.d.ts +0 -15
- package/dist/db/planner/_planner-tagged-root-targets.js +0 -19
- package/dist/db/planner/_planner-tagged-targets.d.ts +0 -8
- package/dist/db/planner/_planner-tagged-targets.js +0 -16
- package/dist/db/planner/_planner-untagged-targets.d.ts +0 -9
- package/dist/db/planner/_planner-untagged-targets.js +0 -91
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ 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.4] - 2026-05-21
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The root action now exposes `summary-json-path` instead of `summary-json`, so command summaries are consumed by file
|
|
15
|
+
path rather than as a large action output payload.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- The GitHub Action now passes cleanup and untag summary JSON between steps by file path instead of large environment
|
|
20
|
+
payloads, avoiding GitHub template-memory and argument-length failures on large cleanup runs.
|
|
21
|
+
|
|
22
|
+
## [0.9.3] - 2026-05-21
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Cleanup selector planning now composes tagged and untagged selector families in one SQL-backed planner path.
|
|
27
|
+
- Cleanup CLI help and docs now describe the composed selector model, including tagged selectors combined with
|
|
28
|
+
`delete-untagged`.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- `exclude-tag` now works correctly when a tagged selector family is combined with `delete-untagged`.
|
|
33
|
+
|
|
10
34
|
## [0.9.2] - 2026-05-21
|
|
11
35
|
|
|
12
36
|
### 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.4
|
|
37
37
|
with:
|
|
38
38
|
command: cleanup
|
|
39
39
|
token: ${{ github.token }}
|
|
@@ -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.4
|
|
76
76
|
with:
|
|
77
77
|
command: cleanup
|
|
78
78
|
token: ${{ github.token }}
|
|
@@ -93,7 +93,7 @@ The action supports three commands:
|
|
|
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.4
|
|
97
97
|
with:
|
|
98
98
|
command: cleanup
|
|
99
99
|
token: ${{ github.token }}
|
|
@@ -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.4
|
|
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.4
|
|
132
132
|
with:
|
|
133
133
|
command: scan
|
|
134
134
|
token: ${{ github.token }}
|
|
@@ -169,12 +169,18 @@ Note: the second scan only runs if cleanup actually makes changes.
|
|
|
169
169
|
|
|
170
170
|
`Cmds`: `s` = `scan`, `c` = `cleanup`, `u` = `untag`
|
|
171
171
|
|
|
172
|
+
Cleanup notes:
|
|
173
|
+
|
|
174
|
+
- Tagged selector families may be combined with `delete-untagged`.
|
|
175
|
+
- `exclude-tags` requires at least one tagged selector family.
|
|
176
|
+
- `delete-untagged` and `keep-n-untagged` cannot be combined.
|
|
177
|
+
|
|
172
178
|
## Outputs
|
|
173
179
|
|
|
174
|
-
| Output
|
|
175
|
-
|
|
|
176
|
-
| `db-path`
|
|
177
|
-
| `summary-json` | Summary JSON for `cleanup` and `untag` |
|
|
180
|
+
| Output | Description |
|
|
181
|
+
| ------------------- | ------------------------------------------------ |
|
|
182
|
+
| `db-path` | SQLite DB path on the runner |
|
|
183
|
+
| `summary-json-path` | Summary JSON file path for `cleanup` and `untag` |
|
|
178
184
|
|
|
179
185
|
## Artifacts
|
|
180
186
|
|
|
@@ -27,14 +27,14 @@ export function resolvePlanCommandInputs(args) {
|
|
|
27
27
|
deletePartialImages ||
|
|
28
28
|
deleteOrphanedImages ||
|
|
29
29
|
keepNTagged !== undefined;
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
32
|
-
throw new Error("
|
|
30
|
+
const hasAnySelector = deleteUntagged || taggedSelectorActive || keepNUntagged !== undefined;
|
|
31
|
+
if (deleteUntagged && keepNUntagged !== undefined) {
|
|
32
|
+
throw new Error("--delete-untagged and --keep-n-untagged cannot be combined");
|
|
33
33
|
}
|
|
34
|
-
if (
|
|
34
|
+
if (!hasAnySelector) {
|
|
35
35
|
throw new Error("missing required cleanup selector: --delete-untagged, --delete-tag, --delete-ghost-images, --delete-partial-images, --delete-orphaned-images, --keep-n-tagged, or --keep-n-untagged");
|
|
36
36
|
}
|
|
37
|
-
if (
|
|
37
|
+
if (!taggedSelectorActive && excludeTags.length > 0) {
|
|
38
38
|
throw new Error("--exclude-tag is only supported with tagged selector families");
|
|
39
39
|
}
|
|
40
40
|
if (olderThanRaw.length > 1) {
|
|
@@ -60,35 +60,17 @@ export function resolvePlanCommandInputs(args) {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
export function loadDeletePlan(repository, inputs) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
cutoffTimestamp: inputs.cutoffTimestamp
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
if (inputs.deleteUntagged) {
|
|
70
|
-
return repository.getDeleteUntaggedPlanWithCutoff(inputs.owner, inputs.packageName, {
|
|
71
|
-
olderThan: inputs.olderThan,
|
|
72
|
-
cutoffTimestamp: inputs.cutoffTimestamp
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
if (!inputs.deleteTagsRequested &&
|
|
76
|
-
!inputs.deleteGhostImages &&
|
|
77
|
-
!inputs.deletePartialImages &&
|
|
78
|
-
!inputs.deleteOrphanedImages &&
|
|
79
|
-
inputs.keepNTagged !== undefined) {
|
|
80
|
-
return repository.getKeepNTaggedPlanWithCutoff(inputs.owner, inputs.packageName, inputs.keepNTagged, [], {
|
|
81
|
-
olderThan: inputs.olderThan,
|
|
82
|
-
cutoffTimestamp: inputs.cutoffTimestamp
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
return repository.getDeleteTagsPlanWithCutoff(inputs.owner, inputs.packageName, inputs.deleteTags, inputs.excludeTags, {
|
|
63
|
+
return repository.getCleanupPlanWithCutoff(inputs.owner, inputs.packageName, {
|
|
64
|
+
deleteTags: inputs.deleteTags,
|
|
65
|
+
deleteTagsRequested: inputs.deleteTagsRequested,
|
|
86
66
|
deleteGhostImages: inputs.deleteGhostImages,
|
|
87
67
|
deletePartialImages: inputs.deletePartialImages,
|
|
88
68
|
deleteOrphanedImages: inputs.deleteOrphanedImages,
|
|
89
|
-
|
|
90
|
-
|
|
69
|
+
excludeTags: inputs.excludeTags,
|
|
70
|
+
deleteUntagged: inputs.deleteUntagged,
|
|
91
71
|
useRegex: inputs.useRegex,
|
|
72
|
+
keepNTagged: inputs.keepNTagged,
|
|
73
|
+
keepNUntagged: inputs.keepNUntagged,
|
|
92
74
|
olderThan: inputs.olderThan,
|
|
93
75
|
cutoffTimestamp: inputs.cutoffTimestamp
|
|
94
76
|
});
|
package/dist/cli/index.js
CHANGED
|
@@ -26,16 +26,24 @@ 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>] --
|
|
30
|
-
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> [--token <token>] --delete-ghost-images [--exclude-tag <tag> ...] [--use-regex] [--keep-n-tagged <count>] [--older-than <interval>]
|
|
31
|
-
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> [--token <token>] --delete-partial-images [--exclude-tag <tag> ...] [--use-regex] [--keep-n-tagged <count>] [--older-than <interval>]
|
|
32
|
-
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> [--token <token>] --delete-orphaned-images [--exclude-tag <tag> ...] [--use-regex] [--keep-n-tagged <count>] [--older-than <interval>]
|
|
33
|
-
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> [--token <token>] --keep-n-tagged <count> [--older-than <interval>]
|
|
34
|
-
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> [--token <token>] --keep-n-untagged <count> [--older-than <interval>]
|
|
35
|
-
ghcr-manager cleanup --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--dry-run] --owner <org> --package <name> [--token <token>] --delete-tag <tag> [--delete-tag <tag> ...] [--exclude-tag <tag> ...] [--use-regex] [--older-than <interval>]
|
|
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>]
|
|
36
30
|
ghcr-manager db-merge --db <target-path> --source-db <path> [--source-db <path> ...]
|
|
37
31
|
ghcr-manager scan --db <path> [--log-level <trace|debug|info|warn|error|silent>] [--github-output <path>] --owner <org> --package <name> --token <token>
|
|
38
|
-
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] --owner <org> --package <name> --token <token> --tag <tag> [--tag <tag> ...]
|
|
33
|
+
|
|
34
|
+
Cleanup selectors:
|
|
35
|
+
--delete-untagged
|
|
36
|
+
--delete-ghost-images
|
|
37
|
+
--delete-partial-images
|
|
38
|
+
--delete-orphaned-images
|
|
39
|
+
--delete-tag <tag> [--delete-tag <tag> ...]
|
|
40
|
+
--keep-n-tagged <count>
|
|
41
|
+
--keep-n-untagged <count>
|
|
42
|
+
|
|
43
|
+
Notes:
|
|
44
|
+
- Tagged selector families may be combined with --delete-untagged.
|
|
45
|
+
- --exclude-tag requires at least one tagged selector family.
|
|
46
|
+
- --delete-untagged and --keep-n-untagged cannot be combined.`);
|
|
39
47
|
}
|
|
40
48
|
const _entryPath = process.argv[1];
|
|
41
49
|
const _isDirectExecution = realpathSync(_entryPath) === realpathSync(fileURLToPath(import.meta.url));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PlannerSql } from "./_planner-sql.js";
|
|
2
|
+
import { type DeletePlanRoot } from "./_planner-types.js";
|
|
3
|
+
export interface DirectTargetRootOptions {
|
|
4
|
+
deleteTags: string[];
|
|
5
|
+
deleteTagsRequested: boolean;
|
|
6
|
+
excludeTags: string[];
|
|
7
|
+
deleteUntagged: boolean;
|
|
8
|
+
keepNTagged?: number;
|
|
9
|
+
keepNUntagged?: number;
|
|
10
|
+
useRegex?: boolean;
|
|
11
|
+
cutoffTimestamp?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class PlannerDirectTargetRoots {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(sql: PlannerSql);
|
|
16
|
+
list(scanId: number, options: DirectTargetRootOptions): DeletePlanRoot[];
|
|
17
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { buildTagSelectorPredicate } from "./_planner-tag-selectors.js";
|
|
2
|
+
import { mapPlanRootRow } from "./_planner-types.js";
|
|
3
|
+
export class PlannerDirectTargetRoots {
|
|
4
|
+
#sql;
|
|
5
|
+
constructor(sql) {
|
|
6
|
+
this.#sql = sql;
|
|
7
|
+
}
|
|
8
|
+
list(scanId, options) {
|
|
9
|
+
const selectedTagPredicate = options.deleteTags.length > 0
|
|
10
|
+
? buildTagSelectorPredicate(this.#sql.database, "t.tag", options.deleteTags, options.useRegex ?? false)
|
|
11
|
+
: undefined;
|
|
12
|
+
const excludedTagPredicate = options.excludeTags.length > 0
|
|
13
|
+
? buildTagSelectorPredicate(this.#sql.database, "xt.tag", options.excludeTags, options.useRegex ?? false)
|
|
14
|
+
: undefined;
|
|
15
|
+
const params = [scanId];
|
|
16
|
+
const cutoffSql = options.cutoffTimestamp ? "AND created_at < ?" : "";
|
|
17
|
+
if (options.cutoffTimestamp) {
|
|
18
|
+
params.push(options.cutoffTimestamp);
|
|
19
|
+
}
|
|
20
|
+
const selectedTagsSql = selectedTagPredicate
|
|
21
|
+
? `
|
|
22
|
+
SELECT DISTINCT t.version_id, t.tag
|
|
23
|
+
FROM tags t
|
|
24
|
+
WHERE t.scan_id = ?
|
|
25
|
+
AND (${selectedTagPredicate.sql})
|
|
26
|
+
`
|
|
27
|
+
: `
|
|
28
|
+
SELECT NULL AS version_id, NULL AS tag
|
|
29
|
+
WHERE 1 = 0
|
|
30
|
+
`;
|
|
31
|
+
if (selectedTagPredicate) {
|
|
32
|
+
params.push(scanId, ...selectedTagPredicate.params);
|
|
33
|
+
}
|
|
34
|
+
const excludedVersionsSql = excludedTagPredicate
|
|
35
|
+
? `
|
|
36
|
+
SELECT DISTINCT xt.version_id
|
|
37
|
+
FROM tags xt
|
|
38
|
+
WHERE xt.scan_id = ?
|
|
39
|
+
AND (${excludedTagPredicate.sql})
|
|
40
|
+
`
|
|
41
|
+
: `
|
|
42
|
+
SELECT NULL AS version_id
|
|
43
|
+
WHERE 1 = 0
|
|
44
|
+
`;
|
|
45
|
+
if (excludedTagPredicate) {
|
|
46
|
+
params.push(scanId, ...excludedTagPredicate.params);
|
|
47
|
+
}
|
|
48
|
+
const taggedBranchEnabled = options.deleteTagsRequested || options.keepNTagged !== undefined ? 1 : 0;
|
|
49
|
+
const deleteTagsRequested = options.deleteTagsRequested ? 1 : 0;
|
|
50
|
+
const keepNTaggedActive = options.keepNTagged !== undefined ? 1 : 0;
|
|
51
|
+
const deleteUntagged = options.deleteUntagged ? 1 : 0;
|
|
52
|
+
const keepNUntaggedActive = options.keepNUntagged !== undefined ? 1 : 0;
|
|
53
|
+
const paramsTail = [
|
|
54
|
+
taggedBranchEnabled,
|
|
55
|
+
deleteTagsRequested,
|
|
56
|
+
deleteTagsRequested,
|
|
57
|
+
keepNTaggedActive,
|
|
58
|
+
deleteTagsRequested,
|
|
59
|
+
keepNTaggedActive,
|
|
60
|
+
options.keepNTagged ?? 0,
|
|
61
|
+
deleteUntagged,
|
|
62
|
+
keepNUntaggedActive,
|
|
63
|
+
deleteUntagged,
|
|
64
|
+
deleteUntagged,
|
|
65
|
+
keepNUntaggedActive,
|
|
66
|
+
options.keepNUntagged ?? 0
|
|
67
|
+
];
|
|
68
|
+
const sql = `
|
|
69
|
+
WITH root_candidates AS (
|
|
70
|
+
SELECT
|
|
71
|
+
root_version_id AS version_id,
|
|
72
|
+
root_digest,
|
|
73
|
+
root_manifest_kind,
|
|
74
|
+
created_at,
|
|
75
|
+
tag_count,
|
|
76
|
+
is_tagged
|
|
77
|
+
FROM v_scan_root_manifests
|
|
78
|
+
WHERE scan_id = ?
|
|
79
|
+
AND has_ancestor = 0
|
|
80
|
+
${cutoffSql}
|
|
81
|
+
),
|
|
82
|
+
selected_tags AS (
|
|
83
|
+
${selectedTagsSql}
|
|
84
|
+
),
|
|
85
|
+
excluded_versions AS (
|
|
86
|
+
${excludedVersionsSql}
|
|
87
|
+
),
|
|
88
|
+
matched_tag_counts AS (
|
|
89
|
+
SELECT
|
|
90
|
+
st.version_id,
|
|
91
|
+
COUNT(DISTINCT st.tag) AS matched_tag_count
|
|
92
|
+
FROM selected_tags st
|
|
93
|
+
GROUP BY st.version_id
|
|
94
|
+
),
|
|
95
|
+
eligible_tagged_roots AS (
|
|
96
|
+
SELECT
|
|
97
|
+
rc.version_id,
|
|
98
|
+
rc.root_digest,
|
|
99
|
+
rc.root_manifest_kind,
|
|
100
|
+
rc.created_at,
|
|
101
|
+
rc.tag_count AS total_tag_count,
|
|
102
|
+
COALESCE(mtc.matched_tag_count, 0) AS matched_tag_count
|
|
103
|
+
FROM root_candidates rc
|
|
104
|
+
LEFT JOIN matched_tag_counts mtc
|
|
105
|
+
ON mtc.version_id = rc.version_id
|
|
106
|
+
LEFT JOIN excluded_versions ev
|
|
107
|
+
ON ev.version_id = rc.version_id
|
|
108
|
+
WHERE rc.is_tagged = 1
|
|
109
|
+
AND ev.version_id IS NULL
|
|
110
|
+
AND ? = 1
|
|
111
|
+
),
|
|
112
|
+
ranked_tagged_roots AS (
|
|
113
|
+
SELECT
|
|
114
|
+
version_id,
|
|
115
|
+
root_digest,
|
|
116
|
+
root_manifest_kind,
|
|
117
|
+
total_tag_count,
|
|
118
|
+
matched_tag_count,
|
|
119
|
+
ROW_NUMBER() OVER (
|
|
120
|
+
ORDER BY created_at DESC, version_id DESC, root_digest DESC
|
|
121
|
+
) AS recency_rank
|
|
122
|
+
FROM eligible_tagged_roots
|
|
123
|
+
WHERE ? = 0
|
|
124
|
+
OR matched_tag_count > 0
|
|
125
|
+
),
|
|
126
|
+
final_tagged_targets AS (
|
|
127
|
+
SELECT
|
|
128
|
+
version_id,
|
|
129
|
+
root_digest,
|
|
130
|
+
root_manifest_kind,
|
|
131
|
+
CASE
|
|
132
|
+
WHEN ? = 0
|
|
133
|
+
THEN 'keep-n-tagged-overflow'
|
|
134
|
+
WHEN ? = 1 AND total_tag_count = matched_tag_count
|
|
135
|
+
THEN 'keep-n-tagged-overflow'
|
|
136
|
+
WHEN total_tag_count = matched_tag_count
|
|
137
|
+
THEN 'delete-tags-all-tags-selected'
|
|
138
|
+
ELSE 'delete-tags-partial-tag-match'
|
|
139
|
+
END AS direct_target_reason,
|
|
140
|
+
CASE
|
|
141
|
+
WHEN ? = 0
|
|
142
|
+
THEN 'delete-root'
|
|
143
|
+
WHEN total_tag_count = matched_tag_count
|
|
144
|
+
THEN 'delete-root'
|
|
145
|
+
ELSE 'untag-only'
|
|
146
|
+
END AS selection_mode
|
|
147
|
+
FROM ranked_tagged_roots
|
|
148
|
+
WHERE ? = 0
|
|
149
|
+
OR recency_rank > ?
|
|
150
|
+
),
|
|
151
|
+
ranked_untagged_roots AS (
|
|
152
|
+
SELECT
|
|
153
|
+
rc.version_id,
|
|
154
|
+
rc.root_digest,
|
|
155
|
+
rc.root_manifest_kind,
|
|
156
|
+
ROW_NUMBER() OVER (
|
|
157
|
+
ORDER BY rc.created_at DESC, rc.version_id DESC, rc.root_digest DESC
|
|
158
|
+
) AS recency_rank
|
|
159
|
+
FROM root_candidates rc
|
|
160
|
+
WHERE rc.is_tagged = 0
|
|
161
|
+
AND (? = 1 OR ? = 1)
|
|
162
|
+
),
|
|
163
|
+
final_untagged_targets AS (
|
|
164
|
+
SELECT
|
|
165
|
+
version_id,
|
|
166
|
+
root_digest,
|
|
167
|
+
root_manifest_kind,
|
|
168
|
+
CASE
|
|
169
|
+
WHEN ? = 1
|
|
170
|
+
THEN 'delete-untagged'
|
|
171
|
+
ELSE 'keep-n-untagged-overflow'
|
|
172
|
+
END AS direct_target_reason,
|
|
173
|
+
'delete-root' AS selection_mode
|
|
174
|
+
FROM ranked_untagged_roots
|
|
175
|
+
WHERE ? = 1
|
|
176
|
+
OR (? = 1 AND recency_rank > ?)
|
|
177
|
+
)
|
|
178
|
+
SELECT
|
|
179
|
+
version_id,
|
|
180
|
+
root_digest,
|
|
181
|
+
root_manifest_kind,
|
|
182
|
+
direct_target_reason,
|
|
183
|
+
selection_mode
|
|
184
|
+
FROM final_tagged_targets
|
|
185
|
+
|
|
186
|
+
UNION ALL
|
|
187
|
+
|
|
188
|
+
SELECT
|
|
189
|
+
version_id,
|
|
190
|
+
root_digest,
|
|
191
|
+
root_manifest_kind,
|
|
192
|
+
direct_target_reason,
|
|
193
|
+
selection_mode
|
|
194
|
+
FROM final_untagged_targets
|
|
195
|
+
ORDER BY root_digest
|
|
196
|
+
`;
|
|
197
|
+
return this.#sql.all(sql, [...params, ...paramsTail]).map(mapPlanRootRow);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ScanRow } from "./_planner-types.js";
|
|
2
|
+
interface LatestScanSql {
|
|
3
|
+
get<T>(sql: string, params: Array<number | string>): T | undefined;
|
|
4
|
+
}
|
|
5
|
+
export declare class PlannerLatestScan {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(sql: LatestScanSql);
|
|
8
|
+
get(owner: string, packageName: string): ScanRow;
|
|
9
|
+
}
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class PlannerLatestScan {
|
|
2
|
+
#sql;
|
|
3
|
+
constructor(sql) {
|
|
4
|
+
this.#sql = sql;
|
|
5
|
+
}
|
|
6
|
+
get(owner, packageName) {
|
|
7
|
+
const sql = `
|
|
8
|
+
SELECT scan_id, owner, package_name, scan_completed_at
|
|
9
|
+
FROM v_latest_scan_per_package
|
|
10
|
+
WHERE owner = ?
|
|
11
|
+
AND package_name = ?
|
|
12
|
+
LIMIT 1
|
|
13
|
+
`;
|
|
14
|
+
const row = this.#sql.get(sql, [owner, packageName]);
|
|
15
|
+
if (!row) {
|
|
16
|
+
throw new Error(`database does not contain completed package scan for ${owner}/${packageName}`);
|
|
17
|
+
}
|
|
18
|
+
return row;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -7,7 +7,6 @@ export declare class PlannerRepository {
|
|
|
7
7
|
getDeleteUntaggedPlan(owner: string, packageName: string): DeletePlan;
|
|
8
8
|
getLatestCompletedScanId(owner: string, packageName: string): number;
|
|
9
9
|
getKeepNUntaggedPlan(owner: string, packageName: string, keepCount: number): DeletePlan;
|
|
10
|
-
getKeepNTaggedPlan(owner: string, packageName: string, keepCount: number): DeletePlan;
|
|
11
10
|
getDeleteUntaggedPlanWithCutoff(owner: string, packageName: string, options?: {
|
|
12
11
|
olderThan?: string;
|
|
13
12
|
cutoffTimestamp?: string;
|
|
@@ -16,10 +15,6 @@ export declare class PlannerRepository {
|
|
|
16
15
|
olderThan?: string;
|
|
17
16
|
cutoffTimestamp?: string;
|
|
18
17
|
}): DeletePlan;
|
|
19
|
-
getKeepNTaggedPlanWithCutoff(owner: string, packageName: string, keepCount: number, excludeTags: string[], options?: {
|
|
20
|
-
olderThan?: string;
|
|
21
|
-
cutoffTimestamp?: string;
|
|
22
|
-
}): DeletePlan;
|
|
23
18
|
getDeleteTagsPlan(owner: string, packageName: string, deleteTags: string[], excludeTags: string[]): DeletePlan;
|
|
24
19
|
getDeleteTagsPlanWithCutoff(owner: string, packageName: string, deleteTags: string[], excludeTags: string[], options?: {
|
|
25
20
|
deleteTagsRequested?: boolean;
|
|
@@ -31,4 +26,18 @@ export declare class PlannerRepository {
|
|
|
31
26
|
olderThan?: string;
|
|
32
27
|
cutoffTimestamp?: string;
|
|
33
28
|
}): DeletePlan;
|
|
29
|
+
getCleanupPlanWithCutoff(owner: string, packageName: string, options?: {
|
|
30
|
+
deleteUntagged?: boolean;
|
|
31
|
+
deleteGhostImages?: boolean;
|
|
32
|
+
deletePartialImages?: boolean;
|
|
33
|
+
deleteOrphanedImages?: boolean;
|
|
34
|
+
deleteTags?: string[];
|
|
35
|
+
deleteTagsRequested?: boolean;
|
|
36
|
+
excludeTags?: string[];
|
|
37
|
+
keepNTagged?: number;
|
|
38
|
+
keepNUntagged?: number;
|
|
39
|
+
useRegex?: boolean;
|
|
40
|
+
olderThan?: string;
|
|
41
|
+
cutoffTimestamp?: string;
|
|
42
|
+
}): DeletePlan;
|
|
34
43
|
}
|
|
@@ -1,95 +1,73 @@
|
|
|
1
|
+
import { PlannerDirectTargetRoots } from "./_planner-direct-target-roots.js";
|
|
2
|
+
import { PlannerDirectTargetTags } from "./_planner-direct-target-tags.js";
|
|
3
|
+
import { PlannerLatestScan } from "./_planner-latest-scan.js";
|
|
1
4
|
import { buildPlanOutputs } from "./_planner-output.js";
|
|
2
5
|
import { PlannerPlanArtifacts } from "./_planner-plan-artifacts.js";
|
|
3
6
|
import { PlannerSql } from "./_planner-sql.js";
|
|
4
|
-
import { PlannerTaggedTargets } from "./_planner-tagged-targets.js";
|
|
5
|
-
import { PlannerUntaggedTargets } from "./_planner-untagged-targets.js";
|
|
6
7
|
export class PlannerRepository {
|
|
7
|
-
#
|
|
8
|
-
#
|
|
8
|
+
#latestScan;
|
|
9
|
+
#directTargetTags;
|
|
10
|
+
#directTargetRoots;
|
|
9
11
|
#planArtifacts;
|
|
10
12
|
constructor(database, logger) {
|
|
11
13
|
const sql = new PlannerSql(database, logger);
|
|
12
|
-
this.#
|
|
13
|
-
this.#
|
|
14
|
+
this.#latestScan = new PlannerLatestScan(sql);
|
|
15
|
+
this.#directTargetTags = new PlannerDirectTargetTags(sql);
|
|
16
|
+
this.#directTargetRoots = new PlannerDirectTargetRoots(sql);
|
|
14
17
|
this.#planArtifacts = new PlannerPlanArtifacts(sql);
|
|
15
18
|
}
|
|
16
19
|
getDeleteUntaggedPlan(owner, packageName) {
|
|
17
20
|
return this.getDeleteUntaggedPlanWithCutoff(owner, packageName);
|
|
18
21
|
}
|
|
19
22
|
getLatestCompletedScanId(owner, packageName) {
|
|
20
|
-
return this.#
|
|
23
|
+
return this.#latestScan.get(owner, packageName).scan_id;
|
|
21
24
|
}
|
|
22
25
|
getKeepNUntaggedPlan(owner, packageName, keepCount) {
|
|
23
26
|
return this.getKeepNUntaggedPlanWithCutoff(owner, packageName, keepCount);
|
|
24
27
|
}
|
|
25
|
-
getKeepNTaggedPlan(owner, packageName, keepCount) {
|
|
26
|
-
return this.getKeepNTaggedPlanWithCutoff(owner, packageName, keepCount, []);
|
|
27
|
-
}
|
|
28
28
|
getDeleteUntaggedPlanWithCutoff(owner, packageName, options) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
packageName: scan.package_name,
|
|
35
|
-
scanCompletedAt: scan.scan_completed_at,
|
|
36
|
-
plannerInputs: _buildPlannerInputs({
|
|
37
|
-
deleteUntagged: true,
|
|
38
|
-
olderThan: options?.olderThan,
|
|
39
|
-
cutoffTimestamp: options?.cutoffTimestamp
|
|
40
|
-
}),
|
|
41
|
-
...buildPlanOutputs([], directTargetRoots, planArtifacts)
|
|
42
|
-
};
|
|
29
|
+
return this.getCleanupPlanWithCutoff(owner, packageName, {
|
|
30
|
+
deleteUntagged: true,
|
|
31
|
+
olderThan: options?.olderThan,
|
|
32
|
+
cutoffTimestamp: options?.cutoffTimestamp
|
|
33
|
+
});
|
|
43
34
|
}
|
|
44
35
|
getKeepNUntaggedPlanWithCutoff(owner, packageName, keepCount, options) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
owner: scan.owner,
|
|
50
|
-
packageName: scan.package_name,
|
|
51
|
-
scanCompletedAt: scan.scan_completed_at,
|
|
52
|
-
plannerInputs: _buildPlannerInputs({
|
|
53
|
-
keepNUntagged: keepCount,
|
|
54
|
-
olderThan: options?.olderThan,
|
|
55
|
-
cutoffTimestamp: options?.cutoffTimestamp
|
|
56
|
-
}),
|
|
57
|
-
...buildPlanOutputs([], directTargetRoots, planArtifacts)
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
getKeepNTaggedPlanWithCutoff(owner, packageName, keepCount, excludeTags, options) {
|
|
61
|
-
const scan = this.#untaggedTargets.getLatestCompletedScan(owner, packageName);
|
|
62
|
-
const directTargetRoots = this.#taggedTargets.listTaggedDirectTargetRoots(scan.scan_id, {
|
|
63
|
-
deleteTags: [],
|
|
64
|
-
excludeTags,
|
|
65
|
-
keepCount,
|
|
36
|
+
return this.getCleanupPlanWithCutoff(owner, packageName, {
|
|
37
|
+
keepNUntagged: keepCount,
|
|
38
|
+
olderThan: options?.olderThan,
|
|
66
39
|
cutoffTimestamp: options?.cutoffTimestamp
|
|
67
40
|
});
|
|
68
|
-
const planArtifacts = this.#planArtifacts.build(scan.scan_id, directTargetRoots);
|
|
69
|
-
return {
|
|
70
|
-
owner: scan.owner,
|
|
71
|
-
packageName: scan.package_name,
|
|
72
|
-
scanCompletedAt: scan.scan_completed_at,
|
|
73
|
-
plannerInputs: _buildPlannerInputs({
|
|
74
|
-
excludeTags,
|
|
75
|
-
keepNTagged: keepCount,
|
|
76
|
-
olderThan: options?.olderThan,
|
|
77
|
-
cutoffTimestamp: options?.cutoffTimestamp
|
|
78
|
-
}),
|
|
79
|
-
...buildPlanOutputs([], directTargetRoots, planArtifacts)
|
|
80
|
-
};
|
|
81
41
|
}
|
|
82
42
|
getDeleteTagsPlan(owner, packageName, deleteTags, excludeTags) {
|
|
83
43
|
return this.getDeleteTagsPlanWithCutoff(owner, packageName, deleteTags, excludeTags);
|
|
84
44
|
}
|
|
85
45
|
getDeleteTagsPlanWithCutoff(owner, packageName, deleteTags, excludeTags, options) {
|
|
86
|
-
|
|
87
|
-
const directTargetTags = this.#taggedTargets.listDeleteTagDirectTargetTags(scan.scan_id, deleteTags, excludeTags, options?.useRegex ?? false, options?.cutoffTimestamp);
|
|
88
|
-
const directTargetRoots = this.#taggedTargets.listTaggedDirectTargetRoots(scan.scan_id, {
|
|
46
|
+
return this.getCleanupPlanWithCutoff(owner, packageName, {
|
|
89
47
|
deleteTags,
|
|
48
|
+
excludeTags,
|
|
49
|
+
deleteGhostImages: options?.deleteGhostImages,
|
|
50
|
+
deletePartialImages: options?.deletePartialImages,
|
|
51
|
+
deleteOrphanedImages: options?.deleteOrphanedImages,
|
|
90
52
|
deleteTagsRequested: options?.deleteTagsRequested ?? true,
|
|
53
|
+
keepNTagged: options?.keepNTagged,
|
|
54
|
+
useRegex: options?.useRegex,
|
|
55
|
+
olderThan: options?.olderThan,
|
|
56
|
+
cutoffTimestamp: options?.cutoffTimestamp
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
getCleanupPlanWithCutoff(owner, packageName, options) {
|
|
60
|
+
const scan = this.#latestScan.get(owner, packageName);
|
|
61
|
+
const deleteTags = options?.deleteTags ?? [];
|
|
62
|
+
const excludeTags = options?.excludeTags ?? [];
|
|
63
|
+
const directTargetTags = this.#directTargetTags.listDeleteTagDirectTargetTags(scan.scan_id, deleteTags, excludeTags, options?.useRegex ?? false, options?.cutoffTimestamp);
|
|
64
|
+
const directTargetRoots = this.#directTargetRoots.list(scan.scan_id, {
|
|
65
|
+
deleteTags,
|
|
66
|
+
deleteTagsRequested: options?.deleteTagsRequested ?? false,
|
|
91
67
|
excludeTags,
|
|
92
|
-
|
|
68
|
+
deleteUntagged: options?.deleteUntagged ?? false,
|
|
69
|
+
keepNTagged: options?.keepNTagged,
|
|
70
|
+
keepNUntagged: options?.keepNUntagged,
|
|
93
71
|
useRegex: options?.useRegex ?? false,
|
|
94
72
|
cutoffTimestamp: options?.cutoffTimestamp
|
|
95
73
|
});
|
|
@@ -103,8 +81,10 @@ export class PlannerRepository {
|
|
|
103
81
|
deletePartialImages: options?.deletePartialImages || undefined,
|
|
104
82
|
deleteOrphanedImages: options?.deleteOrphanedImages || undefined,
|
|
105
83
|
deleteTags,
|
|
84
|
+
deleteUntagged: options?.deleteUntagged || undefined,
|
|
106
85
|
excludeTags,
|
|
107
86
|
keepNTagged: options?.keepNTagged,
|
|
87
|
+
keepNUntagged: options?.keepNUntagged,
|
|
108
88
|
useRegex: options?.useRegex || undefined,
|
|
109
89
|
olderThan: options?.olderThan,
|
|
110
90
|
cutoffTimestamp: options?.cutoffTimestamp
|
|
@@ -118,9 +98,6 @@ function _buildPlannerInputs(inputs) {
|
|
|
118
98
|
if (value === undefined) {
|
|
119
99
|
return false;
|
|
120
100
|
}
|
|
121
|
-
if (value === false) {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
101
|
return !(Array.isArray(value) && value.length === 0);
|
|
125
102
|
}));
|
|
126
103
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { PlannerSql } from "./_planner-sql.js";
|
|
2
|
-
import { type DeletePlanRoot } from "./_planner-types.js";
|
|
3
|
-
export declare class PlannerDeleteTagRootTargets {
|
|
4
|
-
#private;
|
|
5
|
-
constructor(sql: PlannerSql);
|
|
6
|
-
list(scanId: number, deleteTags: string[], excludeTags: string[], useRegex: boolean, keepCount?: number, cutoffTimestamp?: string): DeletePlanRoot[];
|
|
7
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { buildTagSelectorPredicate } from "./_planner-tag-selectors.js";
|
|
2
|
-
import { mapPlanRootRow } from "./_planner-types.js";
|
|
3
|
-
export class PlannerDeleteTagRootTargets {
|
|
4
|
-
#sql;
|
|
5
|
-
constructor(sql) {
|
|
6
|
-
this.#sql = sql;
|
|
7
|
-
}
|
|
8
|
-
list(scanId, deleteTags, excludeTags, useRegex, keepCount, cutoffTimestamp) {
|
|
9
|
-
const selectedTagPredicate = buildTagSelectorPredicate(this.#sql.database, "st.tag", deleteTags, useRegex);
|
|
10
|
-
const excludedTagPredicate = excludeTags.length > 0
|
|
11
|
-
? buildTagSelectorPredicate(this.#sql.database, "xt.tag", excludeTags, useRegex)
|
|
12
|
-
: undefined;
|
|
13
|
-
const excludedVersionsCte = excludedTagPredicate
|
|
14
|
-
? `
|
|
15
|
-
excluded_versions AS (
|
|
16
|
-
SELECT DISTINCT xt.version_id
|
|
17
|
-
FROM tags xt
|
|
18
|
-
WHERE xt.scan_id = ?
|
|
19
|
-
AND (${excludedTagPredicate.sql})
|
|
20
|
-
),
|
|
21
|
-
`
|
|
22
|
-
: "";
|
|
23
|
-
const excludedJoinSql = excludedTagPredicate
|
|
24
|
-
? `
|
|
25
|
-
LEFT JOIN excluded_versions ev
|
|
26
|
-
ON ev.version_id = st.version_id
|
|
27
|
-
`
|
|
28
|
-
: "";
|
|
29
|
-
const excludedWhereSql = excludedTagPredicate ? "AND ev.version_id IS NULL" : "";
|
|
30
|
-
const cutoffSql = cutoffTimestamp ? "AND pv.created_at < ?" : "";
|
|
31
|
-
const keepSql = keepCount !== undefined ? "WHERE recency_rank > ?" : "";
|
|
32
|
-
const params = [scanId, ...selectedTagPredicate.params];
|
|
33
|
-
if (excludedTagPredicate) {
|
|
34
|
-
params.push(scanId, ...excludedTagPredicate.params);
|
|
35
|
-
}
|
|
36
|
-
if (cutoffTimestamp) {
|
|
37
|
-
params.push(cutoffTimestamp);
|
|
38
|
-
}
|
|
39
|
-
params.push(scanId);
|
|
40
|
-
const tailParams = [keepCount !== undefined ? 1 : 0];
|
|
41
|
-
if (keepCount !== undefined) {
|
|
42
|
-
tailParams.push(keepCount);
|
|
43
|
-
}
|
|
44
|
-
const sql = `
|
|
45
|
-
WITH selected_tags AS (
|
|
46
|
-
SELECT st.scan_id, st.version_id, st.tag
|
|
47
|
-
FROM tags st
|
|
48
|
-
WHERE st.scan_id = ?
|
|
49
|
-
AND (${selectedTagPredicate.sql})
|
|
50
|
-
),
|
|
51
|
-
${excludedVersionsCte}
|
|
52
|
-
matched_candidate_roots AS (
|
|
53
|
-
SELECT DISTINCT
|
|
54
|
-
st.version_id AS version_id,
|
|
55
|
-
m.digest AS root_digest,
|
|
56
|
-
m.manifest_kind AS root_manifest_kind,
|
|
57
|
-
pv.created_at
|
|
58
|
-
FROM selected_tags st
|
|
59
|
-
JOIN manifests m
|
|
60
|
-
ON m.scan_id = st.scan_id
|
|
61
|
-
AND m.version_id = st.version_id
|
|
62
|
-
JOIN package_versions pv
|
|
63
|
-
ON pv.scan_id = st.scan_id
|
|
64
|
-
AND pv.version_id = st.version_id
|
|
65
|
-
${excludedJoinSql}
|
|
66
|
-
WHERE 1 = 1
|
|
67
|
-
${excludedWhereSql}
|
|
68
|
-
${cutoffSql}
|
|
69
|
-
AND NOT EXISTS (
|
|
70
|
-
SELECT 1
|
|
71
|
-
FROM manifest_reachability mr
|
|
72
|
-
WHERE mr.scan_id = st.scan_id
|
|
73
|
-
AND mr.descendant_digest = m.digest
|
|
74
|
-
AND mr.min_distance > 0
|
|
75
|
-
)
|
|
76
|
-
),
|
|
77
|
-
matched_roots AS (
|
|
78
|
-
SELECT
|
|
79
|
-
mcr.version_id,
|
|
80
|
-
mcr.root_digest,
|
|
81
|
-
mcr.root_manifest_kind,
|
|
82
|
-
mcr.created_at,
|
|
83
|
-
COUNT(t.tag) AS total_tag_count,
|
|
84
|
-
COUNT(st.tag) AS matched_tag_count
|
|
85
|
-
FROM matched_candidate_roots mcr
|
|
86
|
-
JOIN tags t
|
|
87
|
-
ON t.scan_id = ?
|
|
88
|
-
AND t.version_id = mcr.version_id
|
|
89
|
-
LEFT JOIN selected_tags st
|
|
90
|
-
ON st.scan_id = t.scan_id
|
|
91
|
-
AND st.version_id = t.version_id
|
|
92
|
-
AND st.tag = t.tag
|
|
93
|
-
GROUP BY mcr.version_id, mcr.root_digest, mcr.root_manifest_kind, mcr.created_at
|
|
94
|
-
HAVING matched_tag_count > 0
|
|
95
|
-
),
|
|
96
|
-
ranked_roots AS (
|
|
97
|
-
SELECT
|
|
98
|
-
version_id,
|
|
99
|
-
root_digest,
|
|
100
|
-
root_manifest_kind,
|
|
101
|
-
total_tag_count,
|
|
102
|
-
matched_tag_count,
|
|
103
|
-
ROW_NUMBER() OVER (
|
|
104
|
-
ORDER BY created_at DESC, version_id DESC, root_digest DESC
|
|
105
|
-
) AS recency_rank
|
|
106
|
-
FROM matched_roots
|
|
107
|
-
)
|
|
108
|
-
SELECT
|
|
109
|
-
version_id,
|
|
110
|
-
root_digest,
|
|
111
|
-
root_manifest_kind,
|
|
112
|
-
CASE
|
|
113
|
-
WHEN total_tag_count = matched_tag_count AND ? = 1
|
|
114
|
-
THEN 'keep-n-tagged-overflow'
|
|
115
|
-
WHEN total_tag_count = matched_tag_count
|
|
116
|
-
THEN 'delete-tags-all-tags-selected'
|
|
117
|
-
ELSE 'delete-tags-partial-tag-match'
|
|
118
|
-
END AS direct_target_reason,
|
|
119
|
-
CASE
|
|
120
|
-
WHEN total_tag_count = matched_tag_count
|
|
121
|
-
THEN 'delete-root'
|
|
122
|
-
ELSE 'untag-only'
|
|
123
|
-
END AS selection_mode
|
|
124
|
-
FROM ranked_roots
|
|
125
|
-
${keepSql}
|
|
126
|
-
ORDER BY root_digest
|
|
127
|
-
`;
|
|
128
|
-
return this.#sql.all(sql, [...params, ...tailParams]).map(mapPlanRootRow);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { PlannerSql } from "./_planner-sql.js";
|
|
2
|
-
import { type DeletePlanRoot } from "./_planner-types.js";
|
|
3
|
-
export declare class PlannerKeepTaggedRootTargets {
|
|
4
|
-
#private;
|
|
5
|
-
constructor(sql: PlannerSql);
|
|
6
|
-
list(scanId: number, excludeTags: string[], useRegex: boolean, keepCount?: number, cutoffTimestamp?: string): DeletePlanRoot[];
|
|
7
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { buildTagSelectorPredicate } from "./_planner-tag-selectors.js";
|
|
2
|
-
import { mapPlanRootRow } from "./_planner-types.js";
|
|
3
|
-
export class PlannerKeepTaggedRootTargets {
|
|
4
|
-
#sql;
|
|
5
|
-
constructor(sql) {
|
|
6
|
-
this.#sql = sql;
|
|
7
|
-
}
|
|
8
|
-
list(scanId, excludeTags, useRegex, keepCount, cutoffTimestamp) {
|
|
9
|
-
const excludedTagPredicate = excludeTags.length > 0
|
|
10
|
-
? buildTagSelectorPredicate(this.#sql.database, "xt.tag", excludeTags, useRegex)
|
|
11
|
-
: undefined;
|
|
12
|
-
const excludedRootSql = excludedTagPredicate
|
|
13
|
-
? `
|
|
14
|
-
AND NOT EXISTS (
|
|
15
|
-
SELECT 1
|
|
16
|
-
FROM tags xt
|
|
17
|
-
WHERE xt.scan_id = pv.scan_id
|
|
18
|
-
AND xt.version_id = pv.version_id
|
|
19
|
-
AND (${excludedTagPredicate.sql})
|
|
20
|
-
)
|
|
21
|
-
`
|
|
22
|
-
: "";
|
|
23
|
-
const cutoffSql = cutoffTimestamp ? "AND pv.created_at < ?" : "";
|
|
24
|
-
const keepSql = keepCount !== undefined ? "WHERE recency_rank > ?" : "";
|
|
25
|
-
const params = [scanId, ...(excludedTagPredicate?.params ?? [])];
|
|
26
|
-
if (cutoffTimestamp) {
|
|
27
|
-
params.push(cutoffTimestamp);
|
|
28
|
-
}
|
|
29
|
-
if (keepCount !== undefined) {
|
|
30
|
-
params.push(keepCount);
|
|
31
|
-
}
|
|
32
|
-
const sql = `
|
|
33
|
-
WITH eligible_tagged_roots AS (
|
|
34
|
-
SELECT
|
|
35
|
-
pv.version_id AS version_id,
|
|
36
|
-
m.digest AS root_digest,
|
|
37
|
-
m.manifest_kind AS root_manifest_kind,
|
|
38
|
-
ROW_NUMBER() OVER (
|
|
39
|
-
ORDER BY pv.created_at DESC, pv.version_id DESC, m.digest DESC
|
|
40
|
-
) AS recency_rank
|
|
41
|
-
FROM package_versions pv
|
|
42
|
-
JOIN manifests m
|
|
43
|
-
ON m.scan_id = pv.scan_id
|
|
44
|
-
AND m.version_id = pv.version_id
|
|
45
|
-
WHERE pv.scan_id = ?
|
|
46
|
-
AND EXISTS (
|
|
47
|
-
SELECT 1
|
|
48
|
-
FROM tags t
|
|
49
|
-
WHERE t.scan_id = pv.scan_id
|
|
50
|
-
AND t.version_id = pv.version_id
|
|
51
|
-
)
|
|
52
|
-
${excludedRootSql}
|
|
53
|
-
AND NOT EXISTS (
|
|
54
|
-
SELECT 1
|
|
55
|
-
FROM manifest_reachability mr
|
|
56
|
-
WHERE mr.scan_id = pv.scan_id
|
|
57
|
-
AND mr.descendant_digest = m.digest
|
|
58
|
-
AND mr.min_distance > 0
|
|
59
|
-
)
|
|
60
|
-
${cutoffSql}
|
|
61
|
-
)
|
|
62
|
-
SELECT
|
|
63
|
-
version_id,
|
|
64
|
-
root_digest,
|
|
65
|
-
root_manifest_kind,
|
|
66
|
-
'keep-n-tagged-overflow' AS direct_target_reason,
|
|
67
|
-
'delete-root' AS selection_mode
|
|
68
|
-
FROM eligible_tagged_roots
|
|
69
|
-
${keepSql}
|
|
70
|
-
ORDER BY root_digest
|
|
71
|
-
`;
|
|
72
|
-
return this.#sql.all(sql, params).map(mapPlanRootRow);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { PlannerSql } from "./_planner-sql.js";
|
|
2
|
-
import type { DeletePlanRoot } from "./_planner-types.js";
|
|
3
|
-
export interface TaggedRootTargetOptions {
|
|
4
|
-
deleteTags: string[];
|
|
5
|
-
deleteTagsRequested?: boolean;
|
|
6
|
-
excludeTags: string[];
|
|
7
|
-
keepCount?: number;
|
|
8
|
-
useRegex?: boolean;
|
|
9
|
-
cutoffTimestamp?: string;
|
|
10
|
-
}
|
|
11
|
-
export declare class PlannerTaggedRootTargets {
|
|
12
|
-
#private;
|
|
13
|
-
constructor(sql: PlannerSql);
|
|
14
|
-
listTaggedDirectTargetRoots(scanId: number, options: TaggedRootTargetOptions): DeletePlanRoot[];
|
|
15
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { PlannerDeleteTagRootTargets } from "./_planner-delete-tag-root-targets.js";
|
|
2
|
-
import { PlannerKeepTaggedRootTargets } from "./_planner-keep-tagged-root-targets.js";
|
|
3
|
-
export class PlannerTaggedRootTargets {
|
|
4
|
-
#deleteTagTargets;
|
|
5
|
-
#keepTaggedTargets;
|
|
6
|
-
constructor(sql) {
|
|
7
|
-
this.#deleteTagTargets = new PlannerDeleteTagRootTargets(sql);
|
|
8
|
-
this.#keepTaggedTargets = new PlannerKeepTaggedRootTargets(sql);
|
|
9
|
-
}
|
|
10
|
-
listTaggedDirectTargetRoots(scanId, options) {
|
|
11
|
-
if (options.deleteTagsRequested && options.deleteTags.length === 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
if (options.deleteTags.length === 0) {
|
|
15
|
-
return this.#keepTaggedTargets.list(scanId, options.excludeTags, options.useRegex ?? false, options.keepCount, options.cutoffTimestamp);
|
|
16
|
-
}
|
|
17
|
-
return this.#deleteTagTargets.list(scanId, options.deleteTags, options.excludeTags, options.useRegex ?? false, options.keepCount, options.cutoffTimestamp);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { type TaggedRootTargetOptions } from "./_planner-tagged-root-targets.js";
|
|
2
|
-
import { PlannerSql } from "./_planner-sql.js";
|
|
3
|
-
export declare class PlannerTaggedTargets {
|
|
4
|
-
#private;
|
|
5
|
-
constructor(sql: PlannerSql);
|
|
6
|
-
listDeleteTagDirectTargetTags(scanId: number, deleteTags: string[], excludeTags: string[], useRegex: boolean, cutoffTimestamp?: string): string[];
|
|
7
|
-
listTaggedDirectTargetRoots(scanId: number, options: TaggedRootTargetOptions): import("./_planner-types.js").DeletePlanRoot[];
|
|
8
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { PlannerDirectTargetTags } from "./_planner-direct-target-tags.js";
|
|
2
|
-
import { PlannerTaggedRootTargets } from "./_planner-tagged-root-targets.js";
|
|
3
|
-
export class PlannerTaggedTargets {
|
|
4
|
-
#directTargetTags;
|
|
5
|
-
#rootTargets;
|
|
6
|
-
constructor(sql) {
|
|
7
|
-
this.#directTargetTags = new PlannerDirectTargetTags(sql);
|
|
8
|
-
this.#rootTargets = new PlannerTaggedRootTargets(sql);
|
|
9
|
-
}
|
|
10
|
-
listDeleteTagDirectTargetTags(scanId, deleteTags, excludeTags, useRegex, cutoffTimestamp) {
|
|
11
|
-
return this.#directTargetTags.listDeleteTagDirectTargetTags(scanId, deleteTags, excludeTags, useRegex, cutoffTimestamp);
|
|
12
|
-
}
|
|
13
|
-
listTaggedDirectTargetRoots(scanId, options) {
|
|
14
|
-
return this.#rootTargets.listTaggedDirectTargetRoots(scanId, options);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { PlannerSql } from "./_planner-sql.js";
|
|
2
|
-
import { type DeletePlanRoot, type ScanRow } from "./_planner-types.js";
|
|
3
|
-
export declare class PlannerUntaggedTargets {
|
|
4
|
-
#private;
|
|
5
|
-
constructor(sql: PlannerSql);
|
|
6
|
-
getLatestCompletedScan(owner: string, packageName: string): ScanRow;
|
|
7
|
-
listDeleteUntaggedDirectTargetRoots(scanId: number, cutoffTimestamp?: string): DeletePlanRoot[];
|
|
8
|
-
listKeepNUntaggedDirectTargetRoots(scanId: number, keepCount: number, cutoffTimestamp?: string): DeletePlanRoot[];
|
|
9
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { mapPlanRootRow } from "./_planner-types.js";
|
|
2
|
-
export class PlannerUntaggedTargets {
|
|
3
|
-
#sql;
|
|
4
|
-
constructor(sql) {
|
|
5
|
-
this.#sql = sql;
|
|
6
|
-
}
|
|
7
|
-
getLatestCompletedScan(owner, packageName) {
|
|
8
|
-
const sql = `
|
|
9
|
-
SELECT scan_id, owner, package_name, scan_completed_at
|
|
10
|
-
FROM v_latest_scan_per_package
|
|
11
|
-
WHERE owner = ?
|
|
12
|
-
AND package_name = ?
|
|
13
|
-
LIMIT 1
|
|
14
|
-
`;
|
|
15
|
-
const row = this.#sql.get(sql, [owner, packageName]);
|
|
16
|
-
if (!row) {
|
|
17
|
-
throw new Error(`database does not contain completed package scan for ${owner}/${packageName}`);
|
|
18
|
-
}
|
|
19
|
-
return row;
|
|
20
|
-
}
|
|
21
|
-
listDeleteUntaggedDirectTargetRoots(scanId, cutoffTimestamp) {
|
|
22
|
-
const cutoffSql = cutoffTimestamp ? "AND created_at < ?" : "";
|
|
23
|
-
const sql = `
|
|
24
|
-
SELECT
|
|
25
|
-
root_version_id AS version_id,
|
|
26
|
-
root_digest,
|
|
27
|
-
root_manifest_kind,
|
|
28
|
-
'delete-untagged' AS direct_target_reason,
|
|
29
|
-
'delete-root' AS selection_mode
|
|
30
|
-
FROM v_scan_root_manifests
|
|
31
|
-
WHERE scan_id = ?
|
|
32
|
-
AND is_tagged = 0
|
|
33
|
-
AND has_ancestor = 0
|
|
34
|
-
${cutoffSql}
|
|
35
|
-
ORDER BY root_digest
|
|
36
|
-
`;
|
|
37
|
-
const rows = this.#sql.all(sql, [
|
|
38
|
-
scanId,
|
|
39
|
-
...(cutoffTimestamp ? [cutoffTimestamp] : [])
|
|
40
|
-
]);
|
|
41
|
-
return rows.map(mapPlanRootRow);
|
|
42
|
-
}
|
|
43
|
-
listKeepNUntaggedDirectTargetRoots(scanId, keepCount, cutoffTimestamp) {
|
|
44
|
-
const cutoffSql = cutoffTimestamp ? "AND pv.created_at < ?" : "";
|
|
45
|
-
const sql = `
|
|
46
|
-
WITH eligible_untagged_roots AS (
|
|
47
|
-
SELECT
|
|
48
|
-
pv.version_id AS version_id,
|
|
49
|
-
m.digest AS root_digest,
|
|
50
|
-
m.manifest_kind AS root_manifest_kind,
|
|
51
|
-
ROW_NUMBER() OVER (
|
|
52
|
-
ORDER BY pv.created_at DESC, pv.version_id DESC, m.digest DESC
|
|
53
|
-
) AS recency_rank
|
|
54
|
-
FROM package_versions pv
|
|
55
|
-
JOIN manifests m
|
|
56
|
-
ON m.scan_id = pv.scan_id
|
|
57
|
-
AND m.version_id = pv.version_id
|
|
58
|
-
WHERE pv.scan_id = ?
|
|
59
|
-
AND NOT EXISTS (
|
|
60
|
-
SELECT 1
|
|
61
|
-
FROM tags t
|
|
62
|
-
WHERE t.scan_id = pv.scan_id
|
|
63
|
-
AND t.version_id = pv.version_id
|
|
64
|
-
)
|
|
65
|
-
AND NOT EXISTS (
|
|
66
|
-
SELECT 1
|
|
67
|
-
FROM manifest_reachability mr
|
|
68
|
-
WHERE mr.scan_id = pv.scan_id
|
|
69
|
-
AND mr.descendant_digest = m.digest
|
|
70
|
-
AND mr.min_distance > 0
|
|
71
|
-
)
|
|
72
|
-
${cutoffSql}
|
|
73
|
-
)
|
|
74
|
-
SELECT
|
|
75
|
-
version_id,
|
|
76
|
-
root_digest,
|
|
77
|
-
root_manifest_kind,
|
|
78
|
-
'keep-n-untagged-overflow' AS direct_target_reason,
|
|
79
|
-
'delete-root' AS selection_mode
|
|
80
|
-
FROM eligible_untagged_roots
|
|
81
|
-
WHERE recency_rank > ?
|
|
82
|
-
ORDER BY root_digest
|
|
83
|
-
`;
|
|
84
|
-
const rows = this.#sql.all(sql, [
|
|
85
|
-
scanId,
|
|
86
|
-
...(cutoffTimestamp ? [cutoffTimestamp] : []),
|
|
87
|
-
keepCount
|
|
88
|
-
]);
|
|
89
|
-
return rows.map(mapPlanRootRow);
|
|
90
|
-
}
|
|
91
|
-
}
|