Allow user to filter github tags and releases (#9193)

Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
This commit is contained in:
chris48s
2023-07-03 21:27:31 +01:00
committed by GitHub
parent e7197f6db4
commit 1543d0f363
8 changed files with 222 additions and 22 deletions

View File

@@ -1,4 +1,5 @@
import Joi from 'joi'
import { matcher } from 'matcher'
import { nonNegativeInteger } from '../validators.js'
import { latest } from '../version.js'
import { NotFound } from '../index.js'
@@ -68,8 +69,39 @@ function getLatestRelease({ releases, sort, includePrereleases }) {
const queryParamSchema = Joi.object({
include_prereleases: Joi.equal(''),
sort: Joi.string().valid('date', 'semver').default('date'),
filter: Joi.string(),
}).required()
const filterDocs = `<div>
<p>
The <code>filter</code> param can be used to apply a filter to the
project's tag or release names before selecting the latest from the list.
Two constructs are available: <code>*</code> is a wildcard matching zero
or more characters, and if the pattern starts with a <code>!</code>,
the whole pattern is negated.
</p>
</div>`
function applyFilter({ releases, filter, displayName }) {
if (!filter) {
return releases
}
if (displayName === 'tag') {
const filteredTagNames = matcher(
releases.map(release => release.tag_name),
filter
)
return releases.filter(release =>
filteredTagNames.includes(release.tag_name)
)
}
const filteredReleaseNames = matcher(
releases.map(release => release.name),
filter
)
return releases.filter(release => filteredReleaseNames.includes(release.name))
}
// Fetch the latest release as defined by query params
async function fetchLatestRelease(
serviceInstance,
@@ -78,8 +110,10 @@ async function fetchLatestRelease(
) {
const sort = queryParams.sort
const includePrereleases = queryParams.include_prereleases !== undefined
const filter = queryParams.filter
const displayName = queryParams.display_name
if (!includePrereleases && sort === 'date') {
if (!includePrereleases && sort === 'date' && !filter) {
const releaseInfo = await fetchLatestGitHubRelease(serviceInstance, {
user,
repo,
@@ -87,13 +121,23 @@ async function fetchLatestRelease(
return releaseInfo
}
const releases = await fetchReleases(serviceInstance, { user, repo })
const releases = applyFilter({
releases: await fetchReleases(serviceInstance, { user, repo }),
filter,
displayName,
})
if (releases.length === 0) {
throw new NotFound({ prettyMessage: 'no releases' })
const prettyMessage = filter
? 'no matching releases found'
: 'no releases found'
throw new NotFound({ prettyMessage })
}
const latestRelease = getLatestRelease({ releases, sort, includePrereleases })
return latestRelease
}
export { fetchLatestRelease, queryParamSchema }
export const _getLatestRelease = getLatestRelease // currently only used for tests
export { fetchLatestRelease, filterDocs, queryParamSchema }
// currently only used for tests
export const _getLatestRelease = getLatestRelease
export const _applyFilter = applyFilter

View File

@@ -1,5 +1,5 @@
import { test, given } from 'sazerac'
import { _getLatestRelease } from './github-common-release.js'
import { _applyFilter, _getLatestRelease } from './github-common-release.js'
describe('GithubRelease', function () {
test(_getLatestRelease, () => {
@@ -42,4 +42,50 @@ describe('GithubRelease', function () {
includePrereleases: false,
}).expect({ tag_name: '1.2.0-beta', prerelease: true })
})
test(_applyFilter, () => {
const releases = [
{ name: 'release/1.1.0', tag_name: 'tag/1.1.0', prerelease: false },
{ name: 'release/1.2.0', tag_name: 'tag/1.2.0', prerelease: false },
{
name: 'release/server-2022-01-01',
tag_name: 'tag/server-2022-01-01',
prerelease: false,
},
]
given({ releases, filter: undefined }).expect(releases)
given({ releases, filter: '' }).expect(releases)
given({ releases, filter: '*' }).expect(releases)
given({ releases, filter: '!*' }).expect([])
given({ releases, filter: 'foo' }).expect([])
given({ releases, filter: 'release/server-*' }).expect([
{
name: 'release/server-2022-01-01',
tag_name: 'tag/server-2022-01-01',
prerelease: false,
},
])
given({ releases, filter: '!release/server-*' }).expect([
{ name: 'release/1.1.0', tag_name: 'tag/1.1.0', prerelease: false },
{ name: 'release/1.2.0', tag_name: 'tag/1.2.0', prerelease: false },
])
given({ releases, displayName: 'tag', filter: undefined }).expect(releases)
given({ releases, displayName: 'tag', filter: '' }).expect(releases)
given({ releases, displayName: 'tag', filter: '*' }).expect(releases)
given({ releases, displayName: 'tag', filter: '!*' }).expect([])
given({ releases, displayName: 'tag', filter: 'foo' }).expect([])
given({ releases, displayName: 'tag', filter: 'tag/server-*' }).expect([
{
name: 'release/server-2022-01-01',
tag_name: 'tag/server-2022-01-01',
prerelease: false,
},
])
given({ releases, displayName: 'tag', filter: '!tag/server-*' }).expect([
{ name: 'release/1.1.0', tag_name: 'tag/1.1.0', prerelease: false },
{ name: 'release/1.2.0', tag_name: 'tag/1.2.0', prerelease: false },
])
})
})

View File

@@ -5,6 +5,7 @@ import { redirector } from '../index.js'
import { GithubAuthV3Service } from './github-auth-service.js'
import {
fetchLatestRelease,
filterDocs,
queryParamSchema,
} from './github-common-release.js'
import { documentation } from './github-helpers.js'
@@ -85,6 +86,21 @@ class GithubRelease extends GithubAuthV3Service {
}),
documentation,
},
{
title: 'GitHub release (with filter)',
namedParams: { user: 'RetroMusicPlayer', repo: 'RetroMusicPlayer' },
queryParams: {
sort: 'date',
display_name: 'release',
filter: '*Open Beta',
},
staticPreview: this.render({
version: 'Release v6.0.2 - Open Beta',
sort: 'date',
isPrerelease: false,
}),
documentation: documentation + filterDocs,
},
]
static defaultBadgeData = { label: 'release', namedLogo: 'github' }

View File

@@ -32,7 +32,7 @@ t.create('Release (No releases)')
t.create('Prerelease (No releases)')
.get('/v/release/badges/daily-tests.json?include_prereleases')
.expectBadge({ label: 'release', message: 'no releases' })
.expectBadge({ label: 'release', message: 'no releases found' })
t.create('Release (repo not found)')
.get('/v/release/badges/helmets.json')

View File

@@ -1,11 +1,12 @@
import gql from 'graphql-tag'
import Joi from 'joi'
import { matcher } from 'matcher'
import { addv } from '../text-formatters.js'
import { version as versionColor } from '../color-formatters.js'
import { latest } from '../version.js'
import { NotFound, redirector } from '../index.js'
import { GithubAuthV4Service } from './github-auth-service.js'
import { queryParamSchema } from './github-common-release.js'
import { filterDocs, queryParamSchema } from './github-common-release.js'
import { documentation, transformErrors } from './github-helpers.js'
const schema = Joi.object({
@@ -60,6 +61,16 @@ class GithubTag extends GithubAuthV4Service {
}),
documentation,
},
{
title: 'GitHub tag (with filter)',
namedParams: { user: 'badges', repo: 'shields' },
queryParams: { filter: '!server-*' },
staticPreview: this.render({
version: 'v3.3.1',
sort: 'date',
}),
documentation: documentation + filterDocs,
},
]
static defaultBadgeData = {
@@ -73,8 +84,21 @@ class GithubTag extends GithubAuthV4Service {
}
}
async fetch({ user, repo, sort }) {
const limit = sort === 'semver' ? 100 : 1
static getLimit({ sort, filter }) {
if (!filter && sort === 'date') {
return 1
}
return 100
}
static applyFilter({ tags, filter }) {
if (!filter) {
return tags
}
return matcher(tags, filter)
}
async fetch({ user, repo, limit }) {
return this._requestGraphql({
query: gql`
query ($user: String!, $repo: String!, $limit: Int!) {
@@ -109,11 +133,17 @@ class GithubTag extends GithubAuthV4Service {
async handle({ user, repo }, queryParams) {
const sort = queryParams.sort
const includePrereleases = queryParams.include_prereleases !== undefined
const filter = queryParams.filter
const limit = this.constructor.getLimit({ sort, filter })
const json = await this.fetch({ user, repo, sort })
const tags = json.data.repository.refs.edges.map(edge => edge.node.name)
const json = await this.fetch({ user, repo, limit })
const tags = this.constructor.applyFilter({
tags: json.data.repository.refs.edges.map(edge => edge.node.name),
filter,
})
if (tags.length === 0) {
throw new NotFound({ prettyMessage: 'no tags found' })
const prettyMessage = filter ? 'no matching tags found' : 'no tags found'
throw new NotFound({ prettyMessage })
}
return this.constructor.render({
version: this.constructor.getLatestTag({

View File

@@ -53,4 +53,24 @@ describe('GithubTag', function () {
color: 'blue',
})
})
test(GithubTag.getLimit, () => {
given({ sort: 'date', filter: undefined }).expect(1)
given({ sort: 'date', filter: '' }).expect(1)
given({ sort: 'date', filter: '!*-dev' }).expect(100)
given({ sort: 'semver', filter: undefined }).expect(100)
given({ sort: 'semver', filter: '' }).expect(100)
given({ sort: 'semver', filter: '!*-dev' }).expect(100)
})
test(GithubTag.applyFilter, () => {
const tags = ['v1.1.0', 'v1.2.0', 'server-2022-01-01']
given({ tags, filter: undefined }).expect(tags)
given({ tags, filter: '' }).expect(tags)
given({ tags, filter: '*' }).expect(tags)
given({ tags, filter: '!*' }).expect([])
given({ tags, filter: 'foo' }).expect([])
given({ tags, filter: 'server-*' }).expect(['server-2022-01-01'])
given({ tags, filter: '!server-*' }).expect(['v1.1.0', 'v1.2.0'])
})
})