Skip to content

Commit

Permalink
add support for generating release notes from upstream release (#900)
Browse files Browse the repository at this point in the history
  • Loading branch information
shiftkey committed Aug 11, 2024
1 parent 6d400f0 commit a6c9765
Showing 1 changed file with 190 additions and 26 deletions.
216 changes: 190 additions & 26 deletions script/generate-release-notes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
/* eslint-disable no-sync */

const glob = require('glob')
const { basename } = require('path')
const { basename, dirname, join } = require('path')
const fs = require('fs')

type ChecksumEntry = { filename: string; checksum: string }

type ChecksumGroups = Record<'x64' | 'arm' | 'arm64', Array<ChecksumEntry>>

type ReleaseNotesGroupType = 'new' | 'added' | 'fixed' | 'improved' | 'removed'

type ReleaseNotesGroups = Record<ReleaseNotesGroupType, Array<ReleaseNoteEntry>>

type ReleaseNoteEntry = {
text: string
ids: Array<number>
contributor?: string
}

// 3 architectures * 3 package formats * 2 files (package + checksum file)
const SUCCESSFUL_RELEASE_FILE_COUNT = 3 * 3 * 2

Expand Down Expand Up @@ -74,11 +84,13 @@ const shaEntriesByArchitecture: ChecksumGroups = {
console.log(`Found ${countFiles} files in artifacts directory`)
console.log(shaEntriesByArchitecture)

const releaseNotesByGroup = getReleaseGroups(releaseTagWithoutPrefix)

const draftReleaseNotes = generateDraftReleaseNotes(
[],
releaseNotesByGroup,
shaEntriesByArchitecture
)
const releaseNotesPath = __dirname + '/release_notes.txt'
const releaseNotesPath = join(__dirname, 'release_notes.txt')

fs.writeFileSync(releaseNotesPath, draftReleaseNotes, { encoding: 'utf8' })

Expand All @@ -99,42 +111,194 @@ function getShaContents(filePath: string): {
return { filename, checksum }
}

function formatEntry(e: ChecksumEntry): string {
return `**${e.filename}**\n${e.checksum}\n`
function extractIds(str: string): Array<number> {
const idRegex = /#(\d+)/g

const idArray = new Array<number>()
let match

while ((match = idRegex.exec(str))) {
const textValue = match[1].trim()
const numValue = parseInt(textValue, 10)
if (!isNaN(numValue)) {
idArray.push(numValue)
}
}

return idArray
}

/**
* Takes the release notes entries and the SHA entries, then merges them into the full draft release notes ✨
*/
function generateDraftReleaseNotes(
releaseNotesEntries: Array<string>,
shaEntries: ChecksumGroups
): string {
const changelogText = releaseNotesEntries.join('\n')
function parseCategory(str: string): ReleaseNotesGroupType | null {
const input = str.toLocaleLowerCase()
switch (input) {
case 'added':
case 'fixed':
case 'improved':
case 'new':
case 'removed':
return input
default:
return null
}
}

const x64Section = shaEntries.x64.map(formatEntry).join('\n')
const armSection = shaEntries.arm.map(formatEntry).join('\n')
const arm64Section = shaEntries.arm64.map(formatEntry).join('\n')
function isInitialTag(tag: string): boolean {
return tag.endsWith('-linux1') || tag.endsWith('-test1')
}

const draftReleaseNotes = `${changelogText}
function getVersionWithoutSuffix(tag: string): string {
return tag.replace('-linux1', '').replace('-test1', '')
}

## Fixes and improvements
function getReleaseGroups(version: string): ReleaseNotesGroups {
if (!isInitialTag(version)) {
return {
new: [],
added: [],
fixed: [],
improved: [],
removed: [],
}
}

TODO
const upstreamVersion = getVersionWithoutSuffix(version)
const rootDir = dirname(__dirname)
const changelogFile = fs.readFileSync(join(rootDir, 'changelog.json'))
const changelogJson = JSON.parse(changelogFile)
const releases = changelogJson['releases']
const changelogForVersion: Array<string> | undefined =
releases[upstreamVersion]

if (!changelogForVersion) {
console.error(
`🔴 Changelog version ${upstreamVersion} not found in changelog.json, which is required for publishing a release based off an upstream releease. Aborting...`
)
process.exit(1)
}

## SHA-256 checksums
console.log(`found release notes`, changelogForVersion)

### x64
const releaseNotesByGroup: ReleaseNotesGroups = {
new: [],
added: [],
fixed: [],
improved: [],
removed: [],
}

${x64Section}
const releaseEntryExternalContributor = /\[(.*)\](.*)- (.*)\. Thanks (.*)!/
const releaseEntryRegex = /\[(.*)\](.*)- (.*)/

for (const entry of changelogForVersion) {
const externalMatch = releaseEntryExternalContributor.exec(entry)
if (externalMatch) {
const category = parseCategory(externalMatch[1])
const text = externalMatch[2].trim()
const ids = extractIds(externalMatch[3])
const contributor = externalMatch[4]

if (!category) {
console.warn(`unable to identify category for '${entry}'`)
} else {
releaseNotesByGroup[category].push({
text,
ids,
contributor,
})
}
} else {
const match = releaseEntryRegex.exec(entry)
if (match) {
const category = parseCategory(match[1])
const text = match[2].trim()
const ids = extractIds(match[3])
if (!category) {
console.warn(`unable to identify category for '${entry}'`)
} else {
releaseNotesByGroup[category].push({
text,
ids,
})
}
} else {
console.warn(`release entry does not match any format: '${entry}'`)
}
}
}

### ARM64
return releaseNotesByGroup
}

${arm64Section}
function formatReleaseNote(note: ReleaseNoteEntry): string {
const idsAsUrls = note.ids
.map(id => `https://github.com/desktop/desktop/issues/${id}`)
.join(' ')
const contributorNote = note.contributor
? `. Thanks ${note.contributor}!`
: ''

### ARM
const template = ` - ${note.text} - ${idsAsUrls}${contributorNote}`

${armSection}`
return template.trim()
}

function renderSection(
name: string,
items: Array<ReleaseNoteEntry>,
omitIfEmpty: boolean = true
): string {
if (items.length === 0 && omitIfEmpty) {
return ''
}

const itemsText =
items.length === 0 ? 'TODO' : items.map(formatReleaseNote).join('\n')

return `
## ${name}
${itemsText}
`
}

function formatEntry(e: ChecksumEntry): string {
return `${e.checksum} ${e.filename}`
}

function renderArchitectureIfNotEmpty(
name: string,
items: Array<ChecksumEntry>
): string {
if (items.length === 0) {
return ''
}

const itemsText = items.map(formatEntry).join('\n')

return `
## ${name}
${itemsText}`
}

/**
* Takes the release notes entries and the SHA entries, then merges them into the full draft release notes ✨
*/
function generateDraftReleaseNotes(
releaseNotesGroups: ReleaseNotesGroups,
shaEntries: ChecksumGroups
): string {
const draftReleaseNotes = `
${renderSection('New', releaseNotesGroups.new)}
${renderSection('Added', releaseNotesGroups.added)}
${renderSection('Fixed', releaseNotesGroups.fixed, false)}
${renderSection('Improved', releaseNotesGroups.improved, false)}
${renderSection('Removed', releaseNotesGroups.removed)}
## SHA-256 checksums
${renderArchitectureIfNotEmpty('x64', shaEntries.x64)}
${renderArchitectureIfNotEmpty('ARM64', shaEntries.arm64)}
${renderArchitectureIfNotEmpty('ARM', shaEntries.arm)}`

return draftReleaseNotes
}

0 comments on commit a6c9765

Please sign in to comment.