chore: backport release script updates to 2-0-x (#14191)

* chore: alter release scripts to enable sudowoodo

* add example .env file

* chore: only prepare release if last commit not a bump (#14193)
This commit is contained in:
Shelley Vohr
2018-08-18 19:39:06 -07:00
committed by Samuel Attard
parent f3bd8f6133
commit 74d90fbb33
12 changed files with 934 additions and 85 deletions

7
.env.example Normal file
View File

@@ -0,0 +1,7 @@
# These env vars are only necessary for creating Electron releases.
# See docs/development/releasing.md
APPVEYOR_TOKEN=
CIRCLE_TOKEN=
ELECTRON_GITHUB_TOKEN=
VSTS_TOKEN=

View File

@@ -5,12 +5,12 @@ import re
import sys
import argparse
from lib.util import execute, get_electron_version, parse_version, scoped_cwd
from lib.util import execute, get_electron_version, parse_version, scoped_cwd, \
is_nightly, is_beta, is_stable, get_next_nightly, get_next_beta, \
get_next_stable_from_pre, get_next_stable_from_stable, clean_parse_version
SOURCE_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
def main():
parser = argparse.ArgumentParser(
@@ -34,14 +34,7 @@ def main():
action='store',
default=None,
dest='bump',
help='increment [major | minor | patch | beta]'
)
parser.add_argument(
'--stable',
action='store_true',
default= False,
dest='stable',
help='promote to stable (i.e. remove `-beta.x` suffix)'
help='increment [stable | beta | nightly]'
)
parser.add_argument(
'--dry-run',
@@ -52,36 +45,56 @@ def main():
)
args = parser.parse_args()
curr_version = get_electron_version()
if args.bump not in ['stable', 'beta', 'nightly']:
raise Exception('bump must be set to either stable, beta or nightly')
if is_nightly(curr_version):
if args.bump == 'nightly':
version = get_next_nightly(curr_version)
elif args.bump == 'beta':
version = get_next_beta(curr_version)
elif args.bump == 'stable':
version = get_next_stable_from_pre(curr_version)
else:
not_reached()
elif is_beta(curr_version):
if args.bump == 'nightly':
version = get_next_nightly(curr_version)
elif args.bump == 'beta':
version = get_next_beta(curr_version)
elif args.bump == 'stable':
version = get_next_stable_from_pre(curr_version)
else:
not_reached()
elif is_stable(curr_version):
if args.bump == 'nightly':
version = get_next_nightly(curr_version)
elif args.bump == 'beta':
raise Exception("You can\'t bump to a beta from stable")
elif args.bump == 'stable':
version = get_next_stable_from_stable(curr_version)
else:
not_reached()
else:
raise Exception("Invalid current version: " + curr_version)
if args.new_version == None and args.bump == None and args.stable == False:
parser.print_help()
return 1
increments = ['major', 'minor', 'patch', 'beta']
curr_version = get_electron_version()
versions = parse_version(re.sub('-beta', '', curr_version))
if args.bump in increments:
versions = increase_version(versions, increments.index(args.bump))
if versions[3] == '0':
# beta starts at 1
versions = increase_version(versions, increments.index('beta'))
if args.stable == True:
versions[3] = '0'
if args.new_version != None:
versions = parse_version(re.sub('-beta', '', args.new_version))
version = '.'.join(versions[:3])
suffix = '' if versions[3] == '0' else '-beta.' + versions[3]
versions = clean_parse_version(version)
suffix = ''
if '-' in version:
suffix = '-' + version.split('-')[1]
versions[3] = parse_version(version)[3]
version = version.split('-')[0]
if args.dry_run:
print 'new version number would be: {0}\n'.format(version + suffix)
return 0
with scoped_cwd(SOURCE_ROOT):
update_electron_gyp(version, suffix)
update_win_rc(version, versions)
@@ -92,6 +105,9 @@ def main():
print 'Bumped to version: {0}'.format(version + suffix)
def not_reached():
raise Exception('Unreachable code was reached')
def increase_version(versions, index):
for i in range(index + 1, 4):
versions[i] = '0'
@@ -100,7 +116,8 @@ def increase_version(versions, index):
def update_electron_gyp(version, suffix):
pattern = re.compile(" *'version%' *: *'[0-9.]+(-beta[0-9.]*)?'")
pattern = re.compile(" *'version%' *: *'[0-9.]+(-beta[0-9.]*)?(-dev)?"
+ "(-nightly[0-9.]*)?'")
with open('electron.gyp', 'r') as f:
lines = f.readlines()
@@ -192,7 +209,14 @@ def update_package_json(version, suffix):
def tag_version(version, suffix):
execute(['git', 'commit', '-a', '-m', 'Bump v{0}'.format(version + suffix)])
execute([
'git',
'commit',
'-a',
'-m',
'Bump v{0}'.format(version + suffix),
'-n'
])
if __name__ == '__main__':

View File

@@ -1,3 +1,5 @@
require('dotenv-safe').load()
const assert = require('assert')
const request = require('request')
const buildAppVeyorURL = 'https://windows-ci.electronjs.org/api/builds'
@@ -44,7 +46,6 @@ async function makeRequest (requestOptions, parseResponse) {
}
async function circleCIcall (buildUrl, targetBranch, job, options) {
assert(process.env.CIRCLE_TOKEN, 'CIRCLE_TOKEN not found in environment')
console.log(`Triggering CircleCI to run build job: ${job} on branch: ${targetBranch} with release flag.`)
let buildRequest = {
'build_parameters': {
@@ -77,7 +78,6 @@ async function circleCIcall (buildUrl, targetBranch, job, options) {
}
function buildAppVeyor (targetBranch, options) {
assert(process.env.APPVEYOR_TOKEN, 'APPVEYOR_TOKEN not found in environment')
const validJobs = Object.keys(appVeyorJobs)
if (options.job) {
assert(validJobs.includes(options.job), `Unknown AppVeyor CI job name: ${options.job}. Valid values are: ${validJobs}.`)
@@ -139,7 +139,6 @@ async function buildVSTS (targetBranch, options) {
assert(vstsJobs.includes(options.job), `Unknown VSTS CI job name: ${options.job}. Valid values are: ${vstsJobs}.`)
}
console.log(`Triggering VSTS to run build on branch: ${targetBranch} with release flag.`)
assert(process.env.VSTS_TOKEN, 'VSTS_TOKEN not found in environment')
let environmentVariables = {}
if (!options.ghRelease) {

View File

@@ -1,3 +1,5 @@
if (!process.env.CI) require('dotenv-safe').load()
const GitHub = require('github')
const github = new GitHub()
@@ -12,7 +14,7 @@ async function findRelease () {
github.authenticate({type: 'token', token: process.env.ELECTRON_GITHUB_TOKEN})
let releases = await github.repos.getReleases({
owner: 'electron',
repo: 'electron'
repo: version.indexOf('nightly') > 0 ? 'nightlies' : 'electron'
})
let targetRelease = releases.data.find(release => {
return release.tag_name === version

View File

@@ -0,0 +1,29 @@
const { GitProcess } = require('dugite')
const path = require('path')
const semver = require('semver')
const gitDir = path.resolve(__dirname, '..')
async function determineNextMajorForMaster () {
let branchNames
let result = await GitProcess.exec(['branch', '-a', '--remote', '--list', 'origin/[0-9]-[0-9]-x'], gitDir)
if (result.exitCode === 0) {
branchNames = result.stdout.trim().split('\n')
const filtered = branchNames.map(b => b.replace('origin/', ''))
return getNextReleaseBranch(filtered)
} else {
throw new Error('Release branches could not be fetched.')
}
}
function getNextReleaseBranch (branches) {
const converted = branches.map(b => b.replace(/-/g, '.').replace('x', '0'))
const next = converted.reduce((v1, v2) => {
return semver.gt(v1, v2) ? v1 : v2
})
return parseInt(next.split('.')[0], 10)
}
determineNextMajorForMaster().then(console.info).catch((err) => {
console.error(err)
process.exit(1)
})

View File

@@ -2,6 +2,7 @@
import atexit
import contextlib
import datetime
import errno
import platform
import re
@@ -87,7 +88,7 @@ def download(text, url, path):
downloaded_size = 0
block_size = 128
ci = os.environ.get('CI') == '1'
ci = os.environ.get('CI') is not None
while True:
buf = web_file.read(block_size)
@@ -287,3 +288,67 @@ def update_node_modules(dirname, env=None):
pass
else:
execute_stdout(args, env)
def clean_parse_version(v):
return parse_version(v.split("-")[0])
def is_stable(v):
return len(v.split(".")) == 3
def is_beta(v):
return 'beta' in v
def is_nightly(v):
return 'nightly' in v
def get_nightly_date():
return datetime.datetime.today().strftime('%Y%m%d')
def get_last_major():
return execute(['node', 'script/get-last-major-for-master.js'])
def get_next_nightly(v):
pv = clean_parse_version(v)
major = pv[0]; minor = pv[1]; patch = pv[2]
if (is_stable(v)):
patch = str(int(pv[2]) + 1)
if execute(['git', 'rev-parse', '--abbrev-ref', 'HEAD']) == "master":
major = str(get_last_major() + 1)
minor = '0'
patch = '0'
pre = 'nightly.' + get_nightly_date()
return make_version(major, minor, patch, pre)
def non_empty(thing):
return thing.strip() != ''
def get_next_beta(v):
pv = clean_parse_version(v)
tag_pattern = 'v' + pv[0] + '.' + pv[1] + '.' + pv[2] + '-beta.*'
tag_list = filter(
non_empty,
execute(['git', 'tag', '--list', '-l', tag_pattern]).strip().split('\n')
)
if len(tag_list) == 0:
return make_version(pv[0] , pv[1], pv[2], 'beta.1')
lv = parse_version(tag_list[-1])
return make_version(pv[0] , pv[1], pv[2], 'beta.' + str(int(lv[3]) + 1))
def get_next_stable_from_pre(v):
pv = clean_parse_version(v)
major = pv[0]; minor = pv[1]; patch = pv[2]
return make_version(major, minor, patch)
def get_next_stable_from_stable(v):
pv = clean_parse_version(v)
major = pv[0]; minor = pv[1]; patch = pv[2]
return make_version(major, minor, str(int(patch) + 1))
def make_version(major, minor, patch, pre = None):
if pre is None:
return major + '.' + minor + '.' + patch
return major + "." + minor + "." + patch + '-' + pre

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env node
if (!process.env.CI) require('dotenv-safe').load()
require('colors')
const args = require('minimist')(process.argv.slice(2), {
boolean: ['automaticRelease', 'notesOnly', 'stable']
})
const assert = require('assert')
const ciReleaseBuild = require('./ci-release-build')
const { execSync } = require('child_process')
const fail = '\u2717'.red
@@ -15,13 +15,13 @@ const path = require('path')
const pkg = require('../package.json')
const readline = require('readline')
const versionType = args._[0]
const targetRepo = versionType === 'nightly' ? 'nightlies' : 'electron'
// TODO (future) automatically determine version based on conventional commits
// via conventional-recommended-bump
assert(process.env.ELECTRON_GITHUB_TOKEN, 'ELECTRON_GITHUB_TOKEN not found in environment')
if (!versionType && !args.notesOnly) {
console.log(`Usage: prepare-release versionType [major | minor | patch | beta]` +
console.log(`Usage: prepare-release versionType [stable | beta | nightly]` +
` (--stable) (--notesOnly) (--automaticRelease) (--branch)`)
process.exit(1)
}
@@ -30,10 +30,10 @@ const github = new GitHub()
const gitDir = path.resolve(__dirname, '..')
github.authenticate({type: 'token', token: process.env.ELECTRON_GITHUB_TOKEN})
function getNewVersion (dryRun) {
async function getNewVersion (dryRun) {
console.log(`Bumping for new "${versionType}" version.`)
let bumpScript = path.join(__dirname, 'bump-version.py')
let scriptArgs = [bumpScript, `--bump ${versionType}`]
let scriptArgs = [bumpScript, '--bump', versionType]
if (args.stable) {
scriptArgs.push('--stable')
}
@@ -50,6 +50,7 @@ function getNewVersion (dryRun) {
return newVersion
} catch (err) {
console.log(`${fail} Could not bump version, error was:`, err)
throw err
}
}
@@ -71,10 +72,13 @@ async function getCurrentBranch (gitDir) {
}
async function getReleaseNotes (currentBranch) {
if (versionType === 'nightly') {
return 'Nightlies do not get release notes, please compare tags for info'
}
console.log(`Generating release notes for ${currentBranch}.`)
let githubOpts = {
owner: 'electron',
repo: 'electron',
repo: targetRepo,
base: `v${pkg.version}`,
head: currentBranch
}
@@ -136,11 +140,11 @@ async function getReleaseNotes (currentBranch) {
async function createRelease (branchToTarget, isBeta) {
let releaseNotes = await getReleaseNotes(branchToTarget)
let newVersion = getNewVersion()
let newVersion = await getNewVersion()
await tagRelease(newVersion)
const githubOpts = {
owner: 'electron',
repo: 'electron'
repo: targetRepo
}
console.log(`Checking for existing draft release.`)
let releases = await github.repos.getReleases(githubOpts)
@@ -158,10 +162,17 @@ async function createRelease (branchToTarget, isBeta) {
githubOpts.draft = true
githubOpts.name = `electron ${newVersion}`
if (isBeta) {
githubOpts.body = `Note: This is a beta release. Please file new issues ` +
`for any bugs you find in it.\n \n This release is published to npm ` +
`under the beta tag and can be installed via npm install electron@beta, ` +
`or npm i electron@${newVersion.substr(1)}.\n \n ${releaseNotes}`
if (newVersion.indexOf('nightly') > 0) {
githubOpts.body = `Note: This is a nightly release. Please file new issues ` +
`for any bugs you find in it.\n \n This release is published to npm ` +
`under the nightly tag and can be installed via npm install electron@nightly, ` +
`or npm i electron@${newVersion.substr(1)}.\n \n ${releaseNotes}`
} else {
githubOpts.body = `Note: This is a beta release. Please file new issues ` +
`for any bugs you find in it.\n \n This release is published to npm ` +
`under the beta tag and can be installed via npm install electron@beta, ` +
`or npm i electron@${newVersion.substr(1)}.\n \n ${releaseNotes}`
}
githubOpts.name = `${githubOpts.name}`
githubOpts.prerelease = true
} else {
@@ -209,7 +220,7 @@ async function tagRelease (version) {
}
async function verifyNewVersion () {
let newVersion = getNewVersion(true)
let newVersion = await getNewVersion(true)
let response
if (args.automaticRelease) {
response = 'y'
@@ -237,10 +248,17 @@ async function promptForVersion (version) {
})
}
// function to determine if there have been commits to master since the last release
async function changesToRelease () {
let lastCommitWasRelease = new RegExp(`Bump v[0-9.]*(-beta[0-9.]*)?(-nightly[0-9.]*)?`, 'g')
let lastCommit = await GitProcess.exec(['log', '-n', '1', `--pretty=format:'%s'`], gitDir)
return !lastCommitWasRelease.test(lastCommit.stdout)
}
async function prepareRelease (isBeta, notesOnly) {
if (args.automaticRelease && (pkg.version.indexOf('beta') === -1 ||
versionType !== 'beta')) {
console.log(`${fail} Automatic release is only supported for beta releases`)
versionType !== 'beta') && versionType !== 'nightly' && versionType !== 'stable') {
console.log(`${fail} Automatic release is only supported for beta and nightly releases`)
process.exit(1)
}
let currentBranch
@@ -253,10 +271,16 @@ async function prepareRelease (isBeta, notesOnly) {
let releaseNotes = await getReleaseNotes(currentBranch)
console.log(`Draft release notes are: \n${releaseNotes}`)
} else {
await verifyNewVersion()
await createRelease(currentBranch, isBeta)
await pushRelease(currentBranch)
await runReleaseBuilds(currentBranch)
const changes = await changesToRelease(currentBranch)
if (changes) {
await verifyNewVersion()
await createRelease(currentBranch, isBeta)
await pushRelease(currentBranch)
await runReleaseBuilds(currentBranch)
} else {
console.log(`There are no new changes to this branch since the last release, aborting release.`)
process.exit(1)
}
}
}

View File

@@ -3,10 +3,15 @@ const fs = require('fs')
const path = require('path')
const childProcess = require('child_process')
const GitHubApi = require('github')
const {GitProcess} = require('dugite')
const request = require('request')
const assert = require('assert')
const rootPackageJson = require('../package.json')
if (!process.env.ELECTRON_NPM_OTP) {
console.error('Please set ELECTRON_NPM_OTP')
process.exit(1)
}
const github = new GitHubApi({
// debug: true,
headers: { 'User-Agent': 'electron-npm-publisher' },
@@ -68,7 +73,7 @@ new Promise((resolve, reject) => {
return github.repos.getReleases({
owner: 'electron',
repo: 'electron'
repo: rootPackageJson.version.indexOf('nightly') > 0 ? 'nightlies' : 'electron'
})
})
.then((releases) => {
@@ -103,8 +108,17 @@ new Promise((resolve, reject) => {
})
})
})
.then((release) => {
npmTag = release.prerelease ? 'beta' : 'latest'
.then(async (release) => {
if (release.tag_name.indexOf('nightly') > 0) {
const currentBranch = await getCurrentBranch()
if (currentBranch === 'master') {
npmTag = 'nightly'
} else {
npmTag = `nightly-${currentBranch}`
}
} else {
npmTag = release.prerelease ? 'beta' : 'latest'
}
})
.then(() => childProcess.execSync('npm pack', { cwd: tempDir }))
.then(() => {
@@ -115,13 +129,29 @@ new Promise((resolve, reject) => {
env: Object.assign({}, process.env, { electron_config_cache: tempDir }),
cwd: tempDir
})
const checkVersion = childProcess.execSync(`${path.join(tempDir, 'node_modules', '.bin', 'electron')} -v`)
assert.ok((`v${rootPackageJson.version}`.indexOf(checkVersion.toString().trim()) === 0), `Version is correct`)
resolve(tarballPath)
})
})
.then((tarballPath) => childProcess.execSync(`npm publish ${tarballPath} --tag ${npmTag}`))
.then((tarballPath) => childProcess.execSync(`npm publish ${tarballPath} --tag ${npmTag} --otp=${process.env.ELECTRON_NPM_OTP}`))
.catch((err) => {
console.error(`Error: ${err}`)
process.exit(1)
})
async function getCurrentBranch () {
const gitDir = path.resolve(__dirname, '..')
console.log(`Determining current git branch`)
let gitArgs = ['rev-parse', '--abbrev-ref', 'HEAD']
let branchDetails = await GitProcess.exec(gitArgs, gitDir)
if (branchDetails.exitCode === 0) {
let currentBranch = branchDetails.stdout.trim()
console.log(`Successfully determined current git branch is ` +
`${currentBranch}`)
return currentBranch
} else {
let error = GitProcess.parseError(branchDetails.stderr)
console.log(`Could not get details for the current branch,
error was ${branchDetails.stderr}`, error)
process.exit(1)
}
}

View File

@@ -0,0 +1,478 @@
const { GitProcess } = require('dugite')
const Entities = require('html-entities').AllHtmlEntities
const fetch = require('node-fetch')
const fs = require('fs')
const GitHub = require('github')
const path = require('path')
const semver = require('semver')
const CACHE_DIR = path.resolve(__dirname, '.cache')
// Fill this with tags to ignore if you are generating release notes for older
// versions
//
// E.g. ['v3.0.0-beta.1'] to generate the release notes for 3.0.0-beta.1 :) from
// the current 3-0-x branch
const EXCLUDE_TAGS = []
const entities = new Entities()
const github = new GitHub()
const gitDir = path.resolve(__dirname, '..', '..')
github.authenticate({ type: 'token', token: process.env.ELECTRON_GITHUB_TOKEN })
let currentBranch
const semanticMap = new Map()
for (const line of fs.readFileSync(path.resolve(__dirname, 'legacy-pr-semantic-map.csv'), 'utf8').split('\n')) {
if (!line) continue
const bits = line.split(',')
if (bits.length !== 2) continue
semanticMap.set(bits[0], bits[1])
}
const getCurrentBranch = async () => {
if (currentBranch) return currentBranch
const gitArgs = ['rev-parse', '--abbrev-ref', 'HEAD']
const branchDetails = await GitProcess.exec(gitArgs, gitDir)
if (branchDetails.exitCode === 0) {
currentBranch = branchDetails.stdout.trim()
return currentBranch
}
throw GitProcess.parseError(branchDetails.stderr)
}
const getBranchOffPoint = async (branchName) => {
const gitArgs = ['merge-base', branchName, 'master']
const commitDetails = await GitProcess.exec(gitArgs, gitDir)
if (commitDetails.exitCode === 0) {
return commitDetails.stdout.trim()
}
throw GitProcess.parseError(commitDetails.stderr)
}
const getTagsOnBranch = async (branchName) => {
const gitArgs = ['tag', '--merged', branchName]
const tagDetails = await GitProcess.exec(gitArgs, gitDir)
if (tagDetails.exitCode === 0) {
return tagDetails.stdout.trim().split('\n').filter(tag => !EXCLUDE_TAGS.includes(tag))
}
throw GitProcess.parseError(tagDetails.stderr)
}
const memLastKnownRelease = new Map()
const getLastKnownReleaseOnBranch = async (branchName) => {
if (memLastKnownRelease.has(branchName)) {
return memLastKnownRelease.get(branchName)
}
const tags = await getTagsOnBranch(branchName)
if (!tags.length) {
throw new Error(`Branch ${branchName} has no tags, we have no idea what the last release was`)
}
const branchOffPointTags = await getTagsOnBranch(await getBranchOffPoint(branchName))
if (branchOffPointTags.length >= tags.length) {
// No release on this branch
return null
}
memLastKnownRelease.set(branchName, tags[tags.length - 1])
// Latest tag is the latest release
return tags[tags.length - 1]
}
const getBranches = async () => {
const gitArgs = ['branch', '--remote']
const branchDetails = await GitProcess.exec(gitArgs, gitDir)
if (branchDetails.exitCode === 0) {
return branchDetails.stdout.trim().split('\n').map(b => b.trim()).filter(branch => branch !== 'origin/HEAD -> origin/master')
}
throw GitProcess.parseError(branchDetails.stderr)
}
const semverify = (v) => v.replace(/^origin\//, '').replace('x', '0').replace(/-/g, '.')
const getLastReleaseBranch = async () => {
const current = await getCurrentBranch()
const allBranches = await getBranches()
const releaseBranches = allBranches
.filter(branch => /^origin\/[0-9]+-[0-9]+-x$/.test(branch))
.filter(branch => branch !== current && branch !== `origin/${current}`)
let latest = null
for (const b of releaseBranches) {
if (latest === null) latest = b
if (semver.gt(semverify(b), semverify(latest))) {
latest = b
}
}
return latest
}
const commitBeforeTag = async (commit, tag) => {
const gitArgs = ['tag', '--contains', commit]
const tagDetails = await GitProcess.exec(gitArgs, gitDir)
if (tagDetails.exitCode === 0) {
return tagDetails.stdout.split('\n').includes(tag)
}
throw GitProcess.parseError(tagDetails.stderr)
}
const getCommitsMergedIntoCurrentBranchSincePoint = async (point) => {
return getCommitsBetween(point, 'HEAD')
}
const getCommitsBetween = async (point1, point2) => {
const gitArgs = ['rev-list', `${point1}..${point2}`]
const commitsDetails = await GitProcess.exec(gitArgs, gitDir)
if (commitsDetails.exitCode !== 0) {
throw GitProcess.parseError(commitsDetails.stderr)
}
return commitsDetails.stdout.trim().split('\n')
}
const TITLE_PREFIX = 'Merged Pull Request: '
const getCommitDetails = async (commitHash) => {
const commitInfo = await (await fetch(`https://github.com/electron/electron/branch_commits/${commitHash}`)).text()
const bits = commitInfo.split('</a>)')[0].split('>')
const prIdent = bits[bits.length - 1].trim()
if (!prIdent || commitInfo.indexOf('href="/electron/electron/pull') === -1) {
console.warn(`WARNING: Could not track commit "${commitHash}" to a pull request, it may have been committed directly to the branch`)
return null
}
const title = commitInfo.split('title="')[1].split('"')[0]
if (!title.startsWith(TITLE_PREFIX)) {
console.warn(`WARNING: Unknown PR title for commit "${commitHash}" in PR "${prIdent}"`)
return null
}
return {
mergedFrom: prIdent,
prTitle: entities.decode(title.substr(TITLE_PREFIX.length))
}
}
const doWork = async (items, fn, concurrent = 5) => {
const results = []
const toUse = [].concat(items)
let i = 1
const doBit = async () => {
if (toUse.length === 0) return
console.log(`Running ${i}/${items.length}`)
i += 1
const item = toUse.pop()
const index = toUse.length
results[index] = await fn(item)
await doBit()
}
const bits = []
for (let i = 0; i < concurrent; i += 1) {
bits.push(doBit())
}
await Promise.all(bits)
return results
}
const notes = new Map()
const NoteType = {
FIX: 'fix',
FEATURE: 'feature',
BREAKING_CHANGE: 'breaking-change',
DOCUMENTATION: 'doc',
OTHER: 'other',
UNKNOWN: 'unknown'
}
class Note {
constructor (trueTitle, prNumber, ignoreIfInVersion) {
// Self bindings
this.guessType = this.guessType.bind(this)
this.fetchPrInfo = this.fetchPrInfo.bind(this)
this._getPr = this._getPr.bind(this)
if (!trueTitle.trim()) console.error(prNumber)
this._ignoreIfInVersion = ignoreIfInVersion
this.reverted = false
if (notes.has(trueTitle)) {
console.warn(`Duplicate PR trueTitle: "${trueTitle}", "${prNumber}" this might cause weird reversions (this would be RARE)`)
}
// Memoize
notes.set(trueTitle, this)
this.originalTitle = trueTitle
this.title = trueTitle
this.prNumber = prNumber
this.stripColon = true
if (this.guessType() !== NoteType.UNKNOWN && this.stripColon) {
this.title = trueTitle.split(':').slice(1).join(':').trim()
}
}
guessType () {
if (this.originalTitle.startsWith('fix:') ||
this.originalTitle.startsWith('Fix:')) return NoteType.FIX
if (this.originalTitle.startsWith('feat:')) return NoteType.FEATURE
if (this.originalTitle.startsWith('spec:') ||
this.originalTitle.startsWith('build:') ||
this.originalTitle.startsWith('test:') ||
this.originalTitle.startsWith('chore:') ||
this.originalTitle.startsWith('deps:') ||
this.originalTitle.startsWith('refactor:') ||
this.originalTitle.startsWith('tools:') ||
this.originalTitle.startsWith('vendor:') ||
this.originalTitle.startsWith('perf:') ||
this.originalTitle.startsWith('style:') ||
this.originalTitle.startsWith('ci')) return NoteType.OTHER
if (this.originalTitle.startsWith('doc:') ||
this.originalTitle.startsWith('docs:')) return NoteType.DOCUMENTATION
this.stripColon = false
if (this.pr && this.pr.data.labels.find(label => label.name === 'semver/breaking-change')) {
return NoteType.BREAKING_CHANGE
}
// FIXME: Backported features will not be picked up by this
if (this.pr && this.pr.data.labels.find(label => label.name === 'semver/nonbreaking-feature')) {
return NoteType.FEATURE
}
const n = this.prNumber.replace('#', '')
if (semanticMap.has(n)) {
switch (semanticMap.get(n)) {
case 'feat':
return NoteType.FEATURE
case 'fix':
return NoteType.FIX
case 'breaking-change':
return NoteType.BREAKING_CHANGE
case 'doc':
return NoteType.DOCUMENTATION
case 'build':
case 'vendor':
case 'refactor':
case 'spec':
return NoteType.OTHER
default:
throw new Error(`Unknown semantic mapping: ${semanticMap.get(n)}`)
}
}
return NoteType.UNKNOWN
}
async _getPr (n) {
const cachePath = path.resolve(CACHE_DIR, n)
if (fs.existsSync(cachePath)) {
return JSON.parse(fs.readFileSync(cachePath, 'utf8'))
} else {
try {
const pr = await github.pullRequests.get({
number: n,
owner: 'electron',
repo: 'electron'
})
fs.writeFileSync(cachePath, JSON.stringify({ data: pr.data }))
return pr
} catch (err) {
console.info('#### FAILED:', `#${n}`)
throw err
}
}
}
async fetchPrInfo () {
if (this.pr) return
const n = this.prNumber.replace('#', '')
this.pr = await this._getPr(n)
if (this.pr.data.labels.find(label => label.name === `merged/${this._ignoreIfInVersion.replace('origin/', '')}`)) {
// This means we probably backported this PR, let's try figure out what
// the corresponding backport PR would be by searching through comments
// for trop
let comments
const cacheCommentsPath = path.resolve(CACHE_DIR, `${n}-comments`)
if (fs.existsSync(cacheCommentsPath)) {
comments = JSON.parse(fs.readFileSync(cacheCommentsPath, 'utf8'))
} else {
comments = await github.issues.getComments({
number: n,
owner: 'electron',
repo: 'electron',
per_page: 100
})
fs.writeFileSync(cacheCommentsPath, JSON.stringify({ data: comments.data }))
}
const tropComment = comments.data.find(
c => (
new RegExp(`We have automatically backported this PR to "${this._ignoreIfInVersion.replace('origin/', '')}", please check out #[0-9]+`)
).test(c.body)
)
if (tropComment) {
const commentBits = tropComment.body.split('#')
const tropPrNumber = commentBits[commentBits.length - 1]
const tropPr = await this._getPr(tropPrNumber)
if (tropPr.data.merged && tropPr.data.merge_commit_sha) {
if (await commitBeforeTag(tropPr.data.merge_commit_sha, await getLastKnownReleaseOnBranch(this._ignoreIfInVersion))) {
this.reverted = true
console.log('PR', this.prNumber, 'was backported to a previous version, ignoring from notes')
}
}
}
}
}
}
Note.findByTrueTitle = (trueTitle) => notes.get(trueTitle)
class ReleaseNotes {
constructor (ignoreIfInVersion) {
this._ignoreIfInVersion = ignoreIfInVersion
this._handledPrs = new Set()
this._revertedPrs = new Set()
this.other = []
this.docs = []
this.fixes = []
this.features = []
this.breakingChanges = []
this.unknown = []
}
async parseCommits (commitHashes) {
await doWork(commitHashes, async (commit) => {
const info = await getCommitDetails(commit)
if (!info) return
// Only handle each PR once
if (this._handledPrs.has(info.mergedFrom)) return
this._handledPrs.add(info.mergedFrom)
// Strip the trop backport prefix
const trueTitle = info.prTitle.replace(/^Backport \([0-9]+-[0-9]+-x\) - /, '')
if (this._revertedPrs.has(trueTitle)) return
// Handle PRs that revert other PRs
if (trueTitle.startsWith('Revert "')) {
const revertedTrueTitle = trueTitle.substr(8, trueTitle.length - 9)
this._revertedPrs.add(revertedTrueTitle)
const existingNote = Note.findByTrueTitle(revertedTrueTitle)
if (existingNote) {
existingNote.reverted = true
}
return
}
// Add a note for this PR
const note = new Note(trueTitle, info.mergedFrom, this._ignoreIfInVersion)
try {
await note.fetchPrInfo()
} catch (err) {
console.error(commit, info)
throw err
}
switch (note.guessType()) {
case NoteType.FIX:
this.fixes.push(note)
break
case NoteType.FEATURE:
this.features.push(note)
break
case NoteType.BREAKING_CHANGE:
this.breakingChanges.push(note)
break
case NoteType.OTHER:
this.other.push(note)
break
case NoteType.DOCUMENTATION:
this.docs.push(note)
break
case NoteType.UNKNOWN:
default:
this.unknown.push(note)
break
}
}, 20)
}
list (notes) {
if (notes.length === 0) {
return '_There are no items in this section this release_'
}
return notes
.filter(note => !note.reverted)
.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase()))
.map((note) => `* ${note.title.trim()} ${note.prNumber}`).join('\n')
}
render () {
return `
# Release Notes
## Breaking Changes
${this.list(this.breakingChanges)}
## Features
${this.list(this.features)}
## Fixes
${this.list(this.fixes)}
## Other Changes (E.g. Internal refactors or build system updates)
${this.list(this.other)}
## Documentation Updates
Some documentation updates, fixes and reworks: ${
this.docs.length === 0
? '_None in this release_'
: this.docs.sort((a, b) => a.prNumber.localeCompare(b.prNumber)).map(note => note.prNumber).join(', ')
}
${this.unknown.filter(n => !n.reverted).length > 0
? `## Unknown (fix these before publishing release)
${this.list(this.unknown)}
` : ''}`
}
}
async function main () {
if (!fs.existsSync(CACHE_DIR)) {
fs.mkdirSync(CACHE_DIR)
}
const lastReleaseBranch = await getLastReleaseBranch()
const notes = new ReleaseNotes(lastReleaseBranch)
const lastKnownReleaseInCurrentStream = await getLastKnownReleaseOnBranch(await getCurrentBranch())
const currentBranchOff = await getBranchOffPoint(await getCurrentBranch())
const commits = await getCommitsMergedIntoCurrentBranchSincePoint(
lastKnownReleaseInCurrentStream || currentBranchOff
)
if (!lastKnownReleaseInCurrentStream) {
// This means we are the first release in our stream
// FIXME: This will not work for minor releases!!!!
const lastReleaseBranch = await getLastReleaseBranch()
const lastBranchOff = await getBranchOffPoint(lastReleaseBranch)
commits.push(...await getCommitsBetween(lastBranchOff, currentBranchOff))
}
await notes.parseCommits(commits)
console.log(notes.render())
const badNotes = notes.unknown.filter(n => !n.reverted).length
if (badNotes > 0) {
throw new Error(`You have ${badNotes.length} unknown release notes, please fix them before releasing`)
}
}
if (process.mainModule === module) {
main().catch((err) => {
console.error('Error Occurred:', err)
process.exit(1)
})
}

View File

@@ -0,0 +1,193 @@
12884,fix
12093,feat
12595,doc
12674,doc
12577,doc
12084,doc
12103,doc
12948,build
12496,feat
13133,build
12651,build
12767,doc
12238,build
12646,build
12373,doc
12723,feat
12202,doc
12504,doc
12669,doc
13044,feat
12746,spec
12617,doc
12532,feat
12619,feat
12118,build
12921,build
13281,doc
12059,feat
12131,doc
12123,doc
12080,build
12904,fix
12562,fix
12122,spec
12817,spec
12254,fix
12999,vendor
13248,vendor
12104,build
12477,feat
12648,refactor
12649,refactor
12650,refactor
12673,refactor
12305,refactor
12168,refactor
12627,refactor
12446,doc
12304,refactor
12615,breaking-change
12135,feat
12155,doc
12975,fix
12501,fix
13065,fix
13089,build
12786,doc
12736,doc
11966,doc
12885,fix
12984,refactor
12187,build
12535,refactor
12538,feat
12190,fix
12139,fix
11328,fix
12828,feat
12614,feat
12546,feat
12647,refactor
12987,build
12900,doc
12389,doc
12387,doc
12232,doc
12742,build
12043,fix
12741,fix
12995,fix
12395,fix
12003,build
12216,fix
12132,fix
12062,fix
12968,doc
12422,doc
12149,doc
13339,build
12044,fix
12327,fix
12180,fix
12263,spec
12153,spec
13055,feat
12113,doc
12067,doc
12882,build
13029,build
13067,doc
12196,build
12797,doc
12013,fix
12507,fix
11607,feat
12837,build
11613,feat
12015,spec
12058,doc
12403,spec
12192,feat
12204,doc
13294,doc
12542,doc
12826,refactor
12781,doc
12157,fix
12319,fix
12188,build
12399,doc
12145,doc
12661,refactor
8953,fix
12037,fix
12186,spec
12397,fix
12040,doc
12886,refactor
12008,refactor
12716,refactor
12750,refactor
12787,refactor
12858,refactor
12140,refactor
12503,refactor
12514,refactor
12584,refactor
12596,refactor
12637,refactor
12660,refactor
12696,refactor
12877,refactor
13030,refactor
12916,build
12896,build
13039,breaking-change
11927,build
12847,doc
12852,doc
12194,fix
12870,doc
12924,fix
12682,doc
12004,refactor
12601,refactor
12998,fix
13105,vendor
12452,doc
12738,fix
12536,refactor
12189,spec
13122,spec
12662,fix
12665,doc
12419,feat
12756,doc
12616,refactor
12679,breaking-change
12000,doc
12372,build
12805,build
12348,fix
12315,doc
12072,doc
12912,doc
12982,fix
12105,doc
12917,spec
12400,doc
12101,feat
12642,build
13058,fix
12913,vendor
13298,vendor
13042,build
11230,feat
11459,feat
12476,vendor
11937,doc
12328,build
12539,refactor
12127,build
12537,build
1 12884 fix
2 12093 feat
3 12595 doc
4 12674 doc
5 12577 doc
6 12084 doc
7 12103 doc
8 12948 build
9 12496 feat
10 13133 build
11 12651 build
12 12767 doc
13 12238 build
14 12646 build
15 12373 doc
16 12723 feat
17 12202 doc
18 12504 doc
19 12669 doc
20 13044 feat
21 12746 spec
22 12617 doc
23 12532 feat
24 12619 feat
25 12118 build
26 12921 build
27 13281 doc
28 12059 feat
29 12131 doc
30 12123 doc
31 12080 build
32 12904 fix
33 12562 fix
34 12122 spec
35 12817 spec
36 12254 fix
37 12999 vendor
38 13248 vendor
39 12104 build
40 12477 feat
41 12648 refactor
42 12649 refactor
43 12650 refactor
44 12673 refactor
45 12305 refactor
46 12168 refactor
47 12627 refactor
48 12446 doc
49 12304 refactor
50 12615 breaking-change
51 12135 feat
52 12155 doc
53 12975 fix
54 12501 fix
55 13065 fix
56 13089 build
57 12786 doc
58 12736 doc
59 11966 doc
60 12885 fix
61 12984 refactor
62 12187 build
63 12535 refactor
64 12538 feat
65 12190 fix
66 12139 fix
67 11328 fix
68 12828 feat
69 12614 feat
70 12546 feat
71 12647 refactor
72 12987 build
73 12900 doc
74 12389 doc
75 12387 doc
76 12232 doc
77 12742 build
78 12043 fix
79 12741 fix
80 12995 fix
81 12395 fix
82 12003 build
83 12216 fix
84 12132 fix
85 12062 fix
86 12968 doc
87 12422 doc
88 12149 doc
89 13339 build
90 12044 fix
91 12327 fix
92 12180 fix
93 12263 spec
94 12153 spec
95 13055 feat
96 12113 doc
97 12067 doc
98 12882 build
99 13029 build
100 13067 doc
101 12196 build
102 12797 doc
103 12013 fix
104 12507 fix
105 11607 feat
106 12837 build
107 11613 feat
108 12015 spec
109 12058 doc
110 12403 spec
111 12192 feat
112 12204 doc
113 13294 doc
114 12542 doc
115 12826 refactor
116 12781 doc
117 12157 fix
118 12319 fix
119 12188 build
120 12399 doc
121 12145 doc
122 12661 refactor
123 8953 fix
124 12037 fix
125 12186 spec
126 12397 fix
127 12040 doc
128 12886 refactor
129 12008 refactor
130 12716 refactor
131 12750 refactor
132 12787 refactor
133 12858 refactor
134 12140 refactor
135 12503 refactor
136 12514 refactor
137 12584 refactor
138 12596 refactor
139 12637 refactor
140 12660 refactor
141 12696 refactor
142 12877 refactor
143 13030 refactor
144 12916 build
145 12896 build
146 13039 breaking-change
147 11927 build
148 12847 doc
149 12852 doc
150 12194 fix
151 12870 doc
152 12924 fix
153 12682 doc
154 12004 refactor
155 12601 refactor
156 12998 fix
157 13105 vendor
158 12452 doc
159 12738 fix
160 12536 refactor
161 12189 spec
162 13122 spec
163 12662 fix
164 12665 doc
165 12419 feat
166 12756 doc
167 12616 refactor
168 12679 breaking-change
169 12000 doc
170 12372 build
171 12805 build
172 12348 fix
173 12315 doc
174 12072 doc
175 12912 doc
176 12982 fix
177 12105 doc
178 12917 spec
179 12400 doc
180 12101 feat
181 12642 build
182 13058 fix
183 12913 vendor
184 13298 vendor
185 13042 build
186 11230 feat
187 11459 feat
188 12476 vendor
189 11937 doc
190 12328 build
191 12539 refactor
192 12127 build
193 12537 build

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env node
if (!process.env.CI) require('dotenv-safe').load()
require('colors')
const args = require('minimist')(process.argv.slice(2))
const assert = require('assert')
const fs = require('fs')
const { execSync } = require('child_process')
const GitHub = require('github')
@@ -16,17 +16,16 @@ const fail = '\u2717'.red
const sumchecker = require('sumchecker')
const temp = require('temp').track()
const { URL } = require('url')
const targetRepo = pkgVersion.indexOf('nightly') > 0 ? 'nightlies' : 'electron'
let failureCount = 0
assert(process.env.ELECTRON_GITHUB_TOKEN, 'ELECTRON_GITHUB_TOKEN not found in environment')
const github = new GitHub({
followRedirects: false
})
github.authenticate({type: 'token', token: process.env.ELECTRON_GITHUB_TOKEN})
async function getDraftRelease (version, skipValidation) {
let releaseInfo = await github.repos.getReleases({owner: 'electron', repo: 'electron'})
let releaseInfo = await github.repos.getReleases({owner: 'electron', repo: targetRepo})
let drafts
let versionToCheck
if (version) {
@@ -90,15 +89,12 @@ function assetsForVersion (version, validatingRelease) {
`electron-${version}-darwin-x64-dsym.zip`,
`electron-${version}-darwin-x64-symbols.zip`,
`electron-${version}-darwin-x64.zip`,
`electron-${version}-linux-arm-symbols.zip`,
`electron-${version}-linux-arm.zip`,
`electron-${version}-linux-arm64-symbols.zip`,
`electron-${version}-linux-arm64.zip`,
`electron-${version}-linux-armv7l-symbols.zip`,
`electron-${version}-linux-armv7l.zip`,
`electron-${version}-linux-ia32-symbols.zip`,
`electron-${version}-linux-ia32.zip`,
// `electron-${version}-linux-mips64el.zip`,
`electron-${version}-linux-x64-symbols.zip`,
`electron-${version}-linux-x64.zip`,
`electron-${version}-mas-x64-dsym.zip`,
@@ -113,11 +109,9 @@ function assetsForVersion (version, validatingRelease) {
`electron-api.json`,
`electron.d.ts`,
`ffmpeg-${version}-darwin-x64.zip`,
`ffmpeg-${version}-linux-arm.zip`,
`ffmpeg-${version}-linux-arm64.zip`,
`ffmpeg-${version}-linux-armv7l.zip`,
`ffmpeg-${version}-linux-ia32.zip`,
// `ffmpeg-${version}-linux-mips64el.zip`,
`ffmpeg-${version}-linux-x64.zip`,
`ffmpeg-${version}-mas-x64.zip`,
`ffmpeg-${version}-win32-ia32.zip`,
@@ -147,6 +141,8 @@ function s3UrlsForVersion (version) {
}
function checkVersion () {
if (args.skipVersionCheck) return
console.log(`Verifying that app version matches package version ${pkgVersion}.`)
let startScript = path.join(__dirname, 'start.py')
let scriptArgs = ['--version']
@@ -184,11 +180,7 @@ function uploadNodeShasums () {
function uploadIndexJson () {
console.log('Uploading index.json to S3.')
let scriptPath = path.join(__dirname, 'upload-index-json.py')
let scriptArgs = []
if (args.automaticRelease) {
scriptArgs.push('-R')
}
runScript(scriptPath, scriptArgs)
runScript(scriptPath, [pkgVersion])
console.log(`${pass} Done uploading index.json to S3.`)
}
@@ -199,7 +191,7 @@ async function createReleaseShasums (release) {
console.log(`${fileName} already exists on GitHub; deleting before creating new file.`)
await github.repos.deleteAsset({
owner: 'electron',
repo: 'electron',
repo: targetRepo,
id: existingAssets[0].id
}).catch(err => {
console.log(`${fail} Error deleting ${fileName} on GitHub:`, err)
@@ -218,7 +210,7 @@ async function createReleaseShasums (release) {
async function uploadShasumFile (filePath, fileName, release) {
let githubOpts = {
owner: 'electron',
repo: 'electron',
repo: targetRepo,
id: release.id,
filePath,
name: fileName
@@ -253,7 +245,7 @@ function saveShaSumFile (checksums, fileName) {
async function publishRelease (release) {
let githubOpts = {
owner: 'electron',
repo: 'electron',
repo: targetRepo,
id: release.id,
tag_name: release.tag_name,
draft: false
@@ -280,6 +272,7 @@ async function makeRelease (releaseToValidate) {
let draftRelease = await getDraftRelease()
uploadNodeShasums()
uploadIndexJson()
await createReleaseShasums(draftRelease)
// Fetch latest version of release before verifying
draftRelease = await getDraftRelease(pkgVersion, true)
@@ -307,7 +300,7 @@ async function verifyAssets (release) {
let downloadDir = await makeTempDir()
let githubOpts = {
owner: 'electron',
repo: 'electron',
repo: targetRepo,
headers: {
Accept: 'application/octet-stream'
}

View File

@@ -1,18 +1,23 @@
if (!process.env.CI) require('dotenv-safe').load()
const GitHub = require('github')
const github = new GitHub()
github.authenticate({type: 'token', token: process.env.ELECTRON_GITHUB_TOKEN})
if (process.argv.length < 5) {
if (process.argv.length < 6) {
console.log('Usage: upload-to-github filePath fileName releaseId')
process.exit(1)
}
let filePath = process.argv[2]
let fileName = process.argv[3]
let releaseId = process.argv[4]
let releaseVersion = process.argv[5]
const targetRepo = releaseVersion.indexOf('nightly') > 0 ? 'nightlies' : 'electron'
let githubOpts = {
owner: 'electron',
repo: 'electron',
repo: targetRepo,
id: releaseId,
filePath: filePath,
name: fileName
@@ -34,7 +39,7 @@ function uploadToGitHub () {
console.log(`${fileName} already exists; will delete before retrying upload.`)
github.repos.deleteAsset({
owner: 'electron',
repo: 'electron',
repo: targetRepo,
id: existingAssets[0].id
}).then(uploadToGitHub).catch(uploadToGitHub)
} else {