MCPcopy Index your code
hub / github.com/dataaxiom/ghcr-cleanup-action

github.com/dataaxiom/ghcr-cleanup-action @v1.2.2

Chat with this repo
repository ↗ · DeepWiki ↗ · release v1.2.2 ↗ · + Follow
227 symbols 664 edges 32 files 50 documented · 22% updated 7d agov1.2.2 · 2026-06-06★ 901 open issues
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

GitHub Container Registry Image Cleanup Action

GitHub Super-Linter CI Check dist/ CodeQL Coverage

A GitHub Action that deletes container images from the GitHub Container Registry (ghcr.io). It safely handles multi-architecture images, attestation and Sigstore cosign referrers, and supports flexible retention rules including tag patterns, age, and "keep the N most recent" policies.

Quick start

With no options set, the action deletes all untagged images from the current repository's package. Always test with dry-run: true first.

jobs:
  cleanup:
    runs-on: ubuntu-latest
    permissions:
      packages: write
    steps:
      - uses: dataaxiom/ghcr-cleanup-action@v1
        with:
          dry-run: true

When owner and package are omitted, they default to the current workflow's project. Deleting a multi-architecture image also deletes its child platform images automatically.

Examples

Daily cleanup of untagged images

name: Daily Image Cleanup
on:
  schedule:
    - cron: '30 1 * * *'
  workflow_dispatch:
jobs:
  cleanup:
    runs-on: ubuntu-latest
    permissions:
      packages: write
    steps:
      - uses: dataaxiom/ghcr-cleanup-action@v1

Delete images when a pull request closes

name: Cleanup PR Images
on:
  pull_request:
    types: [closed]
jobs:
  cleanup:
    runs-on: ubuntu-latest
    permissions:
      packages: write
    steps:
      - uses: dataaxiom/ghcr-cleanup-action@v1
        with:
          delete-tags: pr-${{ github.event.pull_request.number }}

Keep the 3 most recent release candidates

jobs:
  cleanup:
    runs-on: ubuntu-latest
    concurrency:
      group: cleanup-images
    permissions:
      packages: write
    steps:
      - uses: dataaxiom/ghcr-cleanup-action@v1
        with:
          packages: 'tiecd/k8s'
          delete-tags: '*-rc*'
          keep-n-tagged: 3

Keep 10 tagged images plus dev, drop everything else

jobs:
  cleanup:
    runs-on: ubuntu-latest
    permissions:
      packages: write
    steps:
      - uses: dataaxiom/ghcr-cleanup-action@v1
        with:
          keep-n-tagged: 10
          exclude-tags: dev
          delete-untagged: true
          delete-partial-images: true

Cross-repository cleanup with a regular expression

jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - uses: dataaxiom/ghcr-cleanup-action@v1
        with:
          token: ${{ secrets.MY_PAT }}
          owner: dataaxiom
          package: tiecd
          delete-tags: '^mytag[12]$'
          use-regex: true

repository is omitted — the action looks up the package by owner + package directly, so the calling workflow's repository can be anywhere. This also works when the target package isn't linked to any repository at all.

Inputs

Repository

Option Default Description
token secrets.GITHUB_TOKEN Token used to call ghcr.io and the Package API.
owner project owner GitHub user or organization that owns the package.
repository repository name Informational only. Shown in logs; does not affect which packages are touched. Safe to omit.
package(s) repository name Comma-separated list of packages. Accepts package or packages. Wildcards require a PAT (below).
expand-packages false Enable wildcard / regular expression matching on package(s). Requires a PAT for token.

Cleanup rules

Option Default Description
delete-tags Comma-separated tags to delete (wildcards by default; regular expression if use-regex: true). Alias: tags.
exclude-tags Tags to always preserve. Takes priority over every other rule.
delete-untagged depends * Delete all untagged images.
keep-n-untagged Number of untagged images to keep, newest first.
keep-n-tagged Number of tagged images to keep, newest first.
delete-ghost-images false Delete multi-arch images whose platform children are all missing.
delete-partial-images false Delete multi-arch images whose platform children are partially missing.
delete-orphaned-images false Delete tagged referrer / cosign images whose parent no longer exists.
older-than Only include images older than this interval (e.g. 5 days, 6 months, 1 year).

* When no delete or keep options are set, delete-untagged defaults to true.

Other

Option Default Description
use-regex false Treat delete-tags, exclude-tags, and package(s) as regular expression patterns.
skip-regex-checks false Skip the length and ReDoS safety checks on regular expression patterns. Use only for intentionally large/complex patterns.
dry-run false Log everything that would be deleted without making changes.
validate false After cleanup, verify all multi-arch images have their platform children.
registry-url https://ghcr.io/ Container registry URL.
github-api-url https://api.github.com GitHub API URL.
log-level info One of error, warn, info, debug.

Conventions

  • Tag and package patterns use wildcard-match syntax (?, *, **) by default. Set use-regex: true to use regular expressions instead.
  • Tag values may also be given as sha256: digest strings.
  • "Untagged" refers to untagged images, not untagged GitHub packages. Cleaning up a multi-arch image may leave untagged platform packages visible in the GitHub UI.

Option details

delete-tags

Deletes the matching tags. If a tag points to an image that also has other tags, the action unlinks the tag without deleting the underlying image — the image is removed only once all its tags are gone.

exclude-tags

Tags listed here are excluded from every other rule. Useful as a safety net alongside keep-n-tagged or older-than.

with:
  exclude-tags: dev,latest,pr*
  keep-n-tagged: 10

delete-untagged

Deletes every untagged image. This is the default when no other rules are set, but it can also be combined explicitly with any rule except keep-n-untagged.

keep-n-untagged

Sorts untagged images by date and keeps the newest N. Setting it to 0 is equivalent to delete-untagged: true. Cannot be combined with delete-untagged.

keep-n-tagged

Sorts tagged images by date and keeps the newest N. By default it operates on all tagged images; combine with delete-tags to restrict it to a subset, or with exclude-tags to protect specific tags.

older-than

Restricts every delete and keep rule to images older than the given interval. Accepts singular and plural units: seconds, minutes, hours, days, weeks, months, years (see human-interval).

with:
  older-than: 1 year
  keep-n-tagged: 10

The example keeps every image younger than a year, plus the 10 newest images older than a year.

delete-ghost-images / delete-partial-images

Clean up multi-architecture images whose platform children are missing. Run with validate: true first to see which images would be affected.

delete-orphaned-images

Finds tagged images named sha256-… whose corresponding sha256: digest no longer exists, and deletes them. Catches stranded referrer and cosign artifacts.

Token setup

Injected GITHUB_TOKEN (default)

Grant the workflow packages: write:

permissions:
  packages: write

Alternatively, set Settings → Actions → General → Workflow permissions to "Read and write permissions" for the repository.

The package itself must also grant the workflow's repository the Admin role under Package Settings → Manage Actions access.

Personal Access Token (PAT)

A PAT is required when:

  • the package lives in a different repository than the workflow, or
  • expand-packages: true is used.

Create a Classic PAT (the GitHub Registry API does not yet support fine-grained tokens) with both write:packages and delete:packages scopes, and pass it via token:

with:
  token: ${{ secrets.MY_PAT }}

Operational notes

Concurrency

The action is not safe to run in parallel against the same package. Use a GitHub concurrency group on busy repositories:

concurrency:
  group: cleanup-images

Validation

validate: true performs a post-run scan of every multi-architecture image and warns if any platform children are missing. It is informational — the action does not fail on warnings.

Multiple packages

package (or packages) accepts a comma-separated list. To match by wildcard or regular expression, set expand-packages: true and use a PAT:

with:
  packages: myimage*,someotherimage
  expand-packages: true
  token: ${{ secrets.MY_PAT }}

Manifest cache

The action caches distilled manifest data between runs via @actions/cache, keyed by digest. Manifests are content-addressed, so cache hits are correct by construction — staleness is impossible. Warm runs only fetch manifests for digests that aren't already in the cache; on a stable repository that's a handful of new manifests per run instead of the entire package.

The cache hit rate is logged at the end of every run, e.g. manifest cache: 5234/5294 digests served from cache (99%).

GitHub evicts cache entries that haven't been read in 7 days. On a very large repository the first run is expensive — it fetches every manifest to warm the cache. If the action then sits idle for more than 7 days, that warmed entry is evicted and the next run pays the cold-start cost again.

To stay fast on large repos, schedule the action to run at least every 7 days. A weekly schedule (or a daily run that may end up no-op) is enough to refresh the cache's last-access timestamp and avoid eviction.

Cache failures degrade gracefully — if @actions/cache is unavailable or a restore/save fails, the action falls back to fetching manifests live without failing the workflow.

Effect on download counts

GitHub records each manifest fetch as a download against the package. On a cold run (no warm manifest cache) the action fetches every manifest, so the package's download count rises by one per package version. On a warm run only the uncached manifests are fetched — for a stable repository that's a small fraction of the total per run, often single digits. The underlying image layers are never downloaded.

Restoring deleted packages

The workflow log prints the ID of every deleted package version. Use the GitHub REST API to restore one:

Extension points exported contracts — how you extend this code

Core symbols most depended-on inside this repo

Shape

Method 89
Function 87
Class 32
Interface 18
Enum 1

Languages

TypeScript100%

Modules by API surface

citester/sourcemap-register.cjs57 symbols
src/utils.ts30 symbols
src/package-repo.ts16 symbols
src/manifest-cache.ts16 symbols
src/registry.ts13 symbols
src/image-deleter.ts10 symbols
src/deletion-strategy.ts9 symbols
src/__tests__/package-repo.test.ts8 symbols
src/manifest-analyzer.ts7 symbols
src/main.ts7 symbols
src/image-validator.ts7 symbols
src/__tests__/main.test.ts7 symbols

For agents

$ claude mcp add ghcr-cleanup-action \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page