diff --git a/README.md b/README.md index b6f6545..79d5282 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,16 @@ In your `package.json` } ``` +Optionnaly additional files can be added to also have their version bumped + +```json +"scripts": { +"release": "mcfly-semantic-release.js --files ./package.json ./bower.json ./config.xml" +} +``` + +Note that the path for the `--files` option is relative to your current root directory + Then, to publish a new version execute the following command: ```bash npm run release diff --git a/bin/mcfly-semantic-release.js b/bin/mcfly-semantic-release.js index e6a164b..822e722 100644 --- a/bin/mcfly-semantic-release.js +++ b/bin/mcfly-semantic-release.js @@ -2,51 +2,57 @@ 'use strict'; global.Promise = require('bluebird'); -var execAsync = Promise.promisify(require('child_process').exec); -const simpleGit = require('simple-git')(); + +const args = require('yargs').argv; const chalk = require('chalk'); -const changelog = require('conventional-changelog'); +const changelogScript = require('../lib/changelog-script'); +const gitHelper = require('../lib/githelper'); +const githubHelper = require('../lib/githubHelper'); const inquirer = require('inquirer'); +const path = require('path'); const versionHelper = require('../lib/versionHelper'); -const githubHelper = require('../lib/githubHelper'); +const _ = require('lodash'); -// Repo constants -// const repoOwner = 'hassony2'; -// const repoName = 'deploy-test'; +var files = args.files ? [].concat(args.files) : []; -function makeChangelog(version) { - return new Promise(function(resolve, reject) { - var changelogString = ''; - var changeStream = changelog({ - preset: 'angular' - }, { - version: version - }); - changeStream.on('error', function(err) { - console.log(err); - reject(err); - }); - changeStream.on('data', (chunk) => { - changelogString += chunk; - }); - changeStream.on('end', () => { - resolve(changelogString); - }); - }); +if (files.length === 0) { + files.push('./package.json'); } - -const pkg = require('../package.json'); -const currentVersion = pkg.version; -let versionarg = process.argv[2]; -if (versionarg !== 'patch' && versionarg !== 'minor' && versionarg !== 'major') { - versionarg = 'patch'; -} -const nextVersion = versionHelper.bump(currentVersion, versionarg); +files = _.map(files, (file) => { + return path.isAbsolute(file) ? file : path.join(process.cwd(), file); +}); var msg = {}; -var changelogContent; +msg.currentVersion = versionHelper.getCurrentVersion(path.join(process.cwd(), './package.json')); +msg.nextVersion = versionHelper.bump(msg.currentVersion, args.type); -githubHelper.getUsername() +gitHelper.getCurrentBranch() + .then((currentBranch) => { + if (currentBranch !== 'master') { + throw new Error('To create a release you must be on the master branch'); + } + return; + }) + .then(() => { + return gitHelper.isClean() + .then(res => { + if (!res) { + throw new Error('Your repository has unstaged changes, you must commit your work before releasing a new version'); + } + return res; + }); + }) + .then(() => { + return gitHelper.getRemoteRepository(); + }) + .then((repoInfo) => { + msg.repo = repoInfo.repo; + msg.owner = repoInfo.owner; + msg.repoUrl = repoInfo.url; + }) + .then(() => { + return githubHelper.getUsername(); + }) .then((username) => { msg.username = username; return inquirer.prompt([{ @@ -70,60 +76,41 @@ githubHelper.getUsername() }]); }) .then((answers) => { + console.log(chalk.yellow('Github authentication...')); msg.username = answers.username || msg.username; msg.password = answers.password; - return githubHelper.getClient(msg.username, msg.password); }) .then((github) => { msg.github = github; - return makeChangelog(nextVersion); + return; }) - .then((changelogResult) => { - changelogContent = changelogResult; - console.log(changelogContent); - console.log('npm version ' + versionarg); - return execAsync('npm version ' + versionarg); - + .then(() => { + console.log(chalk.yellow('Generating changelog...')); + changelogScript.init(msg.repoUrl); + return changelogScript.generate(msg.nextVersion) + .then((changelogContent) => { + msg.changelogContent = changelogContent; + return msg; + }); }) - .then((result) => { - console.log('git push origin v' + nextVersion + ' --porcelain'); - //return execAsync('git push origin v' + nextVersion + ' --porcelain'); - return simpleGit.push('origin', 'v' + nextVersion); + .then((msg) => { + console.log(chalk.yellow('Bumping files...')); + return versionHelper.bumpFiles(files, msg.nextVersion) + .then(() => msg); }) - .then((result) => { - console.log('delay before release...'); + .then((msg) => { + console.log(chalk.yellow('Commiting version...')); + return gitHelper.commitVersion(msg.nextVersion) + .then(() => msg); }) .delay(1000) - .then(() => { - // var versionName = 'v' + nextVersion; - - // github.authenticate({ - // type: 'basic', - // username: msg.username, - // password: msg.password - // }); - - // return new Promise(function(resolve, reject) { - // github.repos.createRelease({ - // user: repoOwner, - // repo: repoName, - // tag_name: versionName, - // name: versionName, - // body: changelogContent - // }, function(err, res) { - // if (err) { - // reject(err); - // } else { - - // resolve(res); - // } - // }); - // }); + .then((msg) => { + console.log(chalk.yellow('Publishing version...')); + return githubHelper.createRelease(msg); }) .then((res) => { - console.log('release published at ', res.published_at); - console.log(chalk.green('finished')); + console.log(chalk.green(`Release ${res.name} successfully published!`)); }) .catch(function(err) { console.log(chalk.red(err)); diff --git a/lib/changelog-script.js b/lib/changelog-script.js index bcf7136..9993111 100644 --- a/lib/changelog-script.js +++ b/lib/changelog-script.js @@ -11,8 +11,8 @@ var GIT_LOG_CMD = 'git log --grep="%s" -E --format=%s %s'; var GIT_TAG_CMD = 'git describe --tags --abbrev=0'; var HEADER_TPL = '\n# %s (%s)\n\n'; -var LINK_ISSUE = ''; //= '[#%s](' + constants.repository + '/issues/%s)'; -var LINK_COMMIT = ''; // = '[%s](' + constants.repository + '/commit/%s)'; +var LINK_ISSUE = ''; +var LINK_COMMIT = ''; var EMPTY_COMPONENT = '$$'; diff --git a/lib/gitHelper.js b/lib/gitHelper.js new file mode 100644 index 0000000..466a69b --- /dev/null +++ b/lib/gitHelper.js @@ -0,0 +1,71 @@ +'use strict'; +global.Promise = require('bluebird'); + +const git = require('simple-git')(); +const _ = require('lodash'); + +var getCurrentBranch = function() { + return Promise + .fromCallback((cb) => { + git.branch(cb); + }) + .then(branches => { + return branches.current; + }); +}; + +var commitVersion = function(version) { + return Promise + .fromCallback((cb) => { + git + .add('./*') + .commit('docs(changelog): version ' + version) + .addAnnotatedTag(version, 'v' + version) + .push('origin', 'master') + .pushTags('origin', cb); + }); +}; + +var getRemoteRepository = function() { + return Promise + .fromCallback((cb) => { + git + .getRemotes(true, cb); + }) + .then(remotes => { + return _.chain(remotes) + .find(remote => { + return remote.name === 'origin'; + }) + .value(); + }) + .then(remote => { + if (!remote) { + throw new Error('"origin" remote repository is not configured'); + } + var repoUrl = remote.refs.push; + var ownerRepo = repoUrl.split('/').slice(-2); + return { + url: repoUrl, + owner: ownerRepo[0], + repo: ownerRepo[1] + }; + }); +}; + +var isClean = function() { + return Promise + .fromCallback(cb => { + git.status(cb); + }) + .then(res => { + return res.deleted.length + res.modified.length + res.created.length + res.conflicted.length <= 0; + }); +}; + +module.exports = { + getCurrentBranch, + commitVersion, + getRemoteRepository, + isClean +}; diff --git a/lib/githubHelper.js b/lib/githubHelper.js index c2100de..2bbb5fa 100644 --- a/lib/githubHelper.js +++ b/lib/githubHelper.js @@ -84,7 +84,7 @@ var checkClient = function(github) { }); }; -var getClient = function(username, password){ +var getClient = function(username, password) { var github = buildClient(username, password); return checkClient(github); }; @@ -189,6 +189,24 @@ var createTokenFile = function(username, password, tokenName, filename) { }; +var createRelease = function(param) { + return new Promise(function(resolve, reject) { + param.github.repos.createRelease({ + user: param.owner, + repo: param.repo, + tag_name: param.nextVersion, + name: 'v' + param.nextVersion, + body: param.changelogContent + }, function(err, res) { + if (err) { + reject(err); + } else { + resolve(res); + } + }); + }); +}; + module.exports = { getUsername, buildClient, @@ -196,5 +214,6 @@ module.exports = { getRepo, getAllRepos, getPackageJson, - createTokenFile + createTokenFile, + createRelease }; diff --git a/lib/versionHelper.js b/lib/versionHelper.js index 08bcb83..cea3a6a 100644 --- a/lib/versionHelper.js +++ b/lib/versionHelper.js @@ -1,11 +1,13 @@ 'use strict'; global.Promise = require('bluebird'); + +const fileHelper = require('./fileHelper'); const fs = require('fs'); const path = require('path'); const semver = require('semver'); -const fileHelper = require('./fileHelper'); const XML = require('node-jsxml').XML; +const _ = require('lodash'); /** * Filters a list of files given an extension @@ -63,22 +65,44 @@ var bumpFiles = function(files, version, outputDir) { var json = fileHelper.readJsonFile(file); json.version = version; + let retval = JSON.stringify(json, null, 4); return Promise.fromCallback(function(cb) { - fs.writeFile(getOutputFile(file, outputDir), JSON.stringify(json, null, 4), cb); - }); + fs.writeFile(getOutputFile(file, outputDir), retval, cb); + }) + .then(() => { + return { + file: file, + content: retval + }; + }); }); var xmlBump = Promise.map(xmlFiles, (file) => { var xml = new XML(String(fileHelper.readTextFile(file))); + let retval; xml.attribute('version').setValue(version); return Promise.fromCallback(function(cb) { - fs.writeFile(getOutputFile(file, outputDir), xml.toXMLString(), cb); - }); + retval = xml.toXMLString(); + fs.writeFile(getOutputFile(file, outputDir), retval, cb); + }) + .then(() => { + return { + file: file, + content: retval + }; + }); }); - return Promise.all([xmlBump, jsonBump]); + return Promise.all([xmlBump, jsonBump]) + .then(_.flatten); +}; + +var getCurrentVersion = function(file) { + var json = fileHelper.readJsonFile(file); + return json.version; }; module.exports = { filterFiles, bump, - bumpFiles + bumpFiles, + getCurrentVersion }; diff --git a/package.json b/package.json index 87b2330..0aaf89f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "node-jsxml": "^0.7.0", "semver": "^5.1.0", "simple-git": "^1.37.0", - "strip-json-comments": "^2.0.1" + "strip-json-comments": "^2.0.1", + "yargs": "^4.7.1" }, "devDependencies": { "chai": "^3.5.0", diff --git a/test/mocha/gitHelper.spec.js b/test/mocha/gitHelper.spec.js new file mode 100644 index 0000000..cceede2 --- /dev/null +++ b/test/mocha/gitHelper.spec.js @@ -0,0 +1,44 @@ +'use strict'; + +var gitHelper = require('../../lib/gitHelper'); +var expect = require('chai').expect; +var _ = require('lodash'); + +describe('gitHelper', () => { + describe('getCurrentBranch()', () => { + it('should return current branch', (done) => { + gitHelper.getCurrentBranch() + .then(branch => { + expect(branch).to.not.be.null; + done(); + }) + .catch(done); + }); + }); + describe('getRemoteRepository()', () => { + it('should return remote repository', (done) => { + + gitHelper.getRemoteRepository() + .then(remote => { + expect(remote.url).to.not.be.null; + expect(remote.repo).to.not.be.null; + expect(remote.owner).to.not.be.null; + expect(_.startsWith(remote.url, 'https://')).to.be.true; + done(); + }) + .catch(done); + }); + }); + + describe('isClean()', () => { + it('should return a boolean', (done) => { + + gitHelper.isClean() + .then(res => { + expect(res).to.be.a('boolean'); + done(); + }) + .catch(done); + }); + }); +}); diff --git a/test/mocha/versionHelper.spec.js b/test/mocha/versionHelper.spec.js index 6e7b090..07b64b7 100644 --- a/test/mocha/versionHelper.spec.js +++ b/test/mocha/versionHelper.spec.js @@ -1,6 +1,8 @@ 'use strict'; const expect = require('chai').expect; -let versionHelper = require('../../lib/versionHelper'); +const versionHelper = require('../../lib/versionHelper'); +const fileHelper = require('../../lib/fileHelper'); +const _ = require('lodash'); describe('versionHelper', () => { describe('filterFiles()', () => { @@ -46,11 +48,30 @@ describe('versionHelper', () => { }); }); - describe.only('bumpFiles()', () => { + describe('bumpFiles()', () => { it('should succeed', (done) => { - versionHelper.bumpFiles(['./test/assets/package.json', './test/assets/bower.json', './test/assets/config.xml'], '2.4.11', './test/results') + var files = ['./test/assets/package.json', './test/assets/bower.json', './test/assets/config.xml']; + var version = '3.4.11'; + versionHelper.bumpFiles(files, version, './test/results') .then(res => { + expect(res).to.be.an('array'); + expect(res.length).to.equal(files.length); + var packageJson = fileHelper.readTextFile('./test/results/package.json'); + var bowerJson = fileHelper.readTextFile('./test/results/bower.json'); + var configXML = fileHelper.readTextFile('./test/results/config.xml'); + expect(JSON.parse(packageJson).version).to.equal(version); + expect(JSON.parse(bowerJson).version).to.equal(version); + expect(configXML.indexOf(`version="${version}"`)).to.be.above(1); + expect(_.find(res, { + file: files[0] + }).content).to.equal(packageJson); + expect(_.find(res, { + file: files[1] + }).content).to.equal(bowerJson); + expect(_.find(res, { + file: files[2] + }).content).to.equal(configXML); done(); }) .catch(done);