From 51bf5d5726c77880c2740d7373e75bad4f734eaf Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 10 Jul 2023 11:05:10 -0300 Subject: [PATCH] add support for generating release notes from upstream release (#900) --- script/generate-release-notes.ts | 209 +++++++++++++++++++++++++++---- 1 file changed, 184 insertions(+), 25 deletions(-) diff --git a/script/generate-release-notes.ts b/script/generate-release-notes.ts index b52d4f622a4..a299e3f860a 100644 --- a/script/generate-release-notes.ts +++ b/script/generate-release-notes.ts @@ -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> +type ReleaseNotesGroupType = 'new' | 'added' | 'fixed' | 'improved' | 'removed' + +type ReleaseNotesGroups = Record> + +type ReleaseNoteEntry = { + text: string + ids: Array + contributor?: string +} + // 3 architectures * 3 package formats * 2 files (package + checksum file) const SUCCESSFUL_RELEASE_FILE_COUNT = 3 * 3 * 2 @@ -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' }) @@ -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 { + const idRegex = /#(\d+)/g + + const idArray = new Array() + 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, - 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 | 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, + 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 +): 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 }