release-it is a pluggable task runner. If it can either be written in Node.js, or executed from the shell, it can be integrated in the release-it process.
Plugins allow additional and custom actions in the release process, such as:
- Publish the package to any registry (this is language-agnostic, e.g. Ruby, Python, ...).
- Implement a different strategy to generate changelogs and/or release notes.
- Trigger web hooks (e.g. post a message to a Slack channel).
- Use a different VCS, such as Mercurial (example: @release-it/mercurial).
- Use Node.js directly (instead of executing shell scripts configured in
hooks.*
). - Replace existing plugins. For instance, integrate with the npm registry using their programmatic API (as opposed
to calling
npm publish
in a child process like release-it itself does).
Internally, release-it uses its own plugin architecture and includes the following plugins:
git
github
gitlab
npm
version
Each plugin has a different responsibility, and each enables itself:
- The
git
plugin is enabled if the current directory contains a.git
directory. - The
github
plugin becomes active ifgithub.release
istrue
. - The
gitlab
plugin is enabled only ifgitlab.release
istrue
. - The
npm
plugin looks for apackage.json
in the current directory. - The
version
plugin is always enabled (it increments the version and prompts for it if needed).
Plugins are local to the project, or external npm packages. Plugin configuration consists of a module name with options.
This example uses the release-it-plugin
module and is configured in package.json
:
{
"devDependencies": {
"release-it": "*",
"release-it-plugin": "*"
},
"release-it": {
"github": {
"release": true
},
"plugins": {
"release-it-plugin": {
"key": "value"
}
}
}
}
Alternatively, here's a release-it-plugin
as a local module:
{
"plugins": {
"./scripts/release-it-plugin.js": {
"key": "value"
}
}
}
To create a plugin, extend the Plugin
class, and implement one or more release-cycle methods. See the "interface"
below (where none of the methods is required). Any of these methods can be async
. See this test helper to get an
idea of the methods a release-it plugin can implement.
Note that release-it
should be a peerDependency
(and probably also a devDependency
to use its helpers in the
plugin tests). Here's an example package.json
:
{
"name": "release-it-plugin",
"version": "1.0.0",
"description": "My release-it plugin",
"main": "index.js",
"peerDependencies": {
"release-it": "^14.2.0"
},
"devDependencies": {
"release-it": "^14.2.0"
}
}
Or see the plugin-starterkit for a good start.
This minimal example reads the current version from a VERSION
file, and bumps it once the new version is known.
class MyPlugin extends Plugin {
getLatestVersion() {
return fs.readFileSync('./VERSION', 'utf8').trim();
}
bump(version) {
this.version = version;
fs.writeFileSync('./VERSION', version);
}
}
This plugin has made itself responsible for providing the latest version by implementing the getLatestVersion
method.
Also, it writes the new version in the ./VERSION
file during the release process.
In the context of the whole release process, this may also be relevant for other plugins:
- If the
npm
plugin is enabled, that plugin will bumppackage.json
with the return value ofgetLatestVersion
. - If the
git
plugin is enabled, itsbeforeRelease
method will stage the changes so the updated./VERSION
will be part of the release commit.
Since order matters here, the release-cycle methods of internal plugins are executed after other plugins. Except for
the release
and afterRelease
methods at the end.
- Interface overview
- Static methods
- Release-cycle methods
- Getter methods
- Helper methods
- Execution Order
class Plugin {
static isEnabled() {}
static disablePlugin() {}
getInitialOptions() {}
init() {}
getName() {}
getLatestVersion() {}
getIncrement() {}
getIncrementedVersionCI() {}
getIncrementedVersion() {}
beforeBump() {}
bump() {}
beforeRelease() {}
release() {}
afterRelease();
}
Note that any of the methods in the plugin can be async
except for disablePlugin()
. In the method signatures below
this is implied everywhere (e.g. β Boolean
means it should return a boolean, or a promise resolving to a boolean).
By default, a plugin is always enabled. Override the static isEnabled
method to enable the plugin based on specific
conditions, such as plugin configuration or the presence of a file or directory.
In case a plugin replaces a core plugin, it should be disabled by returning the name of the core plugin. Return a string
(or array of strings) containing the plugin name (one or more of version
, git
, github
, gitlab
, npm
).
Implement release-cycle methods to execute logic during the release process. All methods are run async, so async/await
can be used freely.
Make sure any method returns false
when it's disabled or skipped, in order to skip the execution of the
after:[plugin]:[method]
hook. this is especially relevant for the release
method.
Implement init
to validate prerequisites, and gather application or package details such as the current version.
Implement beforeBump
to prepare things, gather and/or output interesting information for the user, such as a changelog
or other details to help the user confirm the release will be executed properly.
Implement bump
to increment the version in manifests or other files containing the version of the application or
package (e.g. package.json
for Node.js modules).
Implement beforeRelease
to perform tasks that should happen after the bump, and stage things before the release
.
Implement release
for the main flow of the plugin. This is where the "steps" should be declared (see step in
class API), resulting in prompts (interactive) or spinners (non-interactive) that will execute tasks for confirmed
steps.
Implement afterRelease
to provide details about a successful release, e.g. a link to the release page.
Implement any of the following methods to be ahead of any core plugin and use that during the release process instead.
Provide the name of the package being released.
Implement getLatestVersion
and return the latest version prior to the current release, so release-it can determine the
next version.
By default, every plugin receives the options configured in options[pluginName]
. For instance, the core npm
plugin
receives the options under the npm
property in the configuration. Other plugins receive the options as they are
configured in the plugins
section. However, if a plugin requires additional options from other plugins, the
getInitialOptions
is useful:
getInitialOptions(options, pluginName) {
return Object.assign({}, options[pluginName], {
tagName: options.git.tagName,
});
}
The following methods are mostly internal methods that normally should not be implemented in any plugin, but in rare cases this might be useful.
Implement getIncrement
to override the increment used by getIncrementedVersionCI
by providing major
, minor
or
patch
, otherwise staying with Version.js's default logics.
Implement getIncrementedVersionCI
to provide the next version without prompting the user (i.e. determine the next
version based on the provided increment
value). This method exists to provide the next version
to other elements of
the release process early on, such as the introduction text.
Implement getIncrementedVersion
to provide the next version, and prompt the user if this can't be determined
automatically.
The Plugin
class exposes helper methods, here's an overview:
Set additional data local to the plugin during runtime.
Get the plugin options extended with additional runtime data set with setContext
.
Register one or more prompts and allow the user to confirm actions or provide details.
A prompt object looks like this:
{
type: 'confirm',
name: 'my-prompt',
message: 'Are you sure?'
}
Under the hood, Inquirer.js is used. See Inquirer.js/#objects for more details.
Display a prompt or a spinner during the release
release-cycle method. This automatically shows a prompt if
interactive, or a spinner in CI (non-interactive) mode.
await this.step({
enabled: true,
task: () => this.doTask(),
label: 'Doing task',
prompt: 'my-prompt'
});
If the prompt receives a "No" from the user, the task
callback is not executed.
Execute commands in the child process (i.e. the shell). This is used extensively by release-it to execute git
and
npm
commands. Be aware of cross-OS compatibility.
Use template variables to render replacements. For instance, the command git log ${latestTag}...HEAD
becomes
git log v1.2.3...HEAD
before being executed. The replacements are all configuration options (with the default values
in config/release-it.json), plus the following additional variables:
version
latestVersion
latestTag
changelog
name
repo.remote, repo.protocol, repo.host, repo.owner, repo.repository, repo.project
The additional variables are available in every release-cycle method, except init
.
Note that in dry runs, commands are not executed as they may contain write operations. Read-only operations should
add the write: false
option to run in dry mode:
this.exec('git log', { options: { write: false } });
Insert this.debug(...)
statements to log interesting details when NODE_DEBUG=release-it:* release-it ...
is used.
The output is namespaced automatically (e.g. release-it:foo My log output
).
Use this.log.[verbose|warn|error|log|info]
to log and inform the user about what's going on in the release process.
Assuming there are two plugins configured, "PluginA" and "PluginB":
{
"plugins": {
"PluginA": {},
"PluginB": {}
}
}
First, the init
method is executed for PluginA
, then PluginB
, and then the core plugins: npm
β git
β github
β gitlab
β version
.
Then the same for getName
and getLatestVersion
. For these getter methods, the value of the first plugin that returns
something is used throughout the release process. This allows a plugin to be ahead of core plugins.
After this, the beforeBump
, bump
and beforeRelease
methods are executed for each plugin in the same order.
And finally, for release
and afterRelease
the order is reversed, so that tasks can be executed after release-it core
plugins are done. Examples include to trigger deployment hooks, or send a notification to indicate a successfull release
or deployment.
Here's an example:
- If the
npm
plugin is enabled,npm.getName()
is the first plugin/method that returns something (thename
frompackage.json
is used in this case). - If this plugin is not enabled,
getName
of the next plugin is invoked (e.g. thegit
plugin will infer the name from the remote Git url), etcetera. - The methods of custom plugins are invoked first, so they can override the
name
,latestVersion
,repo
, andchangelog
values that would otherwise be taken from the core plugins.
- All packages tagged with
"release-it-plugin"
on npm. - Recipe: my-version - example plugin
- Internal release-it plugins