mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
Merge pull request #11778 from electron/parallel-releases-1-6-x
Allow multiple releases to run at once for 1-6-x
This commit is contained in:
11
package.json
11
package.json
@@ -5,14 +5,18 @@
|
||||
"asar": "^0.11.0",
|
||||
"browserify": "^13.1.0",
|
||||
"colors": "^1.1.2",
|
||||
"dugite": "^1.45.0",
|
||||
"electabul": "~0.0.4",
|
||||
"electron-docs-linter": "^2.3.3",
|
||||
"electron-typescript-definitions": "1.2.7",
|
||||
"github": "^9.2.0",
|
||||
"heads": "^1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"nugget": "^2.0.1",
|
||||
"request": "*",
|
||||
"standard": "^8.4.0",
|
||||
"standard-markdown": "^2.1.1"
|
||||
"standard-markdown": "^2.1.1",
|
||||
"sumchecker": "^2.0.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"runas": "^3.0.0"
|
||||
@@ -46,8 +50,9 @@
|
||||
"create-api-json": "electron-docs-linter docs --outfile=out/electron-api.json --version=$npm_package_version",
|
||||
"create-typescript-definitions": "npm run create-api-json && electron-typescript-definitions --in=out/electron-api.json --out=out/electron.d.ts",
|
||||
"preinstall": "node -e 'process.exit(0)'",
|
||||
"prerelease": "node ./script/prerelease",
|
||||
"release": "./script/upload.py -p",
|
||||
"prepare-release": "node ./script/prepare-release.js",
|
||||
"prerelease": "python ./script/bootstrap.py -v --dev && npm run build",
|
||||
"release": "node ./script/release.js",
|
||||
"repl": "python ./script/start.py --interactive",
|
||||
"start": "python ./script/start.py",
|
||||
"test": "python ./script/test.py"
|
||||
|
||||
209
script/ci-release-build.js
Normal file
209
script/ci-release-build.js
Normal file
@@ -0,0 +1,209 @@
|
||||
const assert = require('assert')
|
||||
const request = require('request')
|
||||
const buildAppVeyorURL = 'https://windows-ci.electronjs.org/api/builds'
|
||||
const jenkinsServer = 'https://mac-ci.electronjs.org'
|
||||
|
||||
const circleCIJobs = [
|
||||
'electron-linux-arm',
|
||||
'electron-linux-ia32',
|
||||
'electron-linux-x64'
|
||||
]
|
||||
|
||||
const jenkinsJobs = [
|
||||
'electron-mas-x64-release',
|
||||
'electron-osx-x64-release'
|
||||
]
|
||||
|
||||
async function makeRequest (requestOptions, parseResponse) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(requestOptions, (err, res, body) => {
|
||||
if (!err && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
if (parseResponse) {
|
||||
const build = JSON.parse(body)
|
||||
resolve(build)
|
||||
} else {
|
||||
resolve(body)
|
||||
}
|
||||
} else {
|
||||
if (parseResponse) {
|
||||
console.log('Error: ', `(status ${res.statusCode})`, err || JSON.parse(res.body), requestOptions)
|
||||
} else {
|
||||
console.log('Error: ', `(status ${res.statusCode})`, err || res.body, requestOptions)
|
||||
}
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function circleCIcall (buildUrl, targetBranch, job, ghRelease) {
|
||||
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': {
|
||||
'CIRCLE_JOB': job
|
||||
}
|
||||
}
|
||||
|
||||
if (ghRelease) {
|
||||
buildRequest.build_parameters.ELECTRON_RELEASE = 1
|
||||
} else {
|
||||
buildRequest.build_parameters.RUN_RELEASE_BUILD = 'true'
|
||||
}
|
||||
|
||||
let circleResponse = await makeRequest({
|
||||
method: 'POST',
|
||||
url: buildUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(buildRequest)
|
||||
}, true).catch(err => {
|
||||
console.log('Error calling CircleCI:', err)
|
||||
})
|
||||
console.log(`Check ${circleResponse.build_url} for status. (${job})`)
|
||||
}
|
||||
|
||||
async function buildAppVeyor (targetBranch, ghRelease) {
|
||||
console.log(`Triggering AppVeyor to run build on branch: ${targetBranch} with release flag.`)
|
||||
assert(process.env.APPVEYOR_TOKEN, 'APPVEYOR_TOKEN not found in environment')
|
||||
let environmentVariables = {}
|
||||
|
||||
if (ghRelease) {
|
||||
environmentVariables.ELECTRON_RELEASE = 1
|
||||
} else {
|
||||
environmentVariables.RUN_RELEASE_BUILD = 'true'
|
||||
}
|
||||
|
||||
const requestOpts = {
|
||||
url: buildAppVeyorURL,
|
||||
auth: {
|
||||
bearer: process.env.APPVEYOR_TOKEN
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
accountName: 'AppVeyor',
|
||||
projectSlug: 'electron',
|
||||
branch: targetBranch,
|
||||
environmentVariables
|
||||
}),
|
||||
method: 'POST'
|
||||
}
|
||||
let appVeyorResponse = await makeRequest(requestOpts, true).catch(err => {
|
||||
console.log('Error calling AppVeyor:', err)
|
||||
})
|
||||
const buildUrl = `https://windows-ci.electronjs.org/project/AppVeyor/electron/build/${appVeyorResponse.version}`
|
||||
console.log(`AppVeyor release build request successful. Check build status at ${buildUrl}`)
|
||||
}
|
||||
|
||||
function buildCircleCI (targetBranch, ghRelease, job) {
|
||||
const circleBuildUrl = `https://circleci.com/api/v1.1/project/github/electron/electron/tree/${targetBranch}?circle-token=${process.env.CIRCLE_TOKEN}`
|
||||
if (job) {
|
||||
assert(circleCIJobs.includes(job), `Unknown CI job name: ${job}.`)
|
||||
circleCIcall(circleBuildUrl, targetBranch, job, ghRelease)
|
||||
} else {
|
||||
circleCIJobs.forEach((job) => circleCIcall(circleBuildUrl, targetBranch, job, ghRelease))
|
||||
}
|
||||
}
|
||||
|
||||
async function buildJenkins (targetBranch, ghRelease, job) {
|
||||
assert(process.env.JENKINS_AUTH_TOKEN, 'JENKINS_AUTH_TOKEN not found in environment')
|
||||
assert(process.env.JENKINS_BUILD_TOKEN, 'JENKINS_BUILD_TOKEN not found in environment')
|
||||
let jenkinsCrumb = await getJenkinsCrumb()
|
||||
|
||||
if (job) {
|
||||
assert(jenkinsJobs.includes(job), `Unknown CI job name: ${job}.`)
|
||||
callJenkinsBuild(job, jenkinsCrumb, targetBranch, ghRelease)
|
||||
} else {
|
||||
jenkinsJobs.forEach((job) => {
|
||||
callJenkinsBuild(job, jenkinsCrumb, targetBranch, ghRelease)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function callJenkins (path, requestParameters, requestHeaders) {
|
||||
let requestOptions = {
|
||||
url: `${jenkinsServer}/${path}`,
|
||||
auth: {
|
||||
user: 'build',
|
||||
pass: process.env.JENKINS_AUTH_TOKEN
|
||||
},
|
||||
qs: requestParameters
|
||||
}
|
||||
if (requestHeaders) {
|
||||
requestOptions.headers = requestHeaders
|
||||
}
|
||||
let jenkinsResponse = await makeRequest(requestOptions).catch(err => {
|
||||
console.log(`Error calling Jenkins:`, err)
|
||||
})
|
||||
return jenkinsResponse
|
||||
}
|
||||
|
||||
async function callJenkinsBuild (job, jenkinsCrumb, targetBranch, ghRelease) {
|
||||
console.log(`Triggering Jenkins to run build job: ${job} on branch: ${targetBranch} with release flag.`)
|
||||
let jenkinsParams = {
|
||||
token: process.env.JENKINS_BUILD_TOKEN,
|
||||
BRANCH: targetBranch
|
||||
}
|
||||
if (!ghRelease) {
|
||||
jenkinsParams.RUN_RELEASE_BUILD = 1
|
||||
}
|
||||
await callJenkins(`job/${job}/buildWithParameters`, jenkinsParams, jenkinsCrumb)
|
||||
.catch(err => {
|
||||
console.log(`Error calling Jenkins build`, err)
|
||||
})
|
||||
let buildUrl = `${jenkinsServer}/job/${job}/lastBuild/`
|
||||
console.log(`Jenkins build request successful. Check build status at ${buildUrl}.`)
|
||||
}
|
||||
|
||||
async function getJenkinsCrumb () {
|
||||
let crumbResponse = await callJenkins('crumbIssuer/api/xml', {
|
||||
xpath: 'concat(//crumbRequestField,":",//crumb)'
|
||||
}).catch(err => {
|
||||
console.log(`Error getting jenkins crumb:`, err)
|
||||
})
|
||||
let crumbDetails = crumbResponse.split(':')
|
||||
let crumbHeader = {}
|
||||
crumbHeader[crumbDetails[0]] = crumbDetails[1]
|
||||
return crumbHeader
|
||||
}
|
||||
|
||||
function runRelease (targetBranch, options) {
|
||||
if (options.ci) {
|
||||
switch (options.ci) {
|
||||
case 'CircleCI': {
|
||||
buildCircleCI(targetBranch, options.ghRelease, options.job)
|
||||
break
|
||||
}
|
||||
case 'AppVeyor': {
|
||||
buildAppVeyor(targetBranch, options.ghRelease)
|
||||
break
|
||||
}
|
||||
case 'Jenkins': {
|
||||
buildJenkins(targetBranch, options.ghRelease, options.job)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
buildCircleCI(targetBranch, options.ghRelease, options.job)
|
||||
buildAppVeyor(targetBranch, options.ghRelease)
|
||||
buildJenkins(targetBranch, options.ghRelease, options.job)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = runRelease
|
||||
|
||||
if (require.main === module) {
|
||||
const args = require('minimist')(process.argv.slice(2))
|
||||
const targetBranch = args._[0]
|
||||
if (args._.length < 1) {
|
||||
console.log(`Trigger CI to build release builds of electron.
|
||||
Usage: ci-release-build.js [--job=CI_JOB_NAME] [--ci=CircleCI|AppVeyor|Jenkins] [--ghRelease] TARGET_BRANCH
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
runRelease(targetBranch, args)
|
||||
}
|
||||
116
script/merge-release.js
Executable file
116
script/merge-release.js
Executable file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('colors')
|
||||
const assert = require('assert')
|
||||
const branchToRelease = process.argv[2]
|
||||
const fail = '\u2717'.red
|
||||
const { GitProcess, GitError } = require('dugite')
|
||||
const pass = '\u2713'.green
|
||||
const path = require('path')
|
||||
const pkg = require('../package.json')
|
||||
|
||||
assert(process.env.ELECTRON_GITHUB_TOKEN, 'ELECTRON_GITHUB_TOKEN not found in environment')
|
||||
if (!branchToRelease) {
|
||||
console.log(`Usage: merge-release branch`)
|
||||
process.exit(1)
|
||||
}
|
||||
const gitDir = path.resolve(__dirname, '..')
|
||||
|
||||
async function callGit (args, errorMessage, successMessage) {
|
||||
let gitResult = await GitProcess.exec(args, gitDir)
|
||||
if (gitResult.exitCode === 0) {
|
||||
console.log(`${pass} ${successMessage}`)
|
||||
return true
|
||||
} else {
|
||||
console.log(`${fail} ${errorMessage} ${gitResult.stderr}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkoutBranch (branchName) {
|
||||
console.log(`Checking out ${branchName}.`)
|
||||
let errorMessage = `Error checking out branch ${branchName}:`
|
||||
let successMessage = `Successfully checked out branch ${branchName}.`
|
||||
return callGit(['checkout', branchName], errorMessage, successMessage)
|
||||
}
|
||||
|
||||
async function commitMerge () {
|
||||
console.log(`Committing the merge for v${pkg.version}`)
|
||||
let errorMessage = `Error committing merge:`
|
||||
let successMessage = `Successfully committed the merge for v${pkg.version}`
|
||||
let gitArgs = ['commit', '-m', `v${pkg.version}`]
|
||||
return callGit(gitArgs, errorMessage, successMessage)
|
||||
}
|
||||
|
||||
async function mergeReleaseIntoBranch (branchName) {
|
||||
console.log(`Merging release branch into ${branchName}.`)
|
||||
let mergeArgs = ['merge', 'release-1-7-x', '--squash']
|
||||
let mergeDetails = await GitProcess.exec(mergeArgs, gitDir)
|
||||
if (mergeDetails.exitCode === 0) {
|
||||
return true
|
||||
} else {
|
||||
const error = GitProcess.parseError(mergeDetails.stderr)
|
||||
if (error === GitError.MergeConflicts) {
|
||||
console.log(`${fail} Could not merge release branch into ${branchName} ` +
|
||||
`due to merge conflicts.`)
|
||||
return false
|
||||
} else {
|
||||
console.log(`${fail} Could not merge release branch into ${branchName} ` +
|
||||
`due to an error: ${mergeDetails.stderr}.`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function pushBranch (branchName) {
|
||||
console.log(`Pushing branch ${branchName}.`)
|
||||
let pushArgs = ['push', 'origin', branchName]
|
||||
let errorMessage = `Could not push branch ${branchName} due to an error:`
|
||||
let successMessage = `Successfully pushed branch ${branchName}.`
|
||||
return callGit(pushArgs, errorMessage, successMessage)
|
||||
}
|
||||
|
||||
async function pull () {
|
||||
console.log(`Performing a git pull`)
|
||||
let errorMessage = `Could not pull due to an error:`
|
||||
let successMessage = `Successfully performed a git pull`
|
||||
return callGit(['pull'], errorMessage, successMessage)
|
||||
}
|
||||
|
||||
async function rebase (targetBranch) {
|
||||
console.log(`Rebasing release branch from ${targetBranch}`)
|
||||
let errorMessage = `Could not rebase due to an error:`
|
||||
let successMessage = `Successfully rebased release branch from ` +
|
||||
`${targetBranch}`
|
||||
return callGit(['rebase', targetBranch], errorMessage, successMessage)
|
||||
}
|
||||
|
||||
async function mergeRelease () {
|
||||
await checkoutBranch(branchToRelease)
|
||||
let mergeSuccess = await mergeReleaseIntoBranch(branchToRelease)
|
||||
if (mergeSuccess) {
|
||||
console.log(`${pass} Successfully merged release branch into ` +
|
||||
`${branchToRelease}.`)
|
||||
await commitMerge()
|
||||
let pushSuccess = await pushBranch(branchToRelease)
|
||||
if (pushSuccess) {
|
||||
console.log(`${pass} Success!!! ${branchToRelease} now has the latest release!`)
|
||||
}
|
||||
} else {
|
||||
console.log(`Trying rebase of ${branchToRelease} into release branch.`)
|
||||
await pull()
|
||||
await checkoutBranch('release-1-7-x')
|
||||
let rebaseResult = await rebase(branchToRelease)
|
||||
if (rebaseResult) {
|
||||
let pushResult = pushBranch('HEAD')
|
||||
if (pushResult) {
|
||||
console.log(`Rebase of ${branchToRelease} into release branch was ` +
|
||||
`successful. Let release builds run and then try this step again.`)
|
||||
}
|
||||
// Exit as failure so release doesn't continue
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mergeRelease()
|
||||
182
script/prepare-release.js
Executable file
182
script/prepare-release.js
Executable file
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('colors')
|
||||
const args = require('minimist')(process.argv.slice(2))
|
||||
const assert = require('assert')
|
||||
const ciReleaseBuild = require('./ci-release-build')
|
||||
const { execSync } = require('child_process')
|
||||
const fail = '\u2717'.red
|
||||
const { GitProcess, GitError } = require('dugite')
|
||||
const GitHub = require('github')
|
||||
const pass = '\u2713'.green
|
||||
const path = require('path')
|
||||
const pkg = require('../package.json')
|
||||
const versionType = args._[0]
|
||||
|
||||
// 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]` +
|
||||
` (--stable) (--notesOnly)`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const github = new GitHub()
|
||||
const gitDir = path.resolve(__dirname, '..')
|
||||
github.authenticate({type: 'token', token: process.env.ELECTRON_GITHUB_TOKEN})
|
||||
|
||||
async function createReleaseBranch () {
|
||||
console.log(`Creating release branch.`)
|
||||
let checkoutDetails = await GitProcess.exec([ 'checkout', '-b', 'release-1-7-x' ], gitDir)
|
||||
if (checkoutDetails.exitCode === 0) {
|
||||
console.log(`${pass} Successfully created the release branch.`)
|
||||
} else {
|
||||
const error = GitProcess.parseError(checkoutDetails.stderr)
|
||||
if (error === GitError.BranchAlreadyExists) {
|
||||
console.log(`${fail} Release branch already exists, aborting prepare ` +
|
||||
`release process.`)
|
||||
} else {
|
||||
console.log(`${fail} Error creating release branch: ` +
|
||||
`${checkoutDetails.stderr}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
function getNewVersion () {
|
||||
console.log(`Bumping for new "${versionType}" version.`)
|
||||
let bumpScript = path.join(__dirname, 'bump-version.py')
|
||||
let scriptArgs = [bumpScript, `--bump ${versionType}`]
|
||||
if (args.stable) {
|
||||
scriptArgs.push('--stable')
|
||||
}
|
||||
try {
|
||||
let bumpVersion = execSync(scriptArgs.join(' '), {encoding: 'UTF-8'})
|
||||
bumpVersion = bumpVersion.substr(bumpVersion.indexOf(':') + 1).trim()
|
||||
let newVersion = `v${bumpVersion}`
|
||||
console.log(`${pass} Successfully bumped version to ${newVersion}`)
|
||||
return newVersion
|
||||
} catch (err) {
|
||||
console.log(`${fail} Could not bump version, error was:`, err)
|
||||
}
|
||||
}
|
||||
|
||||
async function getCurrentBranch (gitDir) {
|
||||
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(`${pass} Successfully determined current git branch is ` +
|
||||
`${currentBranch}`)
|
||||
return currentBranch
|
||||
} else {
|
||||
let error = GitProcess.parseError(branchDetails.stderr)
|
||||
console.log(`${fail} Could not get details for the current branch,
|
||||
error was ${branchDetails.stderr}`, error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
async function getReleaseNotes (currentBranch) {
|
||||
console.log(`Generating release notes for ${currentBranch}.`)
|
||||
let githubOpts = {
|
||||
owner: 'electron',
|
||||
repo: 'electron',
|
||||
base: `v${pkg.version}`,
|
||||
head: currentBranch
|
||||
}
|
||||
let releaseNotes = '(placeholder)\n'
|
||||
console.log(`Checking for commits from ${pkg.version} to ${currentBranch}`)
|
||||
let commitComparison = await github.repos.compareCommits(githubOpts)
|
||||
.catch(err => {
|
||||
console.log(`{$fail} Error checking for commits from ${pkg.version} to ` +
|
||||
`${currentBranch}`, err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
commitComparison.data.commits.forEach(commitEntry => {
|
||||
let commitMessage = commitEntry.commit.message
|
||||
if (commitMessage.toLowerCase().indexOf('merge') > -1) {
|
||||
releaseNotes += `${commitMessage} \n`
|
||||
}
|
||||
})
|
||||
console.log(`${pass} Done generating release notes for ${currentBranch}.`)
|
||||
return releaseNotes
|
||||
}
|
||||
|
||||
async function createRelease (branchToTarget, isBeta) {
|
||||
let releaseNotes = await getReleaseNotes(branchToTarget)
|
||||
let newVersion = getNewVersion()
|
||||
const githubOpts = {
|
||||
owner: 'electron',
|
||||
repo: 'electron'
|
||||
}
|
||||
console.log(`Checking for existing draft release.`)
|
||||
let releases = await github.repos.getReleases(githubOpts)
|
||||
.catch(err => {
|
||||
console.log('$fail} Could not get releases. Error was', err)
|
||||
})
|
||||
let drafts = releases.data.filter(release => release.draft &&
|
||||
release.tag_name === newVersion)
|
||||
if (drafts.length > 0) {
|
||||
console.log(`${fail} Aborting because draft release for
|
||||
${drafts[0].tag_name} already exists.`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`${pass} A draft release does not exist; creating one.`)
|
||||
githubOpts.body = releaseNotes
|
||||
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)}.`
|
||||
githubOpts.name = `${githubOpts.name}`
|
||||
githubOpts.prerelease = true
|
||||
}
|
||||
githubOpts.tag_name = newVersion
|
||||
githubOpts.target_commitish = branchToTarget
|
||||
await github.repos.createRelease(githubOpts)
|
||||
.catch(err => {
|
||||
console.log(`${fail} Error creating new release: `, err)
|
||||
process.exit(1)
|
||||
})
|
||||
console.log(`${pass} Draft release for ${newVersion} has been created.`)
|
||||
}
|
||||
|
||||
async function pushRelease () {
|
||||
let pushDetails = await GitProcess.exec(['push', 'origin', 'HEAD'], gitDir)
|
||||
if (pushDetails.exitCode === 0) {
|
||||
console.log(`${pass} Successfully pushed the release branch. Wait for ` +
|
||||
`release builds to finish before running "npm run release".`)
|
||||
} else {
|
||||
console.log(`${fail} Error pushing the release branch: ` +
|
||||
`${pushDetails.stderr}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
async function runReleaseBuilds () {
|
||||
await ciReleaseBuild('release-1-7-x', {
|
||||
ghRelease: true
|
||||
})
|
||||
}
|
||||
|
||||
async function prepareRelease (isBeta, notesOnly) {
|
||||
let currentBranch = await getCurrentBranch(gitDir)
|
||||
if (notesOnly) {
|
||||
let releaseNotes = await getReleaseNotes(currentBranch)
|
||||
console.log(`Draft release notes are: ${releaseNotes}`)
|
||||
} else {
|
||||
await createReleaseBranch()
|
||||
await createRelease(currentBranch, isBeta)
|
||||
await pushRelease()
|
||||
await runReleaseBuilds()
|
||||
}
|
||||
}
|
||||
|
||||
prepareRelease(!args.stable, args.notesOnly)
|
||||
456
script/release.js
Executable file
456
script/release.js
Executable file
@@ -0,0 +1,456 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
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')
|
||||
const { GitProcess } = require('dugite')
|
||||
const nugget = require('nugget')
|
||||
const pkg = require('../package.json')
|
||||
const pkgVersion = `v${pkg.version}`
|
||||
const pass = '\u2713'.green
|
||||
const path = require('path')
|
||||
const fail = '\u2717'.red
|
||||
const sumchecker = require('sumchecker')
|
||||
const temp = require('temp').track()
|
||||
const { URL } = require('url')
|
||||
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})
|
||||
const gitDir = path.resolve(__dirname, '..')
|
||||
|
||||
async function getDraftRelease (version, skipValidation) {
|
||||
let releaseInfo = await github.repos.getReleases({owner: 'electron', repo: 'electron'})
|
||||
let drafts
|
||||
let versionToCheck
|
||||
if (version) {
|
||||
versionToCheck = version
|
||||
} else {
|
||||
versionToCheck = pkgVersion
|
||||
}
|
||||
drafts = releaseInfo.data
|
||||
.filter(release => release.tag_name === versionToCheck &&
|
||||
release.draft === true)
|
||||
const draft = drafts[0]
|
||||
if (!skipValidation) {
|
||||
failureCount = 0
|
||||
check(drafts.length === 1, 'one draft exists', true)
|
||||
if (versionToCheck.indexOf('beta') > -1) {
|
||||
check(draft.prerelease, 'draft is a prerelease')
|
||||
}
|
||||
check(draft.body.length > 50 && !draft.body.includes('(placeholder)'), 'draft has release notes')
|
||||
check((failureCount === 0), `Draft release looks good to go.`, true)
|
||||
}
|
||||
return draft
|
||||
}
|
||||
|
||||
async function validateReleaseAssets (release) {
|
||||
const requiredAssets = assetsForVersion(release.tag_name).sort()
|
||||
const extantAssets = release.assets.map(asset => asset.name).sort()
|
||||
const downloadUrls = release.assets.map(asset => asset.browser_download_url).sort()
|
||||
|
||||
failureCount = 0
|
||||
requiredAssets.forEach(asset => {
|
||||
check(extantAssets.includes(asset), asset)
|
||||
})
|
||||
check((failureCount === 0), `All required GitHub assets exist for release`, true)
|
||||
|
||||
if (release.draft) {
|
||||
await verifyAssets(release)
|
||||
} else {
|
||||
await verifyShasums(downloadUrls)
|
||||
.catch(err => {
|
||||
console.log(`${fail} error verifyingShasums`, err)
|
||||
})
|
||||
}
|
||||
const s3Urls = s3UrlsForVersion(release.tag_name)
|
||||
await verifyShasums(s3Urls, true)
|
||||
}
|
||||
|
||||
function check (condition, statement, exitIfFail = false) {
|
||||
if (condition) {
|
||||
console.log(`${pass} ${statement}`)
|
||||
} else {
|
||||
failureCount++
|
||||
console.log(`${fail} ${statement}`)
|
||||
if (exitIfFail) process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
function assetsForVersion (version) {
|
||||
const patterns = [
|
||||
`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-armv7l-symbols.zip`,
|
||||
`electron-${version}-linux-armv7l.zip`,
|
||||
`electron-${version}-linux-ia32-symbols.zip`,
|
||||
`electron-${version}-linux-ia32.zip`,
|
||||
`electron-${version}-linux-x64-symbols.zip`,
|
||||
`electron-${version}-linux-x64.zip`,
|
||||
`electron-${version}-mas-x64-dsym.zip`,
|
||||
`electron-${version}-mas-x64-symbols.zip`,
|
||||
`electron-${version}-mas-x64.zip`,
|
||||
`electron-${version}-win32-ia32-pdb.zip`,
|
||||
`electron-${version}-win32-ia32-symbols.zip`,
|
||||
`electron-${version}-win32-ia32.zip`,
|
||||
`electron-${version}-win32-x64-pdb.zip`,
|
||||
`electron-${version}-win32-x64-symbols.zip`,
|
||||
`electron-${version}-win32-x64.zip`,
|
||||
`electron-api.json`,
|
||||
`electron.d.ts`,
|
||||
`ffmpeg-${version}-darwin-x64.zip`,
|
||||
`ffmpeg-${version}-linux-arm.zip`,
|
||||
`ffmpeg-${version}-linux-armv7l.zip`,
|
||||
`ffmpeg-${version}-linux-ia32.zip`,
|
||||
`ffmpeg-${version}-linux-x64.zip`,
|
||||
`ffmpeg-${version}-mas-x64.zip`,
|
||||
`ffmpeg-${version}-win32-ia32.zip`,
|
||||
`ffmpeg-${version}-win32-x64.zip`,
|
||||
`SHASUMS256.txt`
|
||||
]
|
||||
return patterns
|
||||
}
|
||||
|
||||
function s3UrlsForVersion (version) {
|
||||
const bucket = `https://gh-contractor-zcbenz.s3.amazonaws.com/`
|
||||
const patterns = [
|
||||
`${bucket}atom-shell/dist/${version}/iojs-${version}-headers.tar.gz`,
|
||||
`${bucket}atom-shell/dist/${version}/iojs-${version}.tar.gz`,
|
||||
`${bucket}atom-shell/dist/${version}/node-${version}.tar.gz`,
|
||||
`${bucket}atom-shell/dist/${version}/node.lib`,
|
||||
`${bucket}atom-shell/dist/${version}/win-x64/iojs.lib`,
|
||||
`${bucket}atom-shell/dist/${version}/win-x86/iojs.lib`,
|
||||
`${bucket}atom-shell/dist/${version}/x64/node.lib`,
|
||||
`${bucket}atom-shell/dist/${version}/SHASUMS.txt`,
|
||||
`${bucket}atom-shell/dist/${version}/SHASUMS256.txt`,
|
||||
`${bucket}atom-shell/dist/index.json`
|
||||
]
|
||||
return patterns
|
||||
}
|
||||
|
||||
function checkVersion () {
|
||||
console.log(`Verifying that app version matches package version ${pkgVersion}.`)
|
||||
let startScript = path.join(__dirname, 'start.py')
|
||||
let appVersion = runScript(startScript, ['--version']).trim()
|
||||
check((pkgVersion.indexOf(appVersion) === 0), `App version ${appVersion} matches ` +
|
||||
`package version ${pkgVersion}.`, true)
|
||||
}
|
||||
|
||||
function runScript (scriptName, scriptArgs, cwd) {
|
||||
let scriptCommand = `${scriptName} ${scriptArgs.join(' ')}`
|
||||
let scriptOptions = {
|
||||
encoding: 'UTF-8'
|
||||
}
|
||||
if (cwd) {
|
||||
scriptOptions.cwd = cwd
|
||||
}
|
||||
try {
|
||||
return execSync(scriptCommand, scriptOptions)
|
||||
} catch (err) {
|
||||
console.log(`${fail} Error running ${scriptName}`, err)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
function uploadNodeShasums () {
|
||||
console.log('Uploading Node SHASUMS file to S3.')
|
||||
let scriptPath = path.join(__dirname, 'upload-node-checksums.py')
|
||||
runScript(scriptPath, ['-v', pkgVersion])
|
||||
console.log(`${pass} Done uploading Node SHASUMS file to S3.`)
|
||||
}
|
||||
|
||||
function uploadIndexJson () {
|
||||
console.log('Uploading index.json to S3.')
|
||||
let scriptPath = path.join(__dirname, 'upload-index-json.py')
|
||||
runScript(scriptPath, [])
|
||||
console.log(`${pass} Done uploading index.json to S3.`)
|
||||
}
|
||||
|
||||
async function createReleaseShasums (release) {
|
||||
let fileName = 'SHASUMS256.txt'
|
||||
let existingAssets = release.assets.filter(asset => asset.name === fileName)
|
||||
if (existingAssets.length > 0) {
|
||||
console.log(`${fileName} already exists on GitHub; deleting before creating new file.`)
|
||||
await github.repos.deleteAsset({
|
||||
owner: 'electron',
|
||||
repo: 'electron',
|
||||
id: existingAssets[0].id
|
||||
}).catch(err => {
|
||||
console.log(`${fail} Error deleting ${fileName} on GitHub:`, err)
|
||||
})
|
||||
}
|
||||
console.log(`Creating and uploading the release ${fileName}.`)
|
||||
let scriptPath = path.join(__dirname, 'merge-electron-checksums.py')
|
||||
let checksums = runScript(scriptPath, ['-v', pkgVersion])
|
||||
console.log(`${pass} Generated release SHASUMS.`)
|
||||
let filePath = await saveShaSumFile(checksums, fileName)
|
||||
console.log(`${pass} Created ${fileName} file.`)
|
||||
await uploadShasumFile(filePath, fileName, release)
|
||||
console.log(`${pass} Successfully uploaded ${fileName} to GitHub.`)
|
||||
}
|
||||
|
||||
async function uploadShasumFile (filePath, fileName, release) {
|
||||
let githubOpts = {
|
||||
owner: 'electron',
|
||||
repo: 'electron',
|
||||
id: release.id,
|
||||
filePath,
|
||||
name: fileName
|
||||
}
|
||||
return github.repos.uploadAsset(githubOpts)
|
||||
.catch(err => {
|
||||
console.log(`${fail} Error uploading ${filePath} to GitHub:`, err)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
function saveShaSumFile (checksums, fileName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
temp.open(fileName, (err, info) => {
|
||||
if (err) {
|
||||
console.log(`${fail} Could not create ${fileName} file`)
|
||||
process.exit(1)
|
||||
} else {
|
||||
fs.writeFileSync(info.fd, checksums)
|
||||
fs.close(info.fd, (err) => {
|
||||
if (err) {
|
||||
console.log(`${fail} Could close ${fileName} file`)
|
||||
process.exit(1)
|
||||
}
|
||||
resolve(info.path)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function publishRelease (release) {
|
||||
let githubOpts = {
|
||||
owner: 'electron',
|
||||
repo: 'electron',
|
||||
id: release.id,
|
||||
tag_name: release.tag_name,
|
||||
draft: false
|
||||
}
|
||||
return github.repos.editRelease(githubOpts)
|
||||
.catch(err => {
|
||||
console.log(`${fail} Error publishing release:`, err)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
async function makeRelease (releaseToValidate) {
|
||||
if (releaseToValidate) {
|
||||
console.log(`Validating release ${args.validateRelease}`)
|
||||
let release = await getDraftRelease(args.validateRelease)
|
||||
await validateReleaseAssets(release)
|
||||
} else {
|
||||
checkVersion()
|
||||
let draftRelease = await getDraftRelease()
|
||||
uploadNodeShasums()
|
||||
uploadIndexJson()
|
||||
await createReleaseShasums(draftRelease)
|
||||
// Fetch latest version of release before verifying
|
||||
draftRelease = await getDraftRelease(pkgVersion, true)
|
||||
await validateReleaseAssets(draftRelease)
|
||||
await publishRelease(draftRelease)
|
||||
await cleanupReleaseBranch()
|
||||
console.log(`${pass} SUCCESS!!! Release has been published. Please run ` +
|
||||
`"npm run publish-to-npm" to publish release to npm.`)
|
||||
}
|
||||
}
|
||||
|
||||
async function makeTempDir () {
|
||||
return new Promise((resolve, reject) => {
|
||||
temp.mkdir('electron-publish', (err, dirPath) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(dirPath)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function verifyAssets (release) {
|
||||
let downloadDir = await makeTempDir()
|
||||
let githubOpts = {
|
||||
owner: 'electron',
|
||||
repo: 'electron',
|
||||
headers: {
|
||||
Accept: 'application/octet-stream'
|
||||
}
|
||||
}
|
||||
console.log(`Downloading files from GitHub to verify shasums`)
|
||||
let shaSumFile = 'SHASUMS256.txt'
|
||||
let filesToCheck = await Promise.all(release.assets.map(async (asset) => {
|
||||
githubOpts.id = asset.id
|
||||
let assetDetails = await github.repos.getAsset(githubOpts)
|
||||
await downloadFiles(assetDetails.meta.location, downloadDir, false, asset.name)
|
||||
return asset.name
|
||||
})).catch(err => {
|
||||
console.log(`${fail} Error downloading files from GitHub`, err)
|
||||
process.exit(1)
|
||||
})
|
||||
filesToCheck = filesToCheck.filter(fileName => fileName !== shaSumFile)
|
||||
let checkerOpts
|
||||
await validateChecksums({
|
||||
algorithm: 'sha256',
|
||||
filesToCheck,
|
||||
fileDirectory: downloadDir,
|
||||
shaSumFile,
|
||||
checkerOpts,
|
||||
fileSource: 'GitHub'
|
||||
})
|
||||
}
|
||||
|
||||
function downloadFiles (urls, directory, quiet, targetName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let nuggetOpts = {
|
||||
dir: directory
|
||||
}
|
||||
if (quiet) {
|
||||
nuggetOpts.quiet = quiet
|
||||
}
|
||||
if (targetName) {
|
||||
nuggetOpts.target = targetName
|
||||
}
|
||||
nugget(urls, nuggetOpts, (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function verifyShasums (urls, isS3) {
|
||||
let fileSource = isS3 ? 'S3' : 'GitHub'
|
||||
console.log(`Downloading files from ${fileSource} to verify shasums`)
|
||||
let downloadDir = await makeTempDir()
|
||||
let filesToCheck = []
|
||||
try {
|
||||
if (!isS3) {
|
||||
await downloadFiles(urls, downloadDir)
|
||||
filesToCheck = urls.map(url => {
|
||||
let currentUrl = new URL(url)
|
||||
return path.basename(currentUrl.pathname)
|
||||
}).filter(file => file.indexOf('SHASUMS') === -1)
|
||||
} else {
|
||||
const s3VersionPath = `/atom-shell/dist/${pkgVersion}/`
|
||||
await Promise.all(urls.map(async (url) => {
|
||||
let currentUrl = new URL(url)
|
||||
let dirname = path.dirname(currentUrl.pathname)
|
||||
let filename = path.basename(currentUrl.pathname)
|
||||
let s3VersionPathIdx = dirname.indexOf(s3VersionPath)
|
||||
if (s3VersionPathIdx === -1 || dirname === s3VersionPath) {
|
||||
if (s3VersionPathIdx !== -1 && filename.indexof('SHASUMS') === -1) {
|
||||
filesToCheck.push(filename)
|
||||
}
|
||||
await downloadFiles(url, downloadDir, true)
|
||||
} else {
|
||||
let subDirectory = dirname.substr(s3VersionPathIdx + s3VersionPath.length)
|
||||
let fileDirectory = path.join(downloadDir, subDirectory)
|
||||
try {
|
||||
fs.statSync(fileDirectory)
|
||||
} catch (err) {
|
||||
fs.mkdirSync(fileDirectory)
|
||||
}
|
||||
filesToCheck.push(path.join(subDirectory, filename))
|
||||
await downloadFiles(url, fileDirectory, true)
|
||||
}
|
||||
}))
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`${fail} Error downloading files from ${fileSource}`, err)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`${pass} Successfully downloaded the files from ${fileSource}.`)
|
||||
let checkerOpts
|
||||
if (isS3) {
|
||||
checkerOpts = { defaultTextEncoding: 'binary' }
|
||||
}
|
||||
|
||||
await validateChecksums({
|
||||
algorithm: 'sha256',
|
||||
filesToCheck,
|
||||
fileDirectory: downloadDir,
|
||||
shaSumFile: 'SHASUMS256.txt',
|
||||
checkerOpts,
|
||||
fileSource
|
||||
})
|
||||
|
||||
if (isS3) {
|
||||
await validateChecksums({
|
||||
algorithm: 'sha1',
|
||||
filesToCheck,
|
||||
fileDirectory: downloadDir,
|
||||
shaSumFile: 'SHASUMS.txt',
|
||||
checkerOpts,
|
||||
fileSource
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function validateChecksums (validationArgs) {
|
||||
console.log(`Validating checksums for files from ${validationArgs.fileSource} ` +
|
||||
`against ${validationArgs.shaSumFile}.`)
|
||||
let shaSumFilePath = path.join(validationArgs.fileDirectory, validationArgs.shaSumFile)
|
||||
let checker = new sumchecker.ChecksumValidator(validationArgs.algorithm,
|
||||
shaSumFilePath, validationArgs.checkerOpts)
|
||||
await checker.validate(validationArgs.fileDirectory, validationArgs.filesToCheck)
|
||||
.catch(err => {
|
||||
if (err instanceof sumchecker.ChecksumMismatchError) {
|
||||
console.error(`${fail} The checksum of ${err.filename} from ` +
|
||||
`${validationArgs.fileSource} did not match the shasum in ` +
|
||||
`${validationArgs.shaSumFile}`)
|
||||
} else if (err instanceof sumchecker.ChecksumParseError) {
|
||||
console.error(`${fail} The checksum file ${validationArgs.shaSumFile} ` +
|
||||
`from ${validationArgs.fileSource} could not be parsed.`, err)
|
||||
} else if (err instanceof sumchecker.NoChecksumFoundError) {
|
||||
console.error(`${fail} The file ${err.filename} from ` +
|
||||
`${validationArgs.fileSource} was not in the shasum file ` +
|
||||
`${validationArgs.shaSumFile}.`)
|
||||
} else {
|
||||
console.error(`${fail} Error matching files from ` +
|
||||
`${validationArgs.fileSource} shasums in ${validationArgs.shaSumFile}.`, err)
|
||||
}
|
||||
process.exit(1)
|
||||
})
|
||||
console.log(`${pass} All files from ${validationArgs.fileSource} match ` +
|
||||
`shasums defined in ${validationArgs.shaSumFile}.`)
|
||||
}
|
||||
|
||||
async function cleanupReleaseBranch () {
|
||||
console.log(`Cleaning up release branch.`)
|
||||
let errorMessage = `Could not delete local release branch.`
|
||||
let successMessage = `Successfully deleted local release branch.`
|
||||
await callGit(['branch', '-D', 'release-1-7-x'], errorMessage, successMessage)
|
||||
errorMessage = `Could not delete remote release branch.`
|
||||
successMessage = `Successfully deleted remote release branch.`
|
||||
return callGit(['push', 'origin', ':release'], errorMessage, successMessage)
|
||||
}
|
||||
|
||||
async function callGit (args, errorMessage, successMessage) {
|
||||
let gitResult = await GitProcess.exec(args, gitDir)
|
||||
if (gitResult.exitCode === 0) {
|
||||
console.log(`${pass} ${successMessage}`)
|
||||
return true
|
||||
} else {
|
||||
console.log(`${fail} ${errorMessage} ${gitResult.stderr}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
makeRelease(args.validateRelease)
|
||||
@@ -172,7 +172,7 @@ def get_text_with_editor(name):
|
||||
def create_or_get_release_draft(github, releases, tag, tag_exists):
|
||||
# Search for existing draft.
|
||||
for release in releases:
|
||||
if release['draft']:
|
||||
if release['draft'] and release['tag_name'] == tag:
|
||||
return release
|
||||
|
||||
if tag_exists:
|
||||
|
||||
Reference in New Issue
Block a user