Skip to content

Cleanup Untagged Packages #18

Cleanup Untagged Packages

Cleanup Untagged Packages #18

name: Cleanup Untagged Packages
on:
schedule:
- cron: "0 0 * * 6" # Runs every Saturday at 00:00 UTC
workflow_dispatch:
jobs:
cleanup-ghcr-untagged:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Delete untagged GHCR package versions
uses: actions/github-script@v7
env:
GITHUB_TOKEN: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }}
with:
github-token: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const { classifyVersions, paginate } = require('./scripts/cleanup-untagged-packages.js');
const org = context.repo.owner;
const packageType = "container";
const perPage = 100;
const packages = await paginate(
(page) =>
github.request("GET /orgs/{org}/packages", {
org,
package_type: packageType,
per_page: perPage,
page,
}),
perPage,
);
if (packages.length === 0) {
core.info(`No ${packageType} packages found in ${org}.`);
return;
}
core.info(`Found ${packages.length} ${packageType} packages in ${org}.`);
const ghToken = process.env.GITHUB_TOKEN || "";
const basicAuth = Buffer.from(`USERNAME:${ghToken}`).toString("base64");
async function getRegistryToken(scope) {
const res = await fetch(
`https://ghcr.io/token?service=ghcr.io&scope=${encodeURIComponent(scope)}`,
{ headers: { Authorization: `Basic ${basicAuth}` } },
);
if (!res.ok) return null;
const body = await res.json();
return body.token || null;
}
let totalDeleted = 0;
for (const pkg of packages) {
const versions = await paginate(
(page) =>
github.request(
"GET /orgs/{org}/packages/{package_type}/{package_name}/versions",
{
org,
package_type: packageType,
package_name: pkg.name,
per_page: perPage,
page,
},
),
perPage,
);
const registryToken = await getRegistryToken(
`repository:${org}/${pkg.name}:pull`,
);
const { deletable, skipped } = await classifyVersions({
org,
pkgName: pkg.name,
versions,
registryToken,
log: core,
});
if (skipped) {
core.info(`Package ${pkg.name}: skipping deletion.`);
continue;
}
if (deletable.length === 0) {
core.info(`Package ${pkg.name}: no untagged versions to delete.`);
continue;
}
core.info(
`Package ${pkg.name}: deleting ${deletable.length} untagged version(s).`,
);
for (const version of deletable) {
await github.request(
"DELETE /orgs/{org}/packages/{package_type}/{package_name}/versions/{package_version_id}",
{
org,
package_type: packageType,
package_name: pkg.name,
package_version_id: version.id,
},
);
totalDeleted += 1;
core.info(`Deleted ${pkg.name}@${version.id}`);
}
}
core.info(`Cleanup finished. Deleted ${totalDeleted} untagged version(s).`);