diff --git a/.actrc b/.actrc new file mode 100644 index 0000000..a945ba1 --- /dev/null +++ b/.actrc @@ -0,0 +1 @@ +-P ubuntu-latest=catthehacker/ubuntu:js-latest-dev diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..b7fb416 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,41 @@ +name: Test action + +on: + push: + branches: + - 'master' + pull_request: + branches: + - 'master' + +jobs: + test: + name: Test GitHub action + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ github.repository_owner }} + password: ${{ secrets.CR_WRITE_PAT }} + registry: ghcr.io + - name: Install dependencies + run: yarn install + - name: Build + run: yarn run build + - name: tag hello world image + run: docker pull hello-world:latest && docker tag hello-world:latest ghcr.io/nodefactoryio/github-packages-cleanup-sample:${{github.sha}} + - name: push hello world image + run: docker push ghcr.io/nodefactoryio/github-packages-cleanup-sample:${{github.sha}} + - name: Run cleanup action + uses: ./ + id: deleted-packages + with: + token: ${{ secrets.CR_WRITE_PAT }} + package_name: github-packages-cleanup-sample + num_versions_to_keep: 1 + organization: NodeFactoryIo + - name: Log deleted packages + run: echo "Deleted packages ${{ steps.deleted-packages.outputs.DELETED_PACKAGES }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ecedea --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +.idea +.env +.nyc_output +lib diff --git a/README.md b/README.md index 21d0f9a..bfdd7a9 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # GitHub packages cleanup -GitHub action for deleting versions of a package from [GitHub Packages](https://github.com/features/packages). +GitHub action for ghcr images cleanup from [GitHub Packages](https://github.com/features/packages). ## Things that can be done * Delete a single version * Delete multiple versions * Delete oldest version(s) -* Delete packages owned by an user or organisation +* Delete packages owned by an user or organization * Delete version(s) of a package that is hosted in the same repo that is executing the workflow * Delete version(s) of a package that is hosted in a different repo than the one executing the workflow diff --git a/action.yaml b/action.yaml new file mode 100644 index 0000000..6b1b296 --- /dev/null +++ b/action.yaml @@ -0,0 +1,25 @@ +name: "github ghcr images cleanup" +author: "NodeFactory" +description: "github ghcr images cleanup from the github package registry" +inputs: + excluded_versions: + description: "allows to exclude certain versions, eg. x.y.z, stable, beta, latest,..." + required: false + num_versions_to_keep: + description: "number of most recent versions to keep (excluded versions aren't included into count" + required: true + package_name: + description: "name of github package" + required: true + token: + description: "github auth token" + required: true + username: + description: "github username" + required: false + organization: + description: "github organization" + required: false +runs: + using: "node12" + main: "dist/index.js" diff --git a/package.json b/package.json new file mode 100644 index 0000000..0d7746e --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "github-packages-cleanup", + "version": "1.0.0", + "description": "TypeScript github action for ghcr images cleanup", + "main": "dist/index.js", + "author": { + "name": "NodeFactory", + "email": "info@nodefactory.io" + }, + "license": "MIT", + "private": false, + "repository": { + "type": "git", + "url": "git+https://git@github.com:nodefactoryio/github-packages-cleanup.git" + }, + "scripts": { + "build": "ncc build src/index.ts" + }, + "dependencies": { + "@actions/core": "^1.2.7", + "@actions/github": "^4.0.0" + }, + "devDependencies": { + "@types/node": "^15.0.2", + "@vercel/ncc": "^0.28.5", + "typescript": "^4.2.4" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..7de6e95 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,113 @@ +import { getInput, setFailed, setOutput } from "@actions/core"; +import { getOctokit } from "@actions/github"; +import { OctokitResponse } from "@octokit/types" +import { FetchPackagesResponse } from "./types"; + +const excludedVersion = getInput('excluded_versions', { required: false }); +const numberOfVersionsToKeep = Number(getInput('num_versions_to_keep', { required: true })); +const packageName = getInput('package_name', { required: true }); +const token = getInput('token', { required: true }); +const username = getInput('username', { required: false }); +const organization = getInput('organization', { required: false }); + + +const octokit = getOctokit(token); + +async function main() { + try { + let deletedPckgsOutput = undefined; + // delete packages with token auth + if (token && !username && !organization) { + const fetchedPackages = await getAuthUserPackageVersions(); + const packagesToDelete = filterOutPackages(fetchedPackages); + packagesToDelete.forEach(async (element) => { + deletedPckgsOutput = await deleteAuthUserPackageVersions(element!.id); + }); + setOutput('DELETED_PACKAGES', deletedPckgsOutput ?? 0); + // delete user packages + } else if (token && username && !organization) { + const fetchedPackages = await getUserPackageVersions(); + const packagesToDelete = filterOutPackages(fetchedPackages); + packagesToDelete.forEach(async (element) => { + deletedPckgsOutput = await deleteUserPackageVersions(element!.id); + }); + setOutput('DELETED_PACKAGES', deletedPckgsOutput ?? 0); + // delete organization packages + } else if (token && !username && organization) { + const fetchedPackages = await getOrganizationPackageVersions(); + const packagesToDelete = filterOutPackages(fetchedPackages); + packagesToDelete.forEach(async (element) => { + deletedPckgsOutput = await deleteOrganizationPackageVersions(element!.id); + }); + setOutput('DELETED_PACKAGES', deletedPckgsOutput ?? 0); + } else { + setFailed("Failed to fetch packages"); + } + } catch (e) { + console.error(`Deleting package failed because of: ${e}`); + } +} + +async function getAuthUserPackageVersions(): Promise> { + return await octokit.request('GET /user/packages/{package_type}/{package_name}/versions', { + package_type: 'container', + package_name: packageName, + }); +} + +async function deleteAuthUserPackageVersions(packageId: number): Promise> { + return await octokit.request('DELETE /user/packages/{package_type}/{package_name}/versions/{package_version_id}', { + package_type: 'container', + package_name: packageName, + package_version_id: packageId + }); +} + +async function getUserPackageVersions(): Promise> { + return await octokit.request('GET /users/{username}/packages/{package_type}/{package_name}/versions', { + package_type: 'container', + package_name: packageName, + username: username + }); +} + +async function deleteUserPackageVersions(packageId: number): Promise> { + return await octokit.request('DELETE /users/{username}/packages/{package_type}/{package_name}/versions/{package_version_id}', { + package_type: 'container', + package_name: packageName, + username: username, + package_version_id: packageId + }); +} + +async function getOrganizationPackageVersions(): Promise> { + return await octokit.request('GET /orgs/{org}/packages/{package_type}/{package_name}/versions', { + package_type: 'container', + package_name: packageName, + org: organization + }); +} + +async function deleteOrganizationPackageVersions(packageId: number): Promise> { + return await octokit.request('DELETE /orgs/{org}/packages/{package_type}/{package_name}/versions/{package_version_id}', { + package_type: 'container', + package_name: packageName, + org: organization, + package_version_id: packageId + }); +} + +function filterOutPackages(existingPackages: OctokitResponse): (FetchPackagesResponse | undefined)[] { + return existingPackages.data.map((item: FetchPackagesResponse) => { + // find package versions matching regex + if (item.metadata?.container?.tags[0]?.match(excludedVersion).index == 0) { + return item; + }; + }) + // filter out undefined values + .filter(item => item) + // packages for deletion - omitting last n values + .slice(0, existingPackages.data.length - numberOfVersionsToKeep); +} + +main(); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..7dd51c9 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,20 @@ +export type PackageType = "npm" | "docker" | "container" | "maven" | "rubygems" | "nuget"; + +export type FetchPackagesResponse = { + id: number; + name: string; + url: string; + package_html_url?: string; + html_url?: string; + license?: string; + description?: string; + created_at: string; + updated_at: string; + deleted_at?: string; + metadata?: { + package_type: PackageType; + container?: { + tags: Array + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7264c5a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "sourceMap": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "esModuleInterop": true, + "emitDecoratorMetadata": true, + "strict": true, + "noImplicitAny": true, + "resolveJsonModule": true, + "typeRoots": [ + "./node_modules/@types", + "./@types" + ] + }, + "ts-node": { + "transpileOnly": true, + "files": true + }, + "include": [ + "src/**/*.ts", + ], + "exclude": [ + "node_modules" + ] +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..4e5878a --- /dev/null +++ b/yarn.lock @@ -0,0 +1,168 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@actions/core@^1.2.7": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.2.7.tgz#594f8c45b213f0146e4be7eda8ae5cf4e198e5ab" + integrity sha512-kzLFD5BgEvq6ubcxdgPbRKGD2Qrgya/5j+wh4LZzqT915I0V3rED+MvjH6NXghbvk1MXknpNNQ3uKjXSEN00Ig== + +"@actions/github@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@actions/github/-/github-4.0.0.tgz#d520483151a2bf5d2dc9cd0f20f9ac3a2e458816" + integrity sha512-Ej/Y2E+VV6sR9X7pWL5F3VgEWrABaT292DRqRU6R4hnQjPtC/zD3nagxVdXWiRQvYDh8kHXo7IDmG42eJ/dOMA== + dependencies: + "@actions/http-client" "^1.0.8" + "@octokit/core" "^3.0.0" + "@octokit/plugin-paginate-rest" "^2.2.3" + "@octokit/plugin-rest-endpoint-methods" "^4.0.0" + +"@actions/http-client@^1.0.8": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-1.0.11.tgz#c58b12e9aa8b159ee39e7dd6cbd0e91d905633c0" + integrity sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg== + dependencies: + tunnel "0.0.6" + +"@octokit/auth-token@^2.4.4": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3" + integrity sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA== + dependencies: + "@octokit/types" "^6.0.3" + +"@octokit/core@^3.0.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.4.0.tgz#b48aa27d755b339fe7550548b340dcc2b513b742" + integrity sha512-6/vlKPP8NF17cgYXqucdshWqmMZGXkuvtcrWCgU5NOI0Pl2GjlmZyWgBMrU8zJ3v2MJlM6++CiB45VKYmhiWWg== + dependencies: + "@octokit/auth-token" "^2.4.4" + "@octokit/graphql" "^4.5.8" + "@octokit/request" "^5.4.12" + "@octokit/request-error" "^2.0.5" + "@octokit/types" "^6.0.3" + before-after-hook "^2.2.0" + universal-user-agent "^6.0.0" + +"@octokit/endpoint@^6.0.1": + version "6.0.11" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.11.tgz#082adc2aebca6dcefa1fb383f5efb3ed081949d1" + integrity sha512-fUIPpx+pZyoLW4GCs3yMnlj2LfoXTWDUVPTC4V3MUEKZm48W+XYpeWSZCv+vYF1ZABUm2CqnDVf1sFtIYrj7KQ== + dependencies: + "@octokit/types" "^6.0.3" + is-plain-object "^5.0.0" + universal-user-agent "^6.0.0" + +"@octokit/graphql@^4.5.8": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.6.1.tgz#f975486a46c94b7dbe58a0ca751935edc7e32cc9" + integrity sha512-2lYlvf4YTDgZCTXTW4+OX+9WTLFtEUc6hGm4qM1nlZjzxj+arizM4aHWzBVBCxY9glh7GIs0WEuiSgbVzv8cmA== + dependencies: + "@octokit/request" "^5.3.0" + "@octokit/types" "^6.0.3" + universal-user-agent "^6.0.0" + +"@octokit/openapi-types@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-7.0.0.tgz#0f6992db9854af15eca77d71ab0ec7fad2f20411" + integrity sha512-gV/8DJhAL/04zjTI95a7FhQwS6jlEE0W/7xeYAzuArD0KVAVWDLP2f3vi98hs3HLTczxXdRK/mF0tRoQPpolEw== + +"@octokit/plugin-paginate-rest@^2.2.3": + version "2.13.3" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.13.3.tgz#f0f1792230805108762d87906fb02d573b9e070a" + integrity sha512-46lptzM9lTeSmIBt/sVP/FLSTPGx6DCzAdSX3PfeJ3mTf4h9sGC26WpaQzMEq/Z44cOcmx8VsOhO+uEgE3cjYg== + dependencies: + "@octokit/types" "^6.11.0" + +"@octokit/plugin-rest-endpoint-methods@^4.0.0": + version "4.15.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.15.1.tgz#91a064bee99d0ffcef74a04357e1cf15c27d1cd0" + integrity sha512-4gQg4ySoW7ktKB0Mf38fHzcSffVZd6mT5deJQtpqkuPuAqzlED5AJTeW8Uk7dPRn7KaOlWcXB0MedTFJU1j4qA== + dependencies: + "@octokit/types" "^6.13.0" + deprecation "^2.3.1" + +"@octokit/request-error@^2.0.0", "@octokit/request-error@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.5.tgz#72cc91edc870281ad583a42619256b380c600143" + integrity sha512-T/2wcCFyM7SkXzNoyVNWjyVlUwBvW3igM3Btr/eKYiPmucXTtkxt2RBsf6gn3LTzaLSLTQtNmvg+dGsOxQrjZg== + dependencies: + "@octokit/types" "^6.0.3" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request@^5.3.0", "@octokit/request@^5.4.12": + version "5.4.15" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.15.tgz#829da413dc7dd3aa5e2cdbb1c7d0ebe1f146a128" + integrity sha512-6UnZfZzLwNhdLRreOtTkT9n57ZwulCve8q3IT/Z477vThu6snfdkBuhxnChpOKNGxcQ71ow561Qoa6uqLdPtag== + dependencies: + "@octokit/endpoint" "^6.0.1" + "@octokit/request-error" "^2.0.0" + "@octokit/types" "^6.7.1" + is-plain-object "^5.0.0" + node-fetch "^2.6.1" + universal-user-agent "^6.0.0" + +"@octokit/types@^6.0.3", "@octokit/types@^6.11.0", "@octokit/types@^6.13.0", "@octokit/types@^6.7.1": + version "6.14.2" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.14.2.tgz#64c9457f38fb8522bdbba3c8cc814590a2d61bf5" + integrity sha512-wiQtW9ZSy4OvgQ09iQOdyXYNN60GqjCL/UdMsepDr1Gr0QzpW6irIKbH3REuAHXAhxkEk9/F2a3Gcs1P6kW5jA== + dependencies: + "@octokit/openapi-types" "^7.0.0" + +"@types/node@^15.0.2": + version "15.0.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.2.tgz#51e9c0920d1b45936ea04341aa3e2e58d339fb67" + integrity sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA== + +"@vercel/ncc@^0.28.5": + version "0.28.5" + resolved "https://registry.yarnpkg.com/@vercel/ncc/-/ncc-0.28.5.tgz#6d735379f81b70b708a9c3d2196507b2a841824f" + integrity sha512-ZSwD4EDCon2EsnPZ2/Qcigx4N2DiuBLV/rDnF04giEPFuDeBeUDdnSTyYYfX8KNic/prrJuS1vUEmAOHmj+fRg== + +before-after-hook@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.1.tgz#73540563558687586b52ed217dad6a802ab1549c" + integrity sha512-/6FKxSTWoJdbsLDF8tdIjaRiFXiE6UHsEHE3OPI/cwPURCVi1ukP0gmLn7XWEiFk5TcwQjjY5PWsU+j+tgXgmw== + +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + +node-fetch@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + +once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +tunnel@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + +typescript@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" + integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== + +universal-user-agent@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" + integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=