UNPKG

7.19 kBJavaScriptView Raw
1#!/usr/bin/env node
2
3// Release automation script inspired by
4// https://github.com/geddski/grunt-release
5
6var fs = require('fs');
7var path = require('path');
8
9var _ = require('lodash');
10var S = require('string');
11var shell = require('shelljs');
12var Mustache = require('mustache');
13var semver = require('semver');
14var program = require('commander');
15var Promise = require('bluebird');
16
17
18// Message templates use https://github.com/janl/mustache.js
19var config = {
20 releaseMessage: 'Release {{ version }}',
21 backToDevMessage: 'Bump to dev version',
22 bumpType: 'patch',
23 files: ['package.json'],
24 readmeFile: 'README.md',
25 indentation: 4,
26
27 // If true, don't execute anything, just tell what would have been done
28 dryRun: false,
29
30 // If true, don't push commits/tags or release to npm
31 noPush: false,
32 consolePrefix: '->',
33 devSuffix: '-dev'
34};
35
36var projectRoot = path.join(__dirname, '..');
37process.chdir(projectRoot);
38
39function main() {
40 parseArgs();
41 config = mergeArgsToDefaults(config);
42
43 if (config.dryRun) status('Dry run\n');
44
45 var newVersion = bumpVersion(config.files, config.bumpType);
46
47 _gitBranchName()
48 .then(function(stdout) {
49 if (stdout.trim().toLowerCase() !== 'master') {
50 throw new Error('You should be in master branch before running the script!');
51 }
52
53 return gitAdd([config.readmeFile]);
54 })
55 .then(function() {
56 return gitAdd(config.files);
57 })
58 .then(function() {
59 var message = Mustache.render(config.releaseMessage, {
60 version: newVersion
61 });
62
63 return gitCommit(message);
64 })
65 .then(function() {
66 return gitTag(newVersion);
67 })
68 .then(function() {
69 return gitPushTag(newVersion);
70 })
71 .then(npmPublish)
72 .then(function() {
73 bumpVersion(config.files, 'dev');
74 return gitAdd(config.files.concat(config.readmeFile));
75 })
76 .then(function() {
77 return gitCommit(config.backToDevMessage);
78 })
79 .then(function() {
80 gitPush();
81 })
82 .then(function() {
83 console.log('');
84 status('Release successfully done!');
85 })
86 .catch(function(err) {
87 console.error('\n!! Releasing failed');
88 console.trace(err);
89 process.exit(2);
90 });
91}
92
93function parseArgs() {
94 program
95 .usage('bump');
96
97 program.on('--help', function() {
98 console.log(' Example usage:');
99 console.log('');
100 console.log(' $ ./release.js minor');
101 });
102
103 program.parse(process.argv);
104}
105
106function mergeArgsToDefaults(config) {
107 if (program.args[0]) {
108 config.bumpType = program.args[0];
109
110 if (!_.contains(['major', 'minor', 'patch'], config.bumpType)) {
111 console.error('Error:', config.bumpType, 'is not a valid bump type');
112 process.exit(1);
113 }
114 }
115
116 return config;
117}
118
119function status( /* arguments */ ) {
120 var args = Array.prototype.slice.call(arguments);
121 console.log(config.consolePrefix, args.join(' '));
122}
123
124function run(cmd, msg) {
125 // All calls are actually synchronous but eventually some task
126 // will need async stuff, so keep them promises
127 return new Promise(function(resolve, reject) {
128 if (msg) {
129 status(msg);
130 }
131
132 if (config.dryRun) {
133 return resolve();
134 }
135
136 var exec = shell.exec(cmd);
137 var success = exec.code === 0;
138
139 if (success) {
140 resolve(exec.output);
141 } else {
142 var errMsg = 'Error executing: `' + cmd + '`\nOutput:\n' + exec.output;
143 var err = new Error(errMsg);
144 reject(err);
145 }
146 });
147}
148
149// Task functions
150// All functions should return promise
151
152// Bumps version in specified files.
153// Files are assumed to contain JSON data which has "version" key following
154// semantic versioning
155function bumpVersion(files, bumpType) {
156 status('Bump', bumpType, 'version to files:', files.join(' '));
157 if (config.dryRun) return '[not available in dry run]';
158
159 var newVersion;
160 var originalVersion;
161 files.forEach(function(fileName) {
162 var filePath = path.join(projectRoot, fileName);
163
164 var data = JSON.parse(fs.readFileSync(filePath));
165 originalVersion = data.version;
166 var currentVersion = data.version;
167 if (!semver.valid(currentVersion)) {
168 var msg = 'Invalid version ' + currentVersion +
169 ' in file ' + fileName;
170 var err = new Error(msg);
171 throw err;
172 }
173
174 if (S(currentVersion).endsWith(config.devSuffix)) {
175 currentVersion = S(currentVersion).chompRight(config.devSuffix).s;
176 }
177
178 if (bumpType === 'dev') {
179 newVersion = currentVersion + config.devSuffix;
180 } else {
181 newVersion = semver.inc(currentVersion, bumpType);
182 }
183 data.version = newVersion;
184
185 var content = JSON.stringify(data, null, config.indentation);
186 fs.writeFileSync(filePath, content);
187
188 status('Bump', originalVersion, '->', newVersion, 'in',
189 fileName);
190 });
191
192 bumpReadmeVersion(originalVersion, newVersion, bumpType);
193
194 return newVersion;
195}
196
197function bumpReadmeVersion(oldVersion, newVersion, bumpType) {
198 if (bumpType === 'dev') {
199 // Don't bump readme version in to dev version
200 return;
201 }
202
203 var oldReleaseVersion = oldVersion;
204 if (S(oldReleaseVersion).endsWith(config.devSuffix)) {
205 oldReleaseVersion = S(oldReleaseVersion).chompRight(config.devSuffix).s;
206 }
207
208 status('Replace readme version', oldReleaseVersion, '->', newVersion);
209 if (config.dryRun) return;
210
211 var filePath = path.join(projectRoot, config.readmeFile);
212 var content = fs.readFileSync(filePath, {encoding: 'utf-8'});
213
214 // Update visible version
215 var re = new RegExp('Version: ' + oldReleaseVersion, 'g');
216 var newContent = content.replace(re, 'Version: ' + newVersion);
217
218 // Replace link to previous stable
219 re = new RegExp('tree/[0-9]\\.[0-9]\\.[0-9]');
220 newContent = newContent.replace(re, 'tree/' + oldReleaseVersion);
221
222 fs.writeFileSync(filePath, newContent);
223}
224
225function gitAdd(files) {
226 var cmd = 'git add ' + files.join(' ');
227 var msg = 'Staged ' + files.length + ' files';
228 return run(cmd, msg);
229}
230
231function gitCommit(message) {
232 var cmd = 'git commit -m "' + message + '"';
233 var msg = 'Commit files';
234 return run(cmd, msg);
235}
236
237function gitTag(name) {
238 var cmd = 'git tag ' + name;
239 var msg = 'Created a new git tag: ' + name;
240 return run(cmd, msg);
241}
242
243function gitPush() {
244 if (config.noPush) return;
245
246 var cmd = 'git push';
247 var msg = 'Push to remote';
248 return run(cmd, msg);
249}
250
251function gitPushTag(tagName) {
252 if (config.noPush) return;
253
254 var cmd = 'git push origin ' + tagName;
255 var msg = 'Push created git tag to remote';
256 return run(cmd, msg);
257}
258
259function npmPublish() {
260 if (config.noPush) return;
261
262 var cmd = 'npm publish';
263 var msg = 'Publish to npm';
264 return run(cmd, msg);
265}
266
267function _gitBranchName() {
268 var cmd = 'git rev-parse --abbrev-ref HEAD';
269 return run(cmd, false);
270}
271
272
273main();