Skip to content

Commit ffdff3b

Browse files
committed
Add GitHub release by branch badge service
Implements a version badge that shows the latest release for a specified branch using the GitHub releases API target_commitish field. Closes #7929
1 parent 4604d4e commit ffdff3b

3 files changed

Lines changed: 399 additions & 0 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import Joi from 'joi'
2+
import { NotFound, pathParam } from '../index.js'
3+
import { renderVersionBadge } from '../version.js'
4+
import { GithubAuthV3Service } from './github-auth-service.js'
5+
import {
6+
openApiQueryParams,
7+
queryParamSchema,
8+
} from './github-common-release.js'
9+
import { documentation, httpErrorsFor } from './github-helpers.js'
10+
11+
const releaseSchema = Joi.object({
12+
tag_name: Joi.string().required(),
13+
target_commitish: Joi.string().required(),
14+
prerelease: Joi.boolean().required(),
15+
name: Joi.string().allow(null).allow(''),
16+
}).required()
17+
18+
const releaseArraySchema = Joi.alternatives().try(
19+
Joi.array().items(releaseSchema),
20+
Joi.array().length(0),
21+
)
22+
23+
const branchDescription = `
24+
Returns the latest release associated with the specified branch.
25+
Releases are matched using the \`target_commitish\` field from the GitHub API.
26+
`
27+
28+
export default class GithubReleaseBranch extends GithubAuthV3Service {
29+
static category = 'version'
30+
31+
static route = {
32+
base: 'github/v/release',
33+
pattern: ':user/:repo/:branch',
34+
queryParamSchema,
35+
}
36+
37+
static openApi = {
38+
'/github/v/release/{user}/{repo}/{branch}': {
39+
get: {
40+
summary: 'GitHub Release (by branch)',
41+
description: `${documentation}\n${branchDescription}`,
42+
parameters: [
43+
pathParam({ name: 'user', example: 'laravel' }),
44+
pathParam({ name: 'repo', example: 'framework' }),
45+
pathParam({ name: 'branch', example: '13.x' }),
46+
...openApiQueryParams,
47+
],
48+
},
49+
},
50+
}
51+
52+
static defaultBadgeData = { label: 'latest-release' }
53+
54+
findReleaseOnPage({ releases, branch, includePrereleases }) {
55+
for (const release of releases) {
56+
if (release.target_commitish === branch) {
57+
if (!includePrereleases && release.prerelease) {
58+
continue
59+
}
60+
return release
61+
}
62+
}
63+
return undefined
64+
}
65+
66+
transform({ release, branch }) {
67+
const { tag_name: version, prerelease: isPrerelease } = release
68+
return { version, isPrerelease, branch }
69+
}
70+
71+
static render({ version, isPrerelease, branch }) {
72+
return renderVersionBadge({
73+
version,
74+
isPrerelease,
75+
defaultLabel: GithubReleaseBranch.defaultBadgeData.label,
76+
tag: branch,
77+
})
78+
}
79+
80+
async fetchPage({ user, repo, page }) {
81+
return this._requestJson({
82+
url: `/repos/${user}/${repo}/releases`,
83+
schema: releaseArraySchema,
84+
httpErrors: httpErrorsFor('repo not found'),
85+
options: { searchParams: { per_page: 100, page } },
86+
})
87+
}
88+
89+
async fetchLatestReleaseByBranch({ user, repo, branch, includePrereleases }) {
90+
let page = 1
91+
while (true) {
92+
const releases = await this.fetchPage({ user, repo, page })
93+
if (releases.length === 0) {
94+
const prettyMessage =
95+
page === 1 ? 'no releases found' : 'no release found for branch'
96+
throw new NotFound({ prettyMessage })
97+
}
98+
const release = this.findReleaseOnPage({
99+
releases,
100+
branch,
101+
includePrereleases,
102+
})
103+
if (release) {
104+
return release
105+
}
106+
page++
107+
}
108+
}
109+
110+
async handle({ user, repo, branch }, queryParams) {
111+
const includePrereleases = queryParams.include_prereleases !== undefined
112+
const release = await this.fetchLatestReleaseByBranch({
113+
user,
114+
repo,
115+
branch,
116+
includePrereleases,
117+
})
118+
const result = this.transform({ release, branch })
119+
return this.constructor.render(result)
120+
}
121+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { test, given } from 'sazerac'
2+
import GithubReleaseBranch from './github-release-branch.service.js'
3+
4+
describe('GithubReleaseBranch service', function () {
5+
test(GithubReleaseBranch.prototype.findReleaseOnPage, () => {
6+
const releases = [
7+
{ tag_name: 'v2.0.0', target_commitish: 'main', prerelease: false },
8+
{ tag_name: 'v1.0.0', target_commitish: '1.x', prerelease: false },
9+
]
10+
given({ releases, branch: '1.x', includePrereleases: false }).expect(
11+
releases[1],
12+
)
13+
given({ releases, branch: 'main', includePrereleases: false }).expect(
14+
releases[0],
15+
)
16+
given({ releases, branch: '2.x', includePrereleases: false }).expect(
17+
undefined,
18+
)
19+
given(
20+
{
21+
releases: [
22+
{ tag_name: 'v2.0.0-beta', target_commitish: 'main', prerelease: true },
23+
],
24+
branch: 'main',
25+
includePrereleases: false,
26+
},
27+
).expect(undefined)
28+
given(
29+
{
30+
releases: [
31+
{ tag_name: 'v2.0.0-beta', target_commitish: 'main', prerelease: true },
32+
],
33+
branch: 'main',
34+
includePrereleases: true,
35+
},
36+
).expect({
37+
tag_name: 'v2.0.0-beta',
38+
target_commitish: 'main',
39+
prerelease: true,
40+
})
41+
})
42+
43+
test(GithubReleaseBranch.prototype.transform, () => {
44+
given({
45+
release: {
46+
tag_name: 'v9.11.0',
47+
target_commitish: '9.x',
48+
prerelease: false,
49+
},
50+
branch: '9.x',
51+
}).expect({
52+
version: 'v9.11.0',
53+
isPrerelease: false,
54+
branch: '9.x',
55+
})
56+
given({
57+
release: {
58+
tag_name: 'v2.0.0-beta',
59+
target_commitish: 'main',
60+
prerelease: true,
61+
},
62+
branch: 'main',
63+
}).expect({
64+
version: 'v2.0.0-beta',
65+
isPrerelease: true,
66+
branch: 'main',
67+
})
68+
})
69+
70+
test(GithubReleaseBranch.render, () => {
71+
given({
72+
version: 'v9.11.0',
73+
isPrerelease: false,
74+
branch: '9.x',
75+
}).expect({
76+
label: 'latest-release@9.x',
77+
message: 'v9.11.0',
78+
color: 'blue',
79+
})
80+
given({
81+
version: 'v2.0.0-beta',
82+
isPrerelease: true,
83+
branch: 'main',
84+
}).expect({
85+
label: 'latest-release@main',
86+
message: 'v2.0.0-beta',
87+
color: 'orange',
88+
})
89+
})
90+
})

0 commit comments

Comments
 (0)