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
121 changes: 121 additions & 0 deletions services/github/github-release-branch.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import Joi from 'joi'
import { NotFound, pathParam } from '../index.js'
import { renderVersionBadge } from '../version.js'
import { GithubAuthV3Service } from './github-auth-service.js'
import {
openApiQueryParams,
queryParamSchema,
} from './github-common-release.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 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' }),
...openApiQueryParams,
],
},
},
}

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
}

transform({ release, branch }) {
const { tag_name: version, prerelease: isPrerelease } = release
return { version, isPrerelease, branch }
}

static render({ version, isPrerelease, branch }) {

Check failure on line 71 in services/github/github-release-branch.service.js

View workflow job for this annotation

GitHub Actions / test-lint

Expected static method render to come before method transform
return renderVersionBadge({
version,
isPrerelease,
defaultLabel: GithubReleaseBranch.defaultBadgeData.label,
tag: 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)
}
}
90 changes: 90 additions & 0 deletions services/github/github-release-branch.service.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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(

Check failure on line 19 in services/github/github-release-branch.service.spec.js

View workflow job for this annotation

GitHub Actions / test-lint

Delete `⏎······`
{
releases: [

Check failure on line 21 in services/github/github-release-branch.service.spec.js

View workflow job for this annotation

GitHub Actions / test-lint

Delete `··`
{ tag_name: 'v2.0.0-beta', target_commitish: 'main', prerelease: true },

Check failure on line 22 in services/github/github-release-branch.service.spec.js

View workflow job for this annotation

GitHub Actions / test-lint

Delete `··`
],

Check failure on line 23 in services/github/github-release-branch.service.spec.js

View workflow job for this annotation

GitHub Actions / test-lint

Delete `··`
branch: 'main',

Check failure on line 24 in services/github/github-release-branch.service.spec.js

View workflow job for this annotation

GitHub Actions / test-lint

Delete `··`
includePrereleases: false,

Check failure on line 25 in services/github/github-release-branch.service.spec.js

View workflow job for this annotation

GitHub Actions / test-lint

Delete `··`
},

Check failure on line 26 in services/github/github-release-branch.service.spec.js

View workflow job for this annotation

GitHub Actions / test-lint

Replace `··},⏎····` with `}`
).expect(undefined)
given(

Check failure on line 28 in services/github/github-release-branch.service.spec.js

View workflow job for this annotation

GitHub Actions / test-lint

Delete `⏎······`
{
releases: [

Check failure on line 30 in services/github/github-release-branch.service.spec.js

View workflow job for this annotation

GitHub Actions / test-lint

Delete `··`
{ 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