diff --git a/.travis.yml b/.travis.yml index e127aa499..9c182db8d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ matrix: include: - os: linux dist: trusty - env: NODE_VERSION=6.9.4 DISPLAY=:99.0 CC=clang CXX=clang++ npm_config_clang=1 + env: NODE_VERSION=8.9.3 DISPLAY=:99.0 CC=clang CXX=clang++ npm_config_clang=1 sudo: required diff --git a/appveyor.yml b/appveyor.yml index ad7d47787..262063dbd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,7 +20,7 @@ environment: global: ATOM_DEV_RESOURCE_PATH: c:\projects\atom TEST_JUNIT_XML_ROOT: c:\projects\junit-test-results - NODE_VERSION: 6.9.4 + NODE_VERSION: 8.9.3 matrix: - TASK: test @@ -36,7 +36,6 @@ install: - IF NOT EXIST %TEST_JUNIT_XML_ROOT% MKDIR %TEST_JUNIT_XML_ROOT% - SET PATH=C:\Program Files\Atom\resources\cli;%PATH% - ps: Install-Product node $env:NODE_VERSION $env:PLATFORM - - npm install -g npm@5.3.0 build_script: - CD %APPVEYOR_BUILD_FOLDER% diff --git a/atom.sh b/atom.sh index 7aefe6a24..a1c96abeb 100755 --- a/atom.sh +++ b/atom.sh @@ -13,6 +13,9 @@ case $(basename $0) in atom-beta) CHANNEL=beta ;; + atom-nightly) + CHANNEL=nightly + ;; atom-dev) CHANNEL=dev ;; @@ -78,6 +81,10 @@ if [ $OS == 'Mac' ]; then if [ "$CHANNEL" == 'beta' ]; then ATOM_EXECUTABLE_NAME="Atom Beta" + elif [ "$CHANNEL" == 'nightly' ]; then + ATOM_EXECUTABLE_NAME="Atom Nightly" + elif [ "$CHANNEL" == 'dev' ]; then + ATOM_EXECUTABLE_NAME="Atom Dev" else ATOM_EXECUTABLE_NAME="Atom" fi @@ -114,6 +121,9 @@ elif [ $OS == 'Linux' ]; then beta) ATOM_PATH="$USR_DIRECTORY/share/atom-beta/atom" ;; + nightly) + ATOM_PATH="$USR_DIRECTORY/share/atom-nightly/atom" + ;; dev) ATOM_PATH="$USR_DIRECTORY/share/atom-dev/atom" ;; diff --git a/circle.yml b/circle.yml index b5791e7ad..e0cb18a37 100644 --- a/circle.yml +++ b/circle.yml @@ -17,9 +17,8 @@ general: dependencies: pre: - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.3/install.sh | bash - - nvm install 6.9.4 - - nvm use 6.9.4 - - npm install -g npm@5.3.0 + - nvm install 8.9.3 + - nvm use 8.9.3 override: - script/build --code-sign --compress-artifacts diff --git a/docs/rfcs/002-atom-nightly-releases.md b/docs/rfcs/002-atom-nightly-releases.md index cfd77f204..5bb72c8ff 100644 --- a/docs/rfcs/002-atom-nightly-releases.md +++ b/docs/rfcs/002-atom-nightly-releases.md @@ -2,7 +2,7 @@ ## Status -Accepted +Implemented in PR [#17538](https://github.com/atom/atom/pull/17538) ## Summary diff --git a/package.json b/package.json index be645ff2d..ed1343ea3 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/atom/atom/issues" }, "license": "MIT", - "electronVersion": "2.0.4", + "electronVersion": "2.0.5", "dependencies": { "@atom/nsfw": "^1.0.18", "@atom/watcher": "1.0.3", @@ -91,7 +91,7 @@ "one-light-syntax": "1.8.3", "solarized-dark-syntax": "1.1.5", "solarized-light-syntax": "1.1.5", - "about": "1.9.1", + "about": "1.10.0", "archive-view": "0.65.1", "autocomplete-atom-api": "0.10.7", "autocomplete-css": "0.17.5", @@ -111,7 +111,7 @@ "exception-reporting": "0.43.1", "find-and-replace": "0.215.11", "fuzzy-finder": "1.8.2", - "github": "0.17.2", + "github": "0.17.3", "git-diff": "1.3.9", "go-to-line": "0.33.0", "grammar-selector": "0.50.1", @@ -127,7 +127,7 @@ "package-generator": "1.3.0", "settings-view": "0.255.0", "snippets": "1.3.3", - "spell-check": "0.73.5", + "spell-check": "0.74.0", "status-bar": "1.8.15", "styleguide": "0.49.11", "symbols-view": "0.118.2", diff --git a/resources/app-icons/nightly/atom.icns b/resources/app-icons/nightly/atom.icns new file mode 100644 index 000000000..e49166fe2 Binary files /dev/null and b/resources/app-icons/nightly/atom.icns differ diff --git a/resources/app-icons/nightly/atom.ico b/resources/app-icons/nightly/atom.ico new file mode 100644 index 000000000..158a02f0f Binary files /dev/null and b/resources/app-icons/nightly/atom.ico differ diff --git a/resources/app-icons/nightly/png/1024.png b/resources/app-icons/nightly/png/1024.png new file mode 100644 index 000000000..b81ab2a2e Binary files /dev/null and b/resources/app-icons/nightly/png/1024.png differ diff --git a/resources/app-icons/nightly/png/128.png b/resources/app-icons/nightly/png/128.png new file mode 100644 index 000000000..374043550 Binary files /dev/null and b/resources/app-icons/nightly/png/128.png differ diff --git a/resources/app-icons/nightly/png/16.png b/resources/app-icons/nightly/png/16.png new file mode 100644 index 000000000..582356487 Binary files /dev/null and b/resources/app-icons/nightly/png/16.png differ diff --git a/resources/app-icons/nightly/png/24.png b/resources/app-icons/nightly/png/24.png new file mode 100644 index 000000000..32f6999cf Binary files /dev/null and b/resources/app-icons/nightly/png/24.png differ diff --git a/resources/app-icons/nightly/png/256.png b/resources/app-icons/nightly/png/256.png new file mode 100644 index 000000000..89618fec4 Binary files /dev/null and b/resources/app-icons/nightly/png/256.png differ diff --git a/resources/app-icons/nightly/png/32.png b/resources/app-icons/nightly/png/32.png new file mode 100644 index 000000000..a83e9a4de Binary files /dev/null and b/resources/app-icons/nightly/png/32.png differ diff --git a/resources/app-icons/nightly/png/48.png b/resources/app-icons/nightly/png/48.png new file mode 100644 index 000000000..3637315e4 Binary files /dev/null and b/resources/app-icons/nightly/png/48.png differ diff --git a/resources/app-icons/nightly/png/512.png b/resources/app-icons/nightly/png/512.png new file mode 100644 index 000000000..7afe3fbe3 Binary files /dev/null and b/resources/app-icons/nightly/png/512.png differ diff --git a/resources/app-icons/nightly/png/64.png b/resources/app-icons/nightly/png/64.png new file mode 100644 index 000000000..2403b7b01 Binary files /dev/null and b/resources/app-icons/nightly/png/64.png differ diff --git a/script/build b/script/build index beeeaecf9..8f3f9eb24 100755 --- a/script/build +++ b/script/build @@ -104,8 +104,10 @@ if (!argv.generateApiDocs) { } if (argv.createWindowsInstaller) { return createWindowsInstaller(packagedAppPath) - .then(() => argv.codeSign && codeSignOnWindows([ path.join(CONFIG.buildOutputPath, 'AtomSetup.exe') ])) - .then(() => packagedAppPath) + .then((installerPath) => { + argv.codeSign && codeSignOnWindows([installerPath]) + return packagedAppPath + }) } else { console.log('Skipping creating installer. Specify the --create-windows-installer option to create a Squirrel-based Windows installer.'.gray) } diff --git a/script/config.js b/script/config.js index 1dd670702..187114c4a 100644 --- a/script/config.js +++ b/script/config.js @@ -5,6 +5,7 @@ const fs = require('fs') const path = require('path') +const spawnSync = require('./lib/spawn-sync') const repositoryRootPath = path.resolve(__dirname, '..') const apmRootPath = path.join(repositoryRootPath, 'apm') @@ -19,12 +20,16 @@ const atomHomeDirPath = process.env.ATOM_HOME || path.join(homeDirPath, '.atom') const appMetadata = require(path.join(repositoryRootPath, 'package.json')) const apmMetadata = require(path.join(apmRootPath, 'package.json')) -const channel = getChannel() +const computedAppVersion = computeAppVersion(process.env.ATOM_RELEASE_VERSION || appMetadata.version) +const channel = getChannel(computedAppVersion) +const appName = getAppName(channel) module.exports = { appMetadata, apmMetadata, channel, + appName, + computedAppVersion, repositoryRootPath, apmRootPath, scriptRootPath, @@ -40,14 +45,30 @@ module.exports = { snapshotAuxiliaryData: {} } -function getChannel () { - if (appMetadata.version.match(/dev/)) { - return 'dev' - } else if (appMetadata.version.match(/beta/)) { - return 'beta' - } else { - return 'stable' +function getChannel (version) { + const match = version.match(/\d+\.\d+\.\d+(-([a-z]+)(\d+|-\w{4,})?)?$/) + if (!match) { + throw new Error(`Found incorrectly formatted Atom version ${version}`) + } else if (match[2]) { + return match[2] } + + return 'stable' +} + +function getAppName (channel) { + return channel === 'stable' + ? 'Atom' + : `Atom ${process.env.ATOM_CHANNEL_DISPLAY_NAME || channel.charAt(0).toUpperCase() + channel.slice(1)}` +} + +function computeAppVersion (version) { + if (version.match(/-dev$/)) { + const result = spawnSync('git', ['rev-parse', '--short', 'HEAD'], {cwd: repositoryRootPath}) + const commitHash = result.stdout.toString().trim() + version += '-' + commitHash + } + return version } function getApmBinPath () { diff --git a/script/lib/clean-package-lock.js b/script/lib/clean-package-lock.js index 01376c9c5..546ef069d 100644 --- a/script/lib/clean-package-lock.js +++ b/script/lib/clean-package-lock.js @@ -10,7 +10,7 @@ const path = require('path') module.exports = function () { console.log('Deleting problematic package-lock.json files') - let paths = glob.sync(path.join(CONFIG.repositoryRootPath, '**', 'package-lock.json'), {ignore: path.join('**', 'node_modules', '**')}) + let paths = glob.sync(path.join(CONFIG.repositoryRootPath, '**', 'package-lock.json'), {ignore: [path.join('**', 'node_modules', '**'), path.join('**', 'vsts', '**')]}) for (let path of paths) { fs.unlinkSync(path) diff --git a/script/lib/code-sign-on-mac.js b/script/lib/code-sign-on-mac.js index a97438835..1d8243808 100644 --- a/script/lib/code-sign-on-mac.js +++ b/script/lib/code-sign-on-mac.js @@ -16,6 +16,36 @@ module.exports = function (packagedAppPath) { downloadFileFromGithub(process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath) } try { + console.log(`Ensuring keychain ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN} exists`) + try { + spawnSync('security', [ + 'show-keychain-info', + process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN + ], {stdio: 'inherit'}) + } catch (err) { + console.log(`Creating keychain ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN}`) + // The keychain doesn't exist, try to create it + spawnSync('security', [ + 'create-keychain', + '-p', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD, + process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN + ], {stdio: 'inherit'}) + + // List the keychain to "activate" it. Somehow this seems + // to be needed otherwise the signing operation fails + spawnSync('security', [ + 'list-keychains', + '-s', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN + ], {stdio: 'inherit'}) + + // Make sure it doesn't time out before we use it + spawnSync('security', [ + 'set-keychain-settings', + '-t', '3600', + '-u', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN + ], {stdio: 'inherit'}) + } + console.log(`Unlocking keychain ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN}`) const unlockArgs = ['unlock-keychain'] // For signing on local workstations, password could be entered interactively diff --git a/script/lib/compress-artifacts.js b/script/lib/compress-artifacts.js index 7d9125925..74f31313f 100644 --- a/script/lib/compress-artifacts.js +++ b/script/lib/compress-artifacts.js @@ -3,6 +3,7 @@ const fs = require('fs-extra') const path = require('path') const spawnSync = require('./spawn-sync') +const { path7za } = require('7zip-bin') const CONFIG = require('../config') @@ -19,7 +20,7 @@ module.exports = function (packagedAppPath) { function getArchiveName () { switch (process.platform) { case 'darwin': return 'atom-mac.zip' - case 'win32': return 'atom-windows.zip' + case 'win32': return `atom-${process.arch === 'x64' ? 'x64-' : ''}windows.zip` default: return `atom-${getLinuxArchiveArch()}.tar.gz` } } @@ -44,7 +45,7 @@ function compress (inputDirPath, outputArchivePath) { compressCommand = 'zip' compressArguments = ['-r', '--symlinks'] } else if (process.platform === 'win32') { - compressCommand = '7z.exe' + compressCommand = path7za compressArguments = ['a', '-r'] } else { compressCommand = 'tar' diff --git a/script/lib/create-debian-package.js b/script/lib/create-debian-package.js index 120463f7b..1aa179b70 100644 --- a/script/lib/create-debian-package.js +++ b/script/lib/create-debian-package.js @@ -10,9 +10,8 @@ const CONFIG = require('../config') module.exports = function (packagedAppPath) { console.log(`Creating Debian package for "${packagedAppPath}"`) - const atomExecutableName = CONFIG.channel === 'beta' ? 'atom-beta' : 'atom' - const apmExecutableName = CONFIG.channel === 'beta' ? 'apm-beta' : 'apm' - const appName = CONFIG.channel === 'beta' ? 'Atom Beta' : 'Atom' + const atomExecutableName = CONFIG.channel === 'stable' ? 'atom' : `atom-${CONFIG.channel}` + const apmExecutableName = CONFIG.channel === 'stable' ? 'apm' : `apm-${CONFIG.channel}` const appDescription = CONFIG.appMetadata.description const appVersion = CONFIG.appMetadata.version let arch @@ -88,7 +87,7 @@ module.exports = function (packagedAppPath) { console.log(`Writing desktop entry file into "${debianPackageApplicationsDirPath}"`) const desktopEntryTemplate = fs.readFileSync(path.join(CONFIG.repositoryRootPath, 'resources', 'linux', 'atom.desktop.in')) const desktopEntryContents = template(desktopEntryTemplate)({ - appName: appName, + appName: CONFIG.appName, appFileName: atomExecutableName, description: appDescription, installDir: '/usr', diff --git a/script/lib/create-rpm-package.js b/script/lib/create-rpm-package.js index f76385aab..cdef23300 100644 --- a/script/lib/create-rpm-package.js +++ b/script/lib/create-rpm-package.js @@ -10,9 +10,9 @@ const CONFIG = require('../config') module.exports = function (packagedAppPath) { console.log(`Creating rpm package for "${packagedAppPath}"`) - const atomExecutableName = CONFIG.channel === 'beta' ? 'atom-beta' : 'atom' - const apmExecutableName = CONFIG.channel === 'beta' ? 'apm-beta' : 'apm' - const appName = CONFIG.channel === 'beta' ? 'Atom Beta' : 'Atom' + const atomExecutableName = CONFIG.channel === 'stable' ? 'atom' : `atom-${CONFIG.channel}` + const apmExecutableName = CONFIG.channel === 'stable' ? 'apm' : `apm-${CONFIG.channel}` + const appName = CONFIG.appName const appDescription = CONFIG.appMetadata.description // RPM versions can't have dashes or tildes in them. // (Ref.: https://twiki.cern.ch/twiki/bin/view/Main/RPMAndDebVersioning) diff --git a/script/lib/create-windows-installer.js b/script/lib/create-windows-installer.js index ddc46d484..447d6aa16 100644 --- a/script/lib/create-windows-installer.js +++ b/script/lib/create-windows-installer.js @@ -1,7 +1,7 @@ 'use strict' const electronInstaller = require('electron-winstaller') -const fs = require('fs-extra') +const fs = require('fs') const glob = require('glob') const path = require('path') @@ -16,17 +16,31 @@ module.exports = (packagedAppPath) => { loadingGif: path.join(CONFIG.repositoryRootPath, 'resources', 'win', 'loading.gif'), outputDirectory: CONFIG.buildOutputPath, noMsi: true, - remoteReleases: `https://atom.io/api/updates${archSuffix}?version=${CONFIG.appMetadata.version}`, + remoteReleases: `https://atom.io/api/updates${archSuffix}?version=${CONFIG.computedAppVersion}`, + setupExe: `AtomSetup${process.arch === 'x64' ? '-x64' : ''}.exe`, setupIcon: path.join(CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'atom.ico') } const cleanUp = () => { - for (let nupkgPath of glob.sync(`${CONFIG.buildOutputPath}/*.nupkg`)) { + const releasesPath = `${CONFIG.buildOutputPath}/RELEASES` + if (process.arch === 'x64' && fs.existsSync(releasesPath)) { + fs.renameSync(releasesPath, `${releasesPath}-x64`) + } + + for (let nupkgPath of glob.sync(`${CONFIG.buildOutputPath}/atom-*.nupkg`)) { if (!nupkgPath.includes(CONFIG.appMetadata.version)) { console.log(`Deleting downloaded nupkg for previous version at ${nupkgPath} to prevent it from being stored as an artifact`) - fs.removeSync(nupkgPath) + fs.unlinkSync(nupkgPath) + } else { + if (process.arch === 'x64') { + // Use the original .nupkg filename to generate the `atom-x64` name by inserting `-x64` after `atom` + const newNupkgPath = nupkgPath.replace('atom-', 'atom-x64-') + fs.renameSync(nupkgPath, newNupkgPath) + } } } + + return `${CONFIG.buildOutputPath}/${options.setupExe}` } console.log(`Creating Windows Installer for ${packagedAppPath}`) diff --git a/script/lib/generate-metadata.js b/script/lib/generate-metadata.js index 6bcd18618..ae61b6633 100644 --- a/script/lib/generate-metadata.js +++ b/script/lib/generate-metadata.js @@ -6,7 +6,6 @@ const fs = require('fs-plus') const normalizePackageData = require('normalize-package-data') const path = require('path') const semver = require('semver') -const spawnSync = require('./spawn-sync') const CONFIG = require('../config') @@ -16,7 +15,7 @@ module.exports = function () { CONFIG.appMetadata._atomMenu = buildPlatformMenuMetadata() CONFIG.appMetadata._atomKeymaps = buildPlatformKeymapsMetadata() CONFIG.appMetadata._deprecatedPackages = deprecatedPackagesMetadata - CONFIG.appMetadata.version = computeAppVersion() + CONFIG.appMetadata.version = CONFIG.computedAppVersion checkDeprecatedPackagesMetadata() fs.writeFileSync(path.join(CONFIG.intermediateAppPath, 'package.json'), JSON.stringify(CONFIG.appMetadata)) } @@ -162,13 +161,3 @@ function checkDeprecatedPackagesMetadata () { } } } - -function computeAppVersion () { - let version = CONFIG.appMetadata.version - if (CONFIG.channel === 'dev') { - const result = spawnSync('git', ['rev-parse', '--short', 'HEAD'], {cwd: CONFIG.repositoryRootPath}) - const commitHash = result.stdout.toString().trim() - version += '-' + commitHash - } - return version -} diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index d88b95196..bdf8b6599 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -74,7 +74,7 @@ module.exports = function (packagedAppPath) { const verifySnapshotScriptPath = path.join(CONFIG.repositoryRootPath, 'script', 'verify-snapshot-script') let nodeBundledInElectronPath if (process.platform === 'darwin') { - const executableName = CONFIG.channel === 'beta' ? 'Atom Beta' : 'Atom' + const executableName = CONFIG.appName nodeBundledInElectronPath = path.join(packagedAppPath, 'Contents', 'MacOS', executableName) } else if (process.platform === 'win32') { nodeBundledInElectronPath = path.join(packagedAppPath, 'atom.exe') diff --git a/script/lib/package-application.js b/script/lib/package-application.js index 842883e51..1b3c19b28 100644 --- a/script/lib/package-application.js +++ b/script/lib/package-application.js @@ -114,7 +114,7 @@ function buildAsarUnpackGlobExpression () { function getAppName () { if (process.platform === 'darwin') { - return CONFIG.channel === 'beta' ? 'Atom Beta' : 'Atom' + return CONFIG.appName } else { return 'atom' } @@ -156,7 +156,7 @@ function renamePackagedAppDir (packageOutputDirPath) { if (fs.existsSync(packagedAppPath)) fs.removeSync(packagedAppPath) fs.renameSync(path.join(packageOutputDirPath, appBundleName), packagedAppPath) } else if (process.platform === 'linux') { - const appName = CONFIG.channel === 'beta' ? 'atom-beta' : 'atom' + const appName = CONFIG.channel !== 'stable' ? `atom-${CONFIG.channel}` : 'atom' let architecture if (process.arch === 'ia32') { architecture = 'i386' @@ -169,8 +169,7 @@ function renamePackagedAppDir (packageOutputDirPath) { if (fs.existsSync(packagedAppPath)) fs.removeSync(packagedAppPath) fs.renameSync(packageOutputDirPath, packagedAppPath) } else { - const appName = CONFIG.channel === 'beta' ? 'Atom Beta' : 'Atom' - packagedAppPath = path.join(CONFIG.buildOutputPath, appName) + packagedAppPath = path.join(CONFIG.buildOutputPath, CONFIG.appName) if (process.platform === 'win32' && process.arch !== 'ia32') { packagedAppPath += ` ${process.arch}` } diff --git a/script/lib/upload-to-s3.js b/script/lib/upload-to-s3.js new file mode 100644 index 000000000..91c50b384 --- /dev/null +++ b/script/lib/upload-to-s3.js @@ -0,0 +1,57 @@ +'use strict' + +const fs = require('fs') +const path = require('path') +const aws = require('aws-sdk') + +module.exports = function (s3Key, s3Secret, s3Bucket, directory, assets) { + const s3 = new aws.S3({ + accessKeyId: s3Key, + secretAccessKey: s3Secret, + params: { Bucket: s3Bucket } + }) + + function listExistingAssetsForDirectory (directory) { + return s3.listObjectsV2({ Prefix: directory }).promise().then((res) => { + return res.Contents.map((obj) => { return { Key: obj.Key } }) + }) + } + + function deleteExistingAssets (existingAssets) { + if (existingAssets.length > 0) { + return s3.deleteObjects({ Delete: { Objects: existingAssets } }).promise() + } else { + return Promise.resolve(true) + } + } + + function uploadAssets (assets, directory) { + return assets.reduce( + function (promise, asset) { + return promise.then(() => uploadAsset(directory, asset)) + }, Promise.resolve()) + } + + function uploadAsset (directory, assetPath) { + return new Promise((resolve, reject) => { + console.info(`Uploading ${assetPath}`) + const params = { + Key: `${directory}${path.basename(assetPath)}`, + ACL: 'public-read', + Body: fs.createReadStream(assetPath) + } + + s3.upload(params, error => { + if (error) { + reject(error) + } else { + resolve() + } + }) + }) + } + + return listExistingAssetsForDirectory(directory) + .then(deleteExistingAssets) + .then(() => uploadAssets(assets, directory)) +} diff --git a/script/package.json b/script/package.json index dbc5d69c6..697f289fa 100644 --- a/script/package.json +++ b/script/package.json @@ -2,7 +2,9 @@ "name": "atom-build-scripts", "description": "Atom build scripts", "dependencies": { + "7zip-bin": "^4.0.2", "async": "2.0.1", + "aws-sdk": "^2.5.2", "babel-core": "5.8.38", "coffeelint": "1.15.7", "colors": "1.1.2", @@ -26,6 +28,7 @@ "npm": "5.3.0", "passwd-user": "2.1.0", "pegjs": "0.9.0", + "publish-release": "^1.6.0", "random-seed": "^0.3.0", "season": "5.3.0", "semver": "5.3.0", diff --git a/script/publish-release b/script/publish-release new file mode 100644 index 000000000..47ef55a9b --- /dev/null +++ b/script/publish-release @@ -0,0 +1,52 @@ +#!/usr/bin/env node + +'use strict' + +const path = require('path') +const glob = require('glob') +const publishRelease = require('publish-release') +const uploadToS3 = require('./lib/upload-to-s3') +const CONFIG = require('./config') + +const yargs = require('yargs') +const argv = yargs + .usage('Usage: $0 [options]') + .help('help') + .describe('assets-path', 'Path to the folder where all release assets are stored') + .wrap(yargs.terminalWidth()) + .argv + +let assetsPath = argv.assetsPath || path.join(CONFIG.repositoryRootPath, 'out') +let assets = glob.sync(path.join(assetsPath, '*(*.exe|*.zip|*.nupkg|*.tar.gz|*.rpm|*.deb|RELEASES*)')) + +console.log(`Uploading release assets for ${CONFIG.computedAppVersion} to S3`) + +uploadToS3( + process.env.ATOM_RELEASES_S3_KEY, + process.env.ATOM_RELEASES_S3_SECRET, + process.env.ATOM_RELEASES_S3_BUCKET, + `releases/v${CONFIG.computedAppVersion}/`, + assets).then( + () => { + console.log(`Publishing GitHub release ${CONFIG.computedAppVersion}`) + publishRelease({ + token: process.env.GITHUB_TOKEN, + owner: 'atom', + repo: CONFIG.channel !== 'nightly' ? 'atom' : 'atom-nightly-releases', + name: CONFIG.computedAppVersion, + tag: `v${CONFIG.computedAppVersion}`, + draft: false, + prerelease: CONFIG.channel !== 'stable', + reuseRelease: true, + skipIfPublished: true, + assets + }, function (err, release) { + if (err) { + console.error("An error occurred while publishing the release:\n\n", err) + } else { + console.log("Release published successfully: ", release.html_url) + } + }) + }).catch((err) => { + console.error('An error occurred while uploading the release:', err) + }) diff --git a/script/publish-release.cmd b/script/publish-release.cmd new file mode 100644 index 000000000..46e077a3c --- /dev/null +++ b/script/publish-release.cmd @@ -0,0 +1,5 @@ +@IF EXIST "%~dp0\node.exe" ( + "%~dp0\node.exe" "%~dp0\publish-release" %* +) ELSE ( + node "%~dp0\publish-release" %* +) diff --git a/script/vsts/README.md b/script/vsts/README.md new file mode 100644 index 000000000..0e956d8d0 --- /dev/null +++ b/script/vsts/README.md @@ -0,0 +1,65 @@ +# Atom Release Build Documentation + +## Overview + +This folder contains build configuration and scripts for automating Atom's +release pipeline using [Visual Studio Team Services](https://azure.microsoft.com/en-us/services/visual-studio-team-services/). +VSTS allows us to leverage [multi-phase jobs](https://github.com/Microsoft/vsts-agent/blob/master/docs/preview/yamlgettingstarted-jobs.md) to generate Atom installation packages +on Windows, macOS, and Linux and then publish a new release automatically once +the build completes successfully. + +## Nightly Release Build + +Our scheduled nightly release uses a mutli-phase job to automatically generate Atom +Nightly installation packages and then publish them to GitHub and atom.io. + +The [Atom Nightly build definition](https://github.visualstudio.com/Atom/_build/index?context=mine&path=%5C&definitionId=1&_a=completed) +is configured with the [`nightly-release.yml`](nightly-release.yml) file. More +information on VSTS' YAML configuration format can be found in their [Getting Started](https://github.com/Microsoft/vsts-agent/blob/master/docs/preview/yamlgettingstarted.md) +documentation. + +### Versioning Phase + +In this phase, we run [`script/vsts/generate-version.js`](generate-version.js) to +determine the version of the next Atom Nightly release. This script consults the +GitHub v3 API to get the list of releases on the [`atom/atom-nightly-releases`](https://github.com/atom/atom-nightly-releases) +repo. We look for the most recent, non-draft release and then parse its version +number (e.g. `1.30.0-nightly4`) to extract the base version and the monotonically-increasing +nightly release number. + +Once we have the version and release number, we compare the base version number +(`1.30.0`) against the one in `package.json` of the latest commit in the local +repo. If those versions are the same, we increment the release number (`1.30.0-nightly5`). +If those versions are different, we use `0` for the release number to start a +new series of Nightly releases for the new version (`1.31.0-nightly0`). + +Once the release version has been determined, it is set as our custom `ReleaseVersion` +[output variable](https://github.com/Microsoft/vsts-agent/blob/master/docs/preview/yamlgettingstarted-outputvariables.md) +by writing out a special string to `stdout` which is recognized by VSTS. This +variable will be used in later build steps. + +If any part of the build process fails from this point forward, the same version +number *should* be chosen in the next build unless the base version number has +been changed in `master`. + +### OS-specific Build Phases + +In this part of the build, we use [phase templates](https://github.com/Microsoft/vsts-agent/blob/master/docs/preview/yamlgettingstarted-templates.md) +for [Windows](windows.yml), [macOS](macos.yml), and [Linux](linux.yml) to build +Atom simultaneously across those platforms and then run the Atom test suite to +verify the builds. If build, test, and linting come back clean, we take the build +assets generated in the `out` folder on each OS and then stage them as build artifacts. + +For each OS build, we refer to the `ReleaseVersion` variable, set in the previous +phase, to configure the `ATOM_RELEASE_VERSION` environment variable to override +the version contained in Atom's `package.json`. + +### Publish Phase + +If all three OS builds have completed successfully, the publish phase will launch the +[`script/publish-release`](../publish-release) script to collect the release +artifacts created from those builds and then upload them to the S3 bucket from +which Atom release assets are served. If the upload process is successful, a new +release will be created on the `atom/atom-nightly-releases` repo using the +`ReleaseVersion` with a `v` prefix as the tag name. The release assets will also +be uploaded to the GitHub release at this time. diff --git a/script/vsts/generate-version.js b/script/vsts/generate-version.js new file mode 100644 index 000000000..65206d33c --- /dev/null +++ b/script/vsts/generate-version.js @@ -0,0 +1,33 @@ +const path = require('path') +const request = require('request-promise-native') + +const repositoryRootPath = path.resolve(__dirname, '..', '..') +const appMetadata = require(path.join(repositoryRootPath, 'package.json')) +const baseVersion = appMetadata.version.split('-')[0] + +async function generateNightlyVersion () { + const releases = await request({ + url: 'https://api.github.com/repos/atom/atom-nightly-releases/releases', + headers: {'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'Atom Release Build'}, + json: true + }) + + let releaseNumber = 0 + if (releases && releases.length > 0) { + const latestRelease = releases.find(r => !r.draft) + const versionMatch = latestRelease.tag_name.match(/^v?(\d+\.\d+\.\d+)-nightly(\d+)$/) + + if (versionMatch && versionMatch[1] === baseVersion) { + releaseNumber = parseInt(versionMatch[2]) + 1 + } + } + + // Set our ReleaseVersion build variable and update VSTS' build number to + // include the version. Writing these strings to stdout causes VSTS to set + // the associated variables. + const generatedVersion = `${baseVersion}-nightly${releaseNumber}` + console.log(`##vso[task.setvariable variable=ReleaseVersion;isOutput=true]${generatedVersion}`) + console.log(`##vso[build.updatebuildnumber]${generatedVersion}+${process.env.BUILD_BUILDNUMBER}`) +} + +generateNightlyVersion() diff --git a/script/vsts/linux.yml b/script/vsts/linux.yml new file mode 100644 index 000000000..8c009b4e0 --- /dev/null +++ b/script/vsts/linux.yml @@ -0,0 +1,58 @@ +phases: +- phase: Linux + dependsOn: GetReleaseVersion + variables: + ReleaseVersion: $[ dependencies.GetReleaseVersion.outputs['Version.ReleaseVersion'] ] + queue: + name: Hosted Linux Preview + timeoutInMinutes: 180 + + steps: + - task: NodeTool@0 + inputs: + versionSpec: 8.9.3 + displayName: Install Node.js 8.9.3 + + - script: | + apt-get update + apt-get install -y --no-install-recommends build-essential xvfb clang-3.5 fakeroot git libsecret-1-dev rpm libx11-dev libxkbfile-dev xz-utils xorriso zsync libxss1 libgconf2-4 libgtk-3-0 + displayName: Install apt dependencies + + - script: | + script/build --create-debian-package --create-rpm-package --compress-artifacts + env: + ATOM_RELEASE_VERSION: $(ReleaseVersion) + displayName: Build Atom + + - script: script/lint + displayName: Run linter + + - script: | + /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16 + export DISPLAY=':99.0' + Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + script/test + env: + CI: true + CI_PROVIDER: VSTS + displayName: Run tests + + # This step is necessary in the short term due to a bug in the *NIX + # implementation of the CopyFiles task which scans the entire file + # system structure just to resolve the glob pattern. + - script: rm -rf $(Build.SourcesDirectory)/out/*/ + displayName: Delete Intermediate Output + + - task: CopyFiles@2 + inputs: + sourceFolder: $(Build.SourcesDirectory)/out + contents: '?(*.deb|*.rpm|*.tar.gz)' + targetFolder: $(Build.ArtifactStagingDirectory) + displayName: Stage Artifacts + + - task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: $(Build.ArtifactStagingDirectory) + ArtifactName: Binaries + ArtifactType: Container + displayName: Upload Artifacts diff --git a/script/vsts/macos.yml b/script/vsts/macos.yml new file mode 100644 index 000000000..3e7273e44 --- /dev/null +++ b/script/vsts/macos.yml @@ -0,0 +1,55 @@ +phases: +- phase: macOS + dependsOn: GetReleaseVersion + variables: + ReleaseVersion: $[ dependencies.GetReleaseVersion.outputs['Version.ReleaseVersion'] ] + queue: + name: Hosted macOS Preview + timeoutInMinutes: 180 + + steps: + - task: NodeTool@0 + inputs: + versionSpec: 8.9.3 + displayName: Install Node.js 8.9.3 + + - script: | + script/build --code-sign --compress-artifacts + displayName: Build Atom + env: + ATOM_RELEASE_VERSION: $(ReleaseVersion) + ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL: $(ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL) + ATOM_MAC_CODE_SIGNING_CERT_PASSWORD: $(ATOM_MAC_CODE_SIGNING_CERT_PASSWORD) + ATOM_MAC_CODE_SIGNING_KEYCHAIN: $(ATOM_MAC_CODE_SIGNING_KEYCHAIN) + ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD: $(ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD) + + - script: script/lint + displayName: Run linter + + - script: | + osascript -e 'tell application "System Events" to keystroke "x"' # clear screen saver + caffeinate -s script/test # Run with caffeinate to prevent screen saver + env: + CI: true + CI_PROVIDER: VSTS + displayName: Run tests + + # This step is necessary in the short term due to a bug in the *NIX + # implementation of the CopyFiles task which scans the entire file + # system structure just to resolve the glob pattern. + - script: rm -rf $(Build.SourcesDirectory)/out/*/ + displayName: Delete Intermediate Output + + - task: CopyFiles@2 + inputs: + sourceFolder: $(Build.SourcesDirectory)/out + contents: '*.zip' + targetFolder: $(Build.ArtifactStagingDirectory) + displayName: Stage Artifacts + + - task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: $(Build.ArtifactStagingDirectory) + ArtifactName: Binaries + ArtifactType: Container + displayName: Upload Artifacts diff --git a/script/vsts/nightly-release.yml b/script/vsts/nightly-release.yml new file mode 100644 index 000000000..73ab7a55e --- /dev/null +++ b/script/vsts/nightly-release.yml @@ -0,0 +1,57 @@ +phases: + +- phase: GetReleaseVersion + steps: + # This has to be done separately because VSTS inexplicably + # exits the script block after `npm install` completes. + - script: | + cd script\vsts + npm install + displayName: npm install + - script: node script\vsts\generate-version.js + name: Version + +# Import OS-specific build definitions +- template: windows.yml +- template: macos.yml +- template: linux.yml + +- phase: Release + queue: Hosted # Need this for Python 2.7 + + dependsOn: + - GetReleaseVersion + - Windows + - Linux + - macOS + + variables: + ReleaseVersion: $[ dependencies.GetReleaseVersion.outputs['Version.ReleaseVersion'] ] + + steps: + - task: NodeTool@0 + inputs: + versionSpec: 8.9.3 + displayName: Install Node.js 8.9.3 + + # This has to be done separately because VSTS inexplicably + # exits the script block after `npm install` completes. + - script: | + cd script + npm install + displayName: npm install + + - task: DownloadBuildArtifacts@0 + displayName: Download Release Artifacts + inputs: + artifactName: Binaries + + - script: | + $(Build.SourcesDirectory)\script\publish-release.cmd --assets-path "$(System.ArtifactsDirectory)/Binaries" + env: + GITHUB_TOKEN: $(GITHUB_TOKEN) + ATOM_RELEASE_VERSION: $(ReleaseVersion) + ATOM_RELEASES_S3_KEY: $(ATOM_RELEASES_S3_KEY) + ATOM_RELEASES_S3_SECRET: $(ATOM_RELEASES_S3_SECRET) + ATOM_RELEASES_S3_BUCKET: $(ATOM_RELEASES_S3_BUCKET) + displayName: Create Nightly Release diff --git a/script/vsts/package-lock.json b/script/vsts/package-lock.json new file mode 100644 index 000000000..319b5aab5 --- /dev/null +++ b/script/vsts/package-lock.json @@ -0,0 +1,357 @@ +{ + "name": "atom-release-scripts", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.14.2" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "1.33.0" + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.2", + "safe-buffer": "5.1.2", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.3.0" + } + }, + "request-promise-core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", + "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "requires": { + "lodash": "4.17.10" + } + }, + "request-promise-native": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", + "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", + "requires": { + "request-promise-core": "1.1.1", + "stealthy-require": "1.1.1", + "tough-cookie": "2.3.4" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "safer-buffer": "2.1.2", + "tweetnacl": "0.14.5" + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "uuid": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.0.tgz", + "integrity": "sha512-ijO9N2xY/YaOqQ5yz5c4sy2ZjWmA6AR6zASb/gdpeKZ8+948CxwfMW9RrKVk5may6ev8c0/Xguu32e2Llelpqw==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + } + } +} diff --git a/script/vsts/package.json b/script/vsts/package.json new file mode 100644 index 000000000..61bb04338 --- /dev/null +++ b/script/vsts/package.json @@ -0,0 +1,8 @@ +{ + "name": "atom-release-scripts", + "description": "Atom release scripts", + "dependencies": { + "request": "^2.87.0", + "request-promise-native": "^1.0.5" + } +} diff --git a/script/vsts/windows.yml b/script/vsts/windows.yml new file mode 100644 index 000000000..410ee0dce --- /dev/null +++ b/script/vsts/windows.yml @@ -0,0 +1,54 @@ +phases: +- phase: Windows + dependsOn: GetReleaseVersion + variables: + ReleaseVersion: $[ dependencies.GetReleaseVersion.outputs['Version.ReleaseVersion'] ] + queue: + name: Hosted + timeoutInMinutes: 180 + parallel: 2 + matrix: + x64: + buildArch: x64 + # TODO: x86 is currently not supported on VSTS + # x86: + # buildArch: x86 + + steps: + - task: NodeTool@0 + inputs: + versionSpec: 8.9.3 + displayName: Install Node.js 8.9.3 + + - script: | + IF NOT EXIST C:\tmp MKDIR C:\tmp + SET SQUIRREL_TEMP=C:\tmp + script\build.cmd --create-windows-installer --code-sign --compress-artifacts + env: + ATOM_RELEASE_VERSION: $(ReleaseVersion) + ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL: $(ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL) + ATOM_WIN_CODE_SIGNING_CERT_PASSWORD: $(ATOM_WIN_CODE_SIGNING_CERT_PASSWORD) + displayName: Build Atom + + - script: script\lint.cmd + displayName: Run linter + + - script: script\test.cmd + env: + CI: true + CI_PROVIDER: VSTS + displayName: Run tests + + - task: CopyFiles@2 + inputs: + sourceFolder: $(Build.SourcesDirectory)/out + contents: '?(*.exe|*.zip|*.nupkg|RELEASES*)' + targetFolder: $(Build.ArtifactStagingDirectory) + displayName: Stage Artifacts + + - task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: $(Build.ArtifactStagingDirectory) + ArtifactName: Binaries + ArtifactType: Container + displayName: Upload Artifacts diff --git a/spec/command-installer-spec.js b/spec/command-installer-spec.js index 6a2a31e77..b303d4954 100644 --- a/spec/command-installer-spec.js +++ b/spec/command-installer-spec.js @@ -1,6 +1,7 @@ const path = require('path') const fs = require('fs-plus') const temp = require('temp').track() +const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers'); const CommandInstaller = require('../src/command-installer') describe('CommandInstaller on #darwin', () => { @@ -56,8 +57,8 @@ describe('CommandInstaller on #darwin', () => { const appDelegate = jasmine.createSpyObj('appDelegate', ['confirm']) installer = new CommandInstaller(appDelegate) installer.initialize('2.0.2') - spyOn(installer, 'installAtomCommand').andCallFake((__, callback) => callback()) - spyOn(installer, 'installApmCommand').andCallFake((__, callback) => callback()) + spyOn(installer, 'installAtomCommand').andCallFake((__, callback) => callback(undefined, 'atom')) + spyOn(installer, 'installApmCommand').andCallFake((__, callback) => callback(undefined, 'apm')) installer.installShellCommandsInteractively() @@ -140,4 +141,41 @@ describe('CommandInstaller on #darwin', () => { }) }) }) + + describe('when using a nightly version of atom', () => { + beforeEach(() => { + installer = new CommandInstaller() + installer.initialize('2.2.0-nightly0') + }) + + it("symlinks the atom command as 'atom-nightly'", () => { + const installedAtomPath = path.join(installationPath, 'atom-nightly') + expect(fs.isFileSync(installedAtomPath)).toBeFalsy() + + waitsFor(done => { + installer.installAtomCommand(false, error => { + expect(error).toBeNull() + expect(fs.realpathSync(installedAtomPath)).toBe(fs.realpathSync(atomBinPath)) + expect(fs.isExecutableSync(installedAtomPath)).toBe(true) + expect(fs.isFileSync(path.join(installationPath, 'atom'))).toBe(false) + done() + }) + }) + }) + + it("symlinks the apm command as 'apm-nightly'", () => { + const installedApmPath = path.join(installationPath, 'apm-nightly') + expect(fs.isFileSync(installedApmPath)).toBeFalsy() + + waitsFor(done => { + installer.installApmCommand(false, error => { + expect(error).toBeNull() + expect(fs.realpathSync(installedApmPath)).toBe(fs.realpathSync(apmBinPath)) + expect(fs.isExecutableSync(installedApmPath)).toBeTruthy() + expect(fs.isFileSync(path.join(installationPath, 'nightly'))).toBe(false) + done() + }) + }) + }) + }) }) diff --git a/spec/main-process/file-recovery-service.test.js b/spec/main-process/file-recovery-service.test.js index 2a8f2088c..45c10c25b 100644 --- a/spec/main-process/file-recovery-service.test.js +++ b/spec/main-process/file-recovery-service.test.js @@ -1,6 +1,8 @@ const {dialog} = require('electron') const FileRecoveryService = require('../../src/main-process/file-recovery-service') const fs = require('fs-plus') +const fsreal = require('fs') +const EventEmitter = require('events').EventEmitter const sinon = require('sinon') const {escapeRegExp} = require('underscore-plus') const temp = require('temp').track() @@ -116,13 +118,22 @@ describe("FileRecoveryService", () => { const mockWindow = {} const filePath = temp.path() fs.writeFileSync(filePath, "content") - fs.chmodSync(filePath, 0444) let logs = [] spies.stub(console, 'log', (message) => logs.push(message)) spies.stub(dialog, 'showMessageBox') + // Copy files to be recovered before mocking fs.createWriteStream await recoveryService.willSavePath(mockWindow, filePath) + + // Stub out fs.createWriteStream so that we can return a fake error when + // attempting to copy the recovered file to its original location + var fakeEmitter = new EventEmitter() + var onStub = spies.stub(fakeEmitter, 'on') + onStub.withArgs('error').yields(new Error('Nope')).returns(fakeEmitter) + onStub.withArgs('open').returns(fakeEmitter) + spies.stub(fsreal, 'createWriteStream').withArgs(filePath).returns(fakeEmitter) + await recoveryService.didCrashWindow(mockWindow) let recoveryFiles = fs.listTreeSync(recoveryDirectory) assert.equal(recoveryFiles.length, 1) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index 3a6b56a1b..eb0fefb40 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -212,6 +212,18 @@ describe('TooltipManager', () => { }) }) ) + + describe('when a user types', () => + it('hides the tooltips', () => { + const disposable = manager.add(element, { title: 'Title' }) + hover(element, function () { + expect(document.body.querySelector('.tooltip')).not.toBeNull() + window.dispatchEvent(new CustomEvent('keydown')) + expect(document.body.querySelector('.tooltip')).toBeNull() + disposable.dispose() + }) + }) + ) describe('findTooltips', () => { it('adds and remove tooltips correctly', () => { diff --git a/src/atom-environment.js b/src/atom-environment.js index 8611725ee..59e4da1f6 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -487,21 +487,25 @@ class AtomEnvironment { // Public: Gets the release channel of the Atom application. // - // Returns the release channel as a {String}. Will return one of `dev`, `beta`, or `stable`. + // Returns the release channel as a {String}. Will return a specific release channel + // name like 'beta' or 'nightly' if one is found in the Atom version or 'stable' + // otherwise. getReleaseChannel () { - const version = this.getVersion() - if (version.includes('beta')) { - return 'beta' - } else if (version.includes('dev')) { - return 'dev' - } else { - return 'stable' + // This matches stable, dev (with or without commit hash) and any other + // release channel following the pattern '1.00.0-channel0' + const match = this.getVersion().match(/\d+\.\d+\.\d+(-([a-z]+)(\d+|-\w{4,})?)?$/) + if (!match) { + return 'unrecognized' + } else if (match[2]) { + return match[2] } + + return 'stable' } // Public: Returns a {Boolean} that is `true` if the current version is an official release. isReleasedVersion () { - return !/\w{7}/.test(this.getVersion()) // Check if the release is a 7-character SHA prefix + return this.getReleaseChannel().match(/stable|beta|nightly/) != null } // Public: Get the time taken to completely load the current window. diff --git a/src/command-installer.js b/src/command-installer.js index 85360da17..b432023ba 100644 --- a/src/command-installer.js +++ b/src/command-installer.js @@ -27,22 +27,36 @@ class CommandInstaller { }, () => {}) } - this.installAtomCommand(true, error => { + this.installAtomCommand(true, (error, atomCommandName) => { if (error) return showErrorDialog(error) - this.installApmCommand(true, error => { + this.installApmCommand(true, (error, apmCommandName) => { if (error) return showErrorDialog(error) this.applicationDelegate.confirm({ message: 'Commands installed.', - detail: 'The shell commands `atom` and `apm` are installed.' + detail: `The shell commands \`${atomCommandName}\` and \`${apmCommandName}\` are installed.` }, () => {}) }) }) } + getCommandNameForChannel (commandName) { + let channelMatch = this.appVersion.match(/beta|nightly/) + let channel = channelMatch ? channelMatch[0] : '' + + switch (channel) { + case 'beta': + return `${commandName}-beta` + case 'nightly': + return `${commandName}-nightly` + default: + return commandName + } + } + installAtomCommand (askForPrivilege, callback) { this.installCommand( path.join(this.getResourcesDirectory(), 'app', 'atom.sh'), - this.appVersion.includes('beta') ? 'atom-beta' : 'atom', + this.getCommandNameForChannel('atom'), askForPrivilege, callback ) @@ -51,7 +65,7 @@ class CommandInstaller { installApmCommand (askForPrivilege, callback) { this.installCommand( path.join(this.getResourcesDirectory(), 'app', 'apm', 'node_modules', '.bin', 'apm'), - this.appVersion.includes('beta') ? 'apm-beta' : 'apm', + this.getCommandNameForChannel('apm'), askForPrivilege, callback ) @@ -64,11 +78,11 @@ class CommandInstaller { fs.readlink(destinationPath, (error, realpath) => { if (error && error.code !== 'ENOENT') return callback(error) - if (realpath === commandPath) return callback() + if (realpath === commandPath) return callback(null, commandName) this.createSymlink(fs, commandPath, destinationPath, error => { if (error && error.code === 'EACCES' && askForPrivilege) { const fsAdmin = require('fs-admin') - this.createSymlink(fsAdmin, commandPath, destinationPath, callback) + this.createSymlink(fsAdmin, commandPath, destinationPath, (error) => { callback(error, commandName) }) } else { callback(error) } diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js index 5c074a14e..f24422ef4 100644 --- a/src/main-process/parse-command-line.js +++ b/src/main-process/parse-command-line.js @@ -12,13 +12,18 @@ module.exports = function parseCommandLine (processArgs) { options.usage( dedent`Atom Editor v${version} - Usage: atom [options] [path ...] + Usage: + atom [options] [path ...] + atom file[:line[:column]] One or more paths to files or folders may be specified. If there is an existing Atom window that contains all of the given folders, the paths will be opened in that window. Otherwise, they will be opened in a new window. + A file may be opened at the desired line (and optionally column) by + appending the numbers right after the file name, e.g. \`atom file:5:8\`. + Paths that start with \`atom://\` will be interpreted as URLs. Environment Variables: diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js index 34f96775b..07b4f196a 100644 --- a/src/tooltip-manager.js +++ b/src/tooltip-manager.js @@ -153,9 +153,11 @@ class TooltipManager { } window.addEventListener('resize', hideTooltip) + window.addEventListener('keydown', hideTooltip) const disposable = new Disposable(() => { window.removeEventListener('resize', hideTooltip) + window.removeEventListener('keydown', hideTooltip) hideTooltip() tooltip.destroy()