Browse Source

Allow multiple releases to run at once

Find draft release by draft flag and tag name;also backport release scripts to 1-6-x
John Kleinschmidt 7 years ago
parent
commit
6360acb87b
6 changed files with 972 additions and 4 deletions
  1. 8 3
      package.json
  2. 209 0
      script/ci-release-build.js
  3. 116 0
      script/merge-release.js
  4. 182 0
      script/prepare-release.js
  5. 456 0
      script/release.js
  6. 1 1
      script/upload.py

+ 8 - 3
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 - 0
script/ci-release-build.js

@@ -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 - 0
script/merge-release.js

@@ -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 - 0
script/prepare-release.js

@@ -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 - 0
script/release.js

@@ -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)

+ 1 - 1
script/upload.py

@@ -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: