Merge pull request #58 from actions/improve-existing-action

Allow deletion beyond last 100 packages
This commit is contained in:
Namrata Jha
2021-12-24 15:55:58 +05:30
committed by GitHub
12 changed files with 551 additions and 225 deletions
+18 -9
View File
@@ -1,6 +1,6 @@
# Delete Package Versions # 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 ### 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 version(s) of a package that is hosted in a different repo than the one executing the workflow
* Delete a single version * Delete a single version
* Delete multiple versions * Delete multiple versions
* Delete specific version(s) * Delete specific version(s)
# Usage # 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. # The number of old versions to delete starting from the oldest version.
# Defaults to 1. # Defaults to 1.
# Cannot be more than 100.
num-old-versions-to-delete: num-old-versions-to-delete:
# The number of latest versions to not delete. # The number of latest versions to keep.
# Defaults to 0. # 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 this is set greater than 0 it will delete all deletable package versions except the specified no. # When set to 0, all deletable versions will be deleted.
# This takes precedence over `num-old-versions-to-delete`. # When set greater than 0, all deletable package versions except the specified number will be deleted.
# Cannot be more than 100.
min-versions-to-keep: min-versions-to-keep:
# The package versions to exclude from deletion. # 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. # 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. # When `min-versions-to-keep` is 0, all pre-release versions get deleted.
# Defaults to false. # Defaults to false.
# Cannot be used with `num-old-versions-to-delete` and `ignore-versions`.
delete-only-pre-release-versions: delete-only-pre-release-versions:
# The token used to authenticate with GitHub Packages. # 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: 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 # 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 pre-release versions except y latest pre-release package versions](#delete-all-pre-release-versions-except-y-latest-pre-release-package-versions)
+40 -36
View File
@@ -1,37 +1,40 @@
import {Input, InputParams} from '../src/input' import {Input, InputParams} from '../src/input'
import {deleteVersions, getVersionIds} from '../src/delete' import {deleteVersions, finalIds} from '../src/delete'
describe.skip('index tests -- call graphql', () => { describe.skip('index tests -- call graphql', () => {
it('getVersionIds test -- get oldest version', done => { it('finalIds test -- get oldest version', done => {
const numVersions = 1 const numVersions = 1
getVersionIds(getInput({numOldVersionsToDelete: numVersions})).subscribe( finalIds(getInput({numOldVersionsToDelete: numVersions})).subscribe(ids => {
ids => { expect(ids.length).toBe(numVersions)
expect(ids.length).toBeLessThanOrEqual(numVersions) done()
done() })
}
)
}) })
it('getVersionIds test -- get oldest 3 versions', done => { it.skip('finalIds test -- get oldest 3 versions', done => {
const numVersions = 3 const numVersions = 3
finalIds(getInput({numOldVersionsToDelete: numVersions})).subscribe(ids => {
getVersionIds(getInput({numOldVersionsToDelete: numVersions})).subscribe( expect(ids.length).toBe(numVersions)
ids => { done()
expect(ids.length).toBeLessThanOrEqual(numVersions) })
done()
}
)
}) })
it('getVersionIds test -- supplied package version id', 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('finalIds test -- supplied package version id', done => {
const suppliedIds = [ const suppliedIds = [
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB',
'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC' 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC'
] ]
getVersionIds(getInput({packageVersionIds: suppliedIds})).subscribe(ids => { finalIds(getInput({packageVersionIds: suppliedIds})).subscribe(ids => {
expect(ids).toBe(suppliedIds) expect(ids).toBe(suppliedIds)
done() done()
}) })
@@ -58,28 +61,28 @@ describe.skip('index tests -- call graphql', () => {
}) })
it.skip('deleteVersions test -- delete oldest version', done => { it.skip('deleteVersions test -- delete oldest version', done => {
deleteVersions( deleteVersions(getInput({numOldVersionsToDelete: 1})).subscribe(
getInput({numOldVersionsToDelete: 2, minVersionsToKeep: 1}) isSuccess => {
).subscribe(isSuccess => { expect(isSuccess)
expect(isSuccess).toBe(true) },
done() async () => done()
}) )
}) })
it.skip('deleteVersions test -- delete 3 oldest versions', done => { it.skip('deleteVersions test -- delete 3 oldest versions', done => {
deleteVersions( deleteVersions(getInput({numOldVersionsToDelete: 3})).subscribe(
getInput({numOldVersionsToDelete: 3, minVersionsToKeep: 1}) isSuccess => {
).subscribe(isSuccess => { expect(isSuccess)
expect(isSuccess).toBe(true) },
done() async () => done()
}) )
}) })
it('deleteVersions test -- keep 5 versions', done => { it.skip('deleteVersions test -- keep 5 versions', done => {
deleteVersions(getInput({minVersionsToKeep: 5})).subscribe(isSuccess => { deleteVersions(getInput({minVersionsToKeep: 100})).subscribe(isSuccess => {
expect(isSuccess).toBe(true) expect(isSuccess).toBe(true)
done() }),
}) async () => done()
}) })
}) })
@@ -87,9 +90,10 @@ const defaultInput: InputParams = {
packageVersionIds: [], packageVersionIds: [],
owner: 'namratajha', owner: 'namratajha',
repo: 'only-pkg', repo: 'only-pkg',
packageName: 'onlypkg.maven', packageName: 'only-pkg',
numOldVersionsToDelete: 1, numOldVersionsToDelete: 1,
minVersionsToKeep: 1, minVersionsToKeep: -1,
ignoreVersions: RegExp('^$'),
token: process.env.GITHUB_TOKEN as string token: process.env.GITHUB_TOKEN as string
} }
+4 -4
View File
@@ -5,7 +5,7 @@ const githubToken = process.env.GITHUB_TOKEN as string
describe.skip('delete tests', () => { describe.skip('delete tests', () => {
it('deletePackageVersion', async () => { it('deletePackageVersion', async () => {
const response = await deletePackageVersion( const response = await deletePackageVersion(
'MDE0OlBhY2thZ2VWZXJzaW9uNjg5OTU1', 'PV_lADOGReZt84AEI7FzgDSHEI',
githubToken githubToken
).toPromise() ).toPromise()
expect(response).toBe(true) expect(response).toBe(true)
@@ -14,9 +14,9 @@ describe.skip('delete tests', () => {
it('deletePackageVersions', async () => { it('deletePackageVersions', async () => {
const response = await deletePackageVersions( const response = await deletePackageVersions(
[ [
'MDE0OlBhY2thZ2VWZXJzaW9uNjk4Mjc0', 'PV_lADOGReZt84AEI7FzgDSHDs',
'MDE0OlBhY2thZ2VWZXJzaW9uNjk4Mjcx', 'PV_lADOGReZt84AEI7FzgDSHDY',
'MDE0OlBhY2thZ2VWZXJzaW9uNjk4MjY3' 'PV_lADOGReZt84AEI7FzgDSHC8'
], ],
githubToken githubToken
).toPromise() ).toPromise()
+12 -12
View File
@@ -1,18 +1,15 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
import {mockOldestQueryResponse} from './graphql.mock' import {mockOldestQueryResponse} from './graphql.mock'
import { import {
getOldestVersions as _getOldestVersions, getOldestVersions as _getOldestVersions,
VersionInfo QueryInfo
} from '../../src/version' } from '../../src/version'
import {Observable} from 'rxjs' import {Observable} from 'rxjs'
describe.skip('get versions tests -- call graphql', () => { describe.skip('get versions tests -- call graphql', () => {
it('getOldestVersions -- succeeds', done => { it('getOldestVersions -- succeeds', done => {
const numVersions = 1 const numVersions = 1
getOldestVersions({numVersions}).subscribe(result => {
getOldestVersions({numVersions}).subscribe(versions => { expect(result.versions.length).toBe(numVersions)
expect(versions.length).toBe(numVersions)
done() done()
}) })
}) })
@@ -33,8 +30,8 @@ describe('get versions tests -- mock graphql', () => {
const numVersions = 5 const numVersions = 5
mockOldestQueryResponse(numVersions) mockOldestQueryResponse(numVersions)
getOldestVersions({numVersions}).subscribe(versions => { getOldestVersions({numVersions}).subscribe(result => {
expect(versions.length).toBe(numVersions) expect(result.versions.length).toBe(numVersions)
done() done()
}) })
}) })
@@ -45,24 +42,27 @@ interface Params {
repo?: string repo?: string
packageName?: string packageName?: string
numVersions?: number numVersions?: number
startCursor?: string
token?: string token?: string
} }
const defaultParams = { const defaultParams = {
owner: 'namratajha', owner: 'namratajha',
repo: 'only-pkg', repo: 'test-repo',
packageName: 'onlypkg.maven', packageName: 'test-repo',
numVersions: 3, numVersions: 1,
startCursor: '',
token: process.env.GITHUB_TOKEN as string token: process.env.GITHUB_TOKEN as string
} }
function getOldestVersions(params?: Params): Observable<VersionInfo[]> { function getOldestVersions(params?: Params): Observable<QueryInfo> {
const p: Required<Params> = {...defaultParams, ...params} const p: Required<Params> = {...defaultParams, ...params}
return _getOldestVersions( return _getOldestVersions(
p.owner, p.owner,
p.repo, p.repo,
p.packageName, p.packageName,
p.numVersions, p.numVersions,
p.startCursor,
p.token p.token
) )
} }
+14 -8
View File
@@ -10,7 +10,7 @@ export function getMockedOldestQueryResponse(
numVersions: number numVersions: number
): GetVersionsQueryResponse { ): GetVersionsQueryResponse {
const versions = [] const versions = []
numVersions = numVersions < 100 ? numVersions : numVersions
for (let i = 1; i <= numVersions; ++i) { for (let i = 1; i <= numVersions; ++i) {
versions.push({ versions.push({
node: { node: {
@@ -28,7 +28,12 @@ export function getMockedOldestQueryResponse(
node: { node: {
name: 'test', name: 'test',
versions: { versions: {
edges: versions.reverse() totalCount: 200,
edges: versions.reverse(),
pageInfo: {
startCursor: 'AAA',
hasPreviousPage: false
}
} }
} }
} }
@@ -38,12 +43,13 @@ export function getMockedOldestQueryResponse(
} }
} }
export function mockOldestQueryResponse( export function mockOldestQueryResponse(numVersions: number): void {
numVersions: number const response = new Promise<GetVersionsQueryResponse>(resolve => {
) {
const response = new Promise((resolve) => {
resolve(getMockedOldestQueryResponse(numVersions)) resolve(getMockedOldestQueryResponse(numVersions))
}) as Promise<GraphQlQueryResponseData> }) as Promise<GraphQlQueryResponseData>
jest.spyOn(Graphql, 'graphql').mockImplementation( jest
(token: string, query: string, parameters: RequestParameters) => response) .spyOn(Graphql, 'graphql')
.mockImplementation(
(token: string, query: string, parameters: RequestParameters) => response
)
} }
+4 -3
View File
@@ -37,9 +37,10 @@ inputs:
min-versions-to-keep: min-versions-to-keep:
description: > description: >
Number of versions to keep starting with the latest version 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 required: false
default: "0" default: "-1"
ignore-versions: ignore-versions:
description: > description: >
@@ -50,7 +51,7 @@ inputs:
delete-only-pre-release-versions: delete-only-pre-release-versions:
description: > 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. 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 By default this is set to false
required: false required: false
+189 -64
View File
@@ -23,47 +23,104 @@ module.exports = JSON.parse('{"_args":[["@octokit/[email protected]","/workspaces/del
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", ({ value: true })); 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 rxjs_1 = __nccwpck_require__(5805);
const version_1 = __nccwpck_require__(4428); const version_1 = __nccwpck_require__(4428);
const operators_1 = __nccwpck_require__(7801); 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) { if (input.packageVersionIds.length > 0) {
return rxjs_1.of(input.packageVersionIds); return rxjs_1.of(input.packageVersionIds);
} }
if (input.hasOldestVersionQueryInfo()) { if (input.hasOldestVersionQueryInfo()) {
return version_1.getOldestVersions(input.owner, input.repo, input.packageName, input.numOldVersionsToDelete + input.minVersionsToKeep, input.token).pipe(operators_1.map(versionInfo => { if (input.minVersionsToKeep < 0) {
const numberVersionsToDelete = versionInfo.length - input.minVersionsToKeep; // This code block is when num-old-versions-to-delete is specified.
if (input.minVersionsToKeep > 0) { // Setting input.numOldVersionsToDelete is set as minimum of input.numOldVersionsToDelete and RATE_LIMIT
return numberVersionsToDelete <= 0 input.numOldVersionsToDelete =
? [] input.numOldVersionsToDelete < RATE_LIMIT
: versionInfo ? input.numOldVersionsToDelete
.filter(info => !input.ignoreVersions.test(info.version)) : RATE_LIMIT;
.map(info => info.id) return getVersionIds(input.owner, input.repo, input.packageName, RATE_LIMIT, '', input.token).pipe(
.slice(0, -input.minVersionsToKeep); // This code block executes on batches of 100 versions starting from oldest
} operators_1.map(value => {
else { /*
return numberVersionsToDelete <= 0 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.
: versionInfo */
.filter(info => !input.ignoreVersions.test(info.version)) value = value.filter(info => !input.ignoreVersions.test(info.version));
.map(info => info.id) const temp = input.numOldVersionsToDelete;
.slice(0, numberVersionsToDelete); 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) { function deleteVersions(input) {
if (!input.token) { if (!input.token) {
return rxjs_1.throwError('No token found'); 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'); console.log('Number of old versions to delete input is 0 or less, no versions will be deleted');
return rxjs_1.of(true); 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; exports.deleteVersions = deleteVersions;
@@ -100,22 +157,30 @@ class Input {
this.ignoreVersions = validatedParams.ignoreVersions; this.ignoreVersions = validatedParams.ignoreVersions;
this.deletePreReleaseVersions = validatedParams.deletePreReleaseVersions; this.deletePreReleaseVersions = validatedParams.deletePreReleaseVersions;
this.token = validatedParams.token; this.token = validatedParams.token;
if (this.minVersionsToKeep > 0) { this.numDeleted = 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*))*)$');
}
} }
hasOldestVersionQueryInfo() { hasOldestVersionQueryInfo() {
return !!(this.owner && return !!(this.owner &&
this.repo && this.repo &&
this.packageName && this.packageName &&
this.numOldVersionsToDelete > 0 && this.numOldVersionsToDelete >= 0 &&
this.minVersionsToKeep >= 0 &&
this.token); 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; exports.Input = Input;
@@ -132,6 +197,7 @@ exports.deletePackageVersions = exports.deletePackageVersion = void 0;
const rxjs_1 = __nccwpck_require__(5805); const rxjs_1 = __nccwpck_require__(5805);
const operators_1 = __nccwpck_require__(7801); const operators_1 = __nccwpck_require__(7801);
const graphql_1 = __nccwpck_require__(6320); const graphql_1 = __nccwpck_require__(6320);
let deleted = 0;
const mutation = ` const mutation = `
mutation deletePackageVersion($packageVersionId: String!) { mutation deletePackageVersion($packageVersionId: String!) {
deletePackageVersion(input: {packageVersionId: $packageVersionId}) { deletePackageVersion(input: {packageVersionId: $packageVersionId}) {
@@ -139,32 +205,30 @@ const mutation = `
} }
}`; }`;
function deletePackageVersion(packageVersionId, token) { function deletePackageVersion(packageVersionId, token) {
deleted += 1;
return rxjs_1.from(graphql_1.graphql(token, mutation, { return rxjs_1.from(graphql_1.graphql(token, mutation, {
packageVersionId, packageVersionId,
headers: { headers: {
Accept: 'application/vnd.github.package-deletes-preview+json' Accept: 'application/vnd.github.package-deletes-preview+json'
} }
})).pipe(operators_1.catchError((err) => { })).pipe(operators_1.catchError(err => {
const msg = 'delete version mutation failed.'; const msg = 'delete version mutation failed.';
return rxjs_1.throwError(err.errors && err.errors.length > 0 return rxjs_1.throwError(err.errors && err.errors.length > 0
? `${msg} ${err.errors[0].message}` ? `${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)); }), operators_1.map(response => response.deletePackageVersion.success));
} }
exports.deletePackageVersion = deletePackageVersion; exports.deletePackageVersion = deletePackageVersion;
function deletePackageVersions(packageVersionIds, token) { function deletePackageVersions(packageVersionIds, token) {
if (packageVersionIds.length === 0) { if (packageVersionIds.length === 0) {
console.log('no package version ids found, no versions will be deleted');
return rxjs_1.of(true); return rxjs_1.of(true);
} }
const deletes = packageVersionIds.map(id => deletePackageVersion(id, token).pipe(operators_1.tap(result => { const deletes = packageVersionIds.map(id => deletePackageVersion(id, token).pipe(operators_1.tap(result => {
if (result) { if (!result) {
console.log(`version with id: ${id}, deleted`);
}
else {
console.log(`version with id: ${id}, not deleted`); console.log(`version with id: ${id}, not deleted`);
} }
}))); })));
console.log(`Total versions deleted till now: ${deleted}`);
return rxjs_1.merge(...deletes); return rxjs_1.merge(...deletes);
} }
exports.deletePackageVersions = deletePackageVersions; exports.deletePackageVersions = deletePackageVersions;
@@ -190,48 +254,109 @@ const query = `
node { node {
name name
versions(last: $last) { versions(last: $last) {
totalCount
edges { edges {
node { node {
id id
version version
} }
} }
pageInfo {
startCursor
hasPreviousPage
}
} }
} }
} }
} }
} }
}`; }`;
function queryForOldestVersions(owner, repo, packageName, numVersions, token) { const Paginatequery = `
return rxjs_1.from(graphql_1.graphql(token, query, { query getVersions($owner: String!, $repo: String!, $package: String!, $last: Int!, $before: String!) {
owner, repository(owner: $owner, name: $repo) {
repo, packages(first: 1, names: [$package]) {
package: packageName, edges {
last: numVersions, node {
headers: { name
Accept: 'application/vnd.github.packages-preview+json' 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}` function queryForOldestVersions(owner, repo, packageName, numVersions, startCursor, token) {
: `${msg} verify input parameters are correct`); 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; exports.queryForOldestVersions = queryForOldestVersions;
function getOldestVersions(owner, repo, packageName, numVersions, token) { function getOldestVersions(owner, repo, packageName, numVersions, startCursor, token) {
return queryForOldestVersions(owner, repo, packageName, numVersions, token).pipe(operators_1.map(result => { return queryForOldestVersions(owner, repo, packageName, numVersions, startCursor, token).pipe(operators_1.map(result => {
let r;
if (result.repository.packages.edges.length < 1) { if (result.repository.packages.edges.length < 1) {
console.log(`package: ${packageName} not found for owner: ${owner} in repo: ${repo}`); 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 versions = result.repository.packages.edges[0].node.versions.edges;
if (versions.length !== numVersions) { const pages = result.repository.packages.edges[0].node.versions.pageInfo;
console.log(`number of versions requested was: ${numVersions}, but found: ${versions.length}`); const count = result.repository.packages.edges[0].node.versions.totalCount;
} r = {
return versions versions: versions
.map(value => ({ id: value.node.id, version: value.node.version })) .map(value => ({ id: value.node.id, version: value.node.version }))
.reverse(); .reverse(),
cursor: pages.startCursor,
paginate: pages.hasPreviousPage,
totalCount: count
};
return r;
})); }));
} }
exports.getOldestVersions = getOldestVersions; exports.getOldestVersions = getOldestVersions;
+3 -2
View File
@@ -7,9 +7,10 @@
"scripts": { "scripts": {
"format": "prettier --write **/*.ts", "format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts", "format-check": "prettier --check **/*.ts",
"lint": "eslint src/**/*.ts", "lint": "eslint src/**/*.ts --fix",
"lint-check": "eslint src/**/*.ts",
"test": "jest", "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" "pack": "rm -rf ./lib ./dist && npm run build && ncc build"
}, },
"repository": { "repository": {
+127 -39
View File
@@ -1,46 +1,130 @@
import {Input} from './input' import {Input} from './input'
import {Observable, of, throwError} from 'rxjs' import {EMPTY, Observable, of, throwError} from 'rxjs'
import {deletePackageVersions, getOldestVersions} from './version' import {deletePackageVersions, getOldestVersions, VersionInfo} from './version'
import {concatMap, map} from 'rxjs/operators' import {concatMap, map, expand, tap} from 'rxjs/operators'
export function getVersionIds(input: Input): Observable<string[]> { const RATE_LIMIT = 99
let totalCount = 0
export function getVersionIds(
owner: string,
repo: string,
packageName: string,
numVersions: number,
cursor: string,
token: string
): Observable<VersionInfo[]> {
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<string[]> {
if (input.packageVersionIds.length > 0) { if (input.packageVersionIds.length > 0) {
return of(input.packageVersionIds) return of(input.packageVersionIds)
} }
if (input.hasOldestVersionQueryInfo()) { if (input.hasOldestVersionQueryInfo()) {
return getOldestVersions( if (input.minVersionsToKeep < 0) {
input.owner, // This code block is when num-old-versions-to-delete is specified.
input.repo, // Setting input.numOldVersionsToDelete is set as minimum of input.numOldVersionsToDelete and RATE_LIMIT
input.packageName, input.numOldVersionsToDelete =
input.numOldVersionsToDelete + input.minVersionsToKeep, input.numOldVersionsToDelete < RATE_LIMIT
input.token ? input.numOldVersionsToDelete
).pipe( : RATE_LIMIT
map(versionInfo => { return getVersionIds(
const numberVersionsToDelete = input.owner,
versionInfo.length - input.minVersionsToKeep input.repo,
input.packageName,
if (input.minVersionsToKeep > 0) { RATE_LIMIT,
return numberVersionsToDelete <= 0 '',
? [] input.token
: versionInfo ).pipe(
.filter(info => !input.ignoreVersions.test(info.version)) // This code block executes on batches of 100 versions starting from oldest
.map(info => info.id) map(value => {
.slice(0, -input.minVersionsToKeep) /*
} else { Here first filter out the versions that are to be ignored.
return numberVersionsToDelete <= 0 Then update input.numOldeVersionsToDelete to the no of versions deleted from the next 100 versions batch.
? [] */
: versionInfo value = value.filter(info => !input.ignoreVersions.test(info.version))
.filter(info => !input.ignoreVersions.test(info.version)) const temp = input.numOldVersionsToDelete
.map(info => info.id) input.numOldVersionsToDelete =
.slice(0, numberVersionsToDelete) 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( 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<boolean> {
return throwError('No token found') 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( console.log(
'Number of old versions to delete input is 0 or less, no versions will be deleted' 'Number of old versions to delete input is 0 or less, no versions will be deleted'
) )
return of(true) return of(true)
} }
return getVersionIds(input).pipe( const result = finalIds(input)
concatMap(ids => deletePackageVersions(ids, input.token))
) return result.pipe(concatMap(ids => deletePackageVersions(ids, input.token)))
} }
+24 -11
View File
@@ -32,6 +32,7 @@ export class Input {
ignoreVersions: RegExp ignoreVersions: RegExp
deletePreReleaseVersions: string deletePreReleaseVersions: string
token: string token: string
numDeleted: number
constructor(params?: InputParams) { constructor(params?: InputParams) {
const validatedParams: Required<InputParams> = {...defaultParams, ...params} const validatedParams: Required<InputParams> = {...defaultParams, ...params}
@@ -45,15 +46,7 @@ export class Input {
this.ignoreVersions = validatedParams.ignoreVersions this.ignoreVersions = validatedParams.ignoreVersions
this.deletePreReleaseVersions = validatedParams.deletePreReleaseVersions this.deletePreReleaseVersions = validatedParams.deletePreReleaseVersions
this.token = validatedParams.token this.token = validatedParams.token
this.numDeleted = 0
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*))*)$')
}
} }
hasOldestVersionQueryInfo(): boolean { hasOldestVersionQueryInfo(): boolean {
@@ -61,9 +54,29 @@ export class Input {
this.owner && this.owner &&
this.repo && this.repo &&
this.packageName && this.packageName &&
this.numOldVersionsToDelete > 0 && this.numOldVersionsToDelete >= 0 &&
this.minVersionsToKeep >= 0 &&
this.token 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
}
} }
+7 -8
View File
@@ -1,8 +1,9 @@
import {from, Observable, merge, throwError, of} from 'rxjs' import {from, Observable, merge, throwError, of} from 'rxjs'
import {catchError, map, tap} from 'rxjs/operators' import {catchError, map, tap} from 'rxjs/operators'
import {GraphQlQueryResponse} from '@octokit/graphql/dist-types/types'
import {graphql} from './graphql' import {graphql} from './graphql'
let deleted = 0
export interface DeletePackageVersionMutationResponse { export interface DeletePackageVersionMutationResponse {
deletePackageVersion: { deletePackageVersion: {
success: boolean success: boolean
@@ -20,6 +21,7 @@ export function deletePackageVersion(
packageVersionId: string, packageVersionId: string,
token: string token: string
): Observable<boolean> { ): Observable<boolean> {
deleted += 1
return from( return from(
graphql(token, mutation, { graphql(token, mutation, {
packageVersionId, packageVersionId,
@@ -28,12 +30,12 @@ export function deletePackageVersion(
} }
}) as Promise<DeletePackageVersionMutationResponse> }) as Promise<DeletePackageVersionMutationResponse>
).pipe( ).pipe(
catchError((err: GraphQlQueryResponse) => { catchError(err => {
const msg = 'delete version mutation failed.' const msg = 'delete version mutation failed.'
return throwError( return throwError(
err.errors && err.errors.length > 0 err.errors && err.errors.length > 0
? `${msg} ${err.errors[0].message}` ? `${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) map(response => response.deletePackageVersion.success)
@@ -45,21 +47,18 @@ export function deletePackageVersions(
token: string token: string
): Observable<boolean> { ): Observable<boolean> {
if (packageVersionIds.length === 0) { if (packageVersionIds.length === 0) {
console.log('no package version ids found, no versions will be deleted')
return of(true) return of(true)
} }
const deletes = packageVersionIds.map(id => const deletes = packageVersionIds.map(id =>
deletePackageVersion(id, token).pipe( deletePackageVersion(id, token).pipe(
tap(result => { tap(result => {
if (result) { if (!result) {
console.log(`version with id: ${id}, deleted`)
} else {
console.log(`version with id: ${id}, not deleted`) console.log(`version with id: ${id}, not deleted`)
} }
}) })
) )
) )
console.log(`Total versions deleted till now: ${deleted}`)
return merge(...deletes) return merge(...deletes)
} }
+109 -29
View File
@@ -8,6 +8,13 @@ export interface VersionInfo {
version: string version: string
} }
export interface QueryInfo {
versions: VersionInfo[]
cursor: string
paginate: boolean
totalCount: number
}
export interface GetVersionsQueryResponse { export interface GetVersionsQueryResponse {
repository: { repository: {
packages: { packages: {
@@ -15,7 +22,12 @@ export interface GetVersionsQueryResponse {
node: { node: {
name: string name: string
versions: { versions: {
totalCount: number
edges: {node: VersionInfo}[] edges: {node: VersionInfo}[]
pageInfo: {
startCursor: string
hasPreviousPage: boolean
}
} }
} }
}[] }[]
@@ -31,12 +43,43 @@ const query = `
node { node {
name name
versions(last: $last) { versions(last: $last) {
totalCount
edges { edges {
node { node {
id id
version 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, repo: string,
packageName: string, packageName: string,
numVersions: number, numVersions: number,
startCursor: string,
token: string token: string
): Observable<GetVersionsQueryResponse> { ): Observable<GetVersionsQueryResponse> {
return from( if (startCursor === '') {
graphql(token, query, { return from(
owner, graphql(token, query, {
repo, owner,
package: packageName, repo,
last: numVersions, package: packageName,
headers: { last: numVersions,
Accept: 'application/vnd.github.packages-preview+json' headers: {
} Accept: 'application/vnd.github.packages-preview+json'
}) as Promise<GetVersionsQueryResponse> }
).pipe( }) as Promise<GetVersionsQueryResponse>
catchError((err: GraphQlQueryResponse) => { ).pipe(
const msg = 'query for oldest version failed.' catchError((err: GraphQlQueryResponse) => {
return throwError( const msg = 'query for oldest version failed.'
err.errors && err.errors.length > 0 return throwError(
? `${msg} ${err.errors[0].message}` err.errors && err.errors.length > 0
: `${msg} verify input parameters are correct` ? `${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<GetVersionsQueryResponse>
).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( export function getOldestVersions(
@@ -78,34 +146,46 @@ export function getOldestVersions(
repo: string, repo: string,
packageName: string, packageName: string,
numVersions: number, numVersions: number,
startCursor: string,
token: string token: string
): Observable<VersionInfo[]> { ): Observable<QueryInfo> {
return queryForOldestVersions( return queryForOldestVersions(
owner, owner,
repo, repo,
packageName, packageName,
numVersions, numVersions,
startCursor,
token token
).pipe( ).pipe(
map(result => { map(result => {
let r: QueryInfo
if (result.repository.packages.edges.length < 1) { if (result.repository.packages.edges.length < 1) {
console.log( console.log(
`package: ${packageName} not found for owner: ${owner} in repo: ${repo}` `package: ${packageName} not found for owner: ${owner} in repo: ${repo}`
) )
return [] r = {
versions: <VersionInfo[]>[],
cursor: '',
paginate: false,
totalCount: 0
}
return r
} }
const versions = result.repository.packages.edges[0].node.versions.edges 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) { r = {
console.log( versions: versions
`number of versions requested was: ${numVersions}, but found: ${versions.length}` .map(value => ({id: value.node.id, version: value.node.version}))
) .reverse(),
cursor: pages.startCursor,
paginate: pages.hasPreviousPage,
totalCount: count
} }
return versions return r
.map(value => ({id: value.node.id, version: value.node.version}))
.reverse()
}) })
) )
} }