Cleanup policies set the retention of repository content: the administrator describes in rules what to keep and for how long, and Galleon applies those rules itself, with no manual tidying.

The typical jobs they cover: not keeping snapshot builds longer than a month, removing versions nobody has downloaded in a long time, bounding the size of a proxy repository cache, keeping only the few latest releases of every package.

A policy is a set of criteria: age, time since the last download, path. A component is deleted only when it matches every criterion of the policy at once: a policy with the age “older than 30 days” and the path .*-SNAPSHOT.* deletes only what is both older than thirty days and sits under a matching path.

A policy is created for one format and applies to repositories of that format only: a Maven policy cannot be bound to an npm repository. Several policies can be bound to one repository — a component is deleted when any of them selects it. Their order does not matter: the results add up.

A policy deletes components from the catalog, while the disk space is returned by the garbage collection — it takes the content nothing references any more, no earlier than gc.grace_period (Storage and garbage collection).

Hosted and proxy

Policies work with repositories of both types, but the outcome differs.

In a hosted repository the deletion is irreversible: the artifact was published into Galleon, and the only way to get it back is to publish it again. For hosted repositories, therefore, choose the criteria carefully and check them with a dry run.

In a proxy repository only the cached copy is deleted. The artifact itself stays in the external registry and the next request fetches it again — users notice nothing beyond that first request being slower. Cleanup works as cache eviction here: it frees the space taken by what nobody asks for any more.

Cleanup does not obey the repository write policy. A repository with the DENY write policy is closed for client writes, but a bound cleanup policy still cleans it.

Creating a policy

The “Settings” → “Cleanup Policies” page, the “+ New policy” button.

A policy name starts with a letter, a digit, or a hyphen, contains letters, digits, hyphens, underscores, and dots, and is at most 255 characters long. Names are compared ignoring case, and the name cannot be changed after creation.

The format can be changed while the policy is not bound to any repository. For a bound policy the change is refused: remove the bindings first, then change the format.

A policy without a single criterion deletes nothing — the list marks it as “selects nothing”. The same holds for a policy that only sets “Retain newest versions”: there is nothing to select and therefore nothing to exclude.

Criteria

A component is deleted only when it matches every criterion that is set. The criteria common to all formats:

“Component age (days)” — selects the components whose last content change across all assets is older than the given number of days. The value is an integer between 1 and 24,855.

“Component usage (days)” — selects the components none of whose assets have been downloaded for the given number of days. An asset that has never been downloaded counts from its last content change. The range is the same.

“Asset path regex” — selects the components at least one of whose assets has a matching path. The match is complete: the expression is anchored on both sides and is compared against the full path, the leading slash included. The syntax is RE2, up to 1024 characters; constructs RE2 does not have are refused when the policy is saved — .*(?!-SNAPSHOT)\.jar, for example.

The antlr.* expression matches no path, because it is compared against the whole path. Write .*antlr.* instead.

Format-dependent criteria

“Release type” is available for the Maven and npm formats and selects either releases only or prereleases only. A prerelease is defined differently: for Maven it is a version with the -SNAPSHOT suffix, for npm — a version carrying a semver prerelease suffix.

“Retain newest versions” keeps the given number of the newest versions of a package: they are not deleted even when they match the other criteria. The count is per package, not per repository as a whole. The criterion is available to two formats:

  • Maven — versions are ordered by version number, and the criterion requires the “RELEASES” release type: the exclusion applies to release versions only;
  • Docker — tags are ordered by date and are only counted in the tags scope.

Docker specifics

A Docker component is an image-and-tag pair, so a policy has two scopes and at least one of them has to be on:

  • “Tags” — tagged images, one component per tag. On by default. “Retain newest versions” works in this scope only;
  • “Untagged manifests” — standalone manifests that are not part of a live image index. Off by default.

Cleanup removes tags and deletes manifests; the image files themselves are collected later by the garbage collection.

When both scopes are enabled in one policy, they are evaluated in turn: the untagged manifests first, the tags second. A manifest whose last tag was removed by this very pass therefore comes up for deletion only on the next one — the nightly pass or a manual run. When planning a first large cleanup, count on an image disappearing over two passes: the tags first, the manifest after.

If the tags and the untagged manifests are split across different policies, it all depends on the binding order: a manifest policy listed after a tag policy sees the freed manifests within the current pass.

The age of a Docker component is counted from its tag: creating the tag and a push that changes what the tag points at reset the count, while a repeated push of the same content does not. The age of an untagged manifest counts from the moment it was uploaded to the repository.

The regex selects a Docker component by the manifest path only: /v2/<IMAGE>/manifests/<TAG> for a tagged component and the digest path for an untagged manifest. Layer paths never select a component.

Signatures and attestations

Signatures and attestations live in the repository next to the image, and cleanup treats them differently depending on how they are attached to it.

A signature attached to an image with the subject reference from the OCI 1.1 standard is kept exactly as long as the image itself: while the image is in the repository no policy deletes the signature, and together with the image it deletes the signature too. Signatures are published this way by oras attach and by cosign run with --registry-referrers-mode=oci-1-1. Nothing has to be configured for such signatures — enabling the untagged manifests scope is enough.

A signature published under a separate tag such as sha256-<DIGEST>.sig (and likewise .att and .sbom) is not linked to the image in any way: to Galleon it is a separate image with a tag of its own, and a policy evaluates it on its own. Tags like these are produced by cosign in the legacy mode: that mode predates the OCI 1.1 standard and stays on until the client is told to use oci-1-1. A signature is pulled rarely, only when it is verified, so by time since the last download it falls under deletion even when the image itself is pulled daily. In a hosted repository the signature cannot be brought back afterwards: the policy deletes it, the image stays, and signature verification starts failing where it used to pass. The only way to get it back is to sign the image again. In a proxy repository there are no consequences: the cached copy is deleted, and the next verification fetches the signature from the external registry again.

If a hosted repository holds signatures like these, write the regex so that only the images you mean fall under the policy, set the day windows with a margin, and always do a dry run before binding.

Binding to repositories

A policy takes effect once it is bound to a repository: the repository form (Repositories) has a “Cleanup policies” field where policies of the same format are picked.

Deleting a policy removes the binding from every repository that used it.

Running

The ways to run cleanup:

  1. On schedule — daily at 01:00 UTC over every repository with bound policies. A repository taken out of service (“online” cleared) is skipped by the scheduled pass.
  2. Manually over all repositories — the “Run now” button on the task card of the cleanup policies page.
  3. Manually over one repository — the “Run cleanup” button in the repository form. A manual run works for a repository taken out of service as well.

Only one run per repository happens at a time; a repeated one answers that the cleanup is already running. The outcome of the scheduled pass is shown on the task card in the “Last run” field.

Dry run

The “Dry run” button counts the components a policy would delete from the chosen repository and deletes nothing. The policy does not have to be bound to that repository — this is the way to check the criteria before they start working.

A dry run counts per policy, so a component selected by two bound policies is counted twice; a real run deletes it once.

Images carry one more discrepancy: when a pass deletes an untagged index, the manifests it held become candidates within that same pass, and a real run may delete more than its dry run showed.

Rights

Policies belong to the settings domain: viewing the list requires the nx-settings-read privilege, while creating, changing, and deleting require nx-settings-update (Privileges).

Running a cleanup requires nx-tasks-run, the task state — nx-tasks-read. The run button works without the right to read the task state: only the card with the outcome of the previous pass stays hidden.