UNPKG

8.25 kBJavaScriptView Raw
1/*
2 * grunt-release
3 * https://github.com/geddski/grunt-release
4 *
5 * Copyright (c) 2013 Dave Geddes
6 * Licensed under the MIT license.
7 */
8'use strict';
9
10var shell = require('shelljs');
11var semver = require('semver');
12var request = require('superagent');
13var Q = require('q');
14
15module.exports = function(grunt){
16 grunt.registerTask('release', 'Bump version, git tag, git push, npm publish', function(type){
17
18 function setup(file, type){
19 var pkg = grunt.file.readJSON(file);
20 var newVersion = pkg.version;
21 var files;
22
23 if (options.bump) {
24 newVersion = semver.inc(pkg.version, type || 'patch');
25 }
26
27 // Check if options.additionalFiles is a single file
28 if (typeof options.additionalFiles === 'string') {
29 files = options.additionalFiles.split(',').map(function (value) {
30 return value.trim();
31 });
32
33 // You can also add a string with multiple files separated by `,`
34 options.additionalFiles = [].concat(files);
35 }
36
37 options.additionalFiles.push(file);
38
39 return {
40 files: options.additionalFiles,
41 newVersion: newVersion,
42 pkg: pkg
43 };
44 }
45
46 // Defaults
47 var options = grunt.util._.extend({
48 bump: true,
49 changelog: false, // Update changelog file
50
51 // Text which is inserted into change log
52 changelogText: '### <%= version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n',
53
54 // file is in charge of master information, ie, it is it which define the base version to work on
55 file: grunt.config('pkgFile') || 'package.json',
56
57 // additionalFiles are additional files that also need to be bumped
58 additionalFiles: [],
59 add: true,
60 commit: true,
61 tag: true,
62 push: true,
63 pushTags: true,
64 npm: true,
65 remote: 'origin',
66 beforeReleaseTasks: [],
67 afterReleaseTasks: [],
68 beforeBumpTasks: [],
69 afterBumpTasks: []
70 }, (grunt.config.data[this.name] || {}).options);
71 var config = setup(options.file, type);
72
73 var templateOptions = {
74 data: {
75 name: config.name || '',
76 version: config.newVersion
77 }
78 };
79 var tagName = grunt.template.process(grunt.config.getRaw(this.name + '.options.tagName') || '<%= version %>', templateOptions);
80 var commitMessage = grunt.template.process(grunt.config.getRaw(this.name + '.options.commitMessage') || 'release <%= version %>', templateOptions);
81 var tagMessage = grunt.template.process(grunt.config.getRaw(this.name + '.options.tagMessage') || 'version <%= version %>', templateOptions);
82
83 var nowrite = grunt.option('no-write');
84 var indentation = grunt.option('indentation') || ' ';
85 var done = this.async();
86
87 if (!config.newVersion) {
88 grunt.warn('Resulting version number is empty.');
89 }
90
91 if (nowrite){
92 grunt.log.ok('Release dry run.');
93 }
94
95 function getNpmTag(){
96 var tag = grunt.option('npmtag') || options.npmtag;
97 if(tag === true) {
98 tag = config.newVersion;
99 }
100
101 return tag;
102 }
103
104 function ifEnabled(option, fn){
105 if (options[option]) {
106 return fn;
107 }
108 }
109
110 function run(cmd, msg){
111 var deferred = Q.defer();
112 grunt.verbose.writeln('Running: ' + cmd);
113
114 if (nowrite) {
115 grunt.log.ok(msg || cmd);
116 deferred.resolve();
117 }
118 else {
119 var success = shell.exec(cmd, {silent:true}).code === 0;
120
121 if (success){
122 grunt.log.ok(msg || cmd);
123 deferred.resolve();
124 }
125 else{
126 // fail and stop execution of further tasks
127 deferred.reject('Failed when executing: `' + cmd + '`\n');
128 }
129 }
130 return deferred.promise;
131 }
132
133 function changelog(){
134 var filename = options.changelog;
135
136 // Default filename
137 if(options.changelog === true) {
138 filename = 'CHANGELOG.md';
139 }
140
141 config.files.push(filename);
142
143 return Q.fcall(function () {
144 var changelogText = grunt.template.process(options.changelogText, templateOptions);
145 var changelogContent = changelogText + grunt.file.read(filename);
146
147 grunt.file.write(filename, changelogContent);
148 grunt.log.ok('Changelog ' + filename + ' updated');
149 });
150 }
151
152 function add(){
153 var files = config.files.join(' ');
154 return run('git add ' + files, ' staged ' + files);
155 }
156
157 function commit(){
158 if (typeof commitMessage === 'string') {
159 commitMessage = [commitMessage];
160 }
161
162 var message = commitMessage.map(function(el) {
163 return '-m "' + grunt.template.process(el, templateOptions) + '"';
164 }).join(' ');
165
166 return run('git commit ' + message, 'Committed all files');
167 }
168
169 function tag(){
170 return run('git tag ' + tagName + ' -m "'+ tagMessage +'"', 'created new git tag: ' + tagName);
171 }
172
173 function push(){
174 run('git push ' + options.remote + ' HEAD', 'pushed to remote');
175 }
176
177 function pushTags(){
178 run('git push ' + options.remote + ' ' + tagName, 'pushed new tag '+ config.newVersion +' to remote');
179 }
180
181 function publish(){
182 var cmd = 'npm publish';
183 var msg = 'published version '+ config.newVersion +' to npm';
184 var npmtag = getNpmTag();
185 if (npmtag){
186 cmd += ' --tag ' + npmtag;
187 msg += ' with a tag of "' + npmtag + '"';
188 }
189
190 if (options.folder){ cmd += ' ' + options.folder; }
191 return run(cmd, msg);
192 }
193
194 function bump(){
195 var i, file, pkg, promise;
196 var promises = [];
197 for (i = 0; i < config.files.length; i++) {
198 file = config.files[i];
199 promise = (function(file){
200 return Q.fcall(function () {
201 pkg = grunt.file.readJSON(file);
202 pkg.version = config.newVersion;
203 grunt.file.write(file, JSON.stringify(pkg, null, indentation) + '\n');
204 grunt.log.ok('bumped version of ' + file + ' to ' + config.newVersion);
205 });
206 }(file));
207 promises.push(promise);
208 }
209 return Q.all(promises);
210 }
211
212 function githubRelease(){
213 var deferred = Q.defer();
214
215 function success(){
216 grunt.log.ok('created ' + tagName + ' release on github.');
217 deferred.resolve();
218 }
219
220 if (nowrite){
221 success();
222 return;
223 }
224
225 request
226 .post('https://api.github.com/repos/' + options.github.repo + '/releases')
227 .auth(process.env[options.github.usernameVar], process.env[options.github.passwordVar])
228 .set('Accept', 'application/vnd.github.manifold-preview')
229 .set('User-Agent', 'grunt-release')
230 .send({
231 "tag_name": tagName,
232 "name": tagMessage
233 })
234 .end(function(res){
235 if (res.statusCode === 201){
236 success();
237 } else {
238 deferred.reject('Error creating github release. Response: ' + res.text);
239 }
240 });
241
242 return deferred.promise;
243 }
244
245 function runTasks(taskName) {
246 var map = {
247 beforeBump: 'beforeBumpTasks',
248 afterBump: 'afterBumpTasks',
249 beforeRelease: 'beforeReleaseTasks',
250 afterRelease: 'afterReleaseTasks'
251 };
252 return function () {
253 var method = map[taskName],
254 tasks = options[method];
255 if (tasks.length) {
256 grunt.log.ok('running ' + method + ' ');
257 if (!nowrite) {
258 grunt.task.run(tasks);
259 }
260 }
261 }
262 }
263
264 new Q()
265 .then(ifEnabled('beforeBumpTasks', runTasks('beforeBump')))
266 .then(ifEnabled('bump', bump))
267 .then(ifEnabled('afterBumpTasks', runTasks('afterBump')))
268 .then(ifEnabled('beforeReleaseTasks', runTasks('beforeRelease')))
269 .then(ifEnabled('changelog', changelog))
270 .then(ifEnabled('add', add))
271 .then(ifEnabled('commit', commit))
272 .then(ifEnabled('tag', tag))
273 .then(ifEnabled('push', push))
274 .then(ifEnabled('pushTags', pushTags))
275 .then(ifEnabled('npm', publish))
276 .then(ifEnabled('github', githubRelease))
277 .then(ifEnabled('afterReleaseTasks', runTasks('afterRelease')))
278 .catch(function(msg){
279 grunt.fail.warn(msg || 'release failed');
280 })
281 .finally(done);
282
283 });
284
285};