1 | /**
|
2 | * @fileoverview Build file
|
3 | * @author nzakas
|
4 | * @copyright jQuery Foundation and other contributors, https://jquery.org/
|
5 | * MIT License
|
6 | */
|
7 |
|
8 | ;
|
9 |
|
10 | //------------------------------------------------------------------------------
|
11 | // Requirements
|
12 | //------------------------------------------------------------------------------
|
13 |
|
14 | var 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 | */
|
36 | function 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 | */
|
47 | function 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 | */
|
87 | function 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 | */
|
103 | function 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 | */
|
177 | function 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 | */
|
207 | function 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 | */
|
245 | function 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 | */
|
298 | function 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 | */
|
320 | function 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 | */
|
352 | function 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 | */
|
446 | function 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 |
|
474 | module.exports = {
|
475 | getPrereleaseVersion: getPrereleaseVersion,
|
476 | release: release,
|
477 | calculateReleaseInfo: calculateReleaseInfo,
|
478 | calculateReleaseFromGitLogs: calculateReleaseFromGitLogs,
|
479 | writeChangelog: writeChangelog,
|
480 | publishReleaseToGitHub: publishReleaseToGitHub
|
481 | };
|