chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const API_VERSION = '2022-11-28'
|
||||
const MAX_RELEASE_BODY_LENGTH = 120_000
|
||||
const TRUNCATION_NOTICE =
|
||||
'\n\n---\nRelease notes were truncated because GitHub release bodies are limited to 125,000 characters.'
|
||||
const DESKTOP_RELEASE_TAG_PATTERN = /^v(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/
|
||||
|
||||
export function parseDesktopReleaseTag(tag) {
|
||||
const match = DESKTOP_RELEASE_TAG_PATTERN.exec(tag)
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
tag,
|
||||
major: Number(match[1]),
|
||||
minor: Number(match[2]),
|
||||
patch: Number(match[3]),
|
||||
rc: match[4] === undefined ? null : Number(match[4])
|
||||
}
|
||||
}
|
||||
|
||||
function compareDesktopReleaseTags(a, b) {
|
||||
const versionDiff = a.major - b.major || a.minor - b.minor || a.patch - b.patch
|
||||
if (versionDiff !== 0) {
|
||||
return versionDiff
|
||||
}
|
||||
if (a.rc === b.rc) {
|
||||
return 0
|
||||
}
|
||||
if (a.rc === null) {
|
||||
return 1
|
||||
}
|
||||
if (b.rc === null) {
|
||||
return -1
|
||||
}
|
||||
return a.rc - b.rc
|
||||
}
|
||||
|
||||
export function latestPreviousPublishedDesktopReleaseTag(releases, tag) {
|
||||
const current = parseDesktopReleaseTag(tag)
|
||||
if (!current) {
|
||||
return ''
|
||||
}
|
||||
const previousReleases = releases
|
||||
.filter((release) => release?.draft === false && typeof release.tag_name === 'string')
|
||||
.map((release) => parseDesktopReleaseTag(release.tag_name))
|
||||
.filter((candidate) => candidate && candidate.tag !== current.tag)
|
||||
.filter((candidate) => compareDesktopReleaseTags(candidate, current) < 0)
|
||||
// Why: public changelogs should be bounded by releases users could see;
|
||||
// stable releases summarize since the prior stable, not the latest RC.
|
||||
.filter((candidate) => current.rc !== null || candidate.rc === null)
|
||||
.sort(compareDesktopReleaseTags)
|
||||
return previousReleases.at(-1)?.tag ?? ''
|
||||
}
|
||||
|
||||
function githubHeaders(token) {
|
||||
return {
|
||||
Accept: 'application/vnd.github+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'X-GitHub-Api-Version': API_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
async function githubJson(fetchImpl, url, token, options = {}) {
|
||||
const res = await fetchImpl(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...githubHeaders(token),
|
||||
...options.headers
|
||||
}
|
||||
})
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => '')
|
||||
throw new Error(`GitHub request failed ${res.status} ${res.statusText}: ${body.slice(0, 300)}`)
|
||||
}
|
||||
return res.json()
|
||||
}
|
||||
|
||||
async function fetchRepoReleases(repo, token, fetchImpl) {
|
||||
const releases = []
|
||||
for (let page = 1; ; page += 1) {
|
||||
const pageReleases = await githubJson(
|
||||
fetchImpl,
|
||||
`https://api.github.com/repos/${repo}/releases?per_page=100&page=${page}`,
|
||||
token
|
||||
)
|
||||
if (!Array.isArray(pageReleases)) {
|
||||
throw new Error(`GitHub releases response page ${page} for ${repo} was not an array`)
|
||||
}
|
||||
releases.push(...pageReleases)
|
||||
if (pageReleases.length < 100) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return releases
|
||||
}
|
||||
|
||||
export function truncateReleaseBody(body, maxLength = MAX_RELEASE_BODY_LENGTH) {
|
||||
if (body.length <= maxLength) {
|
||||
return body
|
||||
}
|
||||
|
||||
const availableLength = maxLength - TRUNCATION_NOTICE.length
|
||||
if (availableLength <= 0) {
|
||||
throw new Error('Release truncation notice is longer than the maximum release body length')
|
||||
}
|
||||
|
||||
return `${body.slice(0, availableLength).trimEnd()}${TRUNCATION_NOTICE}`
|
||||
}
|
||||
|
||||
export async function createDraftRelease({
|
||||
repo,
|
||||
tag,
|
||||
token,
|
||||
fetchImpl = fetch,
|
||||
log = console.log
|
||||
}) {
|
||||
if (!repo) {
|
||||
throw new Error('repo is required')
|
||||
}
|
||||
if (!tag) {
|
||||
throw new Error('tag is required')
|
||||
}
|
||||
if (!token) {
|
||||
throw new Error('token is required')
|
||||
}
|
||||
|
||||
const previousTag = latestPreviousPublishedDesktopReleaseTag(
|
||||
await fetchRepoReleases(repo, token, fetchImpl),
|
||||
tag
|
||||
)
|
||||
const generateNotesBody = {
|
||||
tag_name: tag,
|
||||
target_commitish: tag,
|
||||
...(previousTag ? { previous_tag_name: previousTag } : {})
|
||||
}
|
||||
|
||||
// Why: GitHub's generate-notes baseline ignores draft releases, so pass the
|
||||
// previous public changelog boundary explicitly.
|
||||
const releaseNotes = await githubJson(
|
||||
fetchImpl,
|
||||
`https://api.github.com/repos/${repo}/releases/generate-notes`,
|
||||
token,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(generateNotesBody)
|
||||
}
|
||||
)
|
||||
|
||||
const generatedBody = typeof releaseNotes.body === 'string' ? releaseNotes.body : ''
|
||||
const body = truncateReleaseBody(generatedBody)
|
||||
const name =
|
||||
typeof releaseNotes.name === 'string' && releaseNotes.name.length > 0 ? releaseNotes.name : tag
|
||||
const prerelease = tag.includes('-rc.')
|
||||
|
||||
// Why: GitHub's generated release notes can exceed the release body API
|
||||
// limit, so create with a bounded body. Omit target_commitish because the
|
||||
// release-cut tag already exists and GitHub rejects the tag name there.
|
||||
await githubJson(fetchImpl, `https://api.github.com/repos/${repo}/releases`, token, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
tag_name: tag,
|
||||
name,
|
||||
body,
|
||||
draft: true,
|
||||
prerelease
|
||||
})
|
||||
})
|
||||
|
||||
if (generatedBody.length !== body.length) {
|
||||
log(`Created draft release ${tag} with truncated generated notes (${body.length} chars).`)
|
||||
} else {
|
||||
log(`Created draft release ${tag} with generated notes (${body.length} chars).`)
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const tag = process.argv[2]
|
||||
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN
|
||||
const repo = process.env.GITHUB_REPOSITORY || 'stablyai/orca'
|
||||
await createDraftRelease({ repo, tag, token })
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
main().catch((error) => {
|
||||
console.error(error.message)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user