Automate the monotonous tasks when it comes to releasing new versions of your project. The features include:
- Bump the version in
package.json
(andpackage-lock.json
) - Update the changelog headline with the new version and release date whenever a release is made
- Move old changelog entries into another file
- Add the changelog to the release commit and create a tag for it.
- Support for monorepos managed with
lerna
oryarn workspaces
(using additionalyarn
plugins) - Support for custom scripts during the release lifecycle
- Check licenses for outdated copyrights
- Dry runs and checking for errors
- ioBroker only:
- Bump the version in
io-package.json
- Update the
news
inio-package.json
and auto-translate the changelog to the other languages - Remove old
news
- Bump the version in
Together with the corresponding Github Actions workflow (more on that below) this enables auto-publishing on npm
and Github Releases
if the build was successful.
-
Add this module to your
devDependencies
:npm i -D @alcalzone/release-script
-
Add a new
npm
script inpackage.json
:"scripts": { // ... other scripts ... "release": "release-script" }
-
Add a placeholder to
README.md
(for your own convenience)## Changelog <!-- Placeholder for the next version (at the beginning of the line): ### **WORK IN PROGRESS** -->
or
CHANGELOG.md
if you prefer to have a separate changelog (notice that there is one less#
):# Changelog <!-- Placeholder for the next version (at the beginning of the line): ## **WORK IN PROGRESS** -->
-
If necessary (e.g. for custom versioning steps) use a config file
In order to use this script, you need to maintain the changelog in either README.md
or CHANGELOG.md
, because every release must have a changelog. To let the script know which changes are new, use the placeholder:
# Changelog
<!--
Placeholder for the next version (at the beginning of the line):
## **WORK IN PROGRESS**
-->
## **WORK IN PROGRESS**
- Did some changes
- Did some more changes
## v0.0.1 (2020-01-01)
Initial release
If you are using README.md
, the script can automatically move old changelog entries to CHANGELOG_OLD.md
if that exists. The most recent entries are kept in README.md
. To use this feature, simply create a CHANGELOG_OLD.md
and give it a headline you like, e.g.
# Older changes
You can add free text after the placeholder:
## **WORK IN PROGRESS** - 2020 Doomsday release
will be turned into
## 1.2.3 (2020-02-02) - 2020 Doomsday release
for example.
Once you are ready to release the changes, commit everything so the working tree is clean. Also make sure that you are on the master
branch. Now you can create a release by executing:
npm run release [<releaseType> [<postfix>]] -- [options]
or
yarn release [<releaseType> [<postfix>]] [options]
When using
npm
, don't forget the extra--
before specifying options!
You can choose between the following release types: The available release types are:
Release type | Description / when to use | Example |
---|---|---|
major |
Breaking changes are introduced. This may include new features and bugfixes. | 0.9.8 -> 1.0.0 |
premajor |
Like major , but to provide test versions before the final release. WARNING: Using this multiple times increases the major version each time. |
0.9.8 -> 1.0.0-0 -> 2.0.0-0 |
minor |
A new feature was added without breaking things. This may include bugfixes. | 0.9.8 -> 0.10.0 |
preminor |
Like minor , but to provide test versions before the final release. WARNING: Using this multiple times increases the minor version each time. |
0.9.8 -> 0.10.0-0 -> 0.11.0-0 |
patch |
A bug was fixed without adding new functionality. | 0.9.8 -> 0.9.9 |
prepatch |
Like patch , but to provide test versions before the final release. WARNING: Using this multiple times increases the patch version each time. |
0.9.8 -> 0.9.9-0 -> 0.9.10-0 |
prerelease |
Increases only the prerelease suffix. If the current version is not a prerelease, this behaves like prepatch . You might want to use this to create multiple test versions (e.g. multiple premajor versions) |
0.9.8 -> 0.9.9-0 -> 0.9.9-1 |
For the pre*
release types, you can optioinally provide a custom postfix, e.g. beta
. For example, npm run release prepatch beta
would result in the following bump: 0.9.8 -> 0.9.9-beta.0
.
If you omit the
releaseType
orpostfix
options, the release script will ask you for the desired version.
The release script includes a small set of command line options by default. Each loaded plugin may add additional options. Those are described under the corresponding plugin.
Example:
npm run release -- --help
The release script makes it possible to test the command before actually changing anything. To do so, use the --dry
argument. Take care to include the first set of hyphens (-- --dry
) in the command or it won't be a dry run. Here's an example of a dry run that tells you what it would do and that it would fail due to uncommitted changes:
Instead of manually providing all options, you can configure the release process with a configuration file. This must be a JSON file
{
// Boolean, number, string as is:
"dry": false,
// Arrays:
"plugins": ["iobroker", "lerna"],
// Objects
"exec": {
"before_commit": "echo Hello World!"
}
}
or a JS module if you like to do fancy stuff
module.exports = {
dry: Math.random() * 1000 > 500,
};
Example:
npm run release -- -c mysuperduperconfig.json
By default, the release script looks for
.releaseconfig.json
An array of plugin names to be loaded. Example:
npm run release -- -p iobroker lerna
Can help debug the release process. Example:
npm run release -- -V
Example:
npm run release minor -- --yes
By default, only changed packages of monorepos are bumped and published. With this flag, all non-private packages are re-published under a new version, even if they had no changes.
You can version monorepos in two different ways.
Note: If possible, prefer this way. Lerna does not support some advanced Yarn features, like workspace:*
dependency ranges.
Starting with yarn v3.1.0
, the release script can use Yarn plugins to manage versions and releases of monorepos. These plugins can be installed in your repository as follows:
yarn plugin import workspace-tools
yarn plugin import version
yarn plugin import https://github.com/Dcard/yarn-plugins/releases/latest/download/plugin-changed.js
You also need to make sure that there is a "version"
field in the root package.json
file.
To release changed packages, simply run the following command after bumping the versions (ideally during your CI build). This assumes that the root workspace is private and should not be published:
# without dist-tags
yarn workspaces foreach -vti --no-private npm publish --tolerate-republish
# with dist-tags
yarn workspaces foreach -vti --no-private npm publish --tolerate-republish --tag my-tag
Set up lerna
in fixed versioning mode and install and enable the lerna
plugin (see below). Note that this disables the use of yarn
plugins.
Since version 3, the release script is separated into several plugins, making it easy to extend it with custom checks. The following plugins are installed and loaded by default and don't need to be installed separately:
Plugin name | Description |
---|---|
changelog |
Parse and update the changelog |
exec |
Run custom scripts during the release lifecycle |
git |
Check git status, make commits and push |
package |
Check and update package.json , synchronize lockfiles |
version |
Determine the version bump |
If you need some of these plugins, you have to install and load them separately:
Plugin name | Description |
---|---|
iobroker |
Update ioBroker's io-package.json |
lerna |
Monorepo support with lerna |
license |
Check for outdated years in license files |
manual-review |
Asks you to review the changes before committing and lets you do additional edits. |
To do so, add them as a devDependency
:
// package.json
// ...
"devDependencies": {
"@alcalzone/release-script": "3.0.0",
"@alcalzone/release-script-plugin-iobroker": "3.0.0",
"@alcalzone/release-script-plugin-license": "3.0.0",
// ... others ...
}
and add the plugin(s) to your CLI command
npm run release -- -p iobroker license <other options>
or to .releaseconfig.json
if you're using that:
{
// ... other options
"plugins": ["iobroker", "license"]
}
If possible, the version for the plugins should be the same as the one for @alcalzone/release-script
.
The release process is separated into stages. By default, the following stages are active:
check → edit → commit → push → cleanup
but each plugin can define its own stages.
Although the release commit should only include the changes relevant to the version increase, sometimes it makes sense to include other changes in the release commit. For this purpose, you can use the --all
option, e.g.:
npm run release patch -- --all
By default, the release script pushes to the remote the current branch points to. If you want to specify a different one, you can use this flag. Make sure to use the complete remote branch name:
npm run release patch -- -r upstream/master
When this option is set, only the annotated release tag will be pushed to the remote. The temporary commit on the release branch will be removed afterwards. This option can be useful if branch protection rules prevent the release branch from being pushed.
npm run release patch -- --tagOnly
Only applies when the changelog is in README.md
and a CHANGELOG_OLD.md
exists. By default the most recent 5 entries are kept in README.md
.
If you want the release script to add a new empty placeholder to the changelog after a release, this option is for you. By default, this is disabled.
After bumping the version, the lockfile is normally synchronized with package.json
to avoid errors on CI. If this is not desirable, you can disable the behavior.
Pass the --force
flag to the package manager when updating the lockfile (--force-update-lockfile
or -lf
)
In some cases it is necessary to pass the --force
flag to the package manager when updating the lockfile. This can be done with this option. Depending on the package manager in use, this option may or may not do anything.
To replace the version in additional files, you can use the --versionFiles
option. It is recommended to define it via a configuration file. The option expects an array of tuples containing glob patterns to match files and one or more regular expressions to match the version to be replaced.
In order for this to work, the regular expressions must surround the version to replace with lookbehind
(?<=BEFORE_VERSION)
and lookahead(?=AFTER_VERSION)
assertions. Additional backslashes are needed to escape the special characters because of JSON.
{
"versionFiles": [
// Match `>Version: VERSION<` in html files in the widget folder
// and replace VERSION with the new version
["widget/*.html", "(?<=\\>Version: )(.*?)(?=<)"],
// Check all files in the template folder and its subdirectories
["templates/**/*.*", [
// Match `"version": "VERSION",` and replace VERSION with the new version
"(?<=\"version\": \")(.*?)(?=\",)",
// Match `Version: "VERSION"` and replace VERSION with the new version
"(?<=Version: \")(.*?)(?=\")"
]]
]
}
To run custom scripts, you can use these options. They accept an object defining the stage in/after/before which scripts should be executed. To execute multiple commands in sequence, you can also use an array of strings. Example:
{
"exec": {
// Run "echo Hello" in the "commit" stage
"commit": "echo Hello",
// Run "echo 1", "echo 2", "echo 3" after the "push" stage
"after_push": ["echo 1", "echo 2", "echo 3"],
// Run "sudo shutdown" before the "check" stage
"before_check": "sudo shutdown"
}
}
You can also define these on the command line (if you really must):
npm run release -- -x.commit "echo Hello" -x.after_push "echo 1" "echo 2" "echo 3" -x.before_check "sudo shutdown"
The release script expects io-package.json
in the package root. If that is not the case, for example if you're building a monorepo, you can change that path:
npm run release patch -- -io packages/js-controller
Usually, the version in io-package.json
and package.json
must match and the script will raise an error if they don't.
// package.json:
"version": "1.2.4",
// io-package.json:
"common": {
"version": "1.2.3" // not OK ❌
// ...
}
However, if you don't want to put a prerelease version like v1.2.3-alpha.0-abcdefg
in io-package.json
, you can enable this flag:
npm run release patch -- --ioPackageNoPrerelease
In this case, only the major/minor/patch part is used for io-package.json
:
// package.json:
"version": "1.2.3-alpha.0-abcdefg",
// io-package.json:
"common": {
"version": "1.2.3" // OK ✔
// ...
}
The release script tries to find common errors in the ioBroker Github Actions release workflow file. If the check results in a false positive, you can disable this check with the --no-workflow-check
option.
npm run release patch -- --no-workflow-check
By default the most recent 7 news entries are kept in io-package.json
. Using this option, you can change the limit.
By default, the files LICENSE
, README
with and without .md
extension are checked (glob "{LICENSE,README}{,.md}"
). Using this option, you can define an array of glob patterns to change the search paths.
npm run release patch -- --license "**/LICENSE" "packages/*/README.md"
none
When using Github Actions, you can enable automatic release on npm
and Github Releases
after a tagged build was successful. To do so, include the following job in your workflow definition file (e.g. .github/workflows/test-and-release.yml
) and configure it to depend on the build jobs (here, they are called unit-tests
).
The workflow must be configured to run when tags are pushed.
Furthermore, you need to create a token in your npm account and add it to your repository secrets with the name NPM_TOKEN
.
# Run this workflow on all pushes and pull requests
# as well as tags with a semantic version
on:
push:
# Configure your tested branches here, I like to check all of them. You can leave this out if you only want to deploy
branches:
- "*"
# You MUST limit tags to ones that are compatible with the version scheme this script uses. DO NOT include any others or they will be released aswell
tags:
# normal versions
- "v[0-9]+.[0-9]+.[0-9]+"
# pre-releases
- "v[0-9]+.[0-9]+.[0-9]+-**"
# This runs the workflow for all pull requests. You can leave this out if you only want to deploy
pull_request: {}
jobs:
#
# ... your other jobs go here ...
#
# Deploys the final package to NPM and Github Actions
deploy:
# Trigger this step only when a commit on master is tagged with a version number
if: |
contains(github.event.head_commit.message, '[skip ci]') == false &&
github.event_name == 'push' &&
startsWith(github.ref, 'refs/tags/v')
# Define which jobst must succeed before the release
needs: [unit-tests]
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Extract the version and commit body from the tag
id: extract_release
# The body may be multiline, therefore we need to escape some characters
run: |
VERSION="${{ github.ref }}"
VERSION=${VERSION##*/v}
echo "::set-output name=VERSION::$VERSION"
BODY=$(git show -s --format=%b)
BODY="${BODY//'%'/'%25'}"
BODY="${BODY//$'\n'/'%0A'}"
BODY="${BODY//$'\r'/'%0D'}"
echo "::set-output name=BODY::$BODY"
# If you are using TypeScript, additional build steps might be necessary
# Run them here, e.g.:
# - name: Install dependencies
# run: npm ci
# - name: Create a clean build
# run: npx gulp build
# How to release packages to npm depends on the package type.
# Choose one of these possibilities and or comment out the others:
# a) For normal packages (no monorepo):
- name: Publish package to npm
run: |
npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
npm whoami
npm publish
# b) For monorepos managed with lerna (requires lerna to be installed globally)
- name: Publish packages to npm
run: |
npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
npm whoami
lerna publish from-package --yes
# c) For monorepos managed with yarn v3.1+ (no lerna)
- name: Publish packages to npm
run: |
yarn config set npmAuthToken "${{ secrets.NPM_TOKEN }}"
yarn npm whoami
yarn workspaces foreach -vti --no-private npm publish --tolerate-republish
- name: Create Github Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release v${{ steps.extract_release.outputs.VERSION }}
draft: false
# Prerelease versions create prereleases on Github
prerelease: ${{ contains(steps.extract_release.outputs.VERSION, '-') }}
body: ${{ steps.extract_release.outputs.BODY }}