From bd0e1a31eeb3598742c105907e259a8db23daa09 Mon Sep 17 00:00:00 2001 From: Kris Selden Date: Wed, 11 Jul 2018 17:18:01 -0700 Subject: [PATCH] [BUGFIX] Centralize build versioning info Make BUILD_TYPE only affect published buildType if on master so we don't accidentally publish from lts/release branches over the current beta or release. Only use a tag as version if it is parseable by semver. The build-for-publishing script uses git info instead of travis env. --- .eslintrc.js | 1 + bin/build-for-publishing.js | 18 ++-- bin/publish_to_s3.js | 20 ++-- broccoli/build-info.js | 162 +++++++++++++++++++++++++++++++ broccoli/version.js | 35 +------ tests/node/build-info-test.js | 177 ++++++++++++++++++++++++++++++++++ 6 files changed, 360 insertions(+), 53 deletions(-) create mode 100644 broccoli/build-info.js create mode 100644 tests/node/build-info-test.js diff --git a/.eslintrc.js b/.eslintrc.js index 99e9101029d..772b5e4560e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -139,6 +139,7 @@ module.exports = { // matches node-land files that aren't shipped to consumers (allows using Node 6+ features) files: [ 'broccoli/**/*.js', + 'tests/node/**/*.js', 'ember-cli-build.js', 'rollup.config.js', 'd8-runner.js', diff --git a/bin/build-for-publishing.js b/bin/build-for-publishing.js index 46e31ff6b08..7dbc2df57f8 100755 --- a/bin/build-for-publishing.js +++ b/bin/build-for-publishing.js @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); const execa = require('execa'); -const VERSION = require('../broccoli/version').VERSION; +const buildInfo = require('../broccoli/build-info').buildInfo(); function exec(command, args) { // eslint-disable-next-line @@ -23,9 +23,11 @@ function updatePackageJSONVersion() { let pkgContents = fs.readFileSync(packageJSONPath, { encoding: 'utf-8' }); let pkg = JSON.parse(pkgContents); + if (!pkg._originalVersion) { + pkg._originalVersion = pkg.version; + } pkg._versionPreviouslyCalculated = true; - pkg._originalVersion = pkg.version; - pkg.version = VERSION; + pkg.version = buildInfo.version; fs.writeFileSync(packageJSONPath, JSON.stringify(pkg, null, 2), { encoding: 'utf-8', }); @@ -44,7 +46,7 @@ function updateDocumentationVersion() { let contents = fs.readFileSync(docsPath, { encoding: 'utf-8' }); let docs = JSON.parse(contents); - docs.project.version = VERSION; + docs.project.version = buildInfo.version; fs.writeFileSync(docsPath, JSON.stringify(docs, null, 2), { encoding: 'utf-8', }); @@ -69,10 +71,10 @@ Promise.resolve() .then(() => { // generate build-metadata.json const metadata = { - version: VERSION, - buildType: process.env.BUILD_TYPE, - SHA: process.env.TRAVIS_COMMIT, - assetPath: `/${process.env.BUILD_TYPE}/shas/${process.env.TRAVIS_COMMIT}.tgz`, + version: buildInfo.version, + buildType: buildInfo.channel, + SHA: buildInfo.sha, + assetPath: `/${buildInfo.channel}/shas/${buildInfo.sha}.tgz`, }; fs.writeFileSync('build-metadata.json', JSON.stringify(metadata, null, 2), { encoding: 'utf-8', diff --git a/bin/publish_to_s3.js b/bin/publish_to_s3.js index c8cffde8b14..1d1b03d8856 100755 --- a/bin/publish_to_s3.js +++ b/bin/publish_to_s3.js @@ -1,3 +1,7 @@ +'use strict'; + +const buildInfo = require('../broccoli/build-info').buildInfo(); + // To invoke this from the commandline you need the following to env vars to exist: // // S3_BUCKET_NAME @@ -12,21 +16,13 @@ // ```sh // ./bin/publish_to_s3.js // ``` -var S3Publisher = require('ember-publisher'); -var configPath = require('path').join(__dirname, '../config/s3ProjectConfig.js'); +const S3Publisher = require('ember-publisher'); +const configPath = require('path').join(__dirname, '../config/s3ProjectConfig.js'); -var publisher = new S3Publisher({ projectConfigPath: configPath }); +let publisher = new S3Publisher({ projectConfigPath: configPath }); publisher.currentBranch = function() { - return ( - process.env.BUILD_TYPE || - { - master: 'canary', - beta: 'beta', - release: 'release', - 'lts-2-4': 'lts-2-4', - }[this.CURRENT_BRANCH] - ); + return buildInfo.channel; }; publisher.publish(); diff --git a/broccoli/build-info.js b/broccoli/build-info.js new file mode 100644 index 00000000000..1b0aafcd8b2 --- /dev/null +++ b/broccoli/build-info.js @@ -0,0 +1,162 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); +const gitRepoInfo = require('git-repo-info'); +const semver = require('semver'); + +const NON_SEMVER_IDENTIFIER = /[^0-9A-Za-z-]/g; + +/** @type {BuildInfo} */ +let cached; + +/** + * @param {Options=} options + * @returns {BuildInfo} + */ +function buildInfo(options) { + if (!options && cached) { + return cached; + } + let root = (options && options.root) || path.resolve(__dirname, '..'); + let packageVersion = (options && options.packageVersion) || readPackageVersion(root); + let gitInfo = (options && options.gitInfo) || buildGitInfo(root); + let buildInfo = buildFromParts(packageVersion, gitInfo); + if (!options) { + cached = buildInfo; + } + return buildInfo; +} + +/** + * @param {string} root + * @returns {GitInfo} + */ +function buildGitInfo(root) { + let info = gitRepoInfo(root); + return { + sha: process.env.TRAVIS_COMMIT || info.sha, + branch: process.env.TRAVIS_BRANCH || info.branch, + tag: process.env.TRAVIS_TAG || info.tag, + }; +} + +/** + * @typedef {Object} GitInfo + * @property {string} sha + * @property {string=} branch + * @property {string=} tag + */ + +/** + * @typedef {Object} Options + * @property {string=} root + * @property {string=} packageVersion + * @property {GitInfo=} gitInfo + */ + +/** + * @typedef {Object} BuildInfo + * @property {string=} tag + * @property {string=} branch + * @property {string} sha + * @property {string} shortSha + * @property {string=} channel + * @property {string} packageVersion + * @property {string=} tagVersion + * @property {string} version + */ + +/** + * Build info object from parts. + * @param {string} packageVersion + * @param {GitInfo} gitInfo + * @returns {BuildInfo} + */ +function buildFromParts(packageVersion, gitInfo) { + // Travis builds are always detached + let { tag, branch, sha } = gitInfo; + + let tagVersion = parseTagVersion(tag); + let shortSha = sha.slice(0, 8); + let channel = + branch === 'master' + ? process.env.BUILD_TYPE === 'alpha' ? 'alpha' : 'canary' + : branch && escapeSemVerIdentifier(branch); + let version = tagVersion || buildVersion(packageVersion, shortSha, channel); + + return { + tag, + branch, + sha, + shortSha, + channel, + packageVersion, + tagVersion, + version, + }; +} + +/** + * Read package version. + * @param {string} root + * @returns {string} + */ +function readPackageVersion(root) { + let pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); + // use _originalVersion if present if we've already mutated it + return pkg._originalVersion || pkg.version; +} + +/** + * @param {string} tag + */ +function parseTagVersion(tag) { + try { + return tag && semver.parse(tag).version; + } catch (e) { + return; + } +} + +/** + * @param {string} txt + */ +function escapeSemVerIdentifier(txt) { + return txt.replace(NON_SEMVER_IDENTIFIER, '-'); +} + +/** + * @param {string} packageVersion + * @param {string} sha + * @param {string=} channel + */ +function buildVersion(packageVersion, sha, channel) { + let base = semver.parse(packageVersion); + let major = base.major; + let minor = base.minor; + let patch = base.patch; + let suffix = ''; + suffix += toSuffix('-', base.prerelease, channel); + suffix += toSuffix('+', base.build, sha); + return `${major}.${minor}.${patch}${suffix}`; +} + +/** + * @param {string} delim + * @param {string[]} identifiers + * @param {string=} identifier + */ +function toSuffix(delim, identifiers, identifier) { + if (identifier) { + identifiers = identifiers.concat([identifier]); + } + if (identifiers.length > 0) { + return delim + identifiers.join('.'); + } + return ''; +} + +module.exports.buildInfo = buildInfo; +module.exports.buildFromParts = buildFromParts; +module.exports.buildVersion = buildVersion; +module.exports.parseTagVersion = parseTagVersion; diff --git a/broccoli/version.js b/broccoli/version.js index 421ea22f1b2..adbcbfd451b 100644 --- a/broccoli/version.js +++ b/broccoli/version.js @@ -1,36 +1,5 @@ 'use strict'; -const getGitInfo = require('git-repo-info'); -const path = require('path'); +const buildInfo = require('../broccoli/build-info').buildInfo(); -module.exports.VERSION = (() => { - let info = getGitInfo(path.resolve(__dirname, '..')); - // if the current commit _is_ a tagged commit, use the tag as the version - // number - if (info.tag) { - return info.tag.replace(/^v/, ''); - } - - let pkg = require('../package'); - // if `_versionPreviouslyCalculated` is set the `package.json` version string - // _already_ includes the branch, sha, etc (from bin/build-for-publishing.js) - // so just use it directly - if (pkg._versionPreviouslyCalculated) { - return pkg.version; - } - - // otherwise build the version number up of: - // - // * actual package.json version string - // * current "build type" or branch name (in CI this is generally not - // present, but it is very useful for local / testing builds) - // * the sha for the commit - let packageVersion = pkg.version; - let sha = info.sha || ''; - let suffix = process.env.BUILD_TYPE || info.branch; - // * remove illegal non-alphanumeric characters from branch name. - suffix = suffix && suffix.replace(/[^a-zA-Z\d\s-]/g, '-'); - let metadata = sha.slice(0, 8); - - return `${packageVersion}${suffix ? '-' + suffix : ''}+${metadata}`; -})(); +module.exports.VERSION = buildInfo.version; diff --git a/tests/node/build-info-test.js b/tests/node/build-info-test.js new file mode 100644 index 00000000000..ae057d66071 --- /dev/null +++ b/tests/node/build-info-test.js @@ -0,0 +1,177 @@ +'use strict'; + +const { buildVersion, parseTagVersion, buildFromParts } = require('../../broccoli/build-info'); + +QUnit.module('buildVersion', () => { + flatMap( + [ + { + args: ['3.4.4', '396fae9206'], + expected: '3.4.4+396fae9206', + }, + { + args: ['3.2.2', '94f2258f', 'canary'], + expected: '3.2.2-canary+94f2258f', + }, + { + args: ['3.2.2', 'f572d396', 'canary'], + expected: '3.2.2-canary+f572d396', + }, + { + args: ['3.1.1-beta.2', 'f572d396fae9206628714fb2ce00f72e94f2258f'], + expected: '3.1.1-beta.2+f572d396fae9206628714fb2ce00f72e94f2258f', + }, + { + args: ['3.1.1-beta.2', 'f572d396fae9206628714fb2ce00f72e94f2258f', 'beta'], + expected: '3.1.1-beta.2.beta+f572d396fae9206628714fb2ce00f72e94f2258f', + }, + { + args: ['3.1.1-beta.2+build.100', '94f2258f', 'beta'], + expected: '3.1.1-beta.2.beta+build.100.94f2258f', + }, + ], + padEmptyArgs(3, [null, '']) + ).forEach(({ args, expected }) => { + QUnit.test(JSON.stringify(args), assert => { + assert.equal(buildVersion(...args), expected); + }); + }); +}); + +QUnit.module('parseTagVersion', () => { + [ + { + tag: 'v3.4.4', + expected: '3.4.4', + }, + { + tag: 'v3.1.1-beta.2', + expected: '3.1.1-beta.2', + }, + { + tag: 'some-non-version-tag', + expected: undefined, + }, + ].forEach(({ tag, expected }) => { + QUnit.test(JSON.stringify(tag), assert => { + assert.equal(parseTagVersion(tag), expected); + }); + }); +}); + +QUnit.module('buildFromParts', () => { + [ + { + args: [ + '3.4.4', + { + sha: 'f572d396fae9206628714fb2ce00f72e94f2258f', + branch: 'canary', + tag: null, + }, + ], + expected: { + tag: null, + branch: 'canary', + sha: 'f572d396fae9206628714fb2ce00f72e94f2258f', + shortSha: 'f572d396', + channel: 'canary', + packageVersion: '3.4.4', + tagVersion: null, + version: '3.4.4-canary+f572d396', + }, + }, + { + args: [ + '3.4.4', + { + sha: 'f572d396fae9206628714fb2ce00f72e94f2258f', + branch: 'beta', + tag: 'v3.4.4-beta.2', + }, + ], + expected: { + tag: 'v3.4.4-beta.2', + branch: 'beta', + sha: 'f572d396fae9206628714fb2ce00f72e94f2258f', + shortSha: 'f572d396', + channel: 'beta', + packageVersion: '3.4.4', + tagVersion: '3.4.4-beta.2', + version: '3.4.4-beta.2', + }, + }, + { + args: [ + '3.4.4', + { + sha: 'f572d396fae9206628714fb2ce00f72e94f2258f', + branch: 'a "funky" branch', + tag: 'some weird tag', + }, + ], + expected: { + tag: 'some weird tag', + branch: 'a "funky" branch', + sha: 'f572d396fae9206628714fb2ce00f72e94f2258f', + shortSha: 'f572d396', + channel: 'a--funky--branch', + packageVersion: '3.4.4', + tagVersion: undefined, + version: '3.4.4-a--funky--branch+f572d396', + }, + }, + ].forEach(({ args, expected }) => { + QUnit.test(JSON.stringify(args), assert => { + assert.deepEqual(buildFromParts(...args), expected); + }); + }); +}); + +/** + * @typedef {Object} MatrixEntry + * @property {any[]} args + * @property {any} expected + */ + +/** + * Creates additional matrix entries with alternative empty values. + * @param {number} count + * @param {any[]} replacements + */ +function padEmptyArgs(count, replacements) { + /** @type {function(MatrixEntry): MatrixEntry[]} */ + let expand = entry => { + let expanded = [entry]; + let { args, expected } = entry; + if (args.length < count) { + replacements.forEach(replacement => { + expanded.push({ args: padArgs(args, count, replacement), expected }); + }); + } + return expanded; + }; + return expand; +} + +/** + * @param {any[]} args + * @param {number} count + * @param {any} value + */ +function padArgs(args, count, value) { + let padded = args.slice(0); + for (let i = args.length; i < count; i++) { + padded.push(value); + } + return padded; +} + +/** + * @param {MatrixEntry[]} matrix + * @param {function(MatrixEntry): MatrixEntry[]} f + * @returns {MatrixEntry[]} + */ +function flatMap(matrix, f) { + return matrix.reduce((acc, x) => acc.concat(f(x)), []); +}