diff --git a/README.md b/README.md index a73212c..cbc1d86 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,11 @@ This action deletes versions of a package from [GitHub Packages](https://github. # Cannot be used with `num-old-versions-to-delete` and `ignore-versions`. delete-only-pre-release-versions: + # If true it will delete only the untagged versions in case of container package. + # Does not work for other package types and will be ignored. + # Defaults to false. + delete-only-untagged-versions: + # The token used to authenticate with GitHub Packages. # Defaults to github.token. # Required if the repo running the workflow does not have access to delete the package. @@ -88,6 +93,7 @@ This action deletes versions of a package from [GitHub Packages](https://github. - [Valid Input Combinations](#valid-input-combinations) - [Scenarios](#scenarios) - [Delete all pre-release versions except y latest pre-release package versions](#delete-all-pre-release-versions-except-y-latest-pre-release-package-versions) + - [Delete all untagged container versions except y latest untagged versions](#delete-all-untagged-container-versions-except-y-latest-untagged-versions) - [Delete all except y latest versions while ignoring particular package versions](#delete-all-except-y-latest-versions-while-ignoring-particular-package-versions) - [Delete oldest x number of versions while ignoring particular package versions](#delete-oldest-x-number-of-versions-while-ignoring-particular-package-versions) - [Delete all except y latest versions of a package](#delete-all-except-y-latest-versions-of-a-package) @@ -95,8 +101,9 @@ This action deletes versions of a package from [GitHub Packages](https://github. - [Delete oldest version of a package](#delete-oldest-version-of-a-package) - [Delete a specific version of a package](#delete-a-specific-version-of-a-package) - [Delete multiple specific versions of a package](#delete-multiple-specific-versions-of-a-package) +- [GitHub Enterprise Server](#github-enterprise-server) - [License](#license) - + ### Delete all pre-release versions except y latest pre-release package versions @@ -133,6 +140,25 @@ This action deletes versions of a package from [GitHub Packages](https://github.
+ ### Delete all untagged container versions except y latest untagged versions + + To delete all untagged versions of a container package except y latest untagged versions, the __package-name__, __package-type__, __min-versions-to-keep__ and __delete-only-untagged-versions__ inputs are required. __package-type__ must be container for this scenario. + + __Example__ + + Delete all untagged versions except latest 10 + + ```yaml + - uses: actions/delete-package-versions@v4 + with: + package-name: 'test-package' + package-type: 'container' + min-versions-to-keep: 10 + delete-only-untagged-versions: 'true' + ``` + +
+ ### Delete all except y latest versions while ignoring particular package versions To delete all except y latest versions while ignoring particular package versions, the __package-name__, __min-versions-to-keep__ and __ignore-versions__ inputs are required. @@ -375,6 +401,10 @@ This action deletes versions of a package from [GitHub Packages](https://github. token: ${{ secrets.PAT }} ``` +# GitHub Enterprise Server + +This action works with GitHub Enterprise Server. It uses the `GITHUB_API_URL` environment variable to determine the GitHub Enterprise Server URL. The environment variable is automatically set in workflows. GitHub Connect should be configured on the server to allow using the action from dotcom. See - [Enabling automatic access to GitHub.com actions using GitHub Connect](https://docs.github.com/en/enterprise-server/admin/github-actions/managing-access-to-actions-from-githubcom/enabling-automatic-access-to-githubcom-actions-using-github-connect) + # License The scripts and documentation in this project are released under the [MIT License](https://github.com/actions/delete-package-versions/blob/main/LICENSE) @@ -382,4 +412,3 @@ The scripts and documentation in this project are released under the [MIT Licens [api]: https://docs.github.com/en/rest/packages [token]: https://help.github.com/en/packages/publishing-and-managing-packages/about-github-packages#about-tokens [secret]: https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets - diff --git a/__tests__/delete.test.ts b/__tests__/delete.test.ts index b5dbe49..96dda11 100644 --- a/__tests__/delete.test.ts +++ b/__tests__/delete.test.ts @@ -255,6 +255,53 @@ describe('index tests -- call rest', () => { }) }) + it('finalIds test - delete only untagged versions with minVersionsToKeep', done => { + const numVersions = 50 + const numTaggedVersions = 20 + const numUntaggedVersions = numVersions - numTaggedVersions + + const taggedVersions = getMockedVersionsResponse( + numTaggedVersions, + 0, + 'container', + true + ) + const untaggedVersions = getMockedVersionsResponse( + numUntaggedVersions, + numTaggedVersions, + 'container', + false + ) + const versions = taggedVersions.concat(untaggedVersions) + + let apiCalled = 0 + + server.use( + rest.get( + 'https://api.github.com/users/test-owner/packages/container/test-package/versions', + (req, res, ctx) => { + apiCalled++ + return res(ctx.status(200), ctx.json(versions)) + } + ) + ) + + finalIds( + getInput({ + minVersionsToKeep: 10, + deleteUntaggedVersions: 'true', + packageType: 'container' + }) + ).subscribe(ids => { + expect(apiCalled).toBe(1) + expect(ids.length).toBe(numUntaggedVersions - 10) + for (let i = 0; i < numUntaggedVersions - 10; i++) { + expect(ids[i]).toBe(untaggedVersions[i].id.toString()) + } + done() + }) + }) + it('finalIds test - no versions deleted if API error even once', done => { const numVersions = RATE_LIMIT * 2 let apiCalled = 0 diff --git a/__tests__/version/get-version.test.ts b/__tests__/version/get-version.test.ts index 967125b..313a93d 100644 --- a/__tests__/version/get-version.test.ts +++ b/__tests__/version/get-version.test.ts @@ -77,6 +77,54 @@ describe('get versions tests -- mock rest', () => { }) }) + it('getOldestVersions -- success - container tagged versions', done => { + const numVersions = 6 + const numTaggedVersions = 3 + const numUntaggedVersions = numVersions - numTaggedVersions + + const respTagged = getMockedVersionsResponse( + numTaggedVersions, + 0, + 'container', + true + ) + const respUntagged = getMockedVersionsResponse( + numUntaggedVersions, + numTaggedVersions, + 'container', + false + ) + const resp = respTagged.concat(respUntagged) + + server.use( + rest.get( + 'https://api.github.com/users/test-owner/packages/container/test-package/versions', + (req, res, ctx) => { + return res(ctx.status(200), ctx.json(resp)) + } + ) + ) + + getOldestVersions({numVersions, packageType: 'container'}).subscribe( + result => { + expect(result.versions.length).toBe(numVersions) + for (let i = 0; i < numVersions; i++) { + expect(result.versions[i].id).toBe(resp[i].id) + expect(result.versions[i].version).toBe(resp[i].name) + expect(result.versions[i].created_at).toBe(resp[i].created_at) + if (i < numTaggedVersions) { + expect(result.versions[i].tagged).toBe(true) + } else { + expect(result.versions[i].tagged).toBe(false) + } + } + expect(result.paginate).toBe(true) + expect(result.totalCount).toBe(numVersions) + done() + } + ) + }) + it('getOldestVersions -- paginate is false when fetched versions is less than page size', done => { const numVersions = 5 diff --git a/__tests__/version/rest.mock.ts b/__tests__/version/rest.mock.ts index 94d5093..1bc97a4 100644 --- a/__tests__/version/rest.mock.ts +++ b/__tests__/version/rest.mock.ts @@ -2,23 +2,44 @@ import {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods/dis type GetVersionsResponseData = RestEndpointMethodTypes['packages']['getAllPackageVersionsForPackageOwnedByUser']['response']['data'] +type PackageType = + RestEndpointMethodTypes['packages']['getAllPackageVersionsForPackageOwnedByUser']['parameters']['package_type'] export function getMockedVersionsResponse( numVersions: number, - offset = 0 + offset = 0, + packageType = 'npm', + tagged = false ): GetVersionsResponseData { const versions: GetVersionsResponseData = [] for (let i = 1 + offset; i <= numVersions + offset; ++i) { const created_at = new Date() created_at.setUTCFullYear(2000 + Number(i), 1, 1) - versions.push({ + let version = { id: i, name: `${i}.0.0`, url: '', created_at: created_at.toUTCString(), package_html_url: '', - updated_at: '' - }) + updated_at: '', + metadata: { + package_type: packageType as PackageType + } + } as GetVersionsResponseData[0] + + if (packageType === 'container' && tagged) { + version = { + ...version, + metadata: { + package_type: packageType as PackageType, + container: { + tags: [`latest${i}`] as string[] + } + } + } + } + + versions.push(version) } return versions } diff --git a/action.yml b/action.yml index 67192dc..1bf23e2 100644 --- a/action.yml +++ b/action.yml @@ -55,6 +55,13 @@ inputs: required: false default: "false" + delete-only-untagged-versions: + description: > + Deletes only untagged versions in case of a container package. Does not work for other package types. + By default this is set to false + required: false + default: "false" + token: description: > Token with the necessary scopes to delete package versions. diff --git a/dist/index.js b/dist/index.js index 4cb7e48..89172a5 100644 --- a/dist/index.js +++ b/dist/index.js @@ -38,6 +38,9 @@ function finalIds(input) { Then compute number of versions to delete (toDelete) based on the inputs. */ value = value.filter(info => !input.ignoreVersions.test(info.version)); + if (input.deleteUntaggedVersions === 'true') { + value = value.filter(info => !info.tagged); + } let toDelete = 0; if (input.minVersionsToKeep < 0) { toDelete = Math.min(value.length, Math.min(input.numOldVersionsToDelete, exports.RATE_LIMIT)); @@ -88,7 +91,8 @@ const defaultParams = { minVersionsToKeep: 0, ignoreVersions: new RegExp(''), deletePreReleaseVersions: '', - token: '' + token: '', + deleteUntaggedVersions: '' }; class Input { constructor(params) { @@ -103,6 +107,7 @@ class Input { this.deletePreReleaseVersions = validatedParams.deletePreReleaseVersions; this.token = validatedParams.token; this.numDeleted = 0; + this.deleteUntaggedVersions = validatedParams.deleteUntaggedVersions; } hasOldestVersionQueryInfo() { return !!(this.owner && @@ -123,6 +128,9 @@ class Input { this.minVersionsToKeep > 0 ? this.minVersionsToKeep : 0; this.ignoreVersions = new RegExp('^(0|[1-9]\\d*)((\\.(0|[1-9]\\d*))*)$'); } + if (this.packageType.toLowerCase() !== 'container') { + this.deleteUntaggedVersions = 'false'; + } if (this.minVersionsToKeep >= 0) { this.numOldVersionsToDelete = 0; } @@ -214,10 +222,17 @@ function getOldestVersions(owner, packageName, packageType, numVersions, page, t }), (0, operators_1.map)(response => { const resp = { versions: response.data.map((version) => { + let tagged = false; + if (package_type === 'container' && + version.metadata && + version.metadata.container) { + tagged = version.metadata.container.tags.length > 0; + } return { id: version.id, version: version.name, - created_at: version.created_at + created_at: version.created_at, + tagged }; }), page, @@ -43916,7 +43931,8 @@ function getActionInput() { minVersionsToKeep: Number((0, core_1.getInput)('min-versions-to-keep')), ignoreVersions: RegExp((0, core_1.getInput)('ignore-versions')), deletePreReleaseVersions: (0, core_1.getInput)('delete-only-pre-release-versions').toLowerCase(), - token: (0, core_1.getInput)('token') + token: (0, core_1.getInput)('token'), + deleteUntaggedVersions: (0, core_1.getInput)('delete-only-untagged-versions').toLowerCase() }); } function run() { diff --git a/src/delete.ts b/src/delete.ts index e701c16..b5410e3 100644 --- a/src/delete.ts +++ b/src/delete.ts @@ -72,6 +72,11 @@ export function finalIds(input: Input): Observable { Then compute number of versions to delete (toDelete) based on the inputs. */ value = value.filter(info => !input.ignoreVersions.test(info.version)) + + if (input.deleteUntaggedVersions === 'true') { + value = value.filter(info => !info.tagged) + } + let toDelete = 0 if (input.minVersionsToKeep < 0) { toDelete = Math.min( diff --git a/src/input.ts b/src/input.ts index f0af8bf..e71e258 100644 --- a/src/input.ts +++ b/src/input.ts @@ -8,6 +8,7 @@ export interface InputParams { ignoreVersions?: RegExp token?: string deletePreReleaseVersions?: string + deleteUntaggedVersions?: string } const defaultParams = { @@ -19,7 +20,8 @@ const defaultParams = { minVersionsToKeep: 0, ignoreVersions: new RegExp(''), deletePreReleaseVersions: '', - token: '' + token: '', + deleteUntaggedVersions: '' } export class Input { @@ -33,6 +35,7 @@ export class Input { deletePreReleaseVersions: string token: string numDeleted: number + deleteUntaggedVersions: string constructor(params?: InputParams) { const validatedParams: Required = {...defaultParams, ...params} @@ -47,6 +50,7 @@ export class Input { this.deletePreReleaseVersions = validatedParams.deletePreReleaseVersions this.token = validatedParams.token this.numDeleted = 0 + this.deleteUntaggedVersions = validatedParams.deleteUntaggedVersions } hasOldestVersionQueryInfo(): boolean { @@ -76,6 +80,10 @@ export class Input { this.ignoreVersions = new RegExp('^(0|[1-9]\\d*)((\\.(0|[1-9]\\d*))*)$') } + if (this.packageType.toLowerCase() !== 'container') { + this.deleteUntaggedVersions = 'false' + } + if (this.minVersionsToKeep >= 0) { this.numOldVersionsToDelete = 0 } diff --git a/src/main.ts b/src/main.ts index 23fb904..2e3df1b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,7 +19,10 @@ function getActionInput(): Input { deletePreReleaseVersions: getInput( 'delete-only-pre-release-versions' ).toLowerCase(), - token: getInput('token') + token: getInput('token'), + deleteUntaggedVersions: getInput( + 'delete-only-untagged-versions' + ).toLowerCase() }) } diff --git a/src/version/get-versions.ts b/src/version/get-versions.ts index 28a54a4..342cad2 100644 --- a/src/version/get-versions.ts +++ b/src/version/get-versions.ts @@ -8,6 +8,7 @@ export interface RestVersionInfo { id: number version: string created_at: string + tagged: boolean } export interface RestQueryInfo { @@ -56,10 +57,20 @@ export function getOldestVersions( map(response => { const resp = { versions: response.data.map((version: GetVersionsResponse[0]) => { + let tagged = false + if ( + package_type === 'container' && + version.metadata && + version.metadata.container + ) { + tagged = version.metadata.container.tags.length > 0 + } + return { id: version.id, version: version.name, - created_at: version.created_at + created_at: version.created_at, + tagged } }), page,