diff --git a/README.md b/README.md index 7c102e7a..4fd45040 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Delete Package Versions -This action deletes versions of a package from [GitHub Packages](https://github.com/features/packages). +This action deletes versions of a package from [GitHub Packages](https://github.com/features/packages) except ghcr packages. This action will only delete a maximum of 99 versions in one run. ### What It Can Do @@ -12,8 +12,6 @@ This action deletes versions of a package from [GitHub Packages](https://github. * Delete version(s) of a package that is hosted in a different repo than the one executing the workflow * Delete a single version * Delete multiple versions - - * Delete specific version(s) # Usage @@ -42,14 +40,12 @@ This action deletes versions of a package from [GitHub Packages](https://github. # The number of old versions to delete starting from the oldest version. # Defaults to 1. - # Cannot be more than 100. num-old-versions-to-delete: - # The number of latest versions to not delete. - # Defaults to 0. - # When this is set greater than 0 it will delete all deletable package versions except the specified no. - # This takes precedence over `num-old-versions-to-delete`. - # Cannot be more than 100. + # The number of latest versions to keep. + # This cannot be specified with `num-old-versions-to-delete`. By default, `num-old-versions-to-delete` takes precedence over `min-versions-to-keep`. + # When set to 0, all deletable versions will be deleted. + # When set greater than 0, all deletable package versions except the specified number will be deleted. min-versions-to-keep: # The package versions to exclude from deletion. @@ -61,6 +57,7 @@ This action deletes versions of a package from [GitHub Packages](https://github. # The number of pre-release versions to keep can be set by using `min-versions-to-keep` value with this. # When `min-versions-to-keep` is 0, all pre-release versions get deleted. # Defaults to false. + # Cannot be used with `num-old-versions-to-delete` and `ignore-versions`. delete-only-pre-release-versions: # The token used to authenticate with GitHub Packages. @@ -71,6 +68,18 @@ This action deletes versions of a package from [GitHub Packages](https://github. token: ``` +# Valid Input Combinations + +`owner`, `repo`, `package-name` and `token` can be used with the following combinations in a workflow - + + - `num-old-versions-to-delete` + - `min-versions-to-keep` + - `delete-only-pre-release-versions` + - `ignore-versions` + - `num-old-versions-to-delete` + `ignore-versions` + - `min-versions-to-keep` + `ignore-versions` + - `min-versions-to-keep` + `delete-only-pre-release-versions` + # 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) diff --git a/__tests__/delete.test.ts b/__tests__/delete.test.ts index 41e42efb..7dd83c74 100644 --- a/__tests__/delete.test.ts +++ b/__tests__/delete.test.ts @@ -1,37 +1,40 @@ import {Input, InputParams} from '../src/input' -import {deleteVersions, getVersionIds} from '../src/delete' +import {deleteVersions, finalIds} from '../src/delete' describe.skip('index tests -- call graphql', () => { - it('getVersionIds test -- get oldest version', done => { + it('finalIds test -- get oldest version', done => { const numVersions = 1 - getVersionIds(getInput({numOldVersionsToDelete: numVersions})).subscribe( - ids => { - expect(ids.length).toBeLessThanOrEqual(numVersions) - done() - } - ) + finalIds(getInput({numOldVersionsToDelete: numVersions})).subscribe(ids => { + expect(ids.length).toBe(numVersions) + done() + }) }) - it('getVersionIds test -- get oldest 3 versions', done => { + it.skip('finalIds test -- get oldest 3 versions', done => { const numVersions = 3 + finalIds(getInput({numOldVersionsToDelete: numVersions})).subscribe(ids => { + expect(ids.length).toBe(numVersions) + done() + }) + }) - getVersionIds(getInput({numOldVersionsToDelete: numVersions})).subscribe( - ids => { - expect(ids.length).toBeLessThanOrEqual(numVersions) - done() - } - ) + it.skip('finalIds test -- get oldest 110 versions', done => { + const numVersions = 110 + + finalIds(getInput({numOldVersionsToDelete: numVersions})).subscribe(ids => { + expect(ids.length).toBe(99), async () => done() + }) }) - it('getVersionIds test -- supplied package version id', done => { + it('finalIds test -- supplied package version id', done => { const suppliedIds = [ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC' ] - getVersionIds(getInput({packageVersionIds: suppliedIds})).subscribe(ids => { + finalIds(getInput({packageVersionIds: suppliedIds})).subscribe(ids => { expect(ids).toBe(suppliedIds) done() }) @@ -58,28 +61,28 @@ describe.skip('index tests -- call graphql', () => { }) it.skip('deleteVersions test -- delete oldest version', done => { - deleteVersions( - getInput({numOldVersionsToDelete: 2, minVersionsToKeep: 1}) - ).subscribe(isSuccess => { - expect(isSuccess).toBe(true) - done() - }) + deleteVersions(getInput({numOldVersionsToDelete: 1})).subscribe( + isSuccess => { + expect(isSuccess) + }, + async () => done() + ) }) it.skip('deleteVersions test -- delete 3 oldest versions', done => { - deleteVersions( - getInput({numOldVersionsToDelete: 3, minVersionsToKeep: 1}) - ).subscribe(isSuccess => { - expect(isSuccess).toBe(true) - done() - }) + deleteVersions(getInput({numOldVersionsToDelete: 3})).subscribe( + isSuccess => { + expect(isSuccess) + }, + async () => done() + ) }) - it('deleteVersions test -- keep 5 versions', done => { - deleteVersions(getInput({minVersionsToKeep: 5})).subscribe(isSuccess => { + it.skip('deleteVersions test -- keep 5 versions', done => { + deleteVersions(getInput({minVersionsToKeep: 100})).subscribe(isSuccess => { expect(isSuccess).toBe(true) - done() - }) + }), + async () => done() }) }) @@ -87,9 +90,10 @@ const defaultInput: InputParams = { packageVersionIds: [], owner: 'namratajha', repo: 'only-pkg', - packageName: 'onlypkg.maven', + packageName: 'only-pkg', numOldVersionsToDelete: 1, - minVersionsToKeep: 1, + minVersionsToKeep: -1, + ignoreVersions: RegExp('^$'), token: process.env.GITHUB_TOKEN as string } diff --git a/__tests__/version/delete-version.test.ts b/__tests__/version/delete-version.test.ts index a34bbbf1..e93b4864 100644 --- a/__tests__/version/delete-version.test.ts +++ b/__tests__/version/delete-version.test.ts @@ -5,7 +5,7 @@ const githubToken = process.env.GITHUB_TOKEN as string describe.skip('delete tests', () => { it('deletePackageVersion', async () => { const response = await deletePackageVersion( - 'MDE0OlBhY2thZ2VWZXJzaW9uNjg5OTU1', + 'PV_lADOGReZt84AEI7FzgDSHEI', githubToken ).toPromise() expect(response).toBe(true) @@ -14,9 +14,9 @@ describe.skip('delete tests', () => { it('deletePackageVersions', async () => { const response = await deletePackageVersions( [ - 'MDE0OlBhY2thZ2VWZXJzaW9uNjk4Mjc0', - 'MDE0OlBhY2thZ2VWZXJzaW9uNjk4Mjcx', - 'MDE0OlBhY2thZ2VWZXJzaW9uNjk4MjY3' + 'PV_lADOGReZt84AEI7FzgDSHDs', + 'PV_lADOGReZt84AEI7FzgDSHDY', + 'PV_lADOGReZt84AEI7FzgDSHC8' ], githubToken ).toPromise() diff --git a/__tests__/version/get-version.test.ts b/__tests__/version/get-version.test.ts index 481a96d2..d94de3f8 100644 --- a/__tests__/version/get-version.test.ts +++ b/__tests__/version/get-version.test.ts @@ -1,18 +1,15 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-ignore -// @ts-ignore import {mockOldestQueryResponse} from './graphql.mock' import { getOldestVersions as _getOldestVersions, - VersionInfo + QueryInfo } from '../../src/version' import {Observable} from 'rxjs' describe.skip('get versions tests -- call graphql', () => { it('getOldestVersions -- succeeds', done => { const numVersions = 1 - - getOldestVersions({numVersions}).subscribe(versions => { - expect(versions.length).toBe(numVersions) + getOldestVersions({numVersions}).subscribe(result => { + expect(result.versions.length).toBe(numVersions) done() }) }) @@ -33,8 +30,8 @@ describe('get versions tests -- mock graphql', () => { const numVersions = 5 mockOldestQueryResponse(numVersions) - getOldestVersions({numVersions}).subscribe(versions => { - expect(versions.length).toBe(numVersions) + getOldestVersions({numVersions}).subscribe(result => { + expect(result.versions.length).toBe(numVersions) done() }) }) @@ -45,24 +42,27 @@ interface Params { repo?: string packageName?: string numVersions?: number + startCursor?: string token?: string } const defaultParams = { owner: 'namratajha', - repo: 'only-pkg', - packageName: 'onlypkg.maven', - numVersions: 3, + repo: 'test-repo', + packageName: 'test-repo', + numVersions: 1, + startCursor: '', token: process.env.GITHUB_TOKEN as string } -function getOldestVersions(params?: Params): Observable { +function getOldestVersions(params?: Params): Observable { const p: Required = {...defaultParams, ...params} return _getOldestVersions( p.owner, p.repo, p.packageName, p.numVersions, + p.startCursor, p.token ) } diff --git a/__tests__/version/graphql.mock.ts b/__tests__/version/graphql.mock.ts index 4270a683..c2fb04e7 100644 --- a/__tests__/version/graphql.mock.ts +++ b/__tests__/version/graphql.mock.ts @@ -10,7 +10,7 @@ export function getMockedOldestQueryResponse( numVersions: number ): GetVersionsQueryResponse { const versions = [] - + numVersions = numVersions < 100 ? numVersions : numVersions for (let i = 1; i <= numVersions; ++i) { versions.push({ node: { @@ -28,7 +28,12 @@ export function getMockedOldestQueryResponse( node: { name: 'test', versions: { - edges: versions.reverse() + totalCount: 200, + edges: versions.reverse(), + pageInfo: { + startCursor: 'AAA', + hasPreviousPage: false + } } } } @@ -38,12 +43,13 @@ export function getMockedOldestQueryResponse( } } -export function mockOldestQueryResponse( - numVersions: number -) { - const response = new Promise((resolve) => { +export function mockOldestQueryResponse(numVersions: number): void { + const response = new Promise(resolve => { resolve(getMockedOldestQueryResponse(numVersions)) }) as Promise - jest.spyOn(Graphql, 'graphql').mockImplementation( - (token: string, query: string, parameters: RequestParameters) => response) + jest + .spyOn(Graphql, 'graphql') + .mockImplementation( + (token: string, query: string, parameters: RequestParameters) => response + ) } diff --git a/action.yml b/action.yml index 7f21c6f5..9a463915 100644 --- a/action.yml +++ b/action.yml @@ -37,9 +37,10 @@ inputs: min-versions-to-keep: description: > Number of versions to keep starting with the latest version - Defaults to 0. + By default keeps no version. + To delete all versions set this as 0. required: false - default: "0" + default: "-1" ignore-versions: description: > @@ -50,7 +51,7 @@ inputs: delete-only-pre-release-versions: description: > - Deletes only pre-release versions upto. The number of pre-release versions to keep can be specified by min-versions-to-keep. + Deletes only pre-release versions. The number of pre-release versions to keep can be specified by min-versions-to-keep. When this is set num-old-versions-to-delete and ignore-versions will not be taken into account. By default this is set to false required: false diff --git a/dist/index.js b/dist/index.js index 13c7f30a..14c871c1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -23,47 +23,104 @@ module.exports = JSON.parse('{"_args":[["@octokit/rest@16.43.1","/workspaces/del "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.deleteVersions = exports.getVersionIds = void 0; +exports.deleteVersions = exports.finalIds = exports.getVersionIds = void 0; const rxjs_1 = __nccwpck_require__(5805); const version_1 = __nccwpck_require__(4428); const operators_1 = __nccwpck_require__(7801); -function getVersionIds(input) { +const RATE_LIMIT = 99; +let totalCount = 0; +function getVersionIds(owner, repo, packageName, numVersions, cursor, token) { + return version_1.getOldestVersions(owner, repo, packageName, numVersions, cursor, token).pipe(operators_1.expand(value => value.paginate + ? version_1.getOldestVersions(owner, repo, packageName, numVersions, value.cursor, token) + : rxjs_1.EMPTY), operators_1.tap(value => (totalCount = totalCount === 0 ? value.totalCount : totalCount)), operators_1.map(value => value.versions)); +} +exports.getVersionIds = getVersionIds; +function finalIds(input) { if (input.packageVersionIds.length > 0) { return rxjs_1.of(input.packageVersionIds); } if (input.hasOldestVersionQueryInfo()) { - return version_1.getOldestVersions(input.owner, input.repo, input.packageName, input.numOldVersionsToDelete + input.minVersionsToKeep, input.token).pipe(operators_1.map(versionInfo => { - const numberVersionsToDelete = versionInfo.length - input.minVersionsToKeep; - if (input.minVersionsToKeep > 0) { - return numberVersionsToDelete <= 0 - ? [] - : versionInfo - .filter(info => !input.ignoreVersions.test(info.version)) - .map(info => info.id) - .slice(0, -input.minVersionsToKeep); - } - else { - return numberVersionsToDelete <= 0 - ? [] - : versionInfo - .filter(info => !input.ignoreVersions.test(info.version)) - .map(info => info.id) - .slice(0, numberVersionsToDelete); - } - })); + if (input.minVersionsToKeep < 0) { + // This code block is when num-old-versions-to-delete is specified. + // Setting input.numOldVersionsToDelete is set as minimum of input.numOldVersionsToDelete and RATE_LIMIT + input.numOldVersionsToDelete = + input.numOldVersionsToDelete < RATE_LIMIT + ? input.numOldVersionsToDelete + : RATE_LIMIT; + return getVersionIds(input.owner, input.repo, input.packageName, RATE_LIMIT, '', input.token).pipe( + // This code block executes on batches of 100 versions starting from oldest + operators_1.map(value => { + /* + Here first filter out the versions that are to be ignored. + Then update input.numOldeVersionsToDelete to the no of versions deleted from the next 100 versions batch. + */ + value = value.filter(info => !input.ignoreVersions.test(info.version)); + const temp = input.numOldVersionsToDelete; + input.numOldVersionsToDelete = + input.numOldVersionsToDelete - value.length <= 0 + ? 0 + : input.numOldVersionsToDelete - value.length; + return value.map(info => info.id).slice(0, temp); + })); + } + else { + // This code block is when min-versions-to-keep is specified. + return getVersionIds(input.owner, input.repo, input.packageName, RATE_LIMIT, '', input.token).pipe( + // This code block executes on batches of 100 versions starting from oldest + operators_1.map(value => { + /* + Here totalCount is the total no of versions in the package. + First we update totalCount by removing no of ignored versions from it and also filter them out from value. + toDelete is the no of versions that need to be deleted and input.numDeleted is the total no of versions deleted before this batch. + We calculate this from total no of versions in the package, the min no of versions to keep and the no of versions we have deleted in earlier batch. + Then we update toDelete to not exceed the length of current batch of versions. + Now toDelete holds the no of versions to be deleted from the current batch of versions. + */ + totalCount = + totalCount - + value.filter(info => input.ignoreVersions.test(info.version)).length; + value = value.filter(info => !input.ignoreVersions.test(info.version)); + let toDelete = totalCount - input.minVersionsToKeep - input.numDeleted; + toDelete = toDelete > value.length ? value.length : toDelete; + //Checking here if we have any versions to delete and whether we are within the RATE_LIMIT. + if (toDelete > 0 && input.numDeleted < RATE_LIMIT) { + /* + Checking here if we can delete all the versions left in the current batch. + input.numDeleted + toDelete should not exceed RATE_LIMIT. + If it is exceeding we only delete the no of versions from this batch that are allowed within the RATE_LIMIT. + i.e. diff between RATE_LIMIT and versions deleted till now (input.numDeleted) + input.numDeleted is updated accordingly. + */ + if (input.numDeleted + toDelete > RATE_LIMIT) { + toDelete = RATE_LIMIT - input.numDeleted; + input.numDeleted = RATE_LIMIT; + } + else { + input.numDeleted = input.numDeleted + toDelete; + } + return value.map(info => info.id).slice(0, toDelete); + } + else + return []; + })); + } } - return rxjs_1.throwError("Could not get packageVersionIds. Explicitly specify using the 'package-version-ids' input or provide the 'package-name' and 'num-old-versions-to-delete' inputs to dynamically retrieve oldest versions"); + return rxjs_1.throwError("Could not get packageVersionIds. Explicitly specify using the 'package-version-ids' input"); } -exports.getVersionIds = getVersionIds; +exports.finalIds = finalIds; function deleteVersions(input) { if (!input.token) { return rxjs_1.throwError('No token found'); } - if (input.numOldVersionsToDelete <= 0) { + if (!input.checkInput()) { + return rxjs_1.throwError('Invalid input combination'); + } + if (input.numOldVersionsToDelete <= 0 && input.minVersionsToKeep < 0) { console.log('Number of old versions to delete input is 0 or less, no versions will be deleted'); return rxjs_1.of(true); } - return getVersionIds(input).pipe(operators_1.concatMap(ids => version_1.deletePackageVersions(ids, input.token))); + const result = finalIds(input); + return result.pipe(operators_1.concatMap(ids => version_1.deletePackageVersions(ids, input.token))); } exports.deleteVersions = deleteVersions; @@ -100,22 +157,30 @@ class Input { this.ignoreVersions = validatedParams.ignoreVersions; this.deletePreReleaseVersions = validatedParams.deletePreReleaseVersions; this.token = validatedParams.token; - if (this.minVersionsToKeep > 0) { - this.numOldVersionsToDelete = 100 - this.minVersionsToKeep; - } - if (this.deletePreReleaseVersions == 'true') { - this.numOldVersionsToDelete = 100 - this.minVersionsToKeep; - this.ignoreVersions = new RegExp('^(0|[1-9]\\d*)((\\.(0|[1-9]\\d*))*)$'); - } + this.numDeleted = 0; } hasOldestVersionQueryInfo() { return !!(this.owner && this.repo && this.packageName && - this.numOldVersionsToDelete > 0 && - this.minVersionsToKeep >= 0 && + this.numOldVersionsToDelete >= 0 && this.token); } + checkInput() { + if (this.numOldVersionsToDelete > 1 && + (this.minVersionsToKeep >= 0 || this.deletePreReleaseVersions === 'true')) { + return false; + } + if (this.deletePreReleaseVersions === 'true') { + this.minVersionsToKeep = + this.minVersionsToKeep > 0 ? this.minVersionsToKeep : 0; + this.ignoreVersions = new RegExp('^(0|[1-9]\\d*)((\\.(0|[1-9]\\d*))*)$'); + } + if (this.minVersionsToKeep >= 0) { + this.numOldVersionsToDelete = 0; + } + return true; + } } exports.Input = Input; @@ -132,6 +197,7 @@ exports.deletePackageVersions = exports.deletePackageVersion = void 0; const rxjs_1 = __nccwpck_require__(5805); const operators_1 = __nccwpck_require__(7801); const graphql_1 = __nccwpck_require__(6320); +let deleted = 0; const mutation = ` mutation deletePackageVersion($packageVersionId: String!) { deletePackageVersion(input: {packageVersionId: $packageVersionId}) { @@ -139,32 +205,30 @@ const mutation = ` } }`; function deletePackageVersion(packageVersionId, token) { + deleted += 1; return rxjs_1.from(graphql_1.graphql(token, mutation, { packageVersionId, headers: { Accept: 'application/vnd.github.package-deletes-preview+json' } - })).pipe(operators_1.catchError((err) => { + })).pipe(operators_1.catchError(err => { const msg = 'delete version mutation failed.'; return rxjs_1.throwError(err.errors && err.errors.length > 0 ? `${msg} ${err.errors[0].message}` - : `${msg} verify input parameters are correct`); + : `${msg} ${err.message} \n${deleted - 1} versions deleted till now.`); }), operators_1.map(response => response.deletePackageVersion.success)); } exports.deletePackageVersion = deletePackageVersion; function deletePackageVersions(packageVersionIds, token) { if (packageVersionIds.length === 0) { - console.log('no package version ids found, no versions will be deleted'); return rxjs_1.of(true); } const deletes = packageVersionIds.map(id => deletePackageVersion(id, token).pipe(operators_1.tap(result => { - if (result) { - console.log(`version with id: ${id}, deleted`); - } - else { + if (!result) { console.log(`version with id: ${id}, not deleted`); } }))); + console.log(`Total versions deleted till now: ${deleted}`); return rxjs_1.merge(...deletes); } exports.deletePackageVersions = deletePackageVersions; @@ -190,48 +254,109 @@ const query = ` node { name versions(last: $last) { + totalCount edges { node { id version } } + pageInfo { + startCursor + hasPreviousPage + } } } } } } }`; -function queryForOldestVersions(owner, repo, packageName, numVersions, token) { - return rxjs_1.from(graphql_1.graphql(token, query, { - owner, - repo, - package: packageName, - last: numVersions, - headers: { - Accept: 'application/vnd.github.packages-preview+json' +const Paginatequery = ` + query getVersions($owner: String!, $repo: String!, $package: String!, $last: Int!, $before: String!) { + repository(owner: $owner, name: $repo) { + packages(first: 1, names: [$package]) { + edges { + node { + name + versions(last: $last, before: $before) { + totalCount + edges { + node { + id + version + } + } + pageInfo{ + startCursor + hasPreviousPage + } + } + } } - })).pipe(operators_1.catchError((err) => { - const msg = 'query for oldest version failed.'; - return rxjs_1.throwError(err.errors && err.errors.length > 0 - ? `${msg} ${err.errors[0].message}` - : `${msg} verify input parameters are correct`); - })); + } + } + }`; +function queryForOldestVersions(owner, repo, packageName, numVersions, startCursor, token) { + if (startCursor === '') { + return rxjs_1.from(graphql_1.graphql(token, query, { + owner, + repo, + package: packageName, + last: numVersions, + headers: { + Accept: 'application/vnd.github.packages-preview+json' + } + })).pipe(operators_1.catchError((err) => { + const msg = 'query for oldest version failed.'; + return rxjs_1.throwError(err.errors && err.errors.length > 0 + ? `${msg} ${err.errors[0].message}` + : `${msg} verify input parameters are correct`); + })); + } + else { + return rxjs_1.from(graphql_1.graphql(token, Paginatequery, { + owner, + repo, + package: packageName, + last: numVersions, + before: startCursor, + headers: { + Accept: 'application/vnd.github.packages-preview+json' + } + })).pipe(operators_1.catchError((err) => { + const msg = 'query for oldest version failed.'; + return rxjs_1.throwError(err.errors && err.errors.length > 0 + ? `${msg} ${err.errors[0].message}` + : `${msg} verify input parameters are correct`); + })); + } } exports.queryForOldestVersions = queryForOldestVersions; -function getOldestVersions(owner, repo, packageName, numVersions, token) { - return queryForOldestVersions(owner, repo, packageName, numVersions, token).pipe(operators_1.map(result => { +function getOldestVersions(owner, repo, packageName, numVersions, startCursor, token) { + return queryForOldestVersions(owner, repo, packageName, numVersions, startCursor, token).pipe(operators_1.map(result => { + let r; if (result.repository.packages.edges.length < 1) { console.log(`package: ${packageName} not found for owner: ${owner} in repo: ${repo}`); - return []; + r = { + versions: [], + cursor: '', + paginate: false, + totalCount: 0 + }; + return r; } const versions = result.repository.packages.edges[0].node.versions.edges; - if (versions.length !== numVersions) { - console.log(`number of versions requested was: ${numVersions}, but found: ${versions.length}`); - } - return versions - .map(value => ({ id: value.node.id, version: value.node.version })) - .reverse(); + const pages = result.repository.packages.edges[0].node.versions.pageInfo; + const count = result.repository.packages.edges[0].node.versions.totalCount; + r = { + versions: versions + .map(value => ({ id: value.node.id, version: value.node.version })) + .reverse(), + cursor: pages.startCursor, + paginate: pages.hasPreviousPage, + totalCount: count + }; + return r; })); } exports.getOldestVersions = getOldestVersions; diff --git a/package.json b/package.json index 20cdae19..b27e0316 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,10 @@ "scripts": { "format": "prettier --write **/*.ts", "format-check": "prettier --check **/*.ts", - "lint": "eslint src/**/*.ts", + "lint": "eslint src/**/*.ts --fix", + "lint-check": "eslint src/**/*.ts", "test": "jest", - "build": "npm run format-check && npm run lint && npm run test && tsc", + "build": "npm run format-check && npm run lint-check && npm run test && tsc", "pack": "rm -rf ./lib ./dist && npm run build && ncc build" }, "repository": { diff --git a/src/delete.ts b/src/delete.ts index 0b9faa72..c2f9c3c6 100644 --- a/src/delete.ts +++ b/src/delete.ts @@ -1,46 +1,130 @@ import {Input} from './input' -import {Observable, of, throwError} from 'rxjs' -import {deletePackageVersions, getOldestVersions} from './version' -import {concatMap, map} from 'rxjs/operators' +import {EMPTY, Observable, of, throwError} from 'rxjs' +import {deletePackageVersions, getOldestVersions, VersionInfo} from './version' +import {concatMap, map, expand, tap} from 'rxjs/operators' -export function getVersionIds(input: Input): Observable { +const RATE_LIMIT = 99 +let totalCount = 0 + +export function getVersionIds( + owner: string, + repo: string, + packageName: string, + numVersions: number, + cursor: string, + token: string +): Observable { + return getOldestVersions( + owner, + repo, + packageName, + numVersions, + cursor, + token + ).pipe( + expand(value => + value.paginate + ? getOldestVersions( + owner, + repo, + packageName, + numVersions, + value.cursor, + token + ) + : EMPTY + ), + tap( + value => (totalCount = totalCount === 0 ? value.totalCount : totalCount) + ), + map(value => value.versions) + ) +} + +export function finalIds(input: Input): Observable { if (input.packageVersionIds.length > 0) { return of(input.packageVersionIds) } - if (input.hasOldestVersionQueryInfo()) { - return getOldestVersions( - input.owner, - input.repo, - input.packageName, - input.numOldVersionsToDelete + input.minVersionsToKeep, - input.token - ).pipe( - map(versionInfo => { - const numberVersionsToDelete = - versionInfo.length - input.minVersionsToKeep - - if (input.minVersionsToKeep > 0) { - return numberVersionsToDelete <= 0 - ? [] - : versionInfo - .filter(info => !input.ignoreVersions.test(info.version)) - .map(info => info.id) - .slice(0, -input.minVersionsToKeep) - } else { - return numberVersionsToDelete <= 0 - ? [] - : versionInfo - .filter(info => !input.ignoreVersions.test(info.version)) - .map(info => info.id) - .slice(0, numberVersionsToDelete) - } - }) - ) + if (input.minVersionsToKeep < 0) { + // This code block is when num-old-versions-to-delete is specified. + // Setting input.numOldVersionsToDelete is set as minimum of input.numOldVersionsToDelete and RATE_LIMIT + input.numOldVersionsToDelete = + input.numOldVersionsToDelete < RATE_LIMIT + ? input.numOldVersionsToDelete + : RATE_LIMIT + return getVersionIds( + input.owner, + input.repo, + input.packageName, + RATE_LIMIT, + '', + input.token + ).pipe( + // This code block executes on batches of 100 versions starting from oldest + map(value => { + /* + Here first filter out the versions that are to be ignored. + Then update input.numOldeVersionsToDelete to the no of versions deleted from the next 100 versions batch. + */ + value = value.filter(info => !input.ignoreVersions.test(info.version)) + const temp = input.numOldVersionsToDelete + input.numOldVersionsToDelete = + input.numOldVersionsToDelete - value.length <= 0 + ? 0 + : input.numOldVersionsToDelete - value.length + return value.map(info => info.id).slice(0, temp) + }) + ) + } else { + // This code block is when min-versions-to-keep is specified. + return getVersionIds( + input.owner, + input.repo, + input.packageName, + RATE_LIMIT, + '', + input.token + ).pipe( + // This code block executes on batches of 100 versions starting from oldest + map(value => { + /* + Here totalCount is the total no of versions in the package. + First we update totalCount by removing no of ignored versions from it and also filter them out from value. + toDelete is the no of versions that need to be deleted and input.numDeleted is the total no of versions deleted before this batch. + We calculate this from total no of versions in the package, the min no of versions to keep and the no of versions we have deleted in earlier batch. + Then we update toDelete to not exceed the length of current batch of versions. + Now toDelete holds the no of versions to be deleted from the current batch of versions. + */ + totalCount = + totalCount - + value.filter(info => input.ignoreVersions.test(info.version)).length + value = value.filter(info => !input.ignoreVersions.test(info.version)) + let toDelete = totalCount - input.minVersionsToKeep - input.numDeleted + toDelete = toDelete > value.length ? value.length : toDelete + //Checking here if we have any versions to delete and whether we are within the RATE_LIMIT. + if (toDelete > 0 && input.numDeleted < RATE_LIMIT) { + /* + Checking here if we can delete all the versions left in the current batch. + input.numDeleted + toDelete should not exceed RATE_LIMIT. + If it is exceeding we only delete the no of versions from this batch that are allowed within the RATE_LIMIT. + i.e. diff between RATE_LIMIT and versions deleted till now (input.numDeleted) + input.numDeleted is updated accordingly. + */ + if (input.numDeleted + toDelete > RATE_LIMIT) { + toDelete = RATE_LIMIT - input.numDeleted + input.numDeleted = RATE_LIMIT + } else { + input.numDeleted = input.numDeleted + toDelete + } + return value.map(info => info.id).slice(0, toDelete) + } else return [] + }) + ) + } } - return throwError( - "Could not get packageVersionIds. Explicitly specify using the 'package-version-ids' input or provide the 'package-name' and 'num-old-versions-to-delete' inputs to dynamically retrieve oldest versions" + "Could not get packageVersionIds. Explicitly specify using the 'package-version-ids' input" ) } @@ -49,14 +133,18 @@ export function deleteVersions(input: Input): Observable { return throwError('No token found') } - if (input.numOldVersionsToDelete <= 0) { + if (!input.checkInput()) { + return throwError('Invalid input combination') + } + + if (input.numOldVersionsToDelete <= 0 && input.minVersionsToKeep < 0) { console.log( 'Number of old versions to delete input is 0 or less, no versions will be deleted' ) return of(true) } - return getVersionIds(input).pipe( - concatMap(ids => deletePackageVersions(ids, input.token)) - ) + const result = finalIds(input) + + return result.pipe(concatMap(ids => deletePackageVersions(ids, input.token))) } diff --git a/src/input.ts b/src/input.ts index 8685fd6a..35244d52 100644 --- a/src/input.ts +++ b/src/input.ts @@ -32,6 +32,7 @@ export class Input { ignoreVersions: RegExp deletePreReleaseVersions: string token: string + numDeleted: number constructor(params?: InputParams) { const validatedParams: Required = {...defaultParams, ...params} @@ -45,15 +46,7 @@ export class Input { this.ignoreVersions = validatedParams.ignoreVersions this.deletePreReleaseVersions = validatedParams.deletePreReleaseVersions this.token = validatedParams.token - - if (this.minVersionsToKeep > 0) { - this.numOldVersionsToDelete = 100 - this.minVersionsToKeep - } - - if (this.deletePreReleaseVersions == 'true') { - this.numOldVersionsToDelete = 100 - this.minVersionsToKeep - this.ignoreVersions = new RegExp('^(0|[1-9]\\d*)((\\.(0|[1-9]\\d*))*)$') - } + this.numDeleted = 0 } hasOldestVersionQueryInfo(): boolean { @@ -61,9 +54,29 @@ export class Input { this.owner && this.repo && this.packageName && - this.numOldVersionsToDelete > 0 && - this.minVersionsToKeep >= 0 && + this.numOldVersionsToDelete >= 0 && this.token ) } + + checkInput(): boolean { + if ( + this.numOldVersionsToDelete > 1 && + (this.minVersionsToKeep >= 0 || this.deletePreReleaseVersions === 'true') + ) { + return false + } + + if (this.deletePreReleaseVersions === 'true') { + this.minVersionsToKeep = + this.minVersionsToKeep > 0 ? this.minVersionsToKeep : 0 + this.ignoreVersions = new RegExp('^(0|[1-9]\\d*)((\\.(0|[1-9]\\d*))*)$') + } + + if (this.minVersionsToKeep >= 0) { + this.numOldVersionsToDelete = 0 + } + + return true + } } diff --git a/src/version/delete-version.ts b/src/version/delete-version.ts index 44508e99..734f2851 100644 --- a/src/version/delete-version.ts +++ b/src/version/delete-version.ts @@ -1,8 +1,9 @@ import {from, Observable, merge, throwError, of} from 'rxjs' import {catchError, map, tap} from 'rxjs/operators' -import {GraphQlQueryResponse} from '@octokit/graphql/dist-types/types' import {graphql} from './graphql' +let deleted = 0 + export interface DeletePackageVersionMutationResponse { deletePackageVersion: { success: boolean @@ -20,6 +21,7 @@ export function deletePackageVersion( packageVersionId: string, token: string ): Observable { + deleted += 1 return from( graphql(token, mutation, { packageVersionId, @@ -28,12 +30,12 @@ export function deletePackageVersion( } }) as Promise ).pipe( - catchError((err: GraphQlQueryResponse) => { + catchError(err => { const msg = 'delete version mutation failed.' return throwError( err.errors && err.errors.length > 0 ? `${msg} ${err.errors[0].message}` - : `${msg} verify input parameters are correct` + : `${msg} ${err.message} \n${deleted - 1} versions deleted till now.` ) }), map(response => response.deletePackageVersion.success) @@ -45,21 +47,18 @@ export function deletePackageVersions( token: string ): Observable { if (packageVersionIds.length === 0) { - console.log('no package version ids found, no versions will be deleted') return of(true) } const deletes = packageVersionIds.map(id => deletePackageVersion(id, token).pipe( tap(result => { - if (result) { - console.log(`version with id: ${id}, deleted`) - } else { + if (!result) { console.log(`version with id: ${id}, not deleted`) } }) ) ) - + console.log(`Total versions deleted till now: ${deleted}`) return merge(...deletes) } diff --git a/src/version/get-versions.ts b/src/version/get-versions.ts index 8e97f609..c285de48 100644 --- a/src/version/get-versions.ts +++ b/src/version/get-versions.ts @@ -8,6 +8,13 @@ export interface VersionInfo { version: string } +export interface QueryInfo { + versions: VersionInfo[] + cursor: string + paginate: boolean + totalCount: number +} + export interface GetVersionsQueryResponse { repository: { packages: { @@ -15,7 +22,12 @@ export interface GetVersionsQueryResponse { node: { name: string versions: { + totalCount: number edges: {node: VersionInfo}[] + pageInfo: { + startCursor: string + hasPreviousPage: boolean + } } } }[] @@ -31,12 +43,43 @@ const query = ` node { name versions(last: $last) { + totalCount edges { node { id version } } + pageInfo { + startCursor + hasPreviousPage + } + } + } + } + } + } + }` + +const Paginatequery = ` + query getVersions($owner: String!, $repo: String!, $package: String!, $last: Int!, $before: String!) { + repository(owner: $owner, name: $repo) { + packages(first: 1, names: [$package]) { + edges { + node { + name + versions(last: $last, before: $before) { + totalCount + edges { + node { + id + version + } + } + pageInfo{ + startCursor + hasPreviousPage + } } } } @@ -49,28 +92,53 @@ export function queryForOldestVersions( repo: string, packageName: string, numVersions: number, + startCursor: string, token: string ): Observable { - return from( - graphql(token, query, { - owner, - repo, - package: packageName, - last: numVersions, - headers: { - Accept: 'application/vnd.github.packages-preview+json' - } - }) as Promise - ).pipe( - catchError((err: GraphQlQueryResponse) => { - const msg = 'query for oldest version failed.' - return throwError( - err.errors && err.errors.length > 0 - ? `${msg} ${err.errors[0].message}` - : `${msg} verify input parameters are correct` - ) - }) - ) + if (startCursor === '') { + return from( + graphql(token, query, { + owner, + repo, + package: packageName, + last: numVersions, + headers: { + Accept: 'application/vnd.github.packages-preview+json' + } + }) as Promise + ).pipe( + catchError((err: GraphQlQueryResponse) => { + const msg = 'query for oldest version failed.' + return throwError( + err.errors && err.errors.length > 0 + ? `${msg} ${err.errors[0].message}` + : `${msg} verify input parameters are correct` + ) + }) + ) + } else { + return from( + graphql(token, Paginatequery, { + owner, + repo, + package: packageName, + last: numVersions, + before: startCursor, + headers: { + Accept: 'application/vnd.github.packages-preview+json' + } + }) as Promise + ).pipe( + catchError((err: GraphQlQueryResponse) => { + const msg = 'query for oldest version failed.' + return throwError( + err.errors && err.errors.length > 0 + ? `${msg} ${err.errors[0].message}` + : `${msg} verify input parameters are correct` + ) + }) + ) + } } export function getOldestVersions( @@ -78,34 +146,46 @@ export function getOldestVersions( repo: string, packageName: string, numVersions: number, + startCursor: string, token: string -): Observable { +): Observable { return queryForOldestVersions( owner, repo, packageName, numVersions, + startCursor, token ).pipe( map(result => { + let r: QueryInfo if (result.repository.packages.edges.length < 1) { console.log( `package: ${packageName} not found for owner: ${owner} in repo: ${repo}` ) - return [] + r = { + versions: [], + cursor: '', + paginate: false, + totalCount: 0 + } + return r } const versions = result.repository.packages.edges[0].node.versions.edges + const pages = result.repository.packages.edges[0].node.versions.pageInfo + const count = result.repository.packages.edges[0].node.versions.totalCount - if (versions.length !== numVersions) { - console.log( - `number of versions requested was: ${numVersions}, but found: ${versions.length}` - ) + r = { + versions: versions + .map(value => ({id: value.node.id, version: value.node.version})) + .reverse(), + cursor: pages.startCursor, + paginate: pages.hasPreviousPage, + totalCount: count } - return versions - .map(value => ({id: value.node.id, version: value.node.version})) - .reverse() + return r }) ) }