ghcr-manager 0.0.4 → 0.9.0
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 +50 -1
- package/README.md +166 -57
- package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +6 -0
- package/dist/cleanup-summary/_cleanup-summary-markdown.js +113 -0
- package/dist/cleanup-summary/_cleanup-summary.d.ts +40 -0
- package/dist/cleanup-summary/_cleanup-summary.js +40 -0
- package/dist/cleanup-summary/index.d.ts +2 -0
- package/dist/cleanup-summary/index.js +2 -0
- package/dist/cli/_args.d.ts +1 -0
- package/dist/cli/_args.js +3 -0
- package/dist/cli/_cleanup-command.d.ts +1 -0
- package/dist/cli/_cleanup-command.js +63 -0
- package/dist/cli/_db-merge-command.d.ts +1 -0
- package/dist/cli/_db-merge-command.js +41 -0
- package/dist/cli/_github-output.d.ts +10 -0
- package/dist/cli/_github-output.js +13 -0
- package/dist/cli/_logger.d.ts +2 -1
- package/dist/cli/_logger.js +2 -0
- package/dist/cli/_older-than.d.ts +5 -0
- package/dist/cli/_older-than.js +42 -0
- package/dist/cli/_planner-options.d.ts +20 -0
- package/dist/cli/_planner-options.js +101 -0
- package/dist/cli/_scan-command.js +11 -4
- package/dist/cli/_tag-selector-resolver.d.ts +3 -0
- package/dist/cli/_tag-selector-resolver.js +109 -0
- package/dist/cli/_untag-command.d.ts +1 -0
- package/dist/cli/_untag-command.js +57 -0
- package/dist/cli/index.js +19 -1
- package/dist/config/_service-constants.d.ts +3 -0
- package/dist/config/_service-constants.js +3 -0
- package/dist/{tuning → config}/index.d.ts +3 -0
- package/dist/{tuning → config}/index.js +3 -0
- package/dist/core/_github-package-owner.d.ts +11 -0
- package/dist/core/_github-package-owner.js +45 -0
- package/dist/core/_http-error.d.ts +6 -0
- package/dist/core/_http-error.js +33 -0
- package/dist/core/_types.d.ts +3 -2
- package/dist/core/index.d.ts +4 -1
- package/dist/core/index.js +2 -1
- package/dist/db/_cleanup-run-writer.d.ts +10 -0
- package/dist/db/_cleanup-run-writer.js +73 -0
- package/dist/db/_db-merge-cleanup-copy.d.ts +7 -0
- package/dist/db/_db-merge-cleanup-copy.js +122 -0
- package/dist/db/_db-merge-history.d.ts +2 -0
- package/dist/db/_db-merge-history.js +15 -0
- package/dist/db/_db-merge-repository.d.ts +8 -0
- package/dist/db/_db-merge-repository.js +95 -0
- package/dist/db/_db-merge-scan-copy.d.ts +10 -0
- package/dist/db/_db-merge-scan-copy.js +69 -0
- package/dist/db/_db-merge-types.d.ts +44 -0
- package/dist/db/_db-merge-types.js +1 -0
- package/dist/db/_github-actions-run-url.d.ts +1 -0
- package/dist/db/_github-actions-run-url.js +9 -0
- package/dist/db/_scan-writer.d.ts +3 -1
- package/dist/db/_scan-writer.js +28 -13
- package/dist/db/_snapshot-repository.d.ts +9 -9
- package/dist/db/_snapshot-repository.js +37 -49
- package/dist/db/_sql-placeholders.d.ts +2 -0
- package/dist/db/_sql-placeholders.js +16 -0
- package/dist/db/index.d.ts +5 -0
- package/dist/db/index.js +3 -0
- package/dist/db/planner/_planner-delete-tag-root-targets.d.ts +7 -0
- package/dist/db/planner/_planner-delete-tag-root-targets.js +130 -0
- package/dist/db/planner/_planner-direct-target-tags.d.ts +6 -0
- package/dist/db/planner/_planner-direct-target-tags.js +47 -0
- package/dist/db/planner/_planner-keep-tagged-root-targets.d.ts +7 -0
- package/dist/db/planner/_planner-keep-tagged-root-targets.js +74 -0
- package/dist/db/planner/_planner-output.d.ts +5 -0
- package/dist/db/planner/_planner-output.js +101 -0
- package/dist/db/planner/_planner-plan-artifacts.d.ts +7 -0
- package/dist/db/planner/_planner-plan-artifacts.js +211 -0
- package/dist/db/planner/_planner-repository.d.ts +34 -0
- package/dist/db/planner/_planner-repository.js +126 -0
- package/dist/db/planner/_planner-sql.d.ts +12 -0
- package/dist/db/planner/_planner-sql.js +35 -0
- package/dist/db/planner/_planner-tag-selectors.d.ts +8 -0
- package/dist/db/planner/_planner-tag-selectors.js +57 -0
- package/dist/db/planner/_planner-tagged-root-targets.d.ts +15 -0
- package/dist/db/planner/_planner-tagged-root-targets.js +19 -0
- package/dist/db/planner/_planner-tagged-targets.d.ts +8 -0
- package/dist/db/planner/_planner-tagged-targets.js +16 -0
- package/dist/db/planner/_planner-types.d.ts +135 -0
- package/dist/db/planner/_planner-types.js +38 -0
- package/dist/db/planner/_planner-untagged-targets.d.ts +9 -0
- package/dist/db/planner/_planner-untagged-targets.js +91 -0
- package/dist/db/planner/index.d.ts +2 -0
- package/dist/db/planner/index.js +1 -0
- package/dist/execute/_http.d.ts +7 -0
- package/dist/execute/_http.js +48 -0
- package/dist/execute/_manifest-detach.d.ts +4 -0
- package/dist/execute/_manifest-detach.js +31 -0
- package/dist/execute/_package-version-delete-client.d.ts +4 -0
- package/dist/execute/_package-version-delete-client.js +34 -0
- package/dist/execute/_package-version-page-client.d.ts +14 -0
- package/dist/execute/_package-version-page-client.js +64 -0
- package/dist/execute/_package-version-tag-source-client.d.ts +12 -0
- package/dist/execute/_package-version-tag-source-client.js +65 -0
- package/dist/execute/_plan-executor.d.ts +3 -0
- package/dist/execute/_plan-executor.js +47 -0
- package/dist/execute/_registry-manifest-client.d.ts +12 -0
- package/dist/execute/_registry-manifest-client.js +79 -0
- package/dist/execute/_registry-token-client.d.ts +4 -0
- package/dist/execute/_registry-token-client.js +37 -0
- package/dist/execute/_types.d.ts +51 -0
- package/dist/execute/_types.js +1 -0
- package/dist/execute/_untag-client.d.ts +2 -0
- package/dist/execute/_untag-client.js +71 -0
- package/dist/execute/index.d.ts +5 -0
- package/dist/execute/index.js +3 -0
- package/dist/ingest/github/_manifest-client.d.ts +7 -1
- package/dist/ingest/github/_manifest-client.js +8 -0
- package/dist/ingest/github/_manifest-ingest.js +39 -53
- package/dist/ingest/github/_manifest-kind.d.ts +20 -0
- package/dist/ingest/github/_manifest-kind.js +50 -0
- package/dist/ingest/github/_package-metadata-load.d.ts +5 -0
- package/dist/ingest/github/_package-metadata-load.js +45 -0
- package/dist/ingest/github/_package-version-page-load.d.ts +1 -1
- package/dist/ingest/github/_package-version-page-load.js +8 -5
- package/dist/ingest/github/_packages-client.d.ts +1 -1
- package/dist/ingest/github/_packages-client.js +21 -4
- package/dist/ingest/github/_parallel-paginated-ingest.d.ts +1 -0
- package/dist/ingest/github/_parallel-paginated-ingest.js +2 -2
- package/dist/ingest/github/_shared.d.ts +1 -1
- package/dist/ingest/github/_shared.js +2 -34
- package/dist/ingest/github/index.d.ts +4 -0
- package/dist/ingest/github/index.js +8 -5
- package/package.json +7 -5
- package/resources/sql/schema/001_schema.sql +82 -8
- package/resources/sql/views/001_v_latest_scan_per_package.sql +2 -2
- package/resources/sql/views/003_v_scan_root_manifests.sql +43 -0
- package/resources/sql/views/004_v_digest_derived_tag_relations.sql +51 -0
- package/resources/sql/views/005_v_cleanup_root_closure_members.sql +100 -0
- package/resources/sql/views/006_v_cleanup_blocking_overlaps.sql +42 -0
- package/dist/ingest/github/_paginated-ingest.d.ts +0 -11
- package/dist/ingest/github/_paginated-ingest.js +0 -28
- package/resources/sql/views/003_v_missing_digests_related_manifests.sql +0 -78
- package/resources/sql/views/004_v_manifests_related_manifests.sql +0 -142
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,55 @@ 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.0] - 2026-05-21
|
|
11
|
+
|
|
12
|
+
`0.9.0` is the first stable pre-`1.0` release of `ghcr-manager`.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `cleanup` as the main GHCR maintenance flow for both the GitHub Action and the companion CLI.
|
|
17
|
+
- `untag` as a direct tag-removal mode that works without a scan database.
|
|
18
|
+
- `db-merge` and `merge-run-artifacts` support for combining scan databases across packages and workflow runs.
|
|
19
|
+
- Support for both organization-owned and user-owned GitHub Container Registry packages.
|
|
20
|
+
- Cleanup summary JSON output plus GitHub step summary rendering for action runs.
|
|
21
|
+
- Broad live and scenario-based workflow coverage for cleanup, untag, and cross-owner behavior.
|
|
22
|
+
- User-facing documentation for action usage, CLI usage, DB merge workflows, schema orientation, and SQL recipes.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- The GitHub Action now builds and runs the repo-local CLI directly instead of installing `ghcr-manager` from npm at
|
|
27
|
+
runtime.
|
|
28
|
+
- The primary maintenance surface is now `cleanup` with `dry-run` semantics, with `scan` and `untag` as supporting
|
|
29
|
+
command modes.
|
|
30
|
+
- The action input and artifact flow were refined around scan databases, cleanup summaries, and optional post-cleanup
|
|
31
|
+
rescan behavior.
|
|
32
|
+
- Documentation was reorganized around action-first usage, with deeper companion docs for CLI and database workflows.
|
|
33
|
+
- Release validation and workflow gating were tightened around exact version references, changelog readiness, and live
|
|
34
|
+
scenario checks.
|
|
35
|
+
|
|
36
|
+
### Removed
|
|
37
|
+
|
|
38
|
+
- Built-in database artifact encryption and decryption support.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Digest-selector scenario handling and related workflow wiring for `ghcr-manager`.
|
|
43
|
+
- Latest-scan based verification for cleanup and untag test flows.
|
|
44
|
+
- User-owner cleanup workflow behavior and related test setup details.
|
|
45
|
+
- Numerous workflow, artifact-handling, and planner-audit edge cases discovered during pre-release hardening.
|
|
46
|
+
|
|
47
|
+
## [0.0.6] - 2026-04-30
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- Internal workflow/debug wiring.
|
|
52
|
+
|
|
53
|
+
## [0.0.5] - 2026-04-30
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- Publish to npmjs by trusted publisher
|
|
58
|
+
|
|
10
59
|
## [0.0.4] - 2026-04-30
|
|
11
60
|
|
|
12
61
|
### Changed
|
|
@@ -32,7 +81,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
32
81
|
- Immutable per-scan UUID (`package_scans.scan_uuid`) for robust duplicate detection across merged databases.
|
|
33
82
|
- Optional action artifact upload for scan DB export (`upload-db-artifact`, optional retention override).
|
|
34
83
|
- Manual workflow for interactive scan runs (`.github/workflows/manual-run.yml`).
|
|
35
|
-
- Missing-manifest investigation SQL recipes (`docs/missing-manifests-queries.md`) and schema/terminology docs.
|
|
84
|
+
- Missing-manifest investigation SQL recipes (`docs/queries/missing-manifests-queries.md`) and schema/terminology docs.
|
|
36
85
|
|
|
37
86
|
### Changed
|
|
38
87
|
|
package/README.md
CHANGED
|
@@ -3,95 +3,204 @@
|
|
|
3
3
|
[](https://github.com/gh-workflow/ghcr-manager/releases)
|
|
4
4
|
[](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases)
|
|
5
5
|
[](https://github.com/marketplace/actions/ghcr-manager)
|
|
6
|
-
[](#usage)
|
|
6
|
+
[](https://github.com/gh-workflow/ghcr-manager/actions/workflows/change-validation.yml)
|
|
8
7
|
|
|
9
|
-
Inspect,
|
|
8
|
+
Inspect, review, and manage GitHub Container Registry packages.
|
|
10
9
|
|
|
11
|
-
`ghcr-manager` is a
|
|
12
|
-
packages and correct handling of multi-arch images, referrers, and attestations.
|
|
10
|
+
`ghcr-manager` is a GitHub Action for:
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+
- scanning one GHCR package into a SQLite database artifact
|
|
13
|
+
- running cleanup with a GitHub step summary and optional DB artifact
|
|
14
|
+
- previewing cleanup decisions with `dry-run` before making changes
|
|
15
|
+
- directly removing selected tags with `untag`
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
- :white_check_mark: Export database of Container Registry from runs for local analysis
|
|
18
|
-
- :construction: Safe cleanup of GHCR image artifacts in GitHub packages
|
|
17
|
+
## Quick Start
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
### :white_check_mark: Data Loading
|
|
23
|
-
|
|
24
|
-
1. Writes Container Registry metadata to a database
|
|
25
|
-
2. Pre-processes data for optimized lookups
|
|
26
|
-
3. Optional: Export of database from runs for local analysis
|
|
27
|
-
|
|
28
|
-
> :construction: Planned: Support for merging several such databases into one for local analysis
|
|
29
|
-
|
|
30
|
-
### :construction: Consistency Check
|
|
31
|
-
|
|
32
|
-
1. Run consistency check against the database
|
|
33
|
-
2. Optional: Export report of missing manifests from runs
|
|
34
|
-
|
|
35
|
-
### :construction: Safe cleanup of GHCR image artifacts
|
|
36
|
-
|
|
37
|
-
1. Use filter input to query database for related artifacts (images and manifests)
|
|
38
|
-
2. Optional `dry-run`: Export which image artifacts would be deleted
|
|
39
|
-
3. Delete image artifacts (without `dry-run`)
|
|
40
|
-
|
|
41
|
-
## Usage
|
|
19
|
+
For a first run, start with `cleanup` in `dry-run` mode.
|
|
42
20
|
|
|
43
21
|
```yaml
|
|
44
|
-
concurrency:
|
|
45
|
-
group: ghcr-manager__${{ inputs.owner }}__${{ inputs.package }}
|
|
46
22
|
jobs:
|
|
47
|
-
|
|
23
|
+
cleanup:
|
|
48
24
|
runs-on: ubuntu-latest
|
|
49
25
|
permissions:
|
|
50
26
|
contents: read
|
|
51
27
|
packages: read
|
|
52
|
-
actions: write
|
|
28
|
+
actions: write
|
|
53
29
|
concurrency:
|
|
54
|
-
group: ghcr-
|
|
30
|
+
group: ghcr-manager__OWNER__PACKAGE
|
|
55
31
|
steps:
|
|
56
32
|
- uses: actions/checkout@v6
|
|
57
33
|
|
|
58
|
-
- name:
|
|
59
|
-
|
|
34
|
+
- name: Preview GHCR cleanup
|
|
35
|
+
id: ghcr-manager
|
|
36
|
+
uses: gh-workflow/ghcr-manager@0.9.0
|
|
60
37
|
with:
|
|
38
|
+
command: cleanup
|
|
61
39
|
github-token: ${{ github.token }}
|
|
62
40
|
owner: OWNER
|
|
63
41
|
package: PACKAGE
|
|
42
|
+
dry-run: true
|
|
43
|
+
delete-untagged: true
|
|
44
|
+
keep-n-tagged: "10"
|
|
45
|
+
exclude-tags: |
|
|
46
|
+
latest
|
|
64
47
|
upload-db-artifact: true
|
|
65
48
|
```
|
|
66
49
|
|
|
67
|
-
|
|
50
|
+
After the run:
|
|
68
51
|
|
|
69
|
-
|
|
52
|
+
1. Open the GitHub step summary for the action run.
|
|
53
|
+
2. Review which tags matched and which roots would be deleted, untagged, or blocked.
|
|
54
|
+
3. Only download the DB artifact if you need deeper inspection.
|
|
55
|
+
|
|
56
|
+
## Commands
|
|
57
|
+
|
|
58
|
+
The action supports three commands:
|
|
59
|
+
|
|
60
|
+
- `cleanup`: Cleans using filters; use `dry-run` to preview the result
|
|
61
|
+
- `untag`: Removes one or more tags directly
|
|
62
|
+
- `scan`: Scans one package and uploads the resulting DB artifact
|
|
63
|
+
|
|
64
|
+
### Purpose of commands
|
|
65
|
+
|
|
66
|
+
- `cleanup`: Normal entry point for registry maintenance
|
|
67
|
+
- `untag`: Works directly without a full package scan
|
|
68
|
+
- `scan`: For investigation and audit
|
|
69
|
+
|
|
70
|
+
## Common Usage
|
|
71
|
+
|
|
72
|
+
### Preview cleanup
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
- uses: gh-workflow/ghcr-manager@0.9.0
|
|
76
|
+
with:
|
|
77
|
+
command: cleanup
|
|
78
|
+
github-token: ${{ github.token }}
|
|
79
|
+
owner: OWNER
|
|
80
|
+
package: PACKAGE
|
|
81
|
+
dry-run: true
|
|
82
|
+
delete-tags: |
|
|
83
|
+
pr-.*
|
|
84
|
+
use-regex: true
|
|
85
|
+
older-than: 30d
|
|
86
|
+
keep-n-tagged: "5"
|
|
87
|
+
exclude-tags: |
|
|
88
|
+
latest
|
|
89
|
+
stable
|
|
90
|
+
upload-db-artifact: true
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Apply cleanup
|
|
70
94
|
|
|
71
|
-
|
|
95
|
+
```yaml
|
|
96
|
+
- uses: gh-workflow/ghcr-manager@0.9.0
|
|
97
|
+
with:
|
|
98
|
+
command: cleanup
|
|
99
|
+
github-token: ${{ github.token }}
|
|
100
|
+
owner: OWNER
|
|
101
|
+
package: PACKAGE
|
|
102
|
+
delete-untagged: true
|
|
103
|
+
keep-n-tagged: "10"
|
|
104
|
+
upload-db-artifact: true
|
|
105
|
+
scan-after-cleanup: true
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
If `scan-after-cleanup` is `true`, `cleanup` performs a second scan so the uploaded DB reflects post-mutation state.
|
|
109
|
+
|
|
110
|
+
Note: the second scan only runs if cleanup actually makes changes.
|
|
111
|
+
|
|
112
|
+
### Remove selected tags directly
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
- uses: gh-workflow/ghcr-manager@0.9.0
|
|
116
|
+
with:
|
|
117
|
+
command: untag
|
|
118
|
+
github-token: ${{ github.token }}
|
|
119
|
+
owner: OWNER
|
|
120
|
+
package: PACKAGE
|
|
121
|
+
delete-tags: |
|
|
122
|
+
old-tag
|
|
123
|
+
test-tag
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`untag` does not use a scan DB and does not support DB artifact upload.
|
|
127
|
+
|
|
128
|
+
### Scan one package
|
|
72
129
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
130
|
+
```yaml
|
|
131
|
+
- uses: gh-workflow/ghcr-manager@0.9.0
|
|
132
|
+
with:
|
|
133
|
+
command: scan
|
|
134
|
+
github-token: ${{ github.token }}
|
|
135
|
+
owner: OWNER
|
|
136
|
+
package: PACKAGE
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`scan` always uploads a DB artifact.
|
|
140
|
+
|
|
141
|
+
## Inputs
|
|
80
142
|
|
|
81
|
-
<!-- markdownlint-
|
|
143
|
+
<!-- markdownlint-disable MD013 MD060 -->
|
|
144
|
+
|
|
145
|
+
| Input | Description | Cmds | Required | Default |
|
|
146
|
+
| ---------------------------- | ----------------------------------- | ---- | ----------- | ------------------------------ |
|
|
147
|
+
| `command` | `scan`, `cleanup`, or `untag` | all | Yes | |
|
|
148
|
+
| `github-token` | GitHub token for API calls | all | Yes | `${{ github.token }}` |
|
|
149
|
+
| `owner` | Package owner | all | Yes | |
|
|
150
|
+
| `package` | Package name | all | Yes | |
|
|
151
|
+
| `db-path` | Local SQLite DB path | s,c | No | |
|
|
152
|
+
| `upload-db-artifact` | Upload DB and summary artifact | s,c | No | `false` |
|
|
153
|
+
| `scan-after-cleanup` | Run a second scan after cleanup | c | No | `false` |
|
|
154
|
+
| `db-artifact-retention-days` | Override artifact retention days | s,c | No | `${{ github.retention_days }}` |
|
|
155
|
+
| `delete-tags` | Newline-separated tags to delete | c,u | for `untag` | |
|
|
156
|
+
| `exclude-tags` | Newline-separated tags to exclude | c | No | |
|
|
157
|
+
| `keep-n-tagged` | Keep newest tagged roots | c | No | |
|
|
158
|
+
| `keep-n-untagged` | Keep newest untagged roots | c | No | |
|
|
159
|
+
| `delete-untagged` | Delete untagged roots | c | No | `false` |
|
|
160
|
+
| `delete-ghost-images` | Delete ghost multi-arch roots | c | No | `false` |
|
|
161
|
+
| `delete-partial-images` | Delete partial multi-arch roots | c | No | `false` |
|
|
162
|
+
| `delete-orphaned-images` | Delete orphaned digest-derived tags | c | No | `false` |
|
|
163
|
+
| `older-than` | Age cutoff for cleanup selectors | c | No | |
|
|
164
|
+
| `use-regex` | Use regex for cleanup tag selectors | c | No | `false` |
|
|
165
|
+
| `dry-run` | Show changes without mutating GHCR | c,u | No | `false` |
|
|
166
|
+
| `log-level` | CLI log level | all | No | `info` |
|
|
167
|
+
|
|
168
|
+
<!-- markdownlint-enable MD013 MD060 -->
|
|
169
|
+
|
|
170
|
+
`Cmds`: `s` = `scan`, `c` = `cleanup`, `u` = `untag`
|
|
82
171
|
|
|
83
172
|
## Outputs
|
|
84
173
|
|
|
85
|
-
| Output
|
|
86
|
-
|
|
|
87
|
-
| `db-path`
|
|
174
|
+
| Output | Description |
|
|
175
|
+
| -------------- | -------------------------------------- |
|
|
176
|
+
| `db-path` | SQLite DB path on the runner |
|
|
177
|
+
| `summary-json` | Summary JSON for `cleanup` and `untag` |
|
|
88
178
|
|
|
89
179
|
## Artifacts
|
|
90
180
|
|
|
91
|
-
|
|
181
|
+
When artifacts are enabled:
|
|
182
|
+
|
|
183
|
+
- `scan` always uploads one SQLite DB artifact
|
|
184
|
+
- `cleanup` optionally uploads the DB artifact and a cleanup summary JSON artifact
|
|
185
|
+
- `untag` uploads no artifacts
|
|
186
|
+
|
|
187
|
+
Current naming:
|
|
188
|
+
|
|
189
|
+
| Artifact type | Filename pattern |
|
|
190
|
+
| -------------------- | --------------------------------------------------- |
|
|
191
|
+
| scan or cleanup DB | `${OWNER}__${PACKAGE}.sqlite` |
|
|
192
|
+
| cleanup summary JSON | `${OWNER}__${PACKAGE}.sqlite--cleanup-summary.json` |
|
|
193
|
+
|
|
194
|
+
## Documentation Map
|
|
195
|
+
|
|
196
|
+
- [docs/action-usage.md](docs/action-usage.md): action commands, including `cleanup`, `scan`, and `untag`
|
|
197
|
+
- [docs/db-merge-workflows.md](docs/db-merge-workflows.md): cleaning up multiple packages with one combined DB
|
|
198
|
+
- [docs/schema-description.md](docs/schema-description.md): practical explanation of the SQLite schema
|
|
199
|
+
- [docs/queries/missing-manifests-queries.md](docs/queries/missing-manifests-queries.md): SQL recipes for missing
|
|
200
|
+
manifest references
|
|
201
|
+
- [docs/cli-usage.md](docs/cli-usage.md): companion CLI usage
|
|
92
202
|
|
|
93
|
-
|
|
94
|
-
| ----------------------------- | --------------------------------- | -------------------------------------------------------------------- |
|
|
95
|
-
| `${OWNER}__${PACKAGE}.sqlite` | `${OWNER}__${PACKAGE}.sqlite.zip` | Zipped SQLite database containing GitHub Container Registry metadata |
|
|
203
|
+
## Acknowledgment
|
|
96
204
|
|
|
97
|
-
|
|
205
|
+
This project was influenced by [dataaxiom/ghcr-cleanup-action](https://github.com/dataaxiom/ghcr-cleanup-action), with a
|
|
206
|
+
similar problem focus and a different implementation approach.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const _DEFAULT_MAX_DIRECT_TARGET_TAGS = 20;
|
|
2
|
+
const _DEFAULT_MAX_ROOTS_PER_SECTION = 20;
|
|
3
|
+
const _DEFAULT_MAX_TAGS_PER_ROOT = 4;
|
|
4
|
+
export function renderCleanupSummaryMarkdown(summary, options) {
|
|
5
|
+
const maxDirectTargetTags = options.maxDirectTargetTags ?? _DEFAULT_MAX_DIRECT_TARGET_TAGS;
|
|
6
|
+
const maxRootsPerSection = options.maxRootsPerSection ?? _DEFAULT_MAX_ROOTS_PER_SECTION;
|
|
7
|
+
const maxTagsPerRoot = options.maxTagsPerRoot ?? _DEFAULT_MAX_TAGS_PER_ROOT;
|
|
8
|
+
const lines = [
|
|
9
|
+
"## Cleanup Summary",
|
|
10
|
+
"",
|
|
11
|
+
"| Field | Value |",
|
|
12
|
+
"| --- | --- |",
|
|
13
|
+
`| 📦 Package | \`${_escapeInlineCode(`${summary.owner}/${summary.packageName}`)}\` |`,
|
|
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} |`,
|
|
19
|
+
""
|
|
20
|
+
];
|
|
21
|
+
lines.push(..._renderJsonDetails("⚙️ Cleanup filter", summary.plannerInputs));
|
|
22
|
+
lines.push(..._renderDirectTargetTags(summary.directTargetTags, maxDirectTargetTags));
|
|
23
|
+
lines.push(..._renderRootSection("🗑️ Fully deletable roots", summary.fullyDeletableRoots, maxRootsPerSection, maxTagsPerRoot));
|
|
24
|
+
lines.push(..._renderRootSection("🔗 Untag-only roots", summary.untagOnlyRoots, maxRootsPerSection, maxTagsPerRoot));
|
|
25
|
+
lines.push(..._renderRootSection("🛡️ Blocked roots", summary.blockedRoots, maxRootsPerSection, maxTagsPerRoot));
|
|
26
|
+
if (!summary.dryRun && (summary.deletedPackageVersions.length > 0 || summary.untaggedTags.length > 0)) {
|
|
27
|
+
lines.push(..._renderLiveEffects(summary));
|
|
28
|
+
}
|
|
29
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
30
|
+
}
|
|
31
|
+
function _renderJsonDetails(title, value) {
|
|
32
|
+
return [
|
|
33
|
+
`<details>`,
|
|
34
|
+
`<summary>${title}</summary>`,
|
|
35
|
+
"",
|
|
36
|
+
"```json",
|
|
37
|
+
JSON.stringify(value, null, 2),
|
|
38
|
+
"```",
|
|
39
|
+
"",
|
|
40
|
+
"</details>",
|
|
41
|
+
""
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
function _renderDirectTargetTags(tags, maxDirectTargetTags) {
|
|
45
|
+
if (tags.length === 0) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const visibleTags = tags.slice(0, maxDirectTargetTags).map((tag) => `- \`${_escapeInlineCode(tag)}\``);
|
|
49
|
+
const lines = ["<details>", "<summary>🏷️ Matched tags</summary>", "", ...visibleTags];
|
|
50
|
+
if (tags.length > maxDirectTargetTags) {
|
|
51
|
+
lines.push("", `_Showing first ${maxDirectTargetTags} of ${tags.length} matched tags._`);
|
|
52
|
+
}
|
|
53
|
+
lines.push("", "</details>", "");
|
|
54
|
+
return lines;
|
|
55
|
+
}
|
|
56
|
+
function _renderRootSection(title, roots, maxRootsPerSection, maxTagsPerRoot) {
|
|
57
|
+
if (roots.length === 0) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const lines = ["<details>", `<summary>${title}</summary>`, ""];
|
|
61
|
+
lines.push("| Version | Digest | Tags | Reason |");
|
|
62
|
+
lines.push("| --- | --- | --- | --- |");
|
|
63
|
+
for (const root of roots.slice(0, maxRootsPerSection)) {
|
|
64
|
+
lines.push(`| ${root.versionId} | \`${_escapeInlineCode(_shortDigest(root.digest))}\` | ${_escapeMarkdown(_formatTags(root, maxTagsPerRoot))} | ${_escapeMarkdown(_formatReason(root))} |`);
|
|
65
|
+
}
|
|
66
|
+
if (roots.length > maxRootsPerSection) {
|
|
67
|
+
lines.push("", `_Showing first ${maxRootsPerSection} of ${roots.length} ${title.toLowerCase()}._`);
|
|
68
|
+
}
|
|
69
|
+
lines.push("", "</details>", "");
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
function _renderLiveEffects(summary) {
|
|
73
|
+
const lines = ["### Applied changes", ""];
|
|
74
|
+
lines.push(`- Deleted package versions: ${summary.deletedPackageVersions.length}`);
|
|
75
|
+
lines.push(`- Detached tags: ${summary.untaggedTags.length}`);
|
|
76
|
+
if (summary.unsupportedUntagRoots.length > 0) {
|
|
77
|
+
lines.push(`- Unsupported untag roots: ${summary.unsupportedUntagRoots.length}`);
|
|
78
|
+
}
|
|
79
|
+
lines.push("");
|
|
80
|
+
return lines;
|
|
81
|
+
}
|
|
82
|
+
function _formatTags(root, maxTagsPerRoot) {
|
|
83
|
+
const tags = root.rootTags.length > 0 ? root.rootTags : root.matchedTags;
|
|
84
|
+
if (tags.length === 0) {
|
|
85
|
+
return "(untagged)";
|
|
86
|
+
}
|
|
87
|
+
const visible = tags.slice(0, maxTagsPerRoot);
|
|
88
|
+
const suffix = tags.length > maxTagsPerRoot ? `, +${tags.length - maxTagsPerRoot} more` : "";
|
|
89
|
+
return visible.join(", ") + suffix;
|
|
90
|
+
}
|
|
91
|
+
function _formatReason(root) {
|
|
92
|
+
if (root.validationStatus === "blocked") {
|
|
93
|
+
const blocking = root.blockingDigest ? _shortDigest(root.blockingDigest) : "another root";
|
|
94
|
+
const overlap = root.overlapDigest ? ` via ${_shortDigest(root.overlapDigest)}` : "";
|
|
95
|
+
return `Blocked by ${blocking}${overlap}`;
|
|
96
|
+
}
|
|
97
|
+
if (root.validationStatus === "untag-only") {
|
|
98
|
+
return "Selected tags detach; root remains";
|
|
99
|
+
}
|
|
100
|
+
return "Root and closure can be deleted";
|
|
101
|
+
}
|
|
102
|
+
function _shortDigest(value) {
|
|
103
|
+
if (!value.startsWith("sha256:") || value.length <= 20) {
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
return `${value.slice(0, 15)}...${value.slice(-8)}`;
|
|
107
|
+
}
|
|
108
|
+
function _escapeInlineCode(value) {
|
|
109
|
+
return value.replaceAll("`", "\\`");
|
|
110
|
+
}
|
|
111
|
+
function _escapeMarkdown(value) {
|
|
112
|
+
return value.replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
113
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DeletePlan } from "../db/index.js";
|
|
2
|
+
import type { DeleteExecutionSummary } from "../execute/index.js";
|
|
3
|
+
export interface CleanupSummaryRoot {
|
|
4
|
+
versionId: number;
|
|
5
|
+
digest: string;
|
|
6
|
+
manifestKind?: string;
|
|
7
|
+
rootTags: string[];
|
|
8
|
+
matchedTags: string[];
|
|
9
|
+
selectionMode: string;
|
|
10
|
+
selectionReason: string;
|
|
11
|
+
validationStatus: "fully-deletable" | "blocked" | "untag-only";
|
|
12
|
+
validationReasonCode: "untag-only-partial-tag-match" | "fully-deletable-no-retained-overlap" | "blocked-overlap-with-retained-root";
|
|
13
|
+
validationReason: string;
|
|
14
|
+
blockingVersionId?: number;
|
|
15
|
+
blockingDigest?: string;
|
|
16
|
+
overlapDigest?: string;
|
|
17
|
+
overlapManifestKind?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface CleanupSummary {
|
|
20
|
+
command: "cleanup";
|
|
21
|
+
owner: string;
|
|
22
|
+
packageName: string;
|
|
23
|
+
scanCompletedAt: string;
|
|
24
|
+
dryRun: boolean;
|
|
25
|
+
plannerInputs: DeletePlan["plannerInputs"];
|
|
26
|
+
validationSummary: DeletePlan["validationSummary"];
|
|
27
|
+
directTargetTags: string[];
|
|
28
|
+
collateralTags: string[];
|
|
29
|
+
fullyDeletableRoots: CleanupSummaryRoot[];
|
|
30
|
+
untagOnlyRoots: CleanupSummaryRoot[];
|
|
31
|
+
blockedRoots: CleanupSummaryRoot[];
|
|
32
|
+
deletedPackageVersions: DeleteExecutionSummary["deletedPackageVersions"];
|
|
33
|
+
untaggedTags: DeleteExecutionSummary["untaggedTags"];
|
|
34
|
+
unsupportedUntagRoots: DeleteExecutionSummary["unsupportedUntagRoots"];
|
|
35
|
+
}
|
|
36
|
+
export declare function buildCleanupSummary(plan: DeletePlan, options: {
|
|
37
|
+
dryRun: boolean;
|
|
38
|
+
listRootTags: (versionId: number) => string[];
|
|
39
|
+
executionSummary?: DeleteExecutionSummary;
|
|
40
|
+
}): CleanupSummary;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function buildCleanupSummary(plan, options) {
|
|
2
|
+
const directTargetTagSet = new Set(plan.directTargetTags);
|
|
3
|
+
const roots = plan.rootDecisions.map((decision) => _mapRootDecision(decision, directTargetTagSet, options.listRootTags));
|
|
4
|
+
return {
|
|
5
|
+
command: "cleanup",
|
|
6
|
+
owner: plan.owner,
|
|
7
|
+
packageName: plan.packageName,
|
|
8
|
+
scanCompletedAt: plan.scanCompletedAt,
|
|
9
|
+
dryRun: options.dryRun,
|
|
10
|
+
plannerInputs: plan.plannerInputs,
|
|
11
|
+
validationSummary: plan.validationSummary,
|
|
12
|
+
directTargetTags: plan.directTargetTags,
|
|
13
|
+
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
|
+
deletedPackageVersions: options.executionSummary?.deletedPackageVersions ?? [],
|
|
18
|
+
untaggedTags: options.executionSummary?.untaggedTags ?? [],
|
|
19
|
+
unsupportedUntagRoots: options.executionSummary?.unsupportedUntagRoots ?? []
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function _mapRootDecision(decision, directTargetTagSet, listRootTags) {
|
|
23
|
+
const rootTags = listRootTags(decision.versionId);
|
|
24
|
+
return {
|
|
25
|
+
versionId: decision.versionId,
|
|
26
|
+
digest: decision.digest,
|
|
27
|
+
manifestKind: decision.manifestKind,
|
|
28
|
+
rootTags,
|
|
29
|
+
matchedTags: rootTags.filter((tag) => directTargetTagSet.has(tag)),
|
|
30
|
+
selectionMode: decision.selectionMode,
|
|
31
|
+
selectionReason: decision.selectionReason,
|
|
32
|
+
validationStatus: decision.validationStatus,
|
|
33
|
+
validationReasonCode: decision.validationReasonCode,
|
|
34
|
+
validationReason: decision.validationReason,
|
|
35
|
+
blockingVersionId: decision.blockingVersionId,
|
|
36
|
+
blockingDigest: decision.blockingDigest,
|
|
37
|
+
overlapDigest: decision.overlapDigest,
|
|
38
|
+
overlapManifestKind: decision.overlapManifestKind
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/cli/_args.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ import { type LogLevel } from "./_logger.js";
|
|
|
2
2
|
export declare function requireOption(args: string[], name: string): string;
|
|
3
3
|
export declare function findOption(args: string[], name: string): string | undefined;
|
|
4
4
|
export declare function collectRepeatedOption(args: string[], name: string): string[];
|
|
5
|
+
export declare function hasFlag(args: string[], name: string): boolean;
|
|
5
6
|
export declare function resolveGitHubToken(args: string[]): string;
|
|
6
7
|
export declare function resolveLogLevel(args: string[]): LogLevel;
|
package/dist/cli/_args.js
CHANGED
|
@@ -22,6 +22,9 @@ export function collectRepeatedOption(args, name) {
|
|
|
22
22
|
}
|
|
23
23
|
return values;
|
|
24
24
|
}
|
|
25
|
+
export function hasFlag(args, name) {
|
|
26
|
+
return args.includes(name);
|
|
27
|
+
}
|
|
25
28
|
export function resolveGitHubToken(args) {
|
|
26
29
|
const cliToken = findOption(args, "--token");
|
|
27
30
|
if (cliToken) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleCleanup(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { buildCleanupSummary } from "../cleanup-summary/index.js";
|
|
2
|
+
import { CleanupRunWriter, openDatabase, PlannerRepository } from "../db/index.js";
|
|
3
|
+
import { executeDeletePlan } from "../execute/index.js";
|
|
4
|
+
import { hasFlag, resolveGitHubToken, resolveLogLevel } from "./_args.js";
|
|
5
|
+
import { createLogger } from "./_logger.js";
|
|
6
|
+
import { loadDeletePlan, resolvePlanCommandInputs } from "./_planner-options.js";
|
|
7
|
+
import { resolveTagSelectors } from "./_tag-selector-resolver.js";
|
|
8
|
+
export async function handleCleanup(args) {
|
|
9
|
+
const inputs = resolvePlanCommandInputs(args);
|
|
10
|
+
const dryRun = hasFlag(args, "--dry-run");
|
|
11
|
+
const token = dryRun ? undefined : resolveGitHubToken(args);
|
|
12
|
+
const logger = createLogger(resolveLogLevel(args));
|
|
13
|
+
const database = openDatabase(inputs.databasePath);
|
|
14
|
+
try {
|
|
15
|
+
const repository = new PlannerRepository(database, logger);
|
|
16
|
+
const cleanupRunWriter = new CleanupRunWriter(database);
|
|
17
|
+
logger.debug(`Starting cleanup for ${inputs.owner}/${inputs.packageName}`);
|
|
18
|
+
const plan = loadDeletePlan(repository, resolveTagSelectors(database, inputs));
|
|
19
|
+
cleanupRunWriter.persistCleanupRun(repository.getLatestCompletedScanId(inputs.owner, inputs.packageName), plan, {
|
|
20
|
+
dryRun,
|
|
21
|
+
cleanupStartedAt: new Date().toISOString()
|
|
22
|
+
});
|
|
23
|
+
if (dryRun) {
|
|
24
|
+
const summary = buildCleanupSummary(plan, {
|
|
25
|
+
dryRun: true,
|
|
26
|
+
listRootTags: (versionId) => _listRootTags(database, inputs.owner, inputs.packageName, versionId)
|
|
27
|
+
});
|
|
28
|
+
logger.debug(`Completed dry-run cleanup for ${inputs.owner}/${inputs.packageName}`);
|
|
29
|
+
console.log(JSON.stringify(summary));
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
const executionSummary = await executeDeletePlan(plan, {
|
|
33
|
+
token: token,
|
|
34
|
+
logger,
|
|
35
|
+
listRootTags: (root) => _listRootTags(database, root.owner, root.packageName, root.versionId)
|
|
36
|
+
});
|
|
37
|
+
const summary = buildCleanupSummary(plan, {
|
|
38
|
+
dryRun: false,
|
|
39
|
+
listRootTags: (versionId) => _listRootTags(database, inputs.owner, inputs.packageName, versionId),
|
|
40
|
+
executionSummary
|
|
41
|
+
});
|
|
42
|
+
logger.debug(`Completed cleanup for ${inputs.owner}/${inputs.packageName}`);
|
|
43
|
+
console.log(JSON.stringify(summary));
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
database.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function _listRootTags(database, owner, packageName, versionId) {
|
|
51
|
+
const rows = database
|
|
52
|
+
.prepare(`
|
|
53
|
+
SELECT tags.tag
|
|
54
|
+
FROM tags
|
|
55
|
+
INNER JOIN v_latest_scan_per_package latest_scan ON latest_scan.scan_id = tags.scan_id
|
|
56
|
+
WHERE latest_scan.owner = ?
|
|
57
|
+
AND latest_scan.package_name = ?
|
|
58
|
+
AND tags.version_id = ?
|
|
59
|
+
ORDER BY tags.tag
|
|
60
|
+
`)
|
|
61
|
+
.all(owner, packageName, versionId);
|
|
62
|
+
return rows.map((row) => row.tag);
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleDbMerge(args: string[]): Promise<number>;
|