Skip to content

Commit

Permalink
Add ability to open generated changelog for editing.
Browse files Browse the repository at this point in the history
Introduces a new option to be specified: `launchEditor`. When specified,
`release-it-lerna-changelog` will generate the changelog with
`lerna-changelog` (just like it has always done) then launch the
configured editor with a temporary file. This allows the person doing
the release to customize the changelog before continuing.

There are a few valid values for `launchEditor`:

* `false` - Disables the feature.
* `true` - The `process.env.EDITOR` value will be used
* any string - This string will be used as if it were a command. In
  order to interpolate the temporary file path in the string, you can
  use `${file}` in your configuration.
  • Loading branch information
rwjblue committed Mar 9, 2020
1 parent c90d9d5 commit 7dda95d
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 16 deletions.
11 changes: 10 additions & 1 deletion README.md
Expand Up @@ -54,13 +54,22 @@ For example, given the following configuration (in `package.json`):
"release-it": {
"plugins": {
"release-it-lerna-changelog": {
"infile": "CHANGELOG.md"
"infile": "CHANGELOG.md",
"launchEditor": true
}
}
}
}
```

The two options that `release-it-lerna-changelog` is aware of are:

* `infile` -- This represents the filename to put the changelog information into.
* `launchEditor` -- When set to `true`, `release-it-lerna-changelog` will
invoke the `$EDITOR` from your environment passing it the path to a temp file
that you can edit to customize the exact changelog contents. When set to a string,
that specific editor will be executed (as opposed to leveraging `$EDITOR`).

Each release will run `lerna-changelog` and prepend the results into `CHANGELOG.md`.

## License
Expand Down
48 changes: 45 additions & 3 deletions index.js
Expand Up @@ -2,6 +2,7 @@ const { EOL } = require('os');
const fs = require('fs');
const { Plugin } = require('release-it');
const { format } = require('release-it/lib/util');
const tmp = require('tmp');

module.exports = class LernaChangelogGeneratorPlugin extends Plugin {
get lernaPath() {
Expand Down Expand Up @@ -34,6 +35,17 @@ module.exports = class LernaChangelogGeneratorPlugin extends Plugin {
return firstCommit;
}

async _execLernaChangelog(nextVersion, from) {
let changelog = await this.exec(
`${this.lernaPath} --next-version=${nextVersion} --from=${from}`,
{
options: { write: false },
}
);

return changelog;
}

async getChangelog(_from) {
let { version, latestVersion } = this.config.getContext();
let from = _from || this.getTagNameFromVersion(latestVersion);
Expand All @@ -43,9 +55,39 @@ module.exports = class LernaChangelogGeneratorPlugin extends Plugin {
from = await this.getFirstCommit();
}

return this.exec(`${this.lernaPath} --next-version=${nextVersion} --from=${from}`, {
options: { write: false },
});
let changelog = await this._execLernaChangelog(nextVersion, from);

let finalChangelog = await this.reviewChangelog(changelog);

return finalChangelog;
}

async _launchEditor(tmpFile) {
let editorCommand;

if (typeof this.options.launchEditor === 'boolean') {
// `${file}` is actually interpolated by `this.exec`
editorCommand = process.env.EDITOR + ' ${file}';
} else {
editorCommand = this.options.launchEditor;
}

await this.exec(editorCommand, { context: { file: tmpFile } });
}

async reviewChangelog(changelog) {
if (!this.options.launchEditor) {
return changelog;
}

let tmpFile = tmp.fileSync().name;
fs.writeFileSync(tmpFile, changelog, { encoding: 'utf-8' });

await this._launchEditor(tmpFile);

let finalChangelog = fs.readFileSync(tmpFile, { encoding: 'utf-8' });

return finalChangelog;
}

async writeChangelog(changelog) {
Expand Down
9 changes: 5 additions & 4 deletions package.json
Expand Up @@ -23,7 +23,8 @@
},
"dependencies": {
"lerna-changelog": "^1.0.1",
"release-it": "^13.0.2"
"release-it": "^13.0.2",
"tmp": "^0.1.0"
},
"devDependencies": {
"ava": "^3.5.0",
Expand All @@ -32,8 +33,7 @@
"eslint-plugin-node": "^11.0.0",
"eslint-plugin-prettier": "^3.1.2",
"prettier": "^1.19.1",
"sinon": "^9.0.0",
"tmp": "^0.1.0"
"sinon": "^9.0.0"
},
"engines": {
"node": ">= 10"
Expand All @@ -44,7 +44,8 @@
"release-it": {
"plugins": {
"./index.js": {
"infile": "CHANGELOG.md"
"infile": "CHANGELOG.md",
"launchEditor": "nvim"
}
},
"git": {
Expand Down
93 changes: 85 additions & 8 deletions test.js
Expand Up @@ -3,26 +3,36 @@ const tmp = require('tmp');
const test = require('ava');
const { factory, runTasks } = require('release-it/test/util');
const Plugin = require('./index');
const EDITOR = process.env.EDITOR;

tmp.setGracefulCleanup();

const namespace = 'release-it-lerna-changelog';

function resetEDITOR() {
process.env.EDITOR = EDITOR;
}

class TestPlugin extends Plugin {
constructor() {
super(...arguments);

this.commands = [];
this.shell.execFormattedCommand = async (command, options) => {
this.commands.push([command, options]);
};
}

exec() {
this.commands.push([...arguments]);
async _launchEditor(tmpFile) {
this.launchedTmpFile = tmpFile;

return super._launchEditor(tmpFile);
}
}

function buildPlugin(config = {}) {
function buildPlugin(config = {}, _Plugin = TestPlugin) {
const options = { [namespace]: config };
const plugin = factory(TestPlugin, { namespace, options });
const plugin = factory(_Plugin, { namespace, options });

return plugin;
}
Expand All @@ -33,8 +43,8 @@ test('it invokes lerna-changelog', async t => {
await runTasks(plugin);

t.deepEqual(plugin.commands, [
[`git show-ref --tags --quiet --verify -- "refs/tags/1.0.0"`, { options: { write: false } }],
[`${plugin.lernaPath} --next-version=1.0.1 --from=1.0.0`, { options: { write: false } }],
[`git show-ref --tags --quiet --verify -- "refs/tags/1.0.0"`, { write: false }],
[`${plugin.lernaPath} --next-version=1.0.1 --from=1.0.0`, { write: false }],
]);
});

Expand All @@ -46,8 +56,8 @@ test('it honors custom git.tagName formatting', async t => {
await runTasks(plugin);

t.deepEqual(plugin.commands, [
[`git show-ref --tags --quiet --verify -- "refs/tags/v1.0.0"`, { options: { write: false } }],
[`${plugin.lernaPath} --next-version=v1.0.1 --from=v1.0.0`, { options: { write: false } }],
[`git show-ref --tags --quiet --verify -- "refs/tags/v1.0.0"`, { write: false }],
[`${plugin.lernaPath} --next-version=v1.0.1 --from=v1.0.0`, { write: false }],
]);
});

Expand Down Expand Up @@ -87,3 +97,70 @@ test('prepends the changelog to the existing file', async t => {
const changelog = fs.readFileSync(infile);
t.is(changelog.toString().trim(), '## v9.9.9 (2019-01-01)\n\nThe changelog\n\nOld contents');
});

test('uses launchEditor command', async t => {
let infile = tmp.fileSync().name;

let plugin = buildPlugin({ infile, launchEditor: 'foo-editor -w ${file}' });

await runTasks(plugin);

t.deepEqual(plugin.commands, [
[`git show-ref --tags --quiet --verify -- "refs/tags/1.0.0"`, { write: false }],
[`${plugin.lernaPath} --next-version=1.0.1 --from=1.0.0`, { write: false }],
[`foo-editor -w ${plugin.launchedTmpFile}`, {}],
]);
});

test('detects default editor if launchEditor is `true`', async t => {
let infile = tmp.fileSync().name;

let plugin = buildPlugin({ infile, launchEditor: true }, TestPlugin);

try {
process.env.EDITOR = 'foo-editor -w';
await runTasks(plugin);

t.deepEqual(plugin.commands, [
[`git show-ref --tags --quiet --verify -- "refs/tags/1.0.0"`, { write: false }],
[`${plugin.lernaPath} --next-version=1.0.1 --from=1.0.0`, { write: false }],
[`foo-editor -w ${plugin.launchedTmpFile}`, {}],
]);
} finally {
resetEDITOR();
}
});

test('launches configured editor, updates infile, and propogates changes to context', async t => {
class TestPlugin extends Plugin {
constructor() {
super(...arguments);

this.commands = [];
}

async _execLernaChangelog() {
return '## v9.9.9 (2019-01-01)\n\nThe changelog';
}

async _launchEditor(tmpFile) {
let originalChangelog = await this._execLernaChangelog();

fs.writeFileSync(tmpFile, originalChangelog + '\nExtra stuff!', { encoding: 'utf-8' });
}
}

let infile = tmp.fileSync().name;
let plugin = buildPlugin({ infile, launchEditor: 'foo-editor -w ${file}' }, TestPlugin);

await runTasks(plugin);

const changelogFileContents = fs.readFileSync(infile);
t.is(
changelogFileContents.toString().trim(),
'## v9.9.9 (2019-01-01)\n\nThe changelog\nExtra stuff!'
);

const { changelog } = plugin.config.getContext();
t.is(changelog, 'The changelog\nExtra stuff!');
});

0 comments on commit 7dda95d

Please sign in to comment.