publish_task.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. /*
  2. * Jake JavaScript build tool
  3. * Copyright 2112 Matthew Eernisse ([email protected])
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. *
  17. */
  18. var fs = require('fs')
  19. , path = require('path')
  20. , exec = require('child_process').exec
  21. , utils = require('utilities')
  22. , FileList = require('filelist').FileList;
  23. var PublishTask = function () {
  24. var args = Array.prototype.slice.call(arguments).filter(function (item) {
  25. return typeof item != 'undefined';
  26. })
  27. , arg
  28. , opts = {}
  29. , definition
  30. , prereqs = []
  31. , createDef = function (arg) {
  32. return function () {
  33. this.packageFiles.include(arg);
  34. };
  35. };
  36. this.name = args.shift();
  37. // Old API, just name + list of files
  38. if (args.length == 1 && (Array.isArray(args[0]) || typeof args[0] == 'string')) {
  39. definition = createDef(args.pop());
  40. }
  41. // Current API, name + [prereqs] + [opts] + definition
  42. else {
  43. while ((arg = args.pop())) {
  44. // Definition func
  45. if (typeof arg == 'function') {
  46. definition = arg;
  47. }
  48. // Prereqs
  49. else if (Array.isArray(arg) || typeof arg == 'string') {
  50. prereqs = arg;
  51. }
  52. // Opts
  53. else {
  54. opts = arg;
  55. }
  56. }
  57. }
  58. this.prereqs = prereqs;
  59. this.packageFiles = new FileList();
  60. this.publishCmd = opts.publishCmd || 'npm publish %filename';
  61. this.gitCmd = opts.gitCmd || 'git';
  62. this.versionFiles = opts.versionFiles || ['package.json'];
  63. this.scheduleDelay = 5000;
  64. // Override utility funcs for testing
  65. this._ensureRepoClean = function (stdout) {
  66. if (stdout.length) {
  67. fail(new Error('Git repository is not clean.'));
  68. }
  69. };
  70. this._getCurrentBranch = function (stdout) {
  71. return utils.string.trim(stdout);
  72. };
  73. if (typeof definition == 'function') {
  74. definition.call(this);
  75. }
  76. this.define();
  77. };
  78. PublishTask.prototype = new (function () {
  79. var _currentBranch = null;
  80. var getPackage = function () {
  81. var pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(),
  82. '/package.json')).toString());
  83. return pkg;
  84. }
  85. , getPackageVersionNumber = function () {
  86. return getPackage().version;
  87. };
  88. this.define = function () {
  89. var self = this;
  90. namespace('publish', function () {
  91. task('fetchTags', {async: true}, function () {
  92. // Make sure local tags are up to date
  93. var cmds = [
  94. self.gitCmd + ' fetch --tags'
  95. ];
  96. jake.exec(cmds, function () {
  97. console.log('Fetched remote tags.');
  98. complete();
  99. });
  100. });
  101. task('getCurrentBranch', {async: true}, function () {
  102. // Figure out what branch to push to
  103. exec(self.gitCmd + ' symbolic-ref --short HEAD',
  104. function (err, stdout, stderr) {
  105. if (err) {
  106. fail(err);
  107. }
  108. if (stderr) {
  109. fail(new Error(stderr));
  110. }
  111. if (!stdout) {
  112. fail(new Error('No current Git branch found'));
  113. }
  114. _currentBranch = self._getCurrentBranch(stdout);
  115. console.log('On branch ' + _currentBranch);
  116. complete();
  117. });
  118. });
  119. task('ensureClean', {async: true}, function () {
  120. // Only bump, push, and tag if the Git repo is clean
  121. exec(self.gitCmd + ' status --porcelain --untracked-files=no',
  122. function (err, stdout, stderr) {
  123. if (err) {
  124. fail(err);
  125. }
  126. if (stderr) {
  127. fail(new Error(stderr));
  128. }
  129. // Throw if there's output
  130. self._ensureRepoClean(stdout);
  131. complete();
  132. });
  133. });
  134. task('updateVersionFiles', function () {
  135. var pkg
  136. , version
  137. , arr
  138. , patch;
  139. // Grab the current version-string
  140. pkg = getPackage();
  141. version = pkg.version;
  142. // Increment the patch-number for the version
  143. arr = version.split('.');
  144. patch = parseInt(arr.pop(), 10) + 1;
  145. arr.push(patch);
  146. version = arr.join('.');
  147. // Update package.json or other files with the new version-info
  148. self.versionFiles.forEach(function (file) {
  149. var p = path.join(process.cwd(), file)
  150. , data = JSON.parse(fs.readFileSync(p).toString());
  151. data.version = version;
  152. fs.writeFileSync(p, JSON.stringify(data, true, 2) + '\n');
  153. });
  154. // Return the version string so that listeners for the 'complete' event
  155. // for this task can use it (e.g., to update other files before pushing
  156. // to Git)
  157. return version;
  158. });
  159. task('pushVersion', ['ensureClean', 'updateVersionFiles'], {async: true},
  160. function () {
  161. var version = getPackageVersionNumber()
  162. , message = 'Version ' + version
  163. , cmds = [
  164. self.gitCmd + ' commit -a -m "' + message + '"'
  165. , self.gitCmd + ' push origin ' + _currentBranch
  166. , self.gitCmd + ' tag -a v' + version + ' -m "' + message + '"'
  167. , self.gitCmd + ' push --tags'
  168. ];
  169. var execOpts = {};
  170. if (process.platform == 'win32') {
  171. // Windows won't like the quotes in our cmdline
  172. execOpts.windowsVerbatimArguments = true;
  173. }
  174. jake.exec(cmds, function () {
  175. var version = getPackageVersionNumber();
  176. console.log('Bumped version number to v' + version + '.');
  177. complete();
  178. }, execOpts);
  179. });
  180. task('definePackage', function () {
  181. var version = getPackageVersionNumber()
  182. , t;
  183. t = new jake.PackageTask(self.name, 'v' + version, self.prereqs, function () {
  184. // Replace the PackageTask's FileList with the PublishTask's FileList
  185. this.packageFiles = self.packageFiles;
  186. this.needTarGz = true; // Default to tar.gz
  187. // If any of the need<CompressionFormat> or archive opts are set
  188. // proxy them to the PackageTask
  189. for (var p in this) {
  190. if (p.indexOf('need') === 0 || p.indexOf('archive') === 0) {
  191. if (typeof self[p] != 'undefined') {
  192. this[p] = self[p];
  193. }
  194. }
  195. }
  196. });
  197. });
  198. task('package', {async: true}, function () {
  199. var definePack = jake.Task['publish:definePackage']
  200. , pack = jake.Task.package
  201. , version = getPackageVersionNumber();
  202. // May have already been run
  203. definePack.reenable(true);
  204. definePack.addListener('complete', function () {
  205. pack.addListener('complete', function () {
  206. console.log('Created package for ' + self.name + ' v' + version);
  207. complete();
  208. });
  209. pack.invoke();
  210. });
  211. definePack.invoke();
  212. });
  213. task('publish', {async: true}, function () {
  214. var version = getPackageVersionNumber()
  215. , filename
  216. , cmd;
  217. console.log('Publishing ' + self.name + ' v' + version);
  218. if (typeof self.createPublishCommand == 'function') {
  219. cmd = self.createPublishCommand(version);
  220. }
  221. else {
  222. filename = 'pkg/' + self.name + '-v' + version + '.tar.gz';
  223. cmd = self.publishCmd.replace(/%filename/gi, filename);
  224. }
  225. // Hackity hack -- NPM publish sometimes returns errror like:
  226. // Error sending version data\nnpm ERR!
  227. // Error: forbidden 0.2.4 is modified, should match modified time
  228. setTimeout(function () {
  229. jake.exec(cmd, function () {
  230. console.log('BOOM! Published.');
  231. complete();
  232. }, {printStdout: true, printStderr: true});
  233. }, self.scheduleDelay);
  234. });
  235. task('cleanup', {async: true}, function () {
  236. var clobber = jake.Task.clobber;
  237. clobber.reenable(true);
  238. clobber.on('complete', function () {
  239. console.log('Cleaned up package');
  240. complete();
  241. });
  242. clobber.invoke();
  243. });
  244. });
  245. var prefixNs = function (item) {
  246. return 'publish:' + item;
  247. };
  248. // Create aliases in the default namespace
  249. desc('Create a new version and release.');
  250. task('publish', self.prereqs.concat(['version', 'release']
  251. .map(prefixNs)));
  252. desc('Release the existing version.');
  253. task('publishExisting', self.prereqs.concat(['release']
  254. .map(prefixNs)));
  255. task('version', ['fetchTags', 'getCurrentBranch', 'pushVersion']
  256. .map(prefixNs));
  257. task('release', ['package', 'publish', 'cleanup']
  258. .map(prefixNs));
  259. // Invoke proactively so there will be a callable 'package' task
  260. // which can be used apart from 'publish'
  261. jake.Task['publish:definePackage'].invoke();
  262. };
  263. })();
  264. jake.PublishTask = PublishTask;
  265. exports.PublishTask = PublishTask;