diff --git a/README.md b/README.md
index a73212ce..5b755483 100644
--- a/README.md
+++ b/README.md
@@ -8,8 +8,8 @@ This action deletes versions of a package from [GitHub Packages](https://github.
* Delete all package versions except n most recent versions
* Delete oldest version(s)
* Ignore version(s) from deletion through regex
-* Delete version(s) of a package that is hosted from a repo having access to package
-* Delete version(s) of a package that is hosted from a repo not having access to package
+* Delete version(s) of packages that are hosted in the same repo that is executing the workflow
+* Delete version(s) of packages that are hosted in a different repo than the one executing the workflow
* Delete a single version
* Delete multiple versions
* Delete specific version(s)
@@ -28,13 +28,24 @@ This action deletes versions of a package from [GitHub Packages](https://github.
# Required if deleting a version from a package hosted in a different org than the one executing the workflow.
owner:
- # Name of the package.
- # Required
- package-name:
-
# Type of the package. Can be one of container, maven, npm, nuget, or rubygems.
# Required
package-type:
+
+ # Defaults to an empty string.
+ # Required if `package-version-ids` or `package-names` input is not given.
+ package-name:
+
+ # Names of the package.
+ # Can be one of the following:
+ # - a single package name
+ # - a group of packages that matches a wildcard at start, end, both sides, or all packages (e.g. "package*")
+ # - a group of packages that matches a regex, must start with slash at the beginning and end (e.g. "/package.*/")
+ # - a comma separated list of the previous cases (e.g. "package-lorem, *-ipsum, /.*dolor/")
+ # Defaults to an empty string.
+ # Required if `package-version-ids` or `package-name` input is not given.
+ package-names:
+
# The number of old versions to delete starting from the oldest version.
# Defaults to 1.
@@ -70,7 +81,7 @@ This action deletes versions of a package from [GitHub Packages](https://github.
# Valid Input Combinations
-`owner`, `package-name`, `package-type` and `token` can be used with the following combinations in a workflow -
+`owner`, `repo`, `package-name` (or `package-names`) and `token` can be used with the following combinations in a workflow -
- `num-old-versions-to-delete`
- `min-versions-to-keep`
@@ -98,6 +109,7 @@ This action deletes versions of a package from [GitHub Packages](https://github.
- [License](#license)
+
### Delete all pre-release versions except y latest pre-release package versions
To delete all pre release versions except y latest pre-release package versions, the __package-name__, __min-versions-to-keep__ and __delete-only-pre-release-versions__ inputs are required.
@@ -247,6 +259,41 @@ This action deletes versions of a package from [GitHub Packages](https://github.
+ ### Delete all except y latest versions of a package
+
+ To delete all except y latest versions of all packages hosted in the same repo as the workflow the __package-names__ and __min-versions-to-keep__ inputs are required.
+
+ __Example__
+
+ Delete all except latest 2 versions of a package hosted in the same repo as the workflow
+
+ ```yaml
+ - uses: actions/delete-package-versions@v3
+ with:
+ package-names: '*'
+ min-versions-to-keep: 2
+ ```
+
+ To delete all except y latest versions of all packages hosted in a repo other than the workflow the __owner__, __repo__, __package-names__, __token__ and __min-versions-to-keep__ inputs are required.
+
+ The [token][token] needs the delete packages and read packages scope. It is recommended [to store the token as a secret][secret]. In this example the [token][token] was stored as a secret named __GITHUB_PAT__.
+
+ __Example__
+
+ Delete all except latest 2 versions of a package hosted in a repo other than the workflow
+
+ ```yaml
+ - uses: actions/delete-package-versions@v3
+ with:
+ owner: 'github'
+ repo: 'packages'
+ package-names: '*'
+ token: ${{ secrets.PAT }}
+ min-versions-to-keep: 2
+ ```
+
+
+
### Delete oldest x number of versions of a package
To delete the oldest x number of versions of a package hosted, the __package-name__, and __num-old-versions-to-delete__ inputs are required.
diff --git a/__tests__/packages/get-packages.test.ts b/__tests__/packages/get-packages.test.ts
new file mode 100644
index 00000000..27672257
--- /dev/null
+++ b/__tests__/packages/get-packages.test.ts
@@ -0,0 +1,66 @@
+import {mockPackagesQueryResponse} from './graphql.mock'
+import {
+ getRepoPackages as _getRepoPackages,
+ QueryInfo
+} from '../../src/packages'
+import {Observable} from 'rxjs'
+
+describe.skip('get versions tests -- call graphql', () => {
+ it('getRepoPackages -- succeeds', done => {
+ const numPackages = 1
+ getRepoPackages({numPackages}).subscribe(result => {
+ expect(result.packages.length).toBe(numPackages)
+ done()
+ })
+ })
+
+ it('getRepoPackages -- fails for invalid repo', done => {
+ getRepoPackages({repo: 'actions-testin'}).subscribe({
+ error: err => {
+ expect(err).toBeTruthy()
+ done()
+ },
+ complete: async () => done.fail('no error thrown')
+ })
+ })
+ })
+
+ describe('get versions tests -- mock graphql', () => {
+ it('getRepoPackages -- success', done => {
+ const numPackages = 5
+ mockPackagesQueryResponse(numPackages)
+
+ getRepoPackages({numPackages}).subscribe(result => {
+ expect(result.packages.length).toBe(numPackages)
+ done()
+ })
+ })
+ })
+
+ interface Params {
+ owner?: string
+ repo?: string
+ numPackages?: number
+ startCursor?: string
+ token?: string
+ }
+
+ const defaultParams = {
+ owner: 'namratajha',
+ repo: 'test-repo',
+ packageName: 'test-repo',
+ numPackages: 1,
+ startCursor: '',
+ token: process.env.GITHUB_TOKEN as string
+ }
+
+ function getRepoPackages(params?: Params): Observable {
+ const p: Required = {...defaultParams, ...params}
+ return _getRepoPackages(
+ p.owner,
+ p.repo,
+ p.numPackages,
+ p.startCursor,
+ p.token
+ )
+ }
\ No newline at end of file
diff --git a/__tests__/packages/graphql.mock.ts b/__tests__/packages/graphql.mock.ts
new file mode 100644
index 00000000..a6601488
--- /dev/null
+++ b/__tests__/packages/graphql.mock.ts
@@ -0,0 +1,44 @@
+import {
+ GraphQlQueryResponseData,
+ RequestParameters
+} from '@octokit/graphql/dist-types/types'
+
+import * as Graphql from '../../src/common/graphql'
+import {GetPackagesQueryResponse} from '../../src/packages'
+
+export function getMockedPackagesQueryResponse(
+ numPackages: number
+): GetPackagesQueryResponse {
+ const packages: any[] = []
+ for (let i = 1; i <= numPackages; ++i) {
+ packages.push({
+ node: {
+ id: i.toString(),
+ name: `package${i}`
+ }
+ })
+ }
+
+ return {
+ repository: {
+ packages: {
+ pageInfo: {
+ endCursor: 'AAA',
+ hasNextPage: false
+ },
+ edges: packages
+ }
+ }
+ }
+}
+
+export function mockPackagesQueryResponse(numVersions: number): void {
+ const response = new Promise(resolve => {
+ resolve(getMockedPackagesQueryResponse(numVersions))
+ }) as Promise
+ jest
+ .spyOn(Graphql, 'graphql')
+ .mockImplementation(
+ (token: string, query: string, parameters: RequestParameters) => response
+ )
+}
diff --git a/__tests__/packages/package-name-filter.test.ts b/__tests__/packages/package-name-filter.test.ts
new file mode 100644
index 00000000..781d7c74
--- /dev/null
+++ b/__tests__/packages/package-name-filter.test.ts
@@ -0,0 +1,114 @@
+import { getPackageNameFilter } from '../../src/packages'
+
+describe('package name filter -- create filter', () => {
+
+ const packageNameList = [
+ 'com.company.project.module1.package1',
+ 'com.company.project.module1.package2',
+ 'com.company.project.module2.package1',
+ 'com.company.project.module2.package2',
+ 'com.company.project.module3.package-name-lorem',
+ 'com.company.project.module3.package-name-ipsum',
+ 'com.company.project.module3.package-name-dolor',
+ ]
+
+ it('getPackageNameFilter -- wildcard end filter', done => {
+ const filter = getPackageNameFilter('com.company.project.module1.*')
+
+ const result = packageNameList.filter(filter.apply);
+
+ expect(filter.subfilters[0].type).toBe('wildcard')
+ expect(result).toEqual([
+ 'com.company.project.module1.package1',
+ 'com.company.project.module1.package2',
+ ])
+ done()
+ })
+
+ it('getPackageNameFilter -- wildcard start filter', done => {
+ const filter = getPackageNameFilter('*.package1')
+
+ const result = packageNameList.filter(filter.apply);
+
+ expect(filter.subfilters[0].type).toBe('wildcard')
+ expect(result).toEqual([
+ 'com.company.project.module1.package1',
+ 'com.company.project.module2.package1',
+ ])
+ done()
+ })
+
+ it('getPackageNameFilter -- wildcard both sides filter', done => {
+ const filter = getPackageNameFilter('*.project.module3.*')
+
+ const result = packageNameList.filter(filter.apply);
+
+ expect(filter.subfilters[0].type).toBe('wildcard')
+ expect(result).toEqual([
+ 'com.company.project.module3.package-name-lorem',
+ 'com.company.project.module3.package-name-ipsum',
+ 'com.company.project.module3.package-name-dolor'
+ ])
+ done()
+ })
+
+
+ it('getPackageNameFilter -- wildcard all filter', done => {
+ const filter = getPackageNameFilter('*')
+
+ const result = packageNameList.filter(filter.apply);
+
+ expect(filter.subfilters[0].type).toBe('wildcard')
+ expect(result).toEqual(packageNameList.slice())
+ done()
+ })
+
+ it('getPackageNameFilter -- regex filter', done => {
+ const filter = getPackageNameFilter('/com\\.company\\.project\\.module.*\\.package1/')
+ const result = packageNameList.filter(filter.apply);
+
+ expect(filter.subfilters[0].type).toBe('regex')
+ expect(result).toEqual([
+ 'com.company.project.module1.package1',
+ 'com.company.project.module2.package1',
+ ])
+ done()
+ })
+
+ it('getPackageNameFilter -- exact match filter', done => {
+ const filter = getPackageNameFilter('com.company.project.module1.package1')
+ const result = packageNameList.filter(filter.apply);
+
+ expect(filter.subfilters[0].type).toBe('string')
+ expect(result).toEqual(['com.company.project.module1.package1'])
+ done()
+ })
+
+
+ it('getPackageNameFilter -- multiple filters', done => {
+ const filter = getPackageNameFilter('com.company.project.module1.package1, com.company.project.module2.*, /.*module3.*-ipsum/')
+ const result = packageNameList.filter(filter.apply);
+
+ expect(filter.subfilters.length).toBe(3)
+ expect(filter.subfilters[0].type).toBe('string')
+ expect(filter.subfilters[1].type).toBe('wildcard')
+ expect(filter.subfilters[2].type).toBe('regex')
+ expect(result).toEqual([
+ 'com.company.project.module1.package1',
+ 'com.company.project.module2.package1',
+ 'com.company.project.module2.package2',
+ 'com.company.project.module3.package-name-ipsum'
+ ])
+ done()
+ })
+
+
+ it('getPackageNameFilter -- memoization, same input shoud return same output', done => {
+ const filterText = 'com.company.project.module1.package1, com.company.project.module2.*, /.*module3.*-dolor/, *.-lorem'
+ const filter1 = getPackageNameFilter(filterText)
+ const filter2 = getPackageNameFilter(filterText)
+ expect(filter1).toBe(filter2)
+ expect(filter1.subfilters.length).toBe(4)
+ done()
+ })
+ })
\ No newline at end of file
diff --git a/action.yml b/action.yml
index 67192dc7..1e8d1b0d 100644
--- a/action.yml
+++ b/action.yml
@@ -25,6 +25,19 @@ inputs:
Type of package. Can be one of container, maven, npm, nuget, or rubygems.
required: true
+
+ package-names:
+ description: >
+ Names of the package.
+ Can be one of the following:
+ - a single package name
+ - a group of packages that matches a wildcard at start, end, both sides, or all packages (e.g. "package*")
+ - a group of packages that matches a regex, must start with slash at the beginning and end (e.g. "/package.*/")
+ - a comma separated list of the previous cases (e.g. "package-lorem, *-ipsum, /.*dolor/")
+ Defaults to an empty string.
+ Required if `package-version-ids` or `package-name` input is not given.
+ required: false
+
num-old-versions-to-delete:
description: >
Number of versions to delete starting with the oldest version.
diff --git a/package-lock.json b/package-lock.json
index 436811f8..94554553 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@actions/core": "^1.9.1",
"@actions/github": "^2.1.1",
"@octokit/rest": "^19.0.5",
+ "graphql": "^16.6.0",
"rxjs": "^6.5.4"
},
"devDependencies": {
@@ -4131,7 +4132,6 @@
"version": "16.6.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz",
"integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==",
- "dev": true,
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
@@ -11008,8 +11008,7 @@
"graphql": {
"version": "16.6.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz",
- "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==",
- "dev": true
+ "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw=="
},
"has": {
"version": "1.0.3",
diff --git a/package.json b/package.json
index 790ad587..4af36b70 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"@actions/core": "^1.9.1",
"@actions/github": "^2.1.1",
"@octokit/rest": "^19.0.5",
+ "graphql": "^16.6.0",
"rxjs": "^6.5.4"
},
"devDependencies": {
diff --git a/src/common/graphql.ts b/src/common/graphql.ts
new file mode 100644
index 00000000..1f5d5608
--- /dev/null
+++ b/src/common/graphql.ts
@@ -0,0 +1,20 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import {GitHub} from '@actions/github'
+import {GraphQlQueryResponseData} from '@octokit/graphql/dist-types/types'
+import {RequestParameters} from '@octokit/types/dist-types/RequestParameters'
+
+/**
+ * Sends a GraphQL query request based on endpoint options
+ *
+ * @param {string} token Auth token
+ * @param {string} query GraphQL query. Example: `'query { viewer { login } }'`.
+ * @param {object} parameters URL, query or body parameters, as well as `headers`, `mediaType.{format|previews}`, `request`, or `baseUrl`.
+ */
+export async function graphql(
+ token: string,
+ query: string,
+ parameters: RequestParameters
+): Promise {
+ const github = new GitHub(token)
+ return await github.graphql(query, parameters)
+}
\ No newline at end of file
diff --git a/src/delete.ts b/src/delete.ts
index e701c163..40fdb902 100644
--- a/src/delete.ts
+++ b/src/delete.ts
@@ -2,16 +2,35 @@
import {Input} from './input'
import {EMPTY, Observable, of, throwError} from 'rxjs'
-import {reduce, concatMap, map, expand, tap} from 'rxjs/operators'
+import {reduce, concatMap, map, expand, tap, mergeMap, catchError} from 'rxjs/operators'
import {
deletePackageVersions,
getOldestVersions,
RestVersionInfo
} from './version'
+import {getRepoPackages, getPackageNameFilter, PackageInfo} from './packages'
+
export const RATE_LIMIT = 100
let totalCount = 0
+export function getPackageNames(
+ owner: string,
+ repo: string,
+ numPackages: number,
+ cursor: string,
+ token: string
+): Observable {
+ return getRepoPackages(owner, repo, numPackages, cursor, token).pipe(
+ expand(value =>
+ value.paginate
+ ? getRepoPackages(owner, repo, numPackages, value.cursor, token)
+ : EMPTY
+ ),
+ map(value => value.packages)
+ )
+}
+
export function getVersionIds(
owner: string,
packageName: string,
@@ -51,6 +70,34 @@ export function finalIds(input: Input): Observable {
return of(input.packageVersionIds.slice(0, toDelete))
}
if (input.hasOldestVersionQueryInfo()) {
+
+ const filter = getPackageNameFilter(input.packageNames)
+ if (!filter.isEmpty) {
+ return getPackageNames(
+ input.owner,
+ input.repo,
+ RATE_LIMIT,
+ '',
+ input.token
+ )
+ .pipe(
+ mergeMap(value => {
+ return value
+ .filter(info => filter.apply(info.name))
+ .map(info =>
+ finalIds(
+ new Input({
+ ...input,
+ packageNames: '',
+ packageName: info.name
+ })
+ )
+ )
+ })
+ )
+ .pipe(mergeMap(val => val))
+ }
+
return getVersionIds(
input.owner,
input.packageName,
diff --git a/src/input.ts b/src/input.ts
index f0af8bfc..20d6debc 100644
--- a/src/input.ts
+++ b/src/input.ts
@@ -1,8 +1,10 @@
export interface InputParams {
packageVersionIds?: string[]
owner?: string
+ repo?: string
packageName?: string
packageType?: string
+ packageNames?: string
numOldVersionsToDelete?: number
minVersionsToKeep?: number
ignoreVersions?: RegExp
@@ -13,8 +15,10 @@ export interface InputParams {
const defaultParams = {
packageVersionIds: [],
owner: '',
+ repo: '',
packageName: '',
packageType: '',
+ packageNames: '',
numOldVersionsToDelete: 0,
minVersionsToKeep: 0,
ignoreVersions: new RegExp(''),
@@ -25,8 +29,10 @@ const defaultParams = {
export class Input {
packageVersionIds: string[]
owner: string
+ repo: string
packageName: string
packageType: string
+ packageNames: string
numOldVersionsToDelete: number
minVersionsToKeep: number
ignoreVersions: RegExp
@@ -39,8 +45,10 @@ export class Input {
this.packageVersionIds = validatedParams.packageVersionIds
this.owner = validatedParams.owner
+ this.repo = validatedParams.repo
this.packageName = validatedParams.packageName
this.packageType = validatedParams.packageType
+ this.packageNames = validatedParams.packageNames
this.numOldVersionsToDelete = validatedParams.numOldVersionsToDelete
this.minVersionsToKeep = validatedParams.minVersionsToKeep
this.ignoreVersions = validatedParams.ignoreVersions
@@ -52,7 +60,8 @@ export class Input {
hasOldestVersionQueryInfo(): boolean {
return !!(
this.owner &&
- this.packageName &&
+ this.repo &&
+ (this.packageName || this.packageNames) &&
this.numOldVersionsToDelete >= 0 &&
this.token
)
diff --git a/src/main.ts b/src/main.ts
index 152799fc..3d6e7b74 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -14,6 +14,7 @@ function getActionInput(): Input {
owner: getInput('owner') ? getInput('owner') : context.repo.owner,
packageName: getInput('package-name'),
packageType: getInput('package-type'),
+ packageNames: getInput('package-names'),
numOldVersionsToDelete: Number(getInput('num-old-versions-to-delete')),
minVersionsToKeep: Number(getInput('min-versions-to-keep')),
ignoreVersions: RegExp(getInput('ignore-versions')),
diff --git a/src/packages/get-packages.ts b/src/packages/get-packages.ts
new file mode 100644
index 00000000..d714ab63
--- /dev/null
+++ b/src/packages/get-packages.ts
@@ -0,0 +1,165 @@
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import {GraphQlQueryResponse} from '@octokit/graphql/dist-types/types'
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import {Observable, from, throwError} from 'rxjs'
+import {catchError, map} from 'rxjs/operators'
+import {graphql} from '../common/graphql'
+
+export interface PackageInfo {
+ id: string
+ name: string
+}
+
+export interface QueryInfo {
+ packages: PackageInfo[]
+ cursor: string
+ paginate: boolean
+}
+
+export interface GetPackagesQueryResponse {
+ repository: {
+ packages: {
+ edges: {node: PackageInfo}[]
+ pageInfo: {
+ endCursor: string
+ hasNextPage: boolean
+ }
+ }
+ }
+}
+
+const query = `
+ query getPackages($owner: String!, $repo: String!, $first: Int!){
+ repository(owner: $owner, name: $repo) {
+ packages(first:$first){
+ edges {
+ node {
+ name
+ id
+ }
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ }
+ }
+ }
+ }`
+
+const Paginatequery = `
+ query getPackages($owner: String!, $repo: String!, $first: Int!, $after: String!){
+ repository(owner: $owner, name: $repo) {
+ packages(first:$first){
+ edges {
+ node {
+ name
+ id
+ }
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ }
+ }
+ }
+ }`
+
+export function queryForRepoPackages(
+ owner: string,
+ repo: string,
+ numPackages: number,
+ startCursor: string,
+ token: string
+): Observable {
+ if (startCursor === '') {
+ return from(
+ graphql(token, query, {
+ owner,
+ repo,
+ first: numPackages,
+ headers: {
+ Accept: 'application/vnd.github.packages-preview+json'
+ }
+ }) as Promise
+ ).pipe(
+ catchError((err: GraphQlQueryResponse) => {
+ const msg = 'query for packages failed.'
+ return throwError(
+ err.errors && err.errors.length > 0
+ ? `${msg} ${err.errors[0].message}`
+ : `${msg} verify input parameters are correct ${JSON.stringify(
+ err,
+ null,
+ 2
+ )}`
+ )
+ })
+ )
+ } else {
+ return from(
+ graphql(token, Paginatequery, {
+ owner,
+ repo,
+ first: numPackages,
+ before: startCursor,
+ headers: {
+ Accept: 'application/vnd.github.packages-preview+json'
+ }
+ }) as Promise
+ ).pipe(
+ catchError((err: GraphQlQueryResponse) => {
+ const msg = 'query for packages failed.'
+ return throwError(
+ err.errors && err.errors.length > 0
+ ? `${msg} ${err.errors[0].message}`
+ : `${msg} verify input parameters are correct`
+ )
+ })
+ )
+ }
+}
+
+export function getRepoPackages(
+ owner: string,
+ repo: string,
+ numPackages: number,
+ startCursor: string,
+ token: string
+): Observable {
+ return queryForRepoPackages(
+ owner,
+ repo,
+ numPackages,
+ startCursor,
+ token
+ ).pipe(
+ map(result => {
+ let r: QueryInfo
+ if (result.repository.packages.edges.length < 1) {
+ console.log(
+ `package: No packages found for owner: ${owner} in repo: ${repo}`
+ )
+ r = {
+ packages: [] as PackageInfo[],
+ cursor: '',
+ paginate: false
+ }
+ return r
+ }
+
+ const packages = result.repository.packages.edges
+ const pages = result.repository.packages.pageInfo
+
+ r = {
+ packages: packages.map(value => ({
+ id: value.node.id,
+ name: value.node.name
+ })),
+ cursor: pages.endCursor,
+ paginate: pages.hasNextPage
+ }
+
+ return r
+ })
+ )
+}
diff --git a/src/packages/index.ts b/src/packages/index.ts
new file mode 100644
index 00000000..eadf04e4
--- /dev/null
+++ b/src/packages/index.ts
@@ -0,0 +1,2 @@
+export * from './get-packages'
+export * from './package-name-filter'
diff --git a/src/packages/package-name-filter.ts b/src/packages/package-name-filter.ts
new file mode 100644
index 00000000..fba0af00
--- /dev/null
+++ b/src/packages/package-name-filter.ts
@@ -0,0 +1,126 @@
+/**
+ * Used to apply memoization on getPackageNameFilter
+ */
+const resultCache = {} as Record
+
+interface PackageNameSubFilter {
+ type: 'regex' | 'wildcard' | 'string'
+ apply: (packageName: string) => boolean
+}
+
+interface PackageNamesFilter {
+ readonly subfilters: readonly PackageNameSubFilter[]
+ readonly isEmpty: boolean
+ readonly apply: (packageName: string) => boolean
+}
+
+/**
+ * Get a filter based on package names to match
+ *
+ * @param packageNames - serialized package names filter as string
+ * @returns the respective package filter
+ */
+export function getPackageNameFilter(packageNames: string): PackageNamesFilter {
+ if (resultCache[packageNames]) {
+ return resultCache[packageNames]
+ }
+ const result = calculatePackageNameFilter(packageNames)
+ resultCache[packageNames] = result
+ return result
+}
+
+const emptyFilter = Object.freeze({
+ subfilters: Object.freeze([]),
+ isEmpty: true,
+ apply: () => false as boolean
+})
+
+/**
+ * Generates a filter based package names to match
+ *
+ * @param packageNames - serialized package names filter as string
+ * @returns the respective package filter
+ */
+function calculatePackageNameFilter(
+ packageNames: string
+): Readonly {
+ if (packageNames === '') {
+ return emptyFilter
+ }
+ const separatedPackageNames = packageNames
+ .split(',')
+ .map(name => name.trim())
+ .filter(name => name !== '')
+
+ if (separatedPackageNames.length <= 0) {
+ return emptyFilter
+ }
+
+ const subfilters = separatedPackageNames.map(createFilter)
+ return {
+ subfilters,
+ isEmpty: subfilters.length <= 0,
+ apply: names => subfilters.some(filter => filter.apply(names))
+ }
+}
+
+function createFilter(packageName: string): PackageNameSubFilter {
+ if (packageName.startsWith('*') || packageName.endsWith('*')) {
+ return createWildcardFilter(packageName)
+ } else if (packageName.startsWith('/') && packageName.endsWith('/')) {
+ return createRegexFilter(packageName)
+ } else {
+ return createExactMatchFilter(packageName)
+ }
+}
+
+function createWildcardFilter(
+ wildcardPackageName: string
+): PackageNameSubFilter {
+ const startsWithWildCard = wildcardPackageName.startsWith('*')
+ const endsWithWildCard = wildcardPackageName.endsWith('*')
+ let fn: PackageNameSubFilter['apply']
+ if (wildcardPackageName === '*') {
+ fn = () => true
+ } else if (startsWithWildCard && endsWithWildCard) {
+ const targetText = wildcardPackageName.substring(
+ 1,
+ wildcardPackageName.length - 1
+ )
+ fn = (packageName: string) => packageName.includes(targetText)
+ } else if (startsWithWildCard) {
+ const targetText = wildcardPackageName.substring(1)
+ fn = (packageName: string) => packageName.endsWith(targetText)
+ } else {
+ const targetText = wildcardPackageName.substring(
+ 0,
+ wildcardPackageName.length - 1
+ )
+ fn = (packageName: string) => packageName.startsWith(targetText)
+ }
+ return {
+ type: 'wildcard',
+ apply: fn
+ }
+}
+
+function createRegexFilter(regexPackageName: string): PackageNameSubFilter {
+ const regexPattern = regexPackageName.substring(
+ 1,
+ regexPackageName.length - 1
+ )
+ const regex = new RegExp(regexPattern)
+ return {
+ type: 'regex',
+ apply: (packageName: string) => regex.test(packageName)
+ }
+}
+
+function createExactMatchFilter(
+ matchingPackageName: string
+): PackageNameSubFilter {
+ return {
+ type: 'string',
+ apply: (packageName: string) => packageName === matchingPackageName
+ }
+}
diff --git a/src/version/get-versions.ts b/src/version/get-versions.ts
index eb8e274e..07e5fc80 100644
--- a/src/version/get-versions.ts
+++ b/src/version/get-versions.ts
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
-import {from, Observable, merge, throwError, of} from 'rxjs'
+import {from, Observable, merge, throwError, of, EMPTY} from 'rxjs'
import {catchError, map} from 'rxjs/operators'
import {Octokit} from '@octokit/rest'
import {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods/dist-types/generated/parameters-and-response-types'
@@ -45,12 +45,7 @@ export function getOldestVersions(
})
).pipe(
catchError(err => {
- const msg = 'get versions API failed.'
- return throwError(
- err.errors && err.errors.length > 0
- ? `${msg} ${err.errors[0].message}`
- : `${msg} ${err.message}`
- )
+ return EMPTY
}),
map(response => {
const resp = {