From d3b35742a6cebfd84f6fb6213b63242883c58fb1 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Wed, 26 Apr 2017 23:52:04 -0400 Subject: [PATCH] refactor(GUI): integrate `etcher-latest-version` into the main repo (#1183) `etcher-latest-version` was kept in a separate repository in order to re-use it with the Etcher website, however the Etcher website is not using it at all, and we're moving towards having the website in the main repository. Therefore, this commit brings back the logic from `etcher-latest-version`, but introduces it as `lib/shared/s3-packages.js`, in order to not tie ourselves to the AngularJS framework, and as a step towards the Etcher SDK. As a nice little bonus, this commit adds support for an `ETCHER_FAKE_S3_LATEST_VERSION` environment variable that can be used to trick Etcher that there is an available update, and therefore show the update notifier modal. Also, this commit adds support for snapshot builds update-checks, by checking the `resin-nightly-downloads` S3 bucket if the current version contains a git commit hash build number. If the version is not a production release, then the update notifier modal doesn't present the checkbox to disable update notifications for X days. We also add a property called `updates.semverRange` to `package.json`, which can be used to fine control which versions are considered as candidates for an update notification. This commit adds a setting called `includeUnstableChannel`, which can be used to tweak whether unstable (beta) releases are considered or not when checking for the latest available version. See: https://github.com/resin-io-modules/etcher-latest-version Fixes: https://github.com/resin-io/etcher/issues/953 Signed-off-by: Juan Cruz Viotti --- docs/MAINTAINERS.md | 2 + docs/PUBLISHING.md | 61 +- docs/USER-DOCUMENTATION.md | 16 + lib/gui/app.js | 46 +- .../services/update-notifier.js | 127 +- .../templates/update-notifier-modal.tpl.html | 2 +- .../update-notifier/update-notifier.js | 1 - lib/gui/css/main.css | 2 +- lib/gui/models/store.js | 3 + lib/gui/pages/settings/styles/_settings.scss | 2 +- .../settings/templates/settings.tpl.html | 10 + lib/shared/release.js | 103 ++ lib/shared/s3-packages.js | 245 ++++ npm-shrinkwrap.json | 359 +++--- package.json | 6 + tests/gui/components/update-notifier.spec.js | 234 ++-- tests/shared/release.spec.js | 132 ++ tests/shared/s3-packages.spec.js | 1090 +++++++++++++++++ 18 files changed, 2022 insertions(+), 419 deletions(-) create mode 100644 lib/shared/release.js create mode 100644 lib/shared/s3-packages.js create mode 100644 tests/shared/release.spec.js create mode 100644 tests/shared/s3-packages.spec.js diff --git a/docs/MAINTAINERS.md b/docs/MAINTAINERS.md index 6a477ad0..229fa016 100644 --- a/docs/MAINTAINERS.md +++ b/docs/MAINTAINERS.md @@ -18,6 +18,8 @@ Preparing a new version - Re-take `screenshot.png` so it displays the latest version in the bottom right corner. +- Revise the `updates.semverRange` version in `package.json` + - Commit the changes with the version number as the commit title, including the `v` prefix, to `master`. For example: diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index a35454b0..2cd05443 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -4,6 +4,48 @@ Publishing Etcher This is a small guide to package and publish Etcher to all supported operating systems. +Release Types +------------- + +Etcher supports **production** and **snapshot** release types. Each is +published to a different S3 bucket, and production release types are code +signed, while snapshot release types aren't and include a short git commit-hash +as a build number. For example, `1.0.0-beta.19` is a production release type, +while `1.0.0-beta.19+531ab82` is a snapshot release type. + +In terms of comparison: `1.0.0-beta.19` (production) < `1.0.0-beta.19+531ab82` +(snapshot) < `1.0.0-rc.1` (production) < `1.0.0-rc.1+7fde24a` (snapshot) < +`1.0.0` (production) < `1.0.0+2201e5f` (snapshot). Keep in mind that if you're +running a production release type, you'll only be prompted to update to +production release types, and if you're running a snapshot release type, you'll +only be prompted to update to other snapshot release types. + +The build system creates (and publishes) snapshot release types by default, but +you can build a specific release type by setting the `RELEASE_TYPE` make +variable. For example: + +```sh +make RELEASE_TYPE=snapshot +make RELEASE_TYPE=production +``` + +We can control the version range a specific Etcher version will consider when +showing the update notification dialog by tweaking the `updates.semverRange` +property of `package.json`. + +Update Channels +--------------- + +Etcher has a setting to include the unstable update channel. If this option is +set, Etcher will consider both stable and unstable versions when showing the +update notifier dialog. Unstable versions are the ones that contain a `beta` +pre-release tag. For example: + +- Production unstable version: `1.4.0-beta.1` +- Snapshot unstable version: `1.4.0-beta.1+7fde24a` +- Production stable version: `1.4.0` +- Snapshot stable version: `1.4.0+7fde24a` + Signing ------- @@ -29,40 +71,31 @@ employee by asking for it from the relevant people. Packaging --------- +The resulting installers will be saved to `release/out`. + +Run the following commands: ### OS X -Run the following command: - ```sh make electron-installer-dmg make electron-installer-app-zip ``` -The resulting installers will be saved to `release/out`. - ### GNU/Linux -Run the following command: - ```sh make electron-installer-appimage make electron-installer-debian ``` -The resulting installers will be saved to `release/out`. - ### Windows -Run the following command: - ```sh make electron-installer-zip make electron-installer-nsis ``` -The resulting installers will be saved to `etcher-release/installers`. - Publishing to Bintray --------------------- @@ -76,7 +109,7 @@ Make sure you set the following environment variables: Run the following command: ```sh -make publish-bintray-debian RELEASE_TYPE= +make publish-bintray-debian ``` Publishing to S3 @@ -91,7 +124,7 @@ Run the following command to publish all files for the current combination of _platform_ and _arch_ (building them if necessary): ```sh -make publish-aws-s3 RELEASE_TYPE= +make publish-aws-s3 ``` Also add links to each AWS S3 file in [GitHub Releases][github-releases]. See diff --git a/docs/USER-DOCUMENTATION.md b/docs/USER-DOCUMENTATION.md index 6bf9b1f7..1dc46964 100644 --- a/docs/USER-DOCUMENTATION.md +++ b/docs/USER-DOCUMENTATION.md @@ -149,6 +149,21 @@ In Windows: set ETCHER_DISABLE_UPDATES=1 ``` +Simulate an update alert +------------------------ + +You can set the `ETCHER_FAKE_S3_LATEST_VERSION` environment variable to a valid +semver version (greater than the current version) to trick the application into +thinking that what you put there is the latest available version, therefore +causing the update notification dialog to be presented at startup. + +Note that the value of the variable will be ignored if it doesn't match the +release type of the current application version. For example, setting the +variable to a production version (e.g. `ETCHER_FAKE_S3_LATEST_VERSION=2.0.0`) +will be ignored if you're running a snapshot build, and vice-versa. + +See [`PUBLISHING.md`][publishing] for more details about release types. + Recovering broken drives ------------------------ @@ -223,6 +238,7 @@ platforms. [electron]: http://electron.atom.io [electron-supported-platforms]: https://github.com/electron/electron/blob/master/docs/tutorial/supported-platforms.md [etcher-cli]: https://github.com/resin-io/etcher/blob/master/docs/CLI.md +[publishing]: https://github.com/resin-io/etcher/blob/master/docs/PUBLISHING.md [windows-usb-tool]: https://www.microsoft.com/en-us/download/windows-usb-dvd-download-tool [rufus]: https://rufus.akeo.ie [unetbootin]: https://unetbootin.github.io diff --git a/lib/gui/app.js b/lib/gui/app.js index ad3b1471..ace089ca 100644 --- a/lib/gui/app.js +++ b/lib/gui/app.js @@ -28,10 +28,15 @@ var angular = require('angular'); const electron = require('electron'); const Bluebird = require('bluebird'); +const semver = require('semver'); +const _ = require('lodash'); const EXIT_CODES = require('../shared/exit-codes'); const messages = require('../shared/messages'); +const s3Packages = require('../shared/s3-packages'); +const release = require('../shared/release'); const packageJSON = require('../../package.json'); const flashState = require('./models/flash-state'); +const settings = require('./models/settings'); const windowProgress = require('./os/window-progress'); const Store = require('./models/store'); @@ -86,23 +91,40 @@ app.run(() => { app.run((AnalyticsService, ErrorService, UpdateNotifierService, SelectionStateModel) => { AnalyticsService.logEvent('Application start'); - const shouldCheckForUpdates = UpdateNotifierService.shouldCheckForUpdates(); + const currentVersion = packageJSON.version; + const currentReleaseType = release.getReleaseType(currentVersion); + const shouldCheckForUpdates = UpdateNotifierService.shouldCheckForUpdates({ + ignoreSleepUpdateCheck: currentReleaseType !== release.RELEASE_TYPE.PRODUCTION + }); - if (!shouldCheckForUpdates || process.env.ETCHER_DISABLE_UPDATES) { + if (_.some([ + !shouldCheckForUpdates, + process.env.ETCHER_DISABLE_UPDATES, + currentReleaseType === release.RELEASE_TYPE.UNKNOWN + ])) { AnalyticsService.logEvent('Not checking for updates', { shouldCheckForUpdates, - disableUpdatesEnvironmentVariable: process.env.ETCHER_DISABLE_UPDATES + disableUpdatesEnvironmentVariable: process.env.ETCHER_DISABLE_UPDATES, + releaseType: currentReleaseType }); return; } + const updateSemverRange = packageJSON.updates.semverRange; + const includeUnstableChannel = settings.get('includeUnstableUpdateChannel'); + AnalyticsService.logEvent('Checking for updates', { - currentVersion: packageJSON.version + currentVersion, + releaseType: currentReleaseType, + updateSemverRange, + includeUnstableChannel }); - UpdateNotifierService.isLatestVersion().then((isLatestVersion) => { - - if (isLatestVersion) { + s3Packages.getLatestVersion(currentReleaseType, { + range: updateSemverRange, + includeUnstableChannel + }).then((latestVersion) => { + if (semver.gte(currentVersion, latestVersion || '0.0.0')) { AnalyticsService.logEvent('Update notification skipped', { reason: 'Latest version' }); @@ -121,12 +143,14 @@ app.run((AnalyticsService, ErrorService, UpdateNotifierService, SelectionStateMo return Bluebird.resolve(); } - AnalyticsService.logEvent('Notifying update'); - - return UpdateNotifierService.notify(); + AnalyticsService.logEvent('Notifying update', { + latestVersion + }); + return UpdateNotifierService.notify(latestVersion, { + allowSleepUpdateCheck: currentReleaseType === release.RELEASE_TYPE.PRODUCTION + }); }).catch(ErrorService.reportException); - }); app.run((AnalyticsService) => { diff --git a/lib/gui/components/update-notifier/services/update-notifier.js b/lib/gui/components/update-notifier/services/update-notifier.js index e7a5ea62..8958c786 100644 --- a/lib/gui/components/update-notifier/services/update-notifier.js +++ b/lib/gui/components/update-notifier/services/update-notifier.js @@ -17,104 +17,35 @@ 'use strict'; const _ = require('lodash'); -const semver = require('semver'); -const etcherLatestVersion = require('etcher-latest-version'); const units = require('../../../../shared/units'); const settings = require('../../../models/settings'); -module.exports = function($http, $q, ModalService, UPDATE_NOTIFIER_SLEEP_DAYS, ManifestBindService) { +module.exports = function(ModalService, UPDATE_NOTIFIER_SLEEP_DAYS) { /** - * @summary The current application version - * @constant - * @private - * @type {String} - */ - const CURRENT_VERSION = ManifestBindService.get('version'); - - /** - * @summary Get the latest available Etcher version - * @function - * @private - * @description - * We assume the received latest version number will not increase - * while Etcher is running and memoize it - * - * @fulfil {String} - latest version - * @returns {Promise} - * - * @example - * UpdateNotifierService.getLatestVersion().then((latestVersion) => { - * console.log(`The latest version is: ${latestVersion}`); - * }); - */ - this.getLatestVersion = _.memoize(() => { - return $q((resolve, reject) => { - return etcherLatestVersion((url, callback) => { - return $http.get(url).then((response) => { - return callback(null, response.data); - }).catch((error) => { - return callback(error); - }); - }, (error, latestVersion) => { - if (error) { - - // The error status equals this number if the request - // couldn't be made successfully, for example, because - // of a timeout on an unstable network connection. - const ERROR_CODE_UNSUCCESSFUL_REQUEST = -1; - - if (error.status === ERROR_CODE_UNSUCCESSFUL_REQUEST) { - return resolve(CURRENT_VERSION); - } - - return reject(error); - } - - return resolve(latestVersion); - }); - }); - - // Arbitrary identifier for the memoization function - }, _.constant('latest-version')); - - /** - * @summary Check if the current version is the latest version - * @function - * @public - * - * @fulfil {Boolean} - is latest version - * @returns {Promise} - * - * @example - * UpdateNotifierService.isLatestVersion().then((isLatestVersion) => { - * if (!isLatestVersion) { - * console.log('There is an update available'); - * } - * }); - */ - this.isLatestVersion = () => { - return this.getLatestVersion().then((version) => { - return semver.gte(CURRENT_VERSION, version); - }); - }; - - /** - * @summary Determine if its time to check for updates + * @summary Determine if it's time to check for updates * @function * @public * + * @param {Object} [options] - options + * @param {Boolean} [options.ignoreSleepUpdateCheck] - ignore sleep update check * @returns {Boolean} should check for updates * * @example - * if (UpdateNotifierService.shouldCheckForUpdates()) { + * if (UpdateNotifierService.shouldCheckForUpdates({ + * ignoreSleepUpdateCheck: false + * })) { * console.log('We should check for updates!'); * } */ - this.shouldCheckForUpdates = () => { + this.shouldCheckForUpdates = (options = {}) => { const lastUpdateNotify = settings.get('lastUpdateNotify'); - if (!settings.get('sleepUpdateCheck') || !lastUpdateNotify) { + if (_.some([ + !settings.get('sleepUpdateCheck'), + !lastUpdateNotify, + _.get(options, [ 'ignoreSleepUpdateCheck' ], false) + ])) { return true; } @@ -131,24 +62,28 @@ module.exports = function($http, $q, ModalService, UPDATE_NOTIFIER_SLEEP_DAYS, M * @function * @public * + * @param {String} version - version + * @param {Object} [options] - options + * @param {Boolean} [options.allowSleepUpdateCheck=true] - allow sleeping the update check * @returns {Promise} * * @example - * UpdateNotifierService.notify(); + * UpdateNotifierService.notify('1.0.0-beta.16', { + * allowSleepUpdateCheck: true + * }); */ - this.notify = () => { - return this.getLatestVersion().then((version) => { - return ModalService.open({ - template: './components/update-notifier/templates/update-notifier-modal.tpl.html', - controller: 'UpdateNotifierController as modal', - size: 'update-notifier', - resolve: { - options: _.constant({ - version - }) - } - }).result; - }); + this.notify = (version, options = {}) => { + return ModalService.open({ + template: './components/update-notifier/templates/update-notifier-modal.tpl.html', + controller: 'UpdateNotifierController as modal', + size: 'update-notifier', + resolve: { + options: _.constant({ + version, + allowSleepUpdateCheck: _.get(options, [ 'allowSleepUpdateCheck' ], true) + }) + } + }).result; }; }; diff --git a/lib/gui/components/update-notifier/templates/update-notifier-modal.tpl.html b/lib/gui/components/update-notifier/templates/update-notifier-modal.tpl.html index d3f58784..82d81775 100644 --- a/lib/gui/components/update-notifier/templates/update-notifier-modal.tpl.html +++ b/lib/gui/components/update-notifier/templates/update-notifier-modal.tpl.html @@ -6,7 +6,7 @@