github-desktop/script/draft-release/run.ts

265 lines
7.5 KiB
TypeScript
Raw Normal View History

2018-02-04 11:18:09 +00:00
import { sort as semverSort, SemVer } from 'semver'
import { getLogLines } from '../changelog/git'
2018-02-04 04:16:32 +00:00
import {
convertToChangelogFormat,
getChangelogEntriesSince,
} from '../changelog/parser'
2018-02-04 11:18:09 +00:00
import { Channel } from './channel'
import { getNextVersionNumber } from './version'
import { execSync } from 'child_process'
2020-01-30 14:34:14 +00:00
import { writeFileSync } from 'fs'
2020-01-30 14:39:21 +00:00
import { join } from 'path'
2020-01-31 03:13:57 +00:00
import { format } from 'prettier'
2022-05-04 07:27:05 +00:00
import { assertNever, forceUnwrap } from '../../app/src/lib/fatal-error'
import { sh } from '../sh'
import { readFile } from 'fs/promises'
2020-01-30 14:39:21 +00:00
const changelogPath = join(__dirname, '..', '..', 'changelog.json')
2020-02-04 22:33:46 +00:00
/**
* Returns the latest release tag, according to git and semver
* (ignores test releases)
*
* @param options there's only one option `excludeBetaReleases`,
* which is a boolean
*/
2018-02-09 04:04:30 +00:00
async function getLatestRelease(options: {
excludeBetaReleases: boolean
excludeTestReleases: boolean
2018-02-09 04:04:30 +00:00
}): Promise<string> {
let releaseTags = (await sh('git', 'tag'))
.split('\n')
2022-03-02 08:42:10 +00:00
.filter(tag => tag.startsWith('release-'))
.filter(tag => !tag.includes('-linux'))
2018-02-09 04:04:30 +00:00
if (options.excludeBetaReleases) {
2020-01-30 14:34:14 +00:00
releaseTags = releaseTags.filter(tag => !tag.includes('-beta'))
}
if (options.excludeTestReleases) {
releaseTags = releaseTags.filter(tag => !tag.includes('-test'))
}
const releaseVersions = releaseTags.map(tag => tag.substring(8))
const sortedTags = semverSort(releaseVersions)
2022-05-04 07:27:05 +00:00
const latestTag = forceUnwrap(`No tags`, sortedTags.at(-1))
2018-02-09 03:55:45 +00:00
return latestTag instanceof SemVer ? latestTag.raw : latestTag
}
async function createReleaseBranch(version: string): Promise<void> {
try {
const versionBranch = `releases/${version}`
const currentBranch = (
await sh('git', 'rev-parse', '--abbrev-ref', 'HEAD')
).trim()
if (currentBranch !== versionBranch) {
await sh('git', 'checkout', '-b', versionBranch)
}
} catch (error) {
console.log(`Failed to create release branch: ${error}`)
}
}
2020-02-04 22:33:46 +00:00
/** Converts a string to Channel type if possible */
2018-02-04 11:17:04 +00:00
function parseChannel(arg: string): Channel {
2018-02-09 04:04:30 +00:00
if (arg === 'production' || arg === 'beta' || arg === 'test') {
2018-02-04 01:15:29 +00:00
return arg
}
throw new Error(`An invalid channel ${arg} has been provided`)
}
2020-02-04 22:33:46 +00:00
/**
* Prints out next steps to the console
*
* @param nextVersion version for the next release
* @param entries release notes for the next release
*/
2018-02-04 04:16:32 +00:00
function printInstructions(nextVersion: string, entries: Array<string>) {
2020-01-31 03:13:57 +00:00
const baseSteps = [
2020-02-19 14:21:19 +00:00
'Revise the release notes according to https://github.com/desktop/desktop/blob/development/docs/process/writing-release-notes.md',
2020-02-19 14:30:43 +00:00
'Lint them with: yarn draft-release:format',
2020-02-19 14:21:19 +00:00
'Commit these changes (on a "release" branch) and push them to GitHub',
'See the deploy repo for details on performing the release: https://github.com/desktop/deploy',
2018-02-09 04:47:47 +00:00
]
2020-02-19 14:30:50 +00:00
// if an empty list, we assume the new entries have already been
// written to the changelog file
2020-01-31 03:13:57 +00:00
if (entries.length === 0) {
printSteps(baseSteps)
} else {
2020-03-02 17:18:49 +00:00
const object = { [nextVersion]: entries.sort() }
2020-02-19 14:30:43 +00:00
const steps = [
2020-01-31 03:13:57 +00:00
`Concatenate this to the beginning of the 'releases' element in the changelog.json as a starting point:\n${format(
JSON.stringify(object),
2020-02-19 14:30:43 +00:00
{
parser: 'json',
}
2020-01-31 03:13:57 +00:00
)}\n`,
...baseSteps,
2020-02-19 14:30:43 +00:00
]
printSteps(steps)
2020-01-31 03:13:57 +00:00
}
}
2018-02-09 04:47:47 +00:00
2020-02-19 14:30:50 +00:00
/**
2020-11-18 17:42:20 +00:00
* adds a number to the beginning of each line and prints them in sequence
2020-02-19 14:30:50 +00:00
*/
2020-01-31 03:13:57 +00:00
function printSteps(steps: ReadonlyArray<string>) {
2020-02-19 14:51:18 +00:00
console.log("Here's what you should do next:\n")
2018-02-09 04:47:47 +00:00
console.log(steps.map((value, index) => `${index + 1}. ${value}`).join('\n'))
2018-02-04 03:51:21 +00:00
}
export async function run(args: ReadonlyArray<string>): Promise<void> {
if (args.length === 0) {
throw new Error(
`You have not specified a channel to draft this release for. Choose one of 'production' or 'beta'`
)
}
2018-02-04 01:15:29 +00:00
const channel = parseChannel(args[0])
const draftPretext = args[1] === '--pretext'
const previousVersion = await getLatestRelease({
excludeBetaReleases: channel === 'production' || channel === 'test',
excludeTestReleases: channel === 'production' || channel === 'beta',
})
const nextVersion = getNextVersionNumber(previousVersion, channel)
console.log(`Creating release branch for "${nextVersion}"...`)
createReleaseBranch(nextVersion)
console.log(`Done!`)
2020-01-31 03:13:57 +00:00
console.log(`Setting app version to "${nextVersion}" in app/package.json...`)
2020-02-08 00:36:00 +00:00
try {
// this can throw
2020-02-19 14:30:50 +00:00
// sets the npm version in app/
2020-02-08 00:36:00 +00:00
execSync(`npm version ${nextVersion} --allow-same-version`, {
cwd: join(__dirname, '..', '..', 'app'),
encoding: 'utf8',
})
console.log(`Set!`)
} catch (e) {
console.warn(`Setting the app version failed 😿
2022-01-26 14:02:20 +00:00
(${e instanceof Error ? e.message : e})
2020-02-08 00:36:00 +00:00
Please manually set it to ${nextVersion} in app/package.json.`)
}
2020-01-31 03:13:57 +00:00
2020-03-25 23:36:07 +00:00
console.log('Determining changelog entries...')
2020-03-02 17:18:49 +00:00
const currentChangelog: IChangelog = require(changelogPath)
const newEntries = new Array<string>()
if (draftPretext) {
const pretext = await getPretext()
if (pretext !== null) {
newEntries.push(pretext)
}
}
switch (channel) {
case 'production': {
// if it's a new production release, make sure we only include
// entries since the latest production release
newEntries.push(...getChangelogEntriesSince(previousVersion))
break
}
case 'beta': {
const logLines = await getLogLines(`release-${previousVersion}`)
const changelogLines = await convertToChangelogFormat(logLines)
newEntries.push(...changelogLines)
break
}
case 'test': {
// we don't guess at release notes for test releases
break
}
default: {
assertNever(channel, 'missing channel type')
}
}
if (newEntries.length === 0 && channel !== 'test') {
console.warn(
'No new changes found to add to the changelog. 🤔 Continuing...'
)
} else {
console.log('Determined!')
}
2020-01-31 03:13:57 +00:00
if (currentChangelog.releases[nextVersion] === undefined) {
console.log('Adding draft release notes to changelog.json...')
const changelog = makeNewChangelog(
nextVersion,
2020-02-19 14:41:35 +00:00
currentChangelog,
2020-01-31 03:13:57 +00:00
newEntries
)
2020-02-08 00:36:00 +00:00
try {
// this might throw
writeFileSync(
changelogPath,
format(JSON.stringify(changelog), {
parser: 'json',
})
)
2020-03-25 23:36:07 +00:00
console.log('Added!')
2020-02-08 00:36:00 +00:00
printInstructions(nextVersion, [])
} catch (e) {
2022-01-26 14:02:20 +00:00
console.warn(
`Writing the changelog failed 😿\n(${
e instanceof Error ? e.message : e
})`
)
2020-02-08 00:36:00 +00:00
printInstructions(nextVersion, newEntries)
}
} else {
2020-01-30 14:34:14 +00:00
console.log(
2020-01-31 03:13:57 +00:00
`Looks like there are already release notes for ${nextVersion} in changelog.json.`
2020-01-30 14:34:14 +00:00
)
2020-01-31 03:13:57 +00:00
printInstructions(nextVersion, newEntries)
}
}
2020-02-19 14:30:50 +00:00
/**
* Returns the current changelog with new entries added.
* Ensures that the new entry will appear at the beginning
* of the object when printed.
*/
2020-01-31 03:13:57 +00:00
function makeNewChangelog(
nextVersion: string,
2020-03-02 17:18:49 +00:00
currentChangelog: IChangelog,
2020-01-31 03:13:57 +00:00
entries: ReadonlyArray<string>
2020-03-02 17:18:49 +00:00
): IChangelog {
2020-02-19 14:41:35 +00:00
return {
2020-03-02 17:18:49 +00:00
releases: { [nextVersion]: entries, ...currentChangelog.releases },
2020-02-19 14:41:35 +00:00
}
}
2020-03-02 17:18:49 +00:00
type ChangelogReleases = { [key: string]: ReadonlyArray<string> }
interface IChangelog {
releases: ChangelogReleases
}
async function getPretext(): Promise<string | null> {
const pretextPath = join(
__dirname,
'..',
'..',
'app',
'static',
'common',
'pretext-draft.md'
)
const pretext = await readFile(pretextPath, 'utf8')
if (pretext.trim() === '') {
return null
}
return `[Pretext] ${pretext}`
}