UNPKG

15.1 kBJavaScriptView Raw
1/**
2 * @fileoverview Build file
3 * @author nzakas
4 * @copyright jQuery Foundation and other contributors, https://jquery.org/
5 * MIT License
6 */
7
8"use strict";
9
10//------------------------------------------------------------------------------
11// Requirements
12//------------------------------------------------------------------------------
13
14var fs = require("fs"),
15 path = require("path"),
16 shelljs = require("shelljs"),
17 semver = require("semver"),
18 GitHub = require("github-api"),
19 // checker = require("npm-license"),
20 dateformat = require("dateformat"),
21 ShellOps = require("./shell-ops");
22
23//------------------------------------------------------------------------------
24// Private
25//------------------------------------------------------------------------------
26
27// var OPEN_SOURCE_LICENSES = [
28// /MIT/, /BSD/, /Apache/, /ISC/, /WTF/, /Public Domain/
29// ];
30
31/**
32 * Loads the package.json file from the current directory.
33 * @returns {void}
34 * @private
35 */
36function getPackageInfo() {
37 var filePath = path.join(process.cwd(), "package.json");
38 return JSON.parse(fs.readFileSync(filePath, "utf8"));
39}
40
41/**
42 * Run before a release to validate that the project is setup correctly.
43 * @param {boolean} [ciRelease] Set to true to indicate this is a CI release.
44 * @returns {void}
45 * @private
46 */
47function validateSetup(ciRelease) {
48 if (!shelljs.test("-f", "package.json")) {
49 console.error("Missing package.json file");
50 ShellOps.exit(1);
51 }
52
53 var pkg = getPackageInfo();
54 if (!pkg.files || pkg.files.length === 0) {
55 console.error("Missing 'files' property in package.json");
56 ShellOps.exit(1);
57 }
58
59 // special checks for CI release
60 if (ciRelease) {
61
62 // check repository field
63 if (!pkg.repository) {
64 console.error("Missing 'repository' in package.json");
65 ShellOps.exit(1);
66 } else if (!/^[\w\-]+\/[\w\-]+$/.test(pkg.repository)) {
67 console.error("The 'repository' field must be in the format 'foo/bar' in package.json");
68 ShellOps.exit(1);
69 }
70
71 // check NPM_TOKEN
72 if (!process.env.NPM_TOKEN) {
73 console.error("Missing NPM_TOKEN environment variable");
74 ShellOps.exit(1);
75 }
76 }
77}
78
79/**
80 * Determines the next prerelease version based on the current version.
81 * @param {string} currentVersion The current semver version.
82 * @param {string} prereleaseId The ID of the prelease (alpha, beta, rc, etc.)
83 * @param {string} releaseType The type of prerelease to generate (major, minor, patch)
84 * @returns {string} The prerelease version.
85 * @private
86 */
87function getPrereleaseVersion(currentVersion, prereleaseId, releaseType) {
88 var ver = new semver.SemVer(currentVersion);
89
90 // if it's already a prerelease version
91 if (ver.prerelease.length) {
92 return ver.inc("prerelease", prereleaseId).version;
93 } else {
94 return ver.inc("pre" + releaseType, prereleaseId).version;
95 }
96}
97
98/**
99 * Returns the version tags from the git repository
100 * @returns {string[]} Tags
101 * @private
102 */
103function getVersionTags() {
104 var tags = ShellOps.execSilent("git tag").trim().split("\n");
105
106 return tags.reduce(function(list, tag) {
107 if (semver.valid(tag)) {
108 list.push(tag);
109 }
110 return list;
111 }, []).sort(semver.compare);
112}
113
114// TODO: Make this async
115/**
116 * Validates the licenses of all dependencies are valid open source licenses.
117 * @returns {void}
118 * @private
119 */
120// function checkLicenses() {
121
122// /**
123// * Check if a dependency is eligible to be used by us
124// * @param {object} dependency dependency to check
125// * @returns {boolean} true if we have permission
126// * @private
127// */
128// function isPermissible(dependency) {
129// var licenses = dependency.licenses;
130
131// if (Array.isArray(licenses)) {
132// return licenses.some(function(license) {
133// return isPermissible({
134// name: dependency.name,
135// licenses: license
136// });
137// });
138// }
139
140// return OPEN_SOURCE_LICENSES.some(function(license) {
141// return license.test(licenses);
142// });
143// }
144
145// console.log("Validating licenses");
146
147// checker.init({
148// start: process.cwd()
149// }, function(deps) {
150// var impermissible = Object.keys(deps).map(function(dependency) {
151// return {
152// name: dependency,
153// licenses: deps[dependency].licenses
154// };
155// }).filter(function(dependency) {
156// return !isPermissible(dependency);
157// });
158
159// if (impermissible.length) {
160// impermissible.forEach(function(dependency) {
161// console.error("%s license for %s is impermissible.",
162// dependency.licenses,
163// dependency.name
164// );
165// });
166// ShellOps.exit(1);
167// }
168// });
169// }
170
171/**
172 * Extracts data from a commit log in the format --pretty=format:"* %h %s (%an)\n%b".
173 * @param {string[]} logs Output from git log command.
174 * @returns {Object} An object containing the data exracted from the commit log.
175 * @private
176 */
177function parseLogs(logs) {
178 var regexp = /^(?:\* )?([0-9a-f]{7}) ((?:([a-z]+): ?)?.*) \((.*)\)/i,
179 parsed = [];
180
181 logs.forEach(function(log) {
182 var match = log.match(regexp);
183
184 if (match) {
185 parsed.push({
186 raw: match[0],
187 sha: match[1],
188 title: match[2],
189 flag: match[3] ? match[3].toLowerCase() : null,
190 author: match[4],
191 body: ""
192 });
193 } else if (parsed.length) {
194 parsed[parsed.length - 1].body += log + "\n";
195 }
196 });
197
198 return parsed;
199}
200
201/**
202 * Given a list of parsed commit log messages, excludes revert commits and the
203 * commits they reverted.
204 * @param {Object[]} logs An array of parsed commit log messages.
205 * @returns {Object[]} An array of parsed commit log messages.
206 */
207function excludeReverts(logs) {
208 logs = logs.slice();
209
210 var revertRegex = /This reverts commit ([0-9a-f]{40})/,
211 shaIndexMap = Object.create(null), // Map of commit shas to indices
212 i, log, match, sha;
213
214 // iterate in reverse because revert commits have lower indices than the
215 // commits they revert
216 for (i = logs.length - 1; i >= 0; i--) {
217 log = logs[i];
218 match = log.body.match(revertRegex);
219
220 if (match) {
221 sha = match[1].slice(0, 7);
222
223 // only exclude this revert if we can find the commit it reverts
224 if (typeof shaIndexMap[sha] !== "undefined") {
225 logs[shaIndexMap[sha]] = null;
226 logs[i] = null;
227 }
228 } else {
229 shaIndexMap[log.sha] = i;
230 }
231 }
232
233 return logs.filter(Boolean);
234}
235
236/**
237 * Inspects an array of git commit log messages and calculates the release
238 * information based on it.
239 * @param {string} currentVersion The version of the project read from package.json.
240 * @param {string[]} logs An array of log messages for the release.
241 * @param {string} [prereleaseId] If doing a prerelease, the prerelease identifier.
242 * @returns {Object} An object containing all the changes since the last version.
243 * @private
244 */
245function calculateReleaseFromGitLogs(currentVersion, logs, prereleaseId) {
246
247 logs = excludeReverts(parseLogs(logs));
248
249 var changelog = {},
250 releaseInfo = {
251 version: currentVersion,
252 type: "",
253 changelog: changelog,
254 rawChangelog: logs.map(function(log) {
255 return log.raw;
256 }).join("\n")
257 };
258
259 // arrange change types into categories
260 logs.forEach(function(log) {
261
262 // exclude untagged (e.g. revert) commits from version calculation
263 if (!log.flag) {
264 return;
265 }
266
267 if (!changelog[log.flag]) {
268 changelog[log.flag] = [];
269 }
270
271 changelog[log.flag].push(log.raw);
272 });
273
274 if (changelog.breaking) {
275 releaseInfo.type = "major";
276 } else if (changelog.new || changelog.update) {
277 releaseInfo.type = "minor";
278 } else {
279 releaseInfo.type = "patch";
280 }
281
282 // increment version from current version
283 releaseInfo.version = (
284 prereleaseId ?
285 getPrereleaseVersion(currentVersion, prereleaseId, releaseInfo.type) :
286 semver.inc(currentVersion, releaseInfo.type)
287 );
288
289 return releaseInfo;
290}
291
292/**
293 * Gets all changes since the last tag that represents a version.
294 * @param {string} [prereleaseId] The prerelease identifier if this is a prerelease.
295 * @returns {Object} An object containing all the changes since the last version.
296 * @private
297 */
298function calculateReleaseInfo(prereleaseId) {
299
300 // get most recent tag
301 var pkg = getPackageInfo(),
302 tags = getVersionTags(),
303 lastTag = tags[tags.length - 1],
304 commitRange = lastTag ? lastTag + "..HEAD" : "";
305
306 // get log statements
307 var logs = ShellOps.execSilent("git log --no-merges --pretty=format:\"* %h %s (%an)%n%b\" " + commitRange).split(/\n/g);
308 var releaseInfo = calculateReleaseFromGitLogs(pkg.version, logs, prereleaseId);
309 releaseInfo.repository = pkg.repository;
310 return releaseInfo;
311}
312
313/**
314 * Outputs the changelog to disk.
315 * @param {Object} releaseInfo The information about the release.
316 * @param {string} releaseInfo.version The release version.
317 * @param {Object} releaseInfo.changelog The changelog information.
318 * @returns {void}
319 */
320function writeChangelog(releaseInfo) {
321
322 // get most recent two tags
323 var now = new Date(),
324 timestamp = dateformat(now, "mmmm d, yyyy");
325
326 // output header
327 ("v" + releaseInfo.version + " - " + timestamp + "\n").to("CHANGELOG.tmp");
328
329 // output changelog
330 ("\n" + releaseInfo.rawChangelog + "\n").toEnd("CHANGELOG.tmp");
331
332 // ensure there's a CHANGELOG.md file
333 if (!shelljs.test("-f", "CHANGELOG.md")) {
334 fs.writeFileSync("CHANGELOG.md", "");
335 }
336
337 // switch-o change-o
338 fs.writeFileSync("CHANGELOG.md.tmp", shelljs.cat("CHANGELOG.tmp", "CHANGELOG.md"));
339 shelljs.rm("CHANGELOG.tmp");
340 shelljs.rm("CHANGELOG.md");
341 shelljs.mv("CHANGELOG.md.tmp", "CHANGELOG.md");
342}
343
344/**
345 * Creates a release version tag and pushes to origin and npm.
346 * @param {string} [prereleaseId] The prerelease ID (alpha, beta, rc, etc.).
347 * Only include when doing a prerelease.
348 * @param {boolean} [ciRelease] Indicates that the release is being done by the
349 * CI and so shouldn't push back to Git (this will be handled by CI itself).
350 * @returns {Object} The information about the release.
351 */
352function release(prereleaseId, ciRelease) {
353
354 validateSetup(ciRelease);
355
356 // CI release doesn't need to clear node_modules because it installs clean
357 if (!ciRelease) {
358 console.log("Updating dependencies (this may take a while)");
359 shelljs.rm("-rf", "node_modules");
360 ShellOps.execSilent("npm install --silent");
361 }
362
363 // necessary so later "npm install" will install the same versions
364 // console.log("Shrinkwrapping dependencies");
365 // ShellOps.execSilent("npm shrinkwrap");
366
367 // TODO: Make this work
368 // console.log("Checking licenses");
369 // checkLicenses();
370
371 console.log("Running tests");
372 ShellOps.execSilent("npm test");
373
374 console.log("Calculating changes for release");
375 var releaseInfo = calculateReleaseInfo(prereleaseId);
376
377 console.log("Release is %s", releaseInfo.version);
378
379 console.log("Generating changelog");
380 writeChangelog(releaseInfo);
381
382 // console.log("Updating bundled dependencies");
383 // ShellOps.exec("bundle-dependencies update");
384
385 console.log("Committing to git");
386 ShellOps.exec("git add CHANGELOG.md package.json");
387 ShellOps.exec("git commit -m \"Build: package.json and changelog update for " + releaseInfo.version + "\"");
388
389 console.log("Generating %s", releaseInfo.version);
390 ShellOps.execSilent("npm version " + releaseInfo.version);
391
392 // push all the things
393 if (!ciRelease) {
394 console.log("Publishing to git");
395 ShellOps.exec("git push origin master --tags");
396 }
397
398 console.log("Fixing line endings");
399 getPackageInfo().files.filter(function(dirPath) {
400 return fs.lstatSync(dirPath).isDirectory();
401 }).forEach(function(filePath) {
402 ShellOps.execSilent("linefix " + filePath);
403 });
404
405 // NOTE: eslint-release dependencies are no longer available starting here
406
407 // console.log("Fixing dependencies for bundle");
408 // shelljs.rm("-rf", "node_modules");
409 // ShellOps.execSilent("npm install --production");
410
411 // CI release needs a .npmrc file to work properly - token is read from environment
412 if (ciRelease) {
413 console.log("Writing .npmrc file");
414 fs.writeFileSync(".npmrc", "//registry.npmjs.org/:_authToken=${NPM_TOKEN}");
415 }
416
417 // if there's a prerelease ID, publish under "next" tag
418 console.log("Publishing to npm");
419 if (prereleaseId) {
420 ShellOps.exec("npm publish --tag next");
421 } else {
422 ShellOps.exec("npm publish");
423 }
424
425 // undo any differences
426 ShellOps.exec("git reset");
427 ShellOps.exec("git clean -f");
428
429 // restore development environment
430 // ShellOps.exec("npm install");
431
432 // NOTE: eslint-release dependencies are once again available after here
433
434 // delete shrinkwrap file
435 // shelljs.rm("npm-shrinkwrap.json");
436
437 return releaseInfo;
438}
439
440/**
441 * Publishes the release information to GitHub.
442 * @param {Object} releaseInfo The release information object.
443 * @returns {Promise} A promise that resolves when the operation is complete.
444 * @private
445 */
446function publishReleaseToGitHub(releaseInfo) {
447
448 if (!process.env.ESLINT_GITHUB_TOKEN) {
449 console.error("Missing ESLINT_GITHUB_TOKEN environment variable");
450 ShellOps.exit(1);
451 }
452
453 var repoParts = releaseInfo.split("/"),
454 gh = new GitHub({ token: process.env.ESLINT_GITHUB_TOKEN }),
455 repo = gh.getRepo(repoParts[0], repoParts[1]);
456
457 return repo.updateRelease("v" + releaseInfo.version, {
458 body: releaseInfo.rawChangelog
459 }).then(function() {
460 console.log("Posted release notes to GitHub");
461 }).catch(function(ex) {
462 console.error("Could not post release notes to GitHub");
463 if (ex.message) {
464 console.error(ex.message);
465 }
466 });
467
468}
469
470//------------------------------------------------------------------------------
471// Public API
472//------------------------------------------------------------------------------
473
474module.exports = {
475 getPrereleaseVersion: getPrereleaseVersion,
476 release: release,
477 calculateReleaseInfo: calculateReleaseInfo,
478 calculateReleaseFromGitLogs: calculateReleaseFromGitLogs,
479 writeChangelog: writeChangelog,
480 publishReleaseToGitHub: publishReleaseToGitHub
481};