Skip to content

Commit

Permalink
Rollback Git operations when publish fails and on termination (#334)
Browse files Browse the repository at this point in the history
Fixes #197
  • Loading branch information
itaisteinherz authored and sindresorhus committed Mar 31, 2019
1 parent a02e169 commit c7d4cd0
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 10 deletions.
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -29,6 +29,7 @@
"dependencies": {
"@samverschueren/stream-to-observable": "^0.3.0",
"any-observable": "^0.3.0",
"async-exit-hook": "^2.0.1",
"chalk": "^2.3.0",
"del": "^3.0.0",
"execa": "^1.0.0",
Expand All @@ -43,6 +44,7 @@
"log-symbols": "^2.1.0",
"meow": "^5.0.0",
"npm-name": "^5.0.1",
"onetime": "^3.0.0",
"opn": "^5.4.0",
"ow": "^0.10.0",
"p-memoize": "^2.1.0",
Expand Down
1 change: 1 addition & 0 deletions readme.md
Expand Up @@ -26,6 +26,7 @@
- Bumps the version in package.json and npm-shrinkwrap.json (if present) and creates a git tag
- Prevents [accidental publishing](https://github.com/npm/npm/issues/13248) of pre-release versions under the `latest` [dist-tag](https://docs.npmjs.com/cli/dist-tag)
- Publishes the new version to npm, optionally under a dist-tag
- Rolls back the project to its previous state in case publishing fails
- Pushes commits and tags to GitHub/GitLab
- Supports [two-factor authentication](https://docs.npmjs.com/getting-started/using-two-factor-authentication)
- Enables two-factor authentication on new repositories
Expand Down
5 changes: 0 additions & 5 deletions source/cli.js
Expand Up @@ -65,11 +65,6 @@ const cli = meow(`

updateNotifier({pkg: cli.pkg}).notify();

process.on('SIGINT', () => {
console.log('\nAborted!');
process.exit(1);
});

(async () => {
const pkg = util.readPkg();

Expand Down
12 changes: 10 additions & 2 deletions source/git-util.js
Expand Up @@ -2,15 +2,15 @@
const execa = require('execa');
const version = require('./version');

const latestTag = () => execa.stdout('git', ['describe', '--abbrev=0', '--tags']);
exports.latestTag = () => execa.stdout('git', ['describe', '--abbrev=0', '--tags']);

const firstCommit = () => execa.stdout('git', ['rev-list', '--max-parents=0', 'HEAD']);

exports.latestTagOrFirstCommit = async () => {
let latest;
try {
// In case a previous tag exists, we use it to compare the current repo status to.
latest = await latestTag();
latest = await exports.latestTag();
} catch (_) {
// Otherwise, we fallback to using the first commit for comparison.
latest = await firstCommit();
Expand Down Expand Up @@ -110,6 +110,14 @@ exports.commitLogFromRevision = revision => execa.stdout('git', ['log', '--forma

exports.push = () => execa('git', ['push', '--follow-tags']);

exports.deleteTag = async tagName => {
await execa('git', ['tag', '--delete', tagName]);
};

exports.removeLastCommit = async () => {
await execa('git', ['reset', '--hard', 'HEAD~1']);
};

const gitVersion = async () => {
const {stdout} = await execa('git', ['version']);
return stdout.match(/git version (\d+\.\d+\.\d+).*/)[1];
Expand Down
58 changes: 55 additions & 3 deletions source/index.js
Expand Up @@ -7,12 +7,14 @@ const del = require('del');
const Listr = require('listr');
const split = require('split');
const {merge, throwError} = require('rxjs');
const {catchError, filter} = require('rxjs/operators');
const {catchError, filter, finalize} = require('rxjs/operators');
const streamToObservable = require('@samverschueren/stream-to-observable');
const readPkgUp = require('read-pkg-up');
const hasYarn = require('has-yarn');
const pkgDir = require('pkg-dir');
const hostedGitInfo = require('hosted-git-info');
const onetime = require('onetime');
const exitHook = require('async-exit-hook');
const prerequisiteTasks = require('./prerequisite-tasks');
const gitTasks = require('./git-tasks');
const publish = require('./npm/publish');
Expand Down Expand Up @@ -58,6 +60,38 @@ module.exports = async (input = 'patch', options) => {
const hasLockFile = fs.existsSync(path.resolve(rootDir, 'package-lock.json')) || fs.existsSync(path.resolve(rootDir, 'npm-shrinkwrap.json'));
const isOnGitHub = options.repoUrl && hostedGitInfo.fromUrl(options.repoUrl).type === 'github';

let isPublished = false;

const rollback = onetime(async () => {
console.log('\nPublish failed. Rolling back to the previous state…');

const tagVersionPrefix = await util.getTagVersionPrefix(options);

const latestTag = await git.latestTag();
const versionInLatestTag = latestTag.slice(tagVersionPrefix.length);

try {
if (versionInLatestTag === util.readPkg().version &&
versionInLatestTag !== pkg.version) { // Verify that the package's version has been bumped before deleting the last tag and commit.
await git.deleteTag(latestTag);
await git.removeLastCommit();
}

console.log('Successfully rolled back the project to its previous state.');
} catch (error) {
console.log(`Couldn't roll back because of the following error:\n${error}`);
}
});

exitHook(callback => {
if (!isPublished && runPublish) {
(async () => {
await rollback();
callback();
})();
}
});

const tasks = new Listr([
{
title: 'Prerequisite check',
Expand Down Expand Up @@ -148,7 +182,21 @@ module.exports = async (input = 'patch', options) => {
return `Private package: not publishing to ${pkgManagerName}.`;
}
},
task: (context, task) => publish(context, pkgManager, task, options, input)
task: (context, task) => {
let hasError = false;

return publish(context, pkgManager, task, options, input)
.pipe(
catchError(async error => {
hasError = true;
await rollback();
throw new Error(`Error publishing package:\n${error.message}\n\nThe project was rolled back to its previous state.`);
}),
finalize(() => {
isPublished = !hasError;
})
);
}
}
]);

Expand All @@ -166,7 +214,11 @@ module.exports = async (input = 'patch', options) => {
title: 'Pushing tags',
skip: async () => {
if (!(await git.hasUpstream())) {
return 'Upstream branch not found: not pushing.';
return 'Upstream branch not found; not pushing.';
}

if (!isPublished && runPublish) {
return 'Couldn\'t publish package to npm; not pushing.';
}
},
task: () => git.push()
Expand Down

0 comments on commit c7d4cd0

Please sign in to comment.