Browse Source

Update to use new release scripts

John Kleinschmidt 7 years ago
parent
commit
34b75e5e73
8 changed files with 1189 additions and 116 deletions
  1. 42 27
      .circleci/config.yml
  2. 84 22
      script/bump-version.py
  3. 209 0
      script/ci-release-build.js
  4. 116 0
      script/merge-release.js
  5. 181 0
      script/prepare-release.js
  6. 459 0
      script/release.js
  7. 36 6
      script/upload-to-github.js
  8. 62 61
      script/upload.py

+ 42 - 27
.circleci/config.yml

@@ -3,20 +3,19 @@ version: 2
 jobs:
   electron-linux-arm:
     docker:
-      - image: electronbuilds/electron:0.0.3
+      - image: electronbuilds/electron:0.0.4
         environment:
           TARGET_ARCH: arm
-    resource_class: xlarge
+    resource_class: 2xlarge
     steps:
       - checkout
-      - run: sh -e /etc/init.d/xvfb start
       - run:
           name: Check for release
           command: |
-            MESSAGE="$(git log --format=%B -n 1 HEAD)"
-            case ${MESSAGE} in
-              Bump* ) echo 'export ELECTRON_RELEASE=1' >> $BASH_ENV
-            esac
+            if [ -n "${RUN_RELEASE_BUILD}" ]; then
+              echo 'release build triggered from api'
+              echo 'export ELECTRON_RELEASE=1 TRIGGERED_BY_API=1' >> $BASH_ENV
+            fi
       - run:
          name: Bootstrap
          command: |
@@ -50,29 +49,30 @@ jobs:
       - run:
           name: Upload distribution
           command: |
-             if [ "$ELECTRON_RELEASE" == "1" ]; then
-                echo 'Uploading Electron release distribution'
+             if [ "$ELECTRON_RELEASE" == "1" ] && [ "$TRIGGERED_BY_API" != "1" ]; then
+                echo 'Uploading Electron release distribution to github releases'
                 script/upload.py
+             elif [ "$ELECTRON_RELEASE" == "1" ] && [ "$TRIGGERED_BY_API" == "1" ]; then
+                echo 'Uploading Electron release distribution to s3'
+                script/upload.py --upload_to_s3
              else
                 echo 'Skipping upload distribution because build is not for release'
              fi
-
   electron-linux-ia32:
     docker:
-      - image: electronbuilds/electron:0.0.3
+      - image: electronbuilds/electron:0.0.4
         environment:
           TARGET_ARCH: ia32
     resource_class: xlarge
     steps:
       - checkout
-      - run: sh -e /etc/init.d/xvfb start
       - run:
           name: Check for release
           command: |
-            MESSAGE="$(git log --format=%B -n 1 HEAD)"
-            case ${MESSAGE} in
-              Bump* ) echo 'export ELECTRON_RELEASE=1' >> $BASH_ENV
-            esac
+            if [ -n "${RUN_RELEASE_BUILD}" ]; then
+              echo 'release build triggered from api'
+              echo 'export ELECTRON_RELEASE=1 TRIGGERED_BY_API=1' >> $BASH_ENV
+            fi
       - run:
          name: Bootstrap
          command: |
@@ -106,29 +106,34 @@ jobs:
       - run:
           name: Upload distribution
           command: |
-             if [ "$ELECTRON_RELEASE" == "1" ]; then
-                echo 'Uploading Electron release distribution'
+             if [ "$ELECTRON_RELEASE" == "1" ] && [ "$TRIGGERED_BY_API" != "1" ]; then
+                echo 'Uploading Electron release distribution to github releases'
                 script/upload.py
+             elif [ "$ELECTRON_RELEASE" == "1" ] && [ "$TRIGGERED_BY_API" == "1" ]; then
+                echo 'Uploading Electron release distribution to s3'
+                script/upload.py --upload_to_s3
              else
                 echo 'Skipping upload distribution because build is not for release'
              fi
-
   electron-linux-x64:
     docker:
-      - image: electronbuilds/electron:0.0.3
+      - image: electronbuilds/electron:0.0.4
         environment:
           TARGET_ARCH: x64
+          DISPLAY: ':99.0'
     resource_class: xlarge
     steps:
       - checkout
-      - run: sh -e /etc/init.d/xvfb start
+      - run:
+          name: Setup for headless testing
+          command: sh -e /etc/init.d/xvfb start
       - run:
           name: Check for release
           command: |
-            MESSAGE="$(git log --format=%B -n 1 HEAD)"
-            case ${MESSAGE} in
-              Bump* ) echo 'export ELECTRON_RELEASE=1' >> $BASH_ENV
-            esac
+            if [ -n "${RUN_RELEASE_BUILD}" ]; then
+              echo 'release build triggered from api'
+              echo 'export ELECTRON_RELEASE=1 TRIGGERED_BY_API=1' >> $BASH_ENV
+            fi
       - run:
          name: Bootstrap
          command: |
@@ -162,9 +167,12 @@ jobs:
       - run:
           name: Upload distribution
           command: |
-             if [ "$ELECTRON_RELEASE" == "1" ]; then
-                echo 'Uploading Electron release distribution'
+             if [ "$ELECTRON_RELEASE" == "1" ] && [ "$TRIGGERED_BY_API" != "1" ]; then
+                echo 'Uploading Electron release distribution to github releases'
                 script/upload.py
+             elif [ "$ELECTRON_RELEASE" == "1" ] && [ "$TRIGGERED_BY_API" == "1" ]; then
+                echo 'Uploading Electron release distribution to s3'
+                script/upload.py --upload_to_s3
              else
                 echo 'Skipping upload distribution because build is not for release'
              fi
@@ -190,10 +198,17 @@ jobs:
              else
                 echo 'Skipping verify ffmpeg on release build'
              fi
+      - run:
+          name: Generate Typescript Definitions
+          command: npm run create-typescript-definitions
       - store_test_results:
           path: junit
       - store_artifacts:
           path: junit
+      - store_artifacts:
+          path: out/electron.d.ts
+      - store_artifacts:
+          path: out/electron-api.json
 
 workflows:
   version: 2

+ 84 - 22
script/bump-version.py

@@ -3,6 +3,7 @@
 import os
 import re
 import sys
+import argparse
 
 from lib.util import execute, get_electron_version, parse_version, scoped_cwd
 
@@ -11,29 +12,85 @@ SOURCE_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
 
 
 def main():
-  if len(sys.argv) != 2 or sys.argv[1] == '-h':
-    print 'Usage: bump-version.py [<version> | major | minor | patch]'
+
+  parser = argparse.ArgumentParser(
+    description='Bump version numbers. Must specify at least one of the three'
+               +' options:\n'
+               +'   --bump=patch to increment patch version, or\n'
+               +'   --stable to promote current beta to stable, or\n'
+               +'   --version={version} to set version number directly\n'
+               +'Note that you can use both --bump and --stable '
+               +'simultaneously.',
+               formatter_class=argparse.RawTextHelpFormatter
+  )
+  parser.add_argument(
+    '--version',
+    default=None,
+    dest='new_version',
+    help='new version number'
+  )
+  parser.add_argument(
+    '--bump',
+    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)'
+  )
+  parser.add_argument(
+    '--dry-run',
+    action='store_true',
+    default= False,
+    dest='dry_run',
+    help='just to check that version number is correct'
+  )
+
+  args = parser.parse_args()
+
+  if args.new_version == None and args.bump == None and args.stable == False:
+    parser.print_help()
     return 1
 
-  option = sys.argv[1]
-  increments = ['major', 'minor', 'patch', 'build']
-  if option in increments:
-    version = get_electron_version()
-    versions = parse_version(version.split('-')[0])
-    versions = increase_version(versions, increments.index(option))
-  else:
-    versions = parse_version(option)
+  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]
+
+  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)
+    update_electron_gyp(version, suffix)
     update_win_rc(version, versions)
-    update_version_h(versions)
+    update_version_h(versions, suffix)
     update_info_plist(version)
-    update_package_json(version)
-    tag_version(version)
+    update_package_json(version, suffix)
+    tag_version(version, suffix)
 
+  print 'Bumped to version: {0}'.format(version + suffix)
 
 def increase_version(versions, index):
   for i in range(index + 1, 4):
@@ -42,14 +99,14 @@ def increase_version(versions, index):
   return versions
 
 
-def update_electron_gyp(version):
-  pattern = re.compile(" *'version%' *: *'[0-9.]+'")
+def update_electron_gyp(version, suffix):
+  pattern = re.compile(" *'version%' *: *'[0-9.]+(-beta[0-9.]*)?'")
   with open('electron.gyp', 'r') as f:
     lines = f.readlines()
 
   for i in range(0, len(lines)):
     if pattern.match(lines[i]):
-      lines[i] = "    'version%': '{0}',\n".format(version)
+      lines[i] = "    'version%': '{0}',\n".format(version + suffix)
       with open('electron.gyp', 'w') as f:
         f.write(''.join(lines))
       return
@@ -81,7 +138,7 @@ def update_win_rc(version, versions):
     f.write(''.join(lines))
 
 
-def update_version_h(versions):
+def update_version_h(versions, suffix):
   version_h = os.path.join('atom', 'common', 'atom_version.h')
   with open(version_h, 'r') as f:
     lines = f.readlines()
@@ -93,6 +150,11 @@ def update_version_h(versions):
       lines[i + 1] = '#define ATOM_MINOR_VERSION {0}\n'.format(versions[1])
       lines[i + 2] = '#define ATOM_PATCH_VERSION {0}\n'.format(versions[2])
 
+      if (suffix):
+        lines[i + 3] = '#define ATOM_PRE_RELEASE_VERSION {0}\n'.format(suffix)
+      else:
+        lines[i + 3] = '// #define ATOM_PRE_RELEASE_VERSION\n'
+
       with open(version_h, 'w') as f:
         f.write(''.join(lines))
       return
@@ -114,7 +176,7 @@ def update_info_plist(version):
     f.write(''.join(lines))
 
 
-def update_package_json(version):
+def update_package_json(version, suffix):
   package_json = 'package.json'
   with open(package_json, 'r') as f:
     lines = f.readlines()
@@ -122,15 +184,15 @@ def update_package_json(version):
   for i in range(0, len(lines)):
     line = lines[i];
     if 'version' in line:
-      lines[i] = '  "version": "{0}",\n'.format(version)
+      lines[i] = '  "version": "{0}",\n'.format(version + suffix)
       break
 
   with open(package_json, 'w') as f:
     f.write(''.join(lines))
 
 
-def tag_version(version):
-  execute(['git', 'commit', '-a', '-m', 'Bump v{0}'.format(version)])
+def tag_version(version, suffix):
+  execute(['git', 'commit', '-a', '-m', 'Bump v{0}'.format(version + suffix)])
 
 
 if __name__ == '__main__':

+ 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', '--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')
+    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()

+ 181 - 0
script/prepare-release.js

@@ -0,0 +1,181 @@
+#!/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' ], 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)
+  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', {
+    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)

+ 459 - 0
script/release.js

@@ -0,0 +1,459 @@
+#!/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) {
+    drafts = releaseInfo.data
+      .filter(release => release.tag_name === version)
+    versionToCheck = version
+  } else {
+    drafts = releaseInfo.data
+      .filter(release => release.draft)
+    versionToCheck = pkgVersion
+  }
+
+  const draft = drafts[0]
+  if (!skipValidation) {
+    failureCount = 0
+    check(drafts.length === 1, 'one draft exists', true)
+    check(draft.tag_name === versionToCheck, `draft release version matches local package.json (${versionToCheck})`)
+    if (versionToCheck.indexOf('beta')) {
+      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'], 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)

+ 36 - 6
script/upload-to-github.js

@@ -2,6 +2,10 @@ const GitHub = require('github')
 const github = new GitHub()
 github.authenticate({type: 'token', token: process.env.ELECTRON_GITHUB_TOKEN})
 
+if (process.argv.length < 5) {
+  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]
@@ -13,9 +17,35 @@ let githubOpts = {
   filePath: filePath,
   name: fileName
 }
-github.repos.uploadAsset(githubOpts).then(() => {
-  process.exit()
-}).catch((err) => {
-  console.log(`Error uploading ${fileName} to GitHub:`, err)
-  process.exitCode = 1
-})
+
+let retry = 0
+
+function uploadToGitHub () {
+  github.repos.uploadAsset(githubOpts).then(() => {
+    console.log(`Successfully uploaded ${fileName} to GitHub.`)
+    process.exit()
+  }).catch((err) => {
+    if (retry < 4) {
+      console.log(`Error uploading ${fileName} to GitHub, will retry.  Error was:`, err)
+      retry++
+      github.repos.getRelease(githubOpts).then(release => {
+        let existingAssets = release.data.assets.filter(asset => asset.name === fileName)
+        if (existingAssets.length > 0) {
+          console.log(`${fileName} already exists; will delete before retrying upload.`)
+          github.repos.deleteAsset({
+            owner: 'electron',
+            repo: 'electron',
+            id: existingAssets[0].id
+          }).then(uploadToGitHub).catch(uploadToGitHub)
+        } else {
+          uploadToGitHub()
+        }
+      })
+    } else {
+      console.log(`Error retrying uploading ${fileName} to GitHub:`, err)
+      process.exitCode = 1
+    }
+  })
+}
+
+uploadToGitHub()

+ 62 - 61
script/upload.py

@@ -36,70 +36,65 @@ PDB_NAME = get_zip_name(PROJECT_NAME, ELECTRON_VERSION, 'pdb')
 def main():
   args = parse_args()
 
-  if not args.publish_release:
-    if not dist_newer_than_head():
-      run_python_script('create-dist.py')
-
-    build_version = get_electron_build_version()
-    if not ELECTRON_VERSION.startswith(build_version):
-      error = 'Tag name ({0}) should match build version ({1})\n'.format(
-          ELECTRON_VERSION, build_version)
-      sys.stderr.write(error)
-      sys.stderr.flush()
-      return 1
+  if not dist_newer_than_head():
+    run_python_script('create-dist.py')
+
+  build_version = get_electron_build_version()
+  if not ELECTRON_VERSION.startswith(build_version):
+    error = 'Tag name ({0}) should match build version ({1})\n'.format(
+        ELECTRON_VERSION, build_version)
+    sys.stderr.write(error)
+    sys.stderr.flush()
+    return 1
 
   github = GitHub(auth_token())
   releases = github.repos(ELECTRON_REPO).releases.get()
   tag_exists = False
-  for release in releases:
-    if not release['draft'] and release['tag_name'] == args.version:
+  for r in releases:
+    if not r['draft'] and r['tag_name'] == args.version:
+      release = r
       tag_exists = True
       break
 
-  release = create_or_get_release_draft(github, releases, args.version,
-                                        tag_exists)
-
-  if args.publish_release:
-    # Upload the Node SHASUMS*.txt.
-    run_python_script('upload-node-checksums.py', '-v', ELECTRON_VERSION)
-
-    # Upload the index.json.
-    run_python_script('upload-index-json.py')
-
-    # Create and upload the Electron SHASUMS*.txt
-    release_electron_checksums(release)
-
-    # Press the publish button.
-    publish_release(github, release['id'])
-
-    # TODO: run publish-to-npm script here
-
-    # Do not upload other files when passed "-p".
-    return
+  if not args.upload_to_s3:
+    assert tag_exists == args.overwrite, \
+          'You have to pass --overwrite to overwrite a published release'
+    if not args.overwrite:
+      release = create_or_get_release_draft(github, releases, args.version,
+                                            tag_exists)
 
   # Upload Electron with GitHub Releases API.
-  upload_electron(github, release, os.path.join(DIST_DIR, DIST_NAME))
-  upload_electron(github, release, os.path.join(DIST_DIR, SYMBOLS_NAME))
+  upload_electron(github, release, os.path.join(DIST_DIR, DIST_NAME),
+                  args.upload_to_s3)
+  if get_target_arch() != 'mips64el':
+    upload_electron(github, release, os.path.join(DIST_DIR, SYMBOLS_NAME),
+                    args.upload_to_s3)
   if PLATFORM == 'darwin':
     upload_electron(github, release, os.path.join(DIST_DIR,
-                    'electron-api.json'))
-    upload_electron(github, release, os.path.join(DIST_DIR, 'electron.d.ts'))
-    upload_electron(github, release, os.path.join(DIST_DIR, DSYM_NAME))
+                    'electron-api.json'), args.upload_to_s3)
+    upload_electron(github, release, os.path.join(DIST_DIR, 'electron.d.ts'),
+                    args.upload_to_s3)
+    upload_electron(github, release, os.path.join(DIST_DIR, DSYM_NAME),
+                    args.upload_to_s3)
   elif PLATFORM == 'win32':
-    upload_electron(github, release, os.path.join(DIST_DIR, PDB_NAME))
+    upload_electron(github, release, os.path.join(DIST_DIR, PDB_NAME),
+                    args.upload_to_s3)
 
   # Upload free version of ffmpeg.
   ffmpeg = get_zip_name('ffmpeg', ELECTRON_VERSION)
-  upload_electron(github, release, os.path.join(DIST_DIR, ffmpeg))
+  upload_electron(github, release, os.path.join(DIST_DIR, ffmpeg),
+                  args.upload_to_s3)
 
   # Upload chromedriver and mksnapshot for minor version update.
   if parse_version(args.version)[2] == '0':
     chromedriver = get_zip_name('chromedriver', ELECTRON_VERSION)
-    upload_electron(github, release, os.path.join(DIST_DIR, chromedriver))
+    upload_electron(github, release, os.path.join(DIST_DIR, chromedriver),
+                    args.upload_to_s3)
     mksnapshot = get_zip_name('mksnapshot', ELECTRON_VERSION)
-    upload_electron(github, release, os.path.join(DIST_DIR, mksnapshot))
+    upload_electron(github, release, os.path.join(DIST_DIR, mksnapshot),
+                    args.upload_to_s3)
 
-  if PLATFORM == 'win32' and not tag_exists:
+  if PLATFORM == 'win32' and not tag_exists and not args.upload_to_s3:
     # Upload PDBs to Windows symbol server.
     run_python_script('upload-windows-pdb.py')
 
@@ -112,9 +107,18 @@ def parse_args():
   parser = argparse.ArgumentParser(description='upload distribution file')
   parser.add_argument('-v', '--version', help='Specify the version',
                       default=ELECTRON_VERSION)
+  parser.add_argument('-o', '--overwrite',
+                      help='Overwrite a published release',
+                      action='store_true')
   parser.add_argument('-p', '--publish-release',
                       help='Publish the release',
                       action='store_true')
+  parser.add_argument('-s', '--upload_to_s3',
+                      help='Upload assets to s3 bucket',
+                      dest='upload_to_s3',
+                      action='store_true',
+                      default=False,
+                      required=False)
   return parser.parse_args()
 
 
@@ -124,7 +128,7 @@ def run_python_script(script, *args):
 
 
 def get_electron_build_version():
-  if get_target_arch() == 'arm' or os.environ.has_key('CI'):
+  if get_target_arch().startswith('arm') or os.environ.has_key('CI'):
     # In CI we just build as told.
     return ELECTRON_VERSION
   if PLATFORM == 'darwin':
@@ -198,17 +202,17 @@ def create_release_draft(github, tag):
   return r
 
 
-def release_electron_checksums(release):
-  checksums = run_python_script('merge-electron-checksums.py',
-                                '-v', ELECTRON_VERSION)
-  filename = 'SHASUMS256.txt'
-  filepath = os.path.join(SOURCE_ROOT, filename)
-  with open(filepath, 'w') as sha_file:
-      sha_file.write(checksums.decode('utf-8'))
-  upload_io_to_github(release, filename, filepath)
+def upload_electron(github, release, file_path, upload_to_s3):
 
+  # if upload_to_s3 is set, skip github upload.
+  if upload_to_s3:
+    bucket, access_key, secret_key = s3_config()
+    key_prefix = 'electron-artifacts/{0}'.format(release['tag_name'])
+    s3put(bucket, access_key, secret_key, os.path.dirname(file_path),
+          key_prefix, [file_path])
+    upload_sha256_checksum(release['tag_name'], file_path, key_prefix)
+    return
 
-def upload_electron(github, release, file_path):
   # Delete the original file before uploading in CI.
   filename = os.path.basename(file_path)
   if os.environ.has_key('CI'):
@@ -231,7 +235,7 @@ def upload_electron(github, release, file_path):
     arm_filename = filename.replace('armv7l', 'arm')
     arm_file_path = os.path.join(os.path.dirname(file_path), arm_filename)
     shutil.copy2(file_path, arm_file_path)
-    upload_electron(github, release, arm_file_path)
+    upload_electron(github, release, arm_file_path, upload_to_s3)
 
 
 def upload_io_to_github(release, filename, filepath):
@@ -241,9 +245,11 @@ def upload_io_to_github(release, filename, filepath):
   execute(['node', script_path, filepath, filename, str(release['id'])])
 
 
-def upload_sha256_checksum(version, file_path):
+def upload_sha256_checksum(version, file_path, key_prefix=None):
   bucket, access_key, secret_key = s3_config()
   checksum_path = '{}.sha256sum'.format(file_path)
+  if key_prefix is None:
+    key_prefix = 'atom-shell/tmp/{0}'.format(version)
   sha256 = hashlib.sha256()
   with open(file_path, 'rb') as f:
     sha256.update(f.read())
@@ -252,12 +258,7 @@ def upload_sha256_checksum(version, file_path):
   with open(checksum_path, 'w') as checksum:
     checksum.write('{} *{}'.format(sha256.hexdigest(), filename))
   s3put(bucket, access_key, secret_key, os.path.dirname(checksum_path),
-        'atom-shell/tmp/{0}'.format(version), [checksum_path])
-
-
-def publish_release(github, release_id):
-  data = dict(draft=False)
-  github.repos(ELECTRON_REPO).releases(release_id).patch(data=data)
+        key_prefix, [checksum_path])
 
 
 def auth_token():