diff --git a/README.md b/README.md index 168c6697..2d829b9f 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,30 @@ Your application just need to configurer `Update.exe` or `Squirrel.Windows` to u You'll just need to upload as release assets: `RELEASES`, `*-delta.nupkg` and `-full.nupkg` (files generated by `Squirrel.Windows` releaser). +##### Channel Updates + +Nuts lets you manage updates for non-stable release channels. + +By default Nuts will only look for newer versions related to the same primary version as the current version which on the same or higher precedence channel. (Nuts uses the [node-semver module](https://github.com/npm/node-semver) to compare version tags.) + +``` +// Given versions 1.2.3-alpha.1, 1.2.3-beta.1 +GET http://download.myapp.com/update/osx/1.2.3-alpha.1 +// Returns download info for 1.2.3-beta.1 + +// Given versions 1.2.3-alpha.1, 1.2.3-beta.1, 1.2.3, 1.2.4-alpha.1 +GET http://download.mayapp.com/update/osx/1.2.3-alpha.1 +// Returns download info for 1.2.3 +``` + +You can override this behavior and tell Nuts to check updates only on a given channel regardless of which primary version they are related to by appending a channel query parameter after the update URL. + +``` +// Given versions 1.2.3-alpha.1, 1.2.3-beta.1, 1.2.3, 1.2.4-alpha.1 +GET http://download.mayapp.com/update/osx/1.2.3-alpha.1?channel=alpha +// Returns download info for 1.2.4-alpha.1 +``` + #### ChangeLog Nuts provides a `/notes` endpoint that output release notes as text or json. diff --git a/lib/index.js b/lib/index.js index df74809c..2c130d42 100644 --- a/lib/index.js +++ b/lib/index.js @@ -161,6 +161,7 @@ module.exports = function nuts(opts) { router.get('/update/:platform/:version', function(req, res, next) { var platform = req.params.platform; var tag = req.params.version; + var channel = req.query.channel; Q() .then(function() { @@ -169,10 +170,10 @@ module.exports = function nuts(opts) { platform = platforms.detect(platform); - return versions.filter({ - tag: '>='+tag, + return versions.newer({ + tag: tag, platform: platform, - channel: '*' + channel: channel }); }) .then(function(versions) { @@ -182,7 +183,7 @@ module.exports = function nuts(opts) { var releaseNotes = notes.merge(versions.slice(0, -1), { includeTag: false }); res.status(200).send({ - "url": getBaseDownloadUrl(req) + '/version/' + latest.tag + '/' + platform + '?filetype=zip', + "url": getBaseDownloadUrl(req) + '/version/' + latest.tag + '/' + platform + '?filetype=zip', "name": latest.tag, "notes": releaseNotes, "pub_date": latest.published_at.toISOString() @@ -196,15 +197,16 @@ module.exports = function nuts(opts) { router.get('/update/:platform/:version/RELEASES', function(req, res, next) { var platform = 'win_32'; var tag = req.params.version; + var channel = req.query.channel; Q() .then(function() { platform = platforms.detect(platform); - return versions.filter({ - tag: '>='+tag, + return versions.newer({ + tag: tag, platform: platform, - channel: '*' + channel: channel }); }) .then(function(versions) { @@ -249,9 +251,8 @@ module.exports = function nuts(opts) { Q() .then(function() { - return versions.filter({ - tag: tag? '>='+tag : '*', - channel: '*' + return versions.select({ + tag: tag }); }) .then(function(versions) { @@ -301,9 +302,9 @@ module.exports = function nuts(opts) { // List versions router.get('/api/versions', function (req, res, next) { - versions.filter({ + versions.select({ platform: req.query.platform, - channel: req.query.channel || '*' + channel: req.query.channel }) .then(function(results) { res.send(results); diff --git a/lib/versions.js b/lib/versions.js index 26db082d..66b8eb38 100644 --- a/lib/versions.js +++ b/lib/versions.js @@ -4,6 +4,43 @@ var semver = require('semver'); var platforms = require('./platforms'); +// Creates filter function that checks for updates +function newVersionFilter(opts) { + return function(version) { + // Not available for requested paltform + if (!supportsPlatform(opts.platform, version.platforms)) return false; + + // If the caller specifies a channel then stay on that channel. Otherwise + // look for anything closer to the main release. + if (opts.channel) { + return (opts.channel === version.channel && + semver.rcompare(version.tag, opts.tag) < 0); + } else { + return semver.satisfies(version.tag, '>' + opts.tag); + } + } +} + +// Creates generic filter function for all versions +function allVersionFilter(opts) { + return function(version) { + return ((!opts.tag || opts.tag === version.tag) && + matchingChannel(opts.channel, version.channel) && + supportsPlatform(opts.platform, version.platforms)); + } +} + +function matchingChannel(requestedChannel, versionChannel) { + return (!requestedChannel || + requestedChannel === '*' || + versionChannel === requestedChannel); +} + +function supportsPlatform(requestedPlatform, versionPlatforms) { + return (!requestedPlatform || + platforms.satisfies(requestedPlatform, _.pluck(versionPlatforms, 'type'))); +} + module.exports = function(github, opts) { // Normalize tag name function normalizeTag(tag) { @@ -83,35 +120,36 @@ module.exports = function(github, opts) { }); } + // Get new versions for updates + function getNewVersions(opts) { + return filterVersions(opts, newVersionFilter) + } + + // Get all versions, subject to any options passed in + function getMatchingVersions(opts) { + return filterVersions(opts, allVersionFilter) + } + // Filter versions - function filterVersions(opts) { - opts = _.defaults(opts || {}, { - tag: 'latest', - platform: null, - channel: 'stable' - }); + function filterVersions(opts, filterBuilder) { + opts = opts || {}; if (opts.platform) opts.platform = platforms.detect(opts.platform); return listVersions() .then(function(versions) { return _.chain(versions) - .filter(function(version) { - // Check channel - if (opts.channel != '*' && version.channel != opts.channel) return false; - - // Not available for requested paltform - if (opts.platform && !platforms.satisfies(opts.platform, _.pluck(version.platforms, 'type'))) return false; - - // Check tag satisfies request version - return opts.tag == 'latest' || semver.satisfies(version.tag, opts.tag); - }) + .filter(filterBuilder(opts)) .value(); }); } // Resolve a platform function resolveVersion(opts) { - return filterVersions(opts) + opts = opts || {} + opts.channel = opts.channel || 'stable'; + if (opts.tag === 'latest') opts.tag = null; + + return filterVersions(opts, allVersionFilter) .then(function(versions) { var version = _.first(versions); if (!version) throw new Error('Version not found: '+opts.tag); @@ -155,7 +193,8 @@ module.exports = function(github, opts) { return { list: listVersions, get: getVersion, - filter: filterVersions, + newer: getNewVersions, + select: getMatchingVersions, resolve: resolveVersion, channels: listChannels }; diff --git a/package.json b/package.json index db6bd176..e927e836 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,48 @@ { - "name": "nuts-serve", - "version": "2.6.2", - "description": "Server to make GitHub releases (private) available to download with Squirrel support", - "main": "./lib/index.js", - "homepage": "https://github.com/GitbookIO/nuts", - "license": "Apache-2.0", - "main": "./lib/index.js", - "dependencies": { - "express": "^4.13.3", - "lodash": "3.7.0", - "q": "1.2.0", - "body-parser": "1.12.3", - "octonode": "0.7.1", - "semver": "5.0.1", - "request": "2.60.0", - "basic-auth": "1.0.3", - "express-useragent": "0.1.9", - "stores": "0.0.2", - "analytics-node": "1.2.2", - "uuid": "2.0.1", - "github-webhook-handler": "0.5.0", - "strip-bom": "2.0.0", - "destroy": "1.0.3" - }, - "devDependencies": { - "mocha": "1.18.2", - "should": "7.0.4" - }, - "bugs": { - "url": "https://github.com/GitbookIO/nuts/issues" - }, - "authors": [ - { - "name": "Samy Pesse", - "email": "samypesse@gmail.com" - } - ], - "repository": { - "type" : "git", - "url" : "https://github.com/GitbookIO/nuts.git" - }, - "scripts": { - "start": "node bin/web.js", - "test": "mocha --reporter list" + "name": "nuts-serve", + "version": "2.6.2", + "description": "Server to make GitHub releases (private) available to download with Squirrel support", + "main": "./lib/index.js", + "homepage": "https://github.com/GitbookIO/nuts", + "license": "Apache-2.0", + "dependencies": { + "analytics-node": "1.2.2", + "basic-auth": "1.0.3", + "body-parser": "1.12.3", + "destroy": "1.0.3", + "express": "^4.13.3", + "express-useragent": "0.1.9", + "github-webhook-handler": "0.5.0", + "lodash": "3.7.0", + "octonode": "0.7.1", + "q": "1.2.0", + "request": "2.60.0", + "semver": "5.0.1", + "stores": "0.0.2", + "strip-bom": "2.0.0", + "uuid": "2.0.1" + }, + "devDependencies": { + "mocha": "1.18.2", + "mockery": "^1.4.0", + "rewire": "^2.5.1", + "should": "7.0.4" + }, + "bugs": { + "url": "https://github.com/GitbookIO/nuts/issues" + }, + "authors": [ + { + "name": "Samy Pesse", + "email": "samypesse@gmail.com" } + ], + "repository": { + "type": "git", + "url": "https://github.com/GitbookIO/nuts.git" + }, + "scripts": { + "start": "node bin/web.js", + "test": "mocha --reporter list" + } } diff --git a/test/versions.js b/test/versions.js new file mode 100644 index 00000000..a6d4b3ca --- /dev/null +++ b/test/versions.js @@ -0,0 +1,197 @@ +var + _ = require('lodash'), + should = require('should'), + rewire = require('rewire'); + +describe('Versions', function() { + + var Versions = rewire('../lib/versions'); + + var testVersions = [ + { tag: '0.0.8-alpha.1', + channel: 'alpha', + notes: 'More placeholder text.', + published_at: 'Mon Dec 14 2015 12:13:58 GMT-0800 (PST)', + platforms: [{ + type: 'osx_64' + }, { + type: 'windows_32' + }], + download_count: 3 }, + { tag: '0.0.7', + channel: 'stable', + notes: 'Kinda drifted away from the story thing and I\'m just writing random stuff now.', + published_at: 'Mon Dec 14 2015 11:41:30 GMT-0800 (PST)', + platforms: [{ + type: 'osx_64' + }, { + type: 'windows_32' + }], + download_count: 0 }, + { tag: '0.0.7-beta.1', + channel: 'beta', + notes: 'A beautiful brand new release', + published_at: 'Fri Dec 11 2015 17:01:43 GMT-0800 (PST)', + platforms: [{ + type: 'osx_64' + }, { + type: 'windows_32' + }], + download_count: 0 }, + { tag: '0.0.7-alpha.2', + channel: 'alpha', + notes: 'More excitement!!!', + published_at: 'Fri Dec 11 2015 17:21:08 GMT-0800 (PST)', + platforms: [{ + type: 'osx_64' + }, { + type: 'windows_32' + }], + download_count: 0 }, + { tag: '0.0.7-alpha.1', + channel: 'alpha', + notes: 'A beautiful brand new release', + published_at: 'Fri Dec 11 2015 17:01:43 GMT-0800 (PST)', + platforms: [{ + type: 'windows_32' + }], + download_count: 0 }, + { tag: '0.0.6', + channel: 'stable', + notes: 'A wonderful new release!', + published_at: 'Fri Dec 11 2015 15:55:45 GMT-0800 (PST)', + platforms: [{ + type: 'osx_64' + }], + download_count: 2 } + ]; + + function testVersionFilter(filterName, scenarios) { + describe(filterName, function() { + var filterFunction = Versions.__get__(filterName); + + scenarios.forEach(function(scenario) { + var opts = scenario.opts, + description = 'with opts = { tag: ' + opts.tag + ', channel: ' + opts.channel + + ', platform: ' + opts.platform + ' }, return -> [' + scenario.expectation.join(', ') + ']'; + + it(description, function() { + var result = _.chain(testVersions) + .filter(filterFunction(scenario.opts)) + .pluck('tag') + .value() + .join(', ') + .should.equal(scenario.expectation.join(', ')); + }); + }); + }); + } + + testVersionFilter('newVersionFilter', [ + { + opts: { + tag: '0.0.7', + platform: 'osx', + channel: undefined + }, + expectation: [] + }, + { + opts: { + tag: '0.0.6', + platform: 'osx', + channel: undefined + }, + expectation: ['0.0.7'] + }, + { + opts: { + tag: '0.0.7-beta.1', + platform: 'osx', + channel: undefined + }, + expectation: ['0.0.7'] + }, + { + opts: { + tag: '0.0.5', + platform: 'osx', + channel: undefined + }, + expectation: ['0.0.7', '0.0.6'] + }, + { + opts: { + tag: '0.0.7-alpha.2', + platform: 'osx', + channel: 'alpha' + }, + expectation: ['0.0.8-alpha.1'] + }, + { + opts: { + tag: '0.0.7-beta.1', + platform: 'osx', + channel: 'beta' + }, + expectation: [] + }, + { + opts: { + tag: '0.0.7-alpha.1', + platform: 'windows', + channel: 'alpha' + }, + expectation: ['0.0.8-alpha.1', '0.0.7-alpha.2'] + } + ]); + + testVersionFilter('allVersionFilter', [ + { + opts: {}, + expectation: ['0.0.8-alpha.1', '0.0.7', '0.0.7-beta.1', '0.0.7-alpha.2', + '0.0.7-alpha.1', '0.0.6'] + }, + { + opts: { + platform: 'osx' + }, + expectation: ['0.0.8-alpha.1', '0.0.7', '0.0.7-beta.1', '0.0.7-alpha.2', + '0.0.6'] + }, + { + opts: { + platform: 'windows' + }, + expectation: ['0.0.8-alpha.1', '0.0.7', '0.0.7-beta.1', '0.0.7-alpha.2', + '0.0.7-alpha.1'] + }, + { + opts: { + channel: 'alpha' + }, + expectation: ['0.0.8-alpha.1', '0.0.7-alpha.2', '0.0.7-alpha.1'] + }, + { + opts: { + channel: '*' + }, + expectation: ['0.0.8-alpha.1', '0.0.7', '0.0.7-beta.1', '0.0.7-alpha.2', + '0.0.7-alpha.1', '0.0.6'] + }, + { + opts: { + channel: 'alpha', + platform: 'osx' + }, + expectation: ['0.0.8-alpha.1', '0.0.7-alpha.2'] + }, + { + opts: { + tag: '0.0.7-alpha.2' + }, + expectation: ['0.0.7-alpha.2'] + } + ]); + +});