publish_task.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. /*
  2. * Jake JavaScript build tool
  3. * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
  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. let fs = require('fs');
  19. let path = require('path');
  20. let exec = require('child_process').execSync;
  21. let FileList = require('filelist').FileList;
  22. let PublishTask = function () {
  23. let args = Array.prototype.slice.call(arguments).filter(function (item) {
  24. return typeof item != 'undefined';
  25. });
  26. let arg;
  27. let opts = {};
  28. let definition;
  29. let prereqs = [];
  30. let createDef = function (arg) {
  31. return function () {
  32. this.packageFiles.include(arg);
  33. };
  34. };
  35. this.name = args.shift();
  36. // Old API, just name + list of files
  37. if (args.length == 1 && (Array.isArray(args[0]) || typeof args[0] == 'string')) {
  38. definition = createDef(args.pop());
  39. }
  40. // Current API, name + [prereqs] + [opts] + definition
  41. else {
  42. while ((arg = args.pop())) {
  43. // Definition func
  44. if (typeof arg == 'function') {
  45. definition = arg;
  46. }
  47. // Prereqs
  48. else if (Array.isArray(arg) || typeof arg == 'string') {
  49. prereqs = arg;
  50. }
  51. // Opts
  52. else {
  53. opts = arg;
  54. }
  55. }
  56. }
  57. this.prereqs = prereqs;
  58. this.packageFiles = new FileList();
  59. this.publishCmd = opts.publishCmd || 'npm publish %filename';
  60. this.publishMessage = opts.publishMessage || 'BOOM! Published.';
  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 String(stdout).trim();
  72. };
  73. if (typeof definition == 'function') {
  74. definition.call(this);
  75. }
  76. this.define();
  77. };
  78. PublishTask.prototype = new (function () {
  79. let _currentBranch = null;
  80. let getPackage = function () {
  81. let pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(),
  82. '/package.json')).toString());
  83. return pkg;
  84. };
  85. let getPackageVersionNumber = function () {
  86. return getPackage().version;
  87. };
  88. this.define = function () {
  89. let self = this;
  90. namespace('publish', function () {
  91. task('fetchTags', function () {
  92. // Make sure local tags are up to date
  93. exec(self.gitCmd + ' fetch --tags');
  94. console.log('Fetched remote tags.');
  95. });
  96. task('getCurrentBranch', function () {
  97. // Figure out what branch to push to
  98. let stdout = exec(self.gitCmd + ' symbolic-ref --short HEAD').toString();
  99. if (!stdout) {
  100. throw new Error('No current Git branch found');
  101. }
  102. _currentBranch = self._getCurrentBranch(stdout);
  103. console.log('On branch ' + _currentBranch);
  104. });
  105. task('ensureClean', function () {
  106. // Only bump, push, and tag if the Git repo is clean
  107. let stdout = exec(self.gitCmd + ' status --porcelain --untracked-files=no').toString();
  108. // Throw if there's output
  109. self._ensureRepoClean(stdout);
  110. });
  111. task('updateVersionFiles', function () {
  112. let pkg;
  113. let version;
  114. let arr;
  115. let patch;
  116. // Grab the current version-string
  117. pkg = getPackage();
  118. version = pkg.version;
  119. // Increment the patch-number for the version
  120. arr = version.split('.');
  121. patch = parseInt(arr.pop(), 10) + 1;
  122. arr.push(patch);
  123. version = arr.join('.');
  124. // Update package.json or other files with the new version-info
  125. self.versionFiles.forEach(function (file) {
  126. let p = path.join(process.cwd(), file);
  127. let data = JSON.parse(fs.readFileSync(p).toString());
  128. data.version = version;
  129. fs.writeFileSync(p, JSON.stringify(data, true, 2) + '\n');
  130. });
  131. // Return the version string so that listeners for the 'complete' event
  132. // for this task can use it (e.g., to update other files before pushing
  133. // to Git)
  134. return version;
  135. });
  136. task('pushVersion', ['ensureClean', 'updateVersionFiles'], function () {
  137. let version = getPackageVersionNumber();
  138. let message = 'Version ' + version;
  139. let cmds = [
  140. self.gitCmd + ' commit -a -m "' + message + '"',
  141. self.gitCmd + ' push origin ' + _currentBranch,
  142. self.gitCmd + ' tag -a v' + version + ' -m "' + message + '"',
  143. self.gitCmd + ' push --tags'
  144. ];
  145. cmds.forEach((cmd) => {
  146. exec(cmd);
  147. });
  148. version = getPackageVersionNumber();
  149. console.log('Bumped version number to v' + version + '.');
  150. });
  151. let defineTask = task('definePackage', function () {
  152. let version = getPackageVersionNumber();
  153. new jake.PackageTask(self.name, 'v' + version, self.prereqs, function () {
  154. // Replace the PackageTask's FileList with the PublishTask's FileList
  155. this.packageFiles = self.packageFiles;
  156. this.needTarGz = true; // Default to tar.gz
  157. // If any of the need<CompressionFormat> or archive opts are set
  158. // proxy them to the PackageTask
  159. for (let p in this) {
  160. if (p.indexOf('need') === 0 || p.indexOf('archive') === 0) {
  161. if (typeof self[p] != 'undefined') {
  162. this[p] = self[p];
  163. }
  164. }
  165. }
  166. });
  167. });
  168. defineTask._internal = true;
  169. task('package', function () {
  170. let definePack = jake.Task['publish:definePackage'];
  171. let pack = jake.Task['package'];
  172. let version = getPackageVersionNumber();
  173. // May have already been run
  174. if (definePack.taskStatus == jake.Task.runStatuses.DONE) {
  175. definePack.reenable(true);
  176. }
  177. definePack.execute();
  178. definePack.on('complete', function () {
  179. pack.invoke();
  180. console.log('Created package for ' + self.name + ' v' + version);
  181. });
  182. });
  183. task('publish', function () {
  184. return new Promise((resolve) => {
  185. let version = getPackageVersionNumber();
  186. let filename;
  187. let cmd;
  188. console.log('Publishing ' + self.name + ' v' + version);
  189. if (typeof self.createPublishCommand == 'function') {
  190. cmd = self.createPublishCommand(version);
  191. }
  192. else {
  193. filename = './pkg/' + self.name + '-v' + version + '.tar.gz';
  194. cmd = self.publishCmd.replace(/%filename/gi, filename);
  195. }
  196. if (typeof cmd == 'function') {
  197. cmd(function (err) {
  198. if (err) {
  199. throw err;
  200. }
  201. console.log(self.publishMessage);
  202. resolve();
  203. });
  204. }
  205. else {
  206. // Hackity hack -- NPM publish sometimes returns errror like:
  207. // Error sending version data\nnpm ERR!
  208. // Error: forbidden 0.2.4 is modified, should match modified time
  209. setTimeout(function () {
  210. let stdout = exec(cmd).toString() || '';
  211. stdout = stdout.trim();
  212. if (stdout) {
  213. console.log(stdout);
  214. }
  215. console.log(self.publishMessage);
  216. resolve();
  217. }, self.scheduleDelay);
  218. }
  219. });
  220. });
  221. task('cleanup', function () {
  222. return new Promise((resolve) => {
  223. let clobber = jake.Task.clobber;
  224. clobber.reenable(true);
  225. clobber.on('complete', function () {
  226. console.log('Cleaned up package');
  227. resolve();
  228. });
  229. clobber.invoke();
  230. });
  231. });
  232. });
  233. let prefixNs = function (item) {
  234. return 'publish:' + item;
  235. };
  236. // Create aliases in the default namespace
  237. desc('Create a new version and release.');
  238. task('publish', self.prereqs.concat(['version', 'release']
  239. .map(prefixNs)));
  240. desc('Release the existing version.');
  241. task('publishExisting', self.prereqs.concat(['release']
  242. .map(prefixNs)));
  243. task('version', ['fetchTags', 'getCurrentBranch', 'pushVersion']
  244. .map(prefixNs));
  245. task('release', ['package', 'publish', 'cleanup']
  246. .map(prefixNs));
  247. // Invoke proactively so there will be a callable 'package' task
  248. // which can be used apart from 'publish'
  249. jake.Task['publish:definePackage'].invoke();
  250. };
  251. })();
  252. jake.PublishTask = PublishTask;
  253. exports.PublishTask = PublishTask;