Skip to content

Commit

Permalink
Breaking: separate releases into "generate" and "publish" phase (#27)
Browse files Browse the repository at this point in the history
This updates `eslint-release` to have separate commands for generating a release locally, and publishing info about the release to various locations (npm, git, GitHub releases). In addition to making it easier to deal with failures (since we won't end up in a half-published state), this will also make it possible to provide a 2FA code for publishing that will still be valid when it's actually time to publish.

To make future changes easier, this also removes some functionality that is no longer used (e.g. the distinction between CI and non-CI releases, and the ability to publish the changelog to GitHub changelog separately from the rest of the release)

Migration for existing projects:

* Projects that run the `eslint-release` command should run `eslint-generate-release` followed by `eslint-publish-release`.
* Projects that run the `eslint-prerelease <releaseId>` command should run `eslint-generate-prerelease <releaseId>` followed by `eslint-publish-release`.
* Projects that use `ReleaseOps.release(releaseInfo)` should run `ReleaseOps.generateRelease(releaseInfo)` followed by `ReleaseOps.publishRelease()`.
* Projects should add `.eslint-release-info.json` to `.gitignore`.

When publishing, an `NPM_OTP` environment variable can optionally be used to authenticate the publish.
  • Loading branch information
not-an-aardvark committed Oct 10, 2018
1 parent 80a2e80 commit 9d0445a
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 193 deletions.
Expand Up @@ -21,7 +21,7 @@ var ReleaseOps = require("../lib/release-ops");

/*
* Usage:
* $ eslint-prerelease beta
* $ eslint-generate-prerelease beta
*/
var args = process.argv.slice(2),
prereleaseId = (args.length ? args[0] : null);
Expand All @@ -32,5 +32,4 @@ if (!prereleaseId) {
process.exit(1); // eslint-disable-line no-process-exit
}

var releaseInfo = ReleaseOps.release(prereleaseId);
ReleaseOps.publishReleaseToGitHub(releaseInfo);
ReleaseOps.generateRelease(prereleaseId);
7 changes: 3 additions & 4 deletions bin/eslint-ci-release.js → bin/eslint-generate-release.js
@@ -1,7 +1,7 @@
#!/usr/bin/env node

/**
* @fileoverview Main CLI that is run via the eslint-ci-release command.
* @fileoverview Main CLI that is run via the eslint-generate-release command.
* @author Nicholas C. Zakas
* @copyright jQuery Foundation and other contributors, https://jquery.org/
* MIT License
Expand All @@ -21,8 +21,7 @@ var ReleaseOps = require("../lib/release-ops");

/*
* Usage:
* $ eslint-ci-release
* $ eslint-generate-release
*/

var releaseInfo = ReleaseOps.release(null, true);
ReleaseOps.publishReleaseToGitHub(releaseInfo);
ReleaseOps.generateRelease();
29 changes: 0 additions & 29 deletions bin/eslint-gh-release.js

This file was deleted.

4 changes: 2 additions & 2 deletions bin/eslint-release.js → bin/eslint-publish-release.js
Expand Up @@ -21,7 +21,7 @@ var ReleaseOps = require("../lib/release-ops");

/*
* Usage:
* $ eslint-release
* $ eslint-publish-release
*/

ReleaseOps.release();
ReleaseOps.publishRelease();
201 changes: 56 additions & 145 deletions lib/release-ops.js
Expand Up @@ -16,7 +16,6 @@ var fs = require("fs"),
shelljs = require("shelljs"),
semver = require("semver"),
GitHub = require("github-api"),
// checker = require("npm-license"),
dateformat = require("dateformat"),
ShellOps = require("./shell-ops");

Expand All @@ -40,11 +39,10 @@ function getPackageInfo() {

/**
* Run before a release to validate that the project is setup correctly.
* @param {boolean} [ciRelease] Set to true to indicate this is a CI release.
* @returns {void}
* @private
*/
function validateSetup(ciRelease) {
function validateSetup() {
if (!shelljs.test("-f", "package.json")) {
console.error("Missing package.json file");
ShellOps.exit(1);
Expand All @@ -56,23 +54,24 @@ function validateSetup(ciRelease) {
ShellOps.exit(1);
}

// special checks for CI release
if (ciRelease) {
// check repository field
if (!pkg.repository) {
console.error("Missing 'repository' in package.json");
ShellOps.exit(1);
} else if (!/^[\w\-]+\/[\w\-]+$/.test(pkg.repository)) {
console.error("The 'repository' field must be in the format 'foo/bar' in package.json");
ShellOps.exit(1);
}

// check repository field
if (!pkg.repository) {
console.error("Missing 'repository' in package.json");
ShellOps.exit(1);
} else if (!/^[\w\-]+\/[\w\-]+$/.test(pkg.repository)) {
console.error("The 'repository' field must be in the format 'foo/bar' in package.json");
ShellOps.exit(1);
}
// check NPM_TOKEN
if (!process.env.NPM_TOKEN) {
console.error("Missing NPM_TOKEN environment variable");
ShellOps.exit(1);
}

// check NPM_TOKEN
if (!process.env.NPM_TOKEN) {
console.error("Missing NPM_TOKEN environment variable");
ShellOps.exit(1);
}
if (!process.env.ESLINT_GITHUB_TOKEN) {
console.error("Missing ESLINT_GITHUB_TOKEN environment variable");
ShellOps.exit(1);
}
}

Expand Down Expand Up @@ -111,63 +110,6 @@ function getVersionTags() {
}, []).sort(semver.compare);
}

// TODO: Make this async
/**
* Validates the licenses of all dependencies are valid open source licenses.
* @returns {void}
* @private
*/
// function checkLicenses() {

// /**
// * Check if a dependency is eligible to be used by us
// * @param {object} dependency dependency to check
// * @returns {boolean} true if we have permission
// * @private
// */
// function isPermissible(dependency) {
// var licenses = dependency.licenses;

// if (Array.isArray(licenses)) {
// return licenses.some(function(license) {
// return isPermissible({
// name: dependency.name,
// licenses: license
// });
// });
// }

// return OPEN_SOURCE_LICENSES.some(function(license) {
// return license.test(licenses);
// });
// }

// console.log("Validating licenses");

// checker.init({
// start: process.cwd()
// }, function(deps) {
// var impermissible = Object.keys(deps).map(function(dependency) {
// return {
// name: dependency,
// licenses: deps[dependency].licenses
// };
// }).filter(function(dependency) {
// return !isPermissible(dependency);
// });

// if (impermissible.length) {
// impermissible.forEach(function(dependency) {
// console.error("%s license for %s is impermissible.",
// dependency.licenses,
// dependency.name
// );
// });
// ShellOps.exit(1);
// }
// });
// }

/**
* Extracts data from a commit log in the format --pretty=format:"* %h %s (%an)\n%b".
* @param {string[]} logs Output from git log command.
Expand Down Expand Up @@ -345,28 +287,11 @@ function writeChangelog(releaseInfo) {
* Creates a release version tag and pushes to origin and npm.
* @param {string} [prereleaseId] The prerelease ID (alpha, beta, rc, etc.).
* Only include when doing a prerelease.
* @param {boolean} [ciRelease] Indicates that the release is being done by the
* CI and so shouldn't push back to Git (this will be handled by CI itself).
* @returns {Object} The information about the release.
*/
function release(prereleaseId, ciRelease) {
function generateRelease(prereleaseId) {

validateSetup(ciRelease);

// CI release doesn't need to clear node_modules because it installs clean
if (!ciRelease) {
console.log("Updating dependencies (this may take a while)");
shelljs.rm("-rf", "node_modules");
ShellOps.execSilent("npm install --silent");
}

// necessary so later "npm install" will install the same versions
// console.log("Shrinkwrapping dependencies");
// ShellOps.execSilent("npm shrinkwrap");

// TODO: Make this work
// console.log("Checking licenses");
// checkLicenses();
validateSetup();

console.log("Running tests");
ShellOps.execSilent("npm test");
Expand All @@ -379,20 +304,13 @@ function release(prereleaseId, ciRelease) {
console.log("Generating changelog");
writeChangelog(releaseInfo);

// console.log("Updating bundled dependencies");
// ShellOps.exec("bundle-dependencies update");

console.log("Committing to git");
ShellOps.exec("git add CHANGELOG.md");
ShellOps.exec("git commit -m \"Build: changelog update for " + releaseInfo.version + "\"");

console.log("Generating %s", releaseInfo.version);
ShellOps.execSilent("npm version " + releaseInfo.version);

// push all the things
console.log("Publishing to git");
ShellOps.exec("git push origin master --tags");

console.log("Fixing line endings");
getPackageInfo().files.filter(function(dirPath) {
return fs.lstatSync(dirPath).isDirectory();
Expand All @@ -402,42 +320,10 @@ function release(prereleaseId, ciRelease) {
}
});

// NOTE: eslint-release dependencies are no longer available starting here

// console.log("Fixing dependencies for bundle");
// shelljs.rm("-rf", "node_modules");
// ShellOps.execSilent("npm install --production");

if (ciRelease) {

// CI release needs a .npmrc file to work properly - token is read from environment
console.log("Writing .npmrc file");
fs.writeFileSync(".npmrc", "//registry.npmjs.org/:_authToken=${NPM_TOKEN}");
}

// if there's a prerelease ID, publish under "next" tag
console.log("Publishing to npm");
if (prereleaseId) {
ShellOps.exec("npm publish --tag next");
} else {
ShellOps.exec("npm publish");
}

// undo any differences
if (!ciRelease) {
ShellOps.exec("git reset");
ShellOps.exec("git clean -f");
}

// restore development environment
// ShellOps.exec("npm install");

// NOTE: eslint-release dependencies are once again available after here

// delete shrinkwrap file
// shelljs.rm("npm-shrinkwrap.json");

return releaseInfo;
// Release needs a .npmrc file to work properly - token is read from environment
console.log("Writing .npmrc file");
fs.writeFileSync(".npmrc", "//registry.npmjs.org/:_authToken=${NPM_TOKEN}");
fs.writeFileSync(".eslint-release-info.json", JSON.stringify(releaseInfo, null, 4));
}

/**
Expand All @@ -448,11 +334,6 @@ function release(prereleaseId, ciRelease) {
*/
function publishReleaseToGitHub(releaseInfo) {

if (!process.env.ESLINT_GITHUB_TOKEN) {
console.error("Missing ESLINT_GITHUB_TOKEN environment variable");
ShellOps.exit(1);
}

var repoParts = releaseInfo.repository.split("/"),
gh = new GitHub({ token: process.env.ESLINT_GITHUB_TOKEN }),
repo = gh.getRepo(repoParts[0], repoParts[1]),
Expand All @@ -473,15 +354,45 @@ function publishReleaseToGitHub(releaseInfo) {

}

/**
* Push the commit and git tags, publish to npm, and publish a changelog to GitHub release notes.
* @returns {Object} the information about the release.
*/
function publishRelease() {
validateSetup();
var releaseInfo = JSON.parse(fs.readFileSync(".eslint-release-info.json", "utf8"));

// if there's a prerelease ID, publish under "next" tag
console.log("Publishing to npm");

var command = "npm publish";
if (semver.prerelease(releaseInfo.version)) {
command += " --tag next";
}

if (process.env.NPM_OTP && /^\d+$/.test(process.env.NPM_OTP)) {
command += "--otp=" + process.env.NPM_OTP;
}

ShellOps.exec(command);

console.log("Publishing to git");
ShellOps.exec("git push origin master --tags");

publishReleaseToGitHub(releaseInfo);

return releaseInfo;
}

//------------------------------------------------------------------------------
// Public API
//------------------------------------------------------------------------------

module.exports = {
getPrereleaseVersion: getPrereleaseVersion,
release: release,
generateRelease: generateRelease,
publishRelease: publishRelease,
calculateReleaseInfo: calculateReleaseInfo,
calculateReleaseFromGitLogs: calculateReleaseFromGitLogs,
writeChangelog: writeChangelog,
publishReleaseToGitHub: publishReleaseToGitHub
writeChangelog: writeChangelog
};
1 change: 1 addition & 0 deletions lib/shell-ops.js
Expand Up @@ -74,6 +74,7 @@ module.exports = {
* @private
*/
exec: function(cmd) {
console.log("+ " + cmd);
var result = this.execSilent(cmd);
console.log(result);
},
Expand Down

0 comments on commit 9d0445a

Please sign in to comment.