Skip to content
Merged
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
209 changes: 184 additions & 25 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,189 @@ 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
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
}
}

function getReleaseGroups(version: string): ReleaseNotesGroups {
if (!version.endsWith('-linux1')) {
return {
new: [],
added: [],
fixed: [],
improved: [],
removed: [],
}
}

const upstreamVersion = version.replace('-linux1', '')
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)
}

console.log(`found release notes`, changelogForVersion)

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

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}'`)
}
}
}

return releaseNotesByGroup
}

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}!`
: ''

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

return template.trim()
}

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

const x64Section = shaEntries.x64.map(formatEntry).join('\n')
const armSection = shaEntries.arm.map(formatEntry).join('\n')
const arm64Section = shaEntries.arm64.map(formatEntry).join('\n')
const itemsText =
items.length === 0 ? 'TODO' : items.map(formatReleaseNote).join('\n')

const draftReleaseNotes = `${changelogText}
return `
## ${name}

## Fixes and improvements
${itemsText}
`
}

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

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

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

${x64Section}
return `

## ${name}

### ARM64
${itemsText}
`
}

${arm64Section}
/**
* 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)}

### ARM
## SHA-256 checksums

${armSection}`
${renderArchitectureIfNotEmpty('x64', shaEntries.x64)}
${renderArchitectureIfNotEmpty('ARM64', shaEntries.arm64)}
${renderArchitectureIfNotEmpty('ARM', shaEntries.arm)}`

return draftReleaseNotes
}