Something went wrong. Try again.
Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
Something went wrong. Try again.
8.8 kB · 293 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294const VERSION_COMPONENT = '(?:0|[1-9][0-9]*)'const VERSION_PATTERN = new RegExp( `^${VERSION_COMPONENT}\\.${VERSION_COMPONENT}\\.${VERSION_COMPONENT}$`,)const RELEASE_FILENAME_PATTERN = new RegExp( `^RELEASE-(${VERSION_COMPONENT}\\.${VERSION_COMPONENT}\\.${VERSION_COMPONENT})\\.md$`,)const SOURCE_SHA_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/const POSITIVE_INTEGER_PATTERN = /^[1-9][0-9]*$/
const FRONTMATTER_KEYS = [ 'releaseVersion', 'sourceTag', 'sourceSha', 'iosBuildNumber', 'androidVersionCode',]
const PUBLIC_CHANGELOG_START = '<!-- public-changelog:start -->'const PUBLIC_CHANGELOG_END = '<!-- public-changelog:end -->'
export class ReleaseError extends Error { constructor(message) { super(message) this.name = 'ReleaseError' }}
function fail(message) { throw new ReleaseError(message)}
function normalizeLineEndings(markdown) { return markdown.replace(/\r\n?/g, '\n')}
export function assertReleaseVersion(version) { if (typeof version !== 'string' || !VERSION_PATTERN.test(version)) { fail(`Release version must use strict x.y.z format; found '${version}'.`) } return version}
export function deriveReleaseIdentity(version) { assertReleaseVersion(version) return Object.freeze({ version, branch: `release-${version}`, tag: version, filename: `RELEASE-${version}.md`, githubReleaseName: `Release ${version}`, })}
function parseFrontmatter(markdown) { const normalized = normalizeLineEndings(markdown) if (!normalized.startsWith('---\n')) { fail('Release file must start with YAML frontmatter.') }
const end = normalized.indexOf('\n---\n', 4) if (end === -1) fail('Release file frontmatter is not closed.')
const metadata = {} const lines = normalized.slice(4, end).split('\n') for (const line of lines) { if (!line.trim()) continue const match = /^([A-Za-z][A-Za-z0-9]*):[ \t]*(.*)$/.exec(line) if (!match) fail(`Unsupported frontmatter line: '${line}'.`)
const [, key, rawValue] = match if (!FRONTMATTER_KEYS.includes(key)) { fail(`Unknown release frontmatter field '${key}'.`) } if (Object.hasOwn(metadata, key)) { fail(`Duplicate release frontmatter field '${key}'.`) } if (!rawValue) fail(`Release frontmatter field '${key}' cannot be empty.`) metadata[key] = rawValue }
const body = normalized.slice(end + 5) if (!body.startsWith('\n')) { fail('Release file must contain a blank line after frontmatter.') }
return {metadata, body: body.slice(1)}}
function findDelimitedSection(body, startMarker, endMarker) { const start = body.indexOf(startMarker) const end = body.indexOf(endMarker) if (start === -1 || end === -1) { fail('Release file must contain the public changelog delimiters.') } if ( start !== body.lastIndexOf(startMarker) || end !== body.lastIndexOf(endMarker) ) { fail('Public changelog delimiters must occur exactly once.') } if (end < start) fail('Public changelog end delimiter precedes its start delimiter.')
return { before: body.slice(0, start + startMarker.length), content: body.slice(start + startMarker.length, end).trim(), after: body.slice(end), }}
function parseChangelogSections(publicChangelog) { const unsupportedHeading = publicChangelog.match( /^## (?!Initial release\s*$|OTA [1-9][0-9]*\s*$).+$/m, ) if (unsupportedHeading) { fail(`Unsupported public changelog section '${unsupportedHeading[0]}'.`) }
const headings = [ ...publicChangelog.matchAll( /^## (Initial release|OTA ([1-9][0-9]*))\s*$/gm, ), ] if (!headings.length || headings[0][1] !== 'Initial release') { fail("Public changelog must begin with an '## Initial release' section.") }
const prefix = publicChangelog.slice(0, headings[0].index).trim() if (prefix) fail( 'Public changelog cannot contain content before its initial release section.', )
const sections = headings.map((heading, index) => { const start = heading.index + heading[0].length const end = headings[index + 1]?.index ?? publicChangelog.length const content = publicChangelog.slice(start, end).trim() if (!content) fail(`Changelog section '${heading[1]}' cannot be empty.`) return { type: heading[1] === 'Initial release' ? 'initial' : 'ota', sequence: heading[2] ? Number(heading[2]) : null, content, } })
sections.slice(1).forEach((section, index) => { const expected = index + 1 if (section.type !== 'ota' || section.sequence !== expected) { fail( `OTA changelog sections must be contiguous; expected OTA ${expected}.`, ) } })
return sections}
function validateMetadata(metadata, stage) { assertReleaseVersion(metadata.releaseVersion)
if (metadata.sourceTag !== undefined) { assertReleaseVersion(metadata.sourceTag) if (metadata.sourceTag !== metadata.releaseVersion) { fail('sourceTag must match releaseVersion.') } } if ( metadata.sourceSha !== undefined && !SOURCE_SHA_PATTERN.test(metadata.sourceSha) ) { fail('sourceSha must be a full lowercase hexadecimal Git object ID.') } for (const key of ['iosBuildNumber', 'androidVersionCode']) { if ( metadata[key] !== undefined && !POSITIVE_INTEGER_PATTERN.test(metadata[key]) ) { fail(`${key} must be a positive integer.`) } }
if (stage === 'final') { for (const key of FRONTMATTER_KEYS) { if (metadata[key] === undefined) { fail(`Finalized release file is missing '${key}'.`) } } }}
export function parseReleaseDocument(markdown, options = {}) { if (typeof markdown !== 'string') fail('Release document must be a string.') const stage = options.stage ?? 'prepared' if (!['prepared', 'final'].includes(stage)) { fail(`Unknown release validation stage '${stage}'.`) }
const {metadata, body} = parseFrontmatter(markdown) validateMetadata(metadata, stage)
const expectedTitle = `# Release ${metadata.releaseVersion}` if (!body.startsWith(`${expectedTitle}\n`)) { fail( `Release document must begin with '${expectedTitle}' after frontmatter.`, ) }
if (options.filename !== undefined) { const filenameMatch = RELEASE_FILENAME_PATTERN.exec(options.filename) if (!filenameMatch || filenameMatch[1] !== metadata.releaseVersion) { fail(`Filename must be 'RELEASE-${metadata.releaseVersion}.md'.`) } }
const delimited = findDelimitedSection( body, PUBLIC_CHANGELOG_START, PUBLIC_CHANGELOG_END, ) const sections = parseChangelogSections(delimited.content)
return Object.freeze({ metadata: Object.freeze({...metadata}), publicChangelog: delimited.content, sections: Object.freeze(sections.map(section => Object.freeze(section))), })}
export function createReleaseDocument(version, initialChangelog) { assertReleaseVersion(version) const content = initialChangelog?.trim() if (!content) fail('Initial release changelog cannot be empty.')
return `---releaseVersion: ${version}---
# Release ${version}
${PUBLIC_CHANGELOG_START}
## Initial release
${content}
${PUBLIC_CHANGELOG_END}`}
export function extractPublicChangelog(markdown) { return parseReleaseDocument(markdown).publicChangelog}
export function appendOtaChangelog(markdown, sequence, changelog) { const normalized = normalizeLineEndings(markdown) const parsed = parseReleaseDocument(normalized) const expectedSequence = parsed.sections.length if (!Number.isSafeInteger(sequence) || sequence !== expectedSequence) { fail(`Next OTA sequence must be ${expectedSequence}; found '${sequence}'.`) } const content = changelog?.trim() if (!content) fail('OTA changelog cannot be empty.')
const delimiter = `\n\n${PUBLIC_CHANGELOG_END}` if (!normalized.includes(delimiter)) { fail('Could not find the public changelog end delimiter to append the OTA.') } const replacement = `\n\n## OTA ${sequence}\n\n${content}${delimiter}` const updated = normalized.replace(delimiter, replacement) parseReleaseDocument(updated) return updated}
export function finalizeReleaseDocument(markdown, finalMetadata) { const normalized = normalizeLineEndings(markdown) const parsed = parseReleaseDocument(normalized) const metadata = {...parsed.metadata, ...finalMetadata} validateMetadata(metadata, 'final')
const frontmatter = FRONTMATTER_KEYS.map( key => `${key}: ${metadata[key]}`, ).join('\n') const frontmatterPattern = /^---\n[\s\S]*?\n---\n/ if (!frontmatterPattern.test(normalized)) fail('Could not replace the release frontmatter.') const updated = normalized.replace( frontmatterPattern, `---\n${frontmatter}\n---\n`, ) parseReleaseDocument(updated, {stage: 'final'}) return updated}