Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions services/github/github-release-branch.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import Joi from 'joi'
import { NotFound, pathParam, queryParam } from '../index.js'
import { renderVersionBadge } from '../version.js'
import { GithubAuthV3Service } from './github-auth-service.js'
import { documentation, httpErrorsFor } from './github-helpers.js'

const releaseSchema = Joi.object({
tag_name: Joi.string().required(),
target_commitish: Joi.string().required(),
prerelease: Joi.boolean().required(),
name: Joi.string().allow(null).allow(''),
}).required()

const releaseArraySchema = Joi.alternatives().try(
Joi.array().items(releaseSchema),
Joi.array().length(0),
)

const queryParamSchema = Joi.object({
include_prereleases: Joi.equal(''),
}).required()

const branchDescription = `
Returns the latest release associated with the specified branch.
Releases are matched using the \`target_commitish\` field from the GitHub API.
`

export default class GithubReleaseBranch extends GithubAuthV3Service {
static category = 'version'

static route = {
base: 'github/v/release',
pattern: ':user/:repo/:branch',
queryParamSchema,
}

static openApi = {
'/github/v/release/{user}/{repo}/{branch}': {
get: {
summary: 'GitHub Release (by branch)',
description: `${documentation}\n${branchDescription}`,
parameters: [
pathParam({ name: 'user', example: 'laravel' }),
pathParam({ name: 'repo', example: 'framework' }),
pathParam({ name: 'branch', example: '13.x' }),
queryParam({
name: 'include_prereleases',
example: null,
schema: { type: 'boolean' },
}),
],
},
},
}

static defaultBadgeData = { label: 'latest-release' }

findReleaseOnPage({ releases, branch, includePrereleases }) {
for (const release of releases) {
if (release.target_commitish === branch) {
if (!includePrereleases && release.prerelease) {
continue
}
return release
}
}
return undefined
}

static render({ version, isPrerelease, branch }) {
return renderVersionBadge({
version,
isPrerelease,
defaultLabel: GithubReleaseBranch.defaultBadgeData.label,
tag: branch,
})
}
transform({ release, branch }) {
const { tag_name: version, prerelease: isPrerelease } = release
return { version, isPrerelease, branch }
}

async fetchPage({ user, repo, page }) {
return this._requestJson({
url: `/repos/${user}/${repo}/releases`,
schema: releaseArraySchema,
httpErrors: httpErrorsFor('repo not found'),
options: { searchParams: { per_page: 100, page } },
})
}

async fetchLatestReleaseByBranch({ user, repo, branch, includePrereleases }) {
let page = 1
while (true) {
const releases = await this.fetchPage({ user, repo, page })
if (releases.length === 0) {
const prettyMessage =
page === 1 ? 'no releases found' : 'no release found for branch'
throw new NotFound({ prettyMessage })
}
const release = this.findReleaseOnPage({
releases,
branch,
includePrereleases,
})
if (release) {
return release
}
page++
}
}

async handle({ user, repo, branch }, queryParams) {
const includePrereleases = queryParams.include_prereleases !== undefined
const release = await this.fetchLatestReleaseByBranch({
user,
repo,
branch,
includePrereleases,
})
const result = this.transform({ release, branch })
return this.constructor.render(result)
}
}
86 changes: 86 additions & 0 deletions services/github/github-release-branch.service.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { test, given } from 'sazerac'
import GithubReleaseBranch from './github-release-branch.service.js'

describe('GithubReleaseBranch service', function () {
test(GithubReleaseBranch.prototype.findReleaseOnPage, () => {
const releases = [
{ tag_name: 'v2.0.0', target_commitish: 'main', prerelease: false },
{ tag_name: 'v1.0.0', target_commitish: '1.x', prerelease: false },
]
given({ releases, branch: '1.x', includePrereleases: false }).expect(
releases[1],
)
given({ releases, branch: 'main', includePrereleases: false }).expect(
releases[0],
)
given({ releases, branch: '2.x', includePrereleases: false }).expect(
undefined,
)
given({
releases: [
{ tag_name: 'v2.0.0-beta', target_commitish: 'main', prerelease: true },
],
branch: 'main',
includePrereleases: false,
}).expect(undefined)
given({
releases: [
{ tag_name: 'v2.0.0-beta', target_commitish: 'main', prerelease: true },
],
branch: 'main',
includePrereleases: true,
}).expect({
tag_name: 'v2.0.0-beta',
target_commitish: 'main',
prerelease: true,
})
})

test(GithubReleaseBranch.prototype.transform, () => {
given({
release: {
tag_name: 'v9.11.0',
target_commitish: '9.x',
prerelease: false,
},
branch: '9.x',
}).expect({
version: 'v9.11.0',
isPrerelease: false,
branch: '9.x',
})
given({
release: {
tag_name: 'v2.0.0-beta',
target_commitish: 'main',
prerelease: true,
},
branch: 'main',
}).expect({
version: 'v2.0.0-beta',
isPrerelease: true,
branch: 'main',
})
})

test(GithubReleaseBranch.render, () => {
given({
version: 'v9.11.0',
isPrerelease: false,
branch: '9.x',
}).expect({
label: 'latest-release@9.x',
message: 'v9.11.0',
color: 'blue',
})
given({
version: 'v2.0.0-beta',
isPrerelease: true,
branch: 'main',
}).expect({
label: 'latest-release@main',
message: 'v2.0.0-beta',
color: 'orange',
})
})
})
180 changes: 180 additions & 0 deletions services/github/github-release-branch.tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import Joi from 'joi'
import { isSemver } from '../test-validators.js'
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()

t.create('Release by branch (valid)')
.get('/laravel/framework/13.x.json')
.expectBadge({
label: 'latest-release@13.x',
message: isSemver,
color: 'blue',
})

t.create('Release by branch (valid, mocked)')
.get('/user/repo/main.json')
.intercept(nock =>
nock('https://api.github.com')
.get('/repos/user/repo/releases')
.query({ per_page: 100, page: 1 })
.reply(200, [
{
tag_name: 'v2.0.0',
target_commitish: 'main',
prerelease: false,
name: '',
},
]),
)
.expectBadge({
label: 'latest-release@main',
message: 'v2.0.0',
color: 'blue',
})

t.create('Release by branch (prerelease, mocked)')
.get('/user/repo/main.json?include_prereleases')
.intercept(nock =>
nock('https://api.github.com')
.get('/repos/user/repo/releases')
.query({ per_page: 100, page: 1 })
.reply(200, [
{
tag_name: 'v2.0.0-beta',
target_commitish: 'main',
prerelease: true,
name: '',
},
]),
)
.expectBadge({
label: 'latest-release@main',
message: 'v2.0.0-beta',
color: 'orange',
})

t.create('Release by branch (paginated, mocked)')
.get('/user/repo/1.x.json')
.intercept(nock =>
nock('https://api.github.com')
.get('/repos/user/repo/releases')
.query({ per_page: 100, page: 1 })
.reply(200, [
{
tag_name: 'v2.0.0',
target_commitish: 'main',
prerelease: false,
name: '',
},
])
.get('/repos/user/repo/releases')
.query({ per_page: 100, page: 2 })
.reply(200, [
{
tag_name: 'v1.0.0',
target_commitish: '1.x',
prerelease: false,
name: '',
},
]),
)
.expectBadge({
label: 'latest-release@1.x',
message: 'v1.0.0',
color: 'blue',
})

t.create('Release by branch (no matching branch, mocked)')
.get('/user/repo/2.x.json')
.intercept(nock =>
nock('https://api.github.com')
.get('/repos/user/repo/releases')
.query({ per_page: 100, page: 1 })
.reply(200, [
{
tag_name: 'v1.0.0',
target_commitish: '1.x',
prerelease: false,
name: '',
},
])
.get('/repos/user/repo/releases')
.query({ per_page: 100, page: 2 })
.reply(200, []),
)
.expectBadge({
label: 'latest-release',
message: 'no release found for branch',
})

t.create('Release by branch (repo not found)')
.get('/badges/helmets/1.x.json')
.expectBadge({ label: 'latest-release', message: 'repo not found' })

t.create('Release by branch (no releases, mocked)')
.get('/user/repo/main.json')
.intercept(nock =>
nock('https://api.github.com')
.get('/repos/user/repo/releases')
.query({ per_page: 100, page: 1 })
.reply(200, []),
)
.expectBadge({ label: 'latest-release', message: 'no releases found' })

t.create('Release by branch (prerelease excluded, mocked)')
.get('/user/repo/main.json')
.intercept(nock =>
nock('https://api.github.com')
.get('/repos/user/repo/releases')
.query({ per_page: 100, page: 1 })
.reply(200, [
{
tag_name: 'v2.0.0-beta',
target_commitish: 'main',
prerelease: true,
name: '',
},
])
.get('/repos/user/repo/releases')
.query({ per_page: 100, page: 2 })
.reply(200, []),
)
.expectBadge({
label: 'latest-release',
message: 'no release found for branch',
})

t.create('Release by branch (stable after prerelease, mocked)')
.get('/user/repo/main.json')
.intercept(nock =>
nock('https://api.github.com')
.get('/repos/user/repo/releases')
.query({ per_page: 100, page: 1 })
.reply(200, [
{
tag_name: 'v2.0.0-beta',
target_commitish: 'main',
prerelease: true,
name: '',
},
{
tag_name: 'v2.0.0',
target_commitish: 'main',
prerelease: false,
name: '',
},
]),
)
.expectBadge({
label: 'latest-release@main',
message: 'v2.0.0',
color: 'blue',
})

t.create('Release by branch (prerelease color)')
.get('/laravel/framework/13.x.json?include_prereleases')
.expectBadge({
label: 'latest-release@13.x',
message: isSemver,
color: Joi.equal('blue', 'orange').required(),
})
Loading