Introduction to Standard Version
standard-version is a popular npm utility that automates the versioning and changelog generation process for JavaScript projects. It leverages Conventional Commits โ a structured commit message format โ to determine the next semantic version bump and to produce a clean, human-readable changelog. By integrating standard-version into your workflow, you eliminate the manual overhead of editing package.json, tagging releases, and writing changelogs by hand.
Originally maintained under the standard-version package and now succeeded by community forks like standard-version-exp and release-please, the tool remains a reference implementation for convention-based releases. This guide focuses on the original package's complete configuration model, which applies to most forks as well.
Why It Matters
- Consistency: Every release follows the same deterministic rules based on commit history.
- Automation: Version bumps, changelog entries, and Git tags are generated in a single command.
- Traceability: The changelog links each entry back to its commit, making audits easier.
- Team alignment: Conventional Commits enforce a shared vocabulary across contributors.
Installation
Install standard-version as a development dependency in your project:
npm install --save-dev standard-version
Add a convenience script to your package.json:
{
"scripts": {
"release": "standard-version"
}
}
You can now run npm run release to trigger a release. Before doing so, however, you should configure the tool to match your project's conventions.
Conventional Commits Primer
The entire system depends on commit messages following the Conventional Commits specification. The format is:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Common types include:
featโ a new feature (triggers a minor bump)fixโ a bug fix (triggers a patch bump)BREAKING CHANGEin the footer or!after the type โ triggers a major bumpchore,docs,style,refactor,perf,testโ typically do not trigger a release
Example commit messages:
feat(auth): add OAuth2 login flow
fix(parser): handle empty input without crashing
feat(api)!: remove deprecated v1 endpoints
Basic Usage
Once installed, run the release command:
npm run release
This command will:
- Analyze commits since the last tag.
- Determine the next version number.
- Update
package.jsonandpackage-lock.json. - Update
CHANGELOG.md. - Create a new Git commit and tag.
To preview what would happen without making changes, use the dry-run flag:
npx standard-version --dry-run
Complete Configuration
The real power of standard-version emerges through its configuration options. You can define these in a .versionrc, .versionrc.json, or .versionrc.js file at the project root, or under the "version" key in package.json.
Configuration File Example
Create a .versionrc.json file:
{
"types": [
{ "type": "feat", "section": "Features" },
{ "type": "fix", "section": "Bug Fixes" },
{ "type": "perf", "section": "Performance Improvements" },
{ "type": "refactor", "section": "Code Refactoring" },
{ "type": "docs", "section": "Documentation", "hidden": false },
{ "type": "test", "hidden": true },
{ "type": "chore", "hidden": true },
{ "type": "style", "hidden": true },
{ "type": "ci", "hidden": true }
],
"skip": {
"tag": false,
"commit": false,
"changelog": false
},
"commitAll": false,
"header": "# Changelog\n\nAll notable changes to this project are documented in this file.",
"packageFiles": ["package.json"],
"bumpFiles": ["package.json", "package-lock.json"],
"tagPrefix": "v",
"scripts": {
"prerelease": "npm test",
"postbump": "npm run build",
"postchangelog": "echo 'Changelog updated'"
}
}
Using a JavaScript Configuration
For dynamic configuration, use .versionrc.js:
module.exports = {
types: [
{ type: 'feat', section: 'โจ Features' },
{ type: 'fix', section: '๐ Bug Fixes' },
{ type: 'perf', section: 'โก Performance' },
{ type: 'refactor', section: 'โป๏ธ Refactoring' },
{ type: 'docs', section: '๐ Documentation' }
],
skip: {
tag: process.env.SKIP_TAG === 'true'
},
scripts: {
prerelease: 'npm run lint && npm test'
}
};
Key Configuration Options Explained
types
The types array controls which commit types appear in the changelog and how they are grouped. Each entry supports:
typeโ the commit type keyword.sectionโ the heading shown in the changelog.hiddenโ whentrue, commits of this type are excluded from the changelog.
skip
The skip object lets you bypass specific stages:
{
"skip": {
"bump": false,
"changelog": false,
"commit": false,
"tag": false
}
}
This is useful in CI pipelines where you may want to generate the changelog but handle tagging separately.
bumpFiles
By default, standard-version updates package.json and package-lock.json. You can extend this to other files that contain version strings:
{
"bumpFiles": [
"package.json",
"package-lock.json",
{
"filename": "src/version.ts",
"updater": "standard-version-updater.js"
}
]
}
A custom updater is a Node module exporting an updateVersion function:
// standard-version-updater.js
module.exports.readVersion = function(contents) {
return contents.match(/VERSION = '(.*)'/)[1];
};
module.exports.writeVersion = function(contents, version) {
return contents.replace(
/VERSION = '(.*)'/,
`VERSION = '${version}'`
);
};
tagPrefix
Customize the prefix applied to Git tags. The default is v, producing tags like v1.2.3. For monorepos or scoped releases, you might use a different prefix:
{
"tagPrefix": "release-"
}
scripts
Hook scripts run at specific lifecycle points:
prereleaseโ runs before any release work begins.prebump/postbumpโ around the version bump.prechangelog/postchangelogโ around changelog generation.precommit/postcommitโ around the release commit.pretag/posttagโ around Git tag creation.
{
"scripts": {
"prerelease": "npm run test",
"posttag": "git push --follow-tags origin main && npm publish"
}
}
Pre-release and First Release Scenarios
First Release
If your project has no tags yet, force an initial release:
npx standard-version --first-release
This sets the version to 1.0.0 (or whatever is currently in package.json) and generates the initial changelog without bumping.
Pre-release Versions
Generate alpha, beta, or rc releases:
npx standard-version --prerelease alpha
npx standard-version --prerelease beta
npx standard-version --prerelease rc
This produces versions like 1.2.0-alpha.0, 1.2.0-alpha.1, and so on. When you are ready for the stable release, simply run npm run release without the flag, and the pre-release suffix is dropped.
Manual Release Types
Override the automatically detected bump level:
npx standard-version --release-as major
npx standard-version --release-as minor
npx standard-version --release-as patch
npx standard-version --release-as 2.5.0
Customizing the Changelog
Custom Commit Message Format
If your team uses a slightly different commit format, you can adjust the parser:
{
"commitUrlFormat": "https://github.com/myorg/myrepo/commit/{{hash}}",
"compareUrlFormat": "https://github.com/myorg/myrepo/compare/{{previousTag}}...{{currentTag}}",
"issueUrlFormat": "https://jira.example.com/browse/{{id}}"
}
Custom Writer Templates
For advanced changelog formatting, supply a custom writer template using conventional-changelog-writer conventions:
{
"writerOpts": {
"transform": function(commit, context) {
if (!commit.type) {
return;
}
if (commit.scope === '*') {
commit.scope = '';
}
if (typeof commit.hash === 'string') {
commit.shortHash = commit.hash.substring(0, 7);
}
return commit;
},
"groupSort": ["Features", "Bug Fixes", "Performance Improvements"]
}
}
Integrating with CI/CD
A typical CI pipeline runs tests, then performs the release. Here is a GitHub Actions example:
name: Release
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
- name: Configure Git
run: |
git config user.name "CI Bot"
git config user.email "ci-bot@example.com"
- name: Run standard-version
run: npx standard-version
- name: Push tags
run: git push --follow-tags origin main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
The fetch-depth: 0 setting is critical โ standard-version needs the full commit history to analyze changes since the last tag.
Monorepo Considerations
For monorepos, standard-version alone is limited. You typically combine it with tools like lerna or use --tagPrefix to distinguish packages:
{
"tagPrefix": "@myorg/core@"
}
Alternatively, configure separate .versionrc files per package and run releases individually from each package directory.
Best Practices
- Enforce Conventional Commits early. Use tools like
commitlintandhuskyto validate commit messages before they land in your history. - Always dry-run first. Use
--dry-runin CI before executing a real release to catch configuration errors. - Keep configuration in version control. Commit your
.versionrcso all team members and CI share the same release behavior. - Run tests in
prerelease. Never release code that fails its test suite. - Use pre-releases for unstable work. Publish alpha or beta versions to gather feedback before a stable release.
- Review the changelog before publishing. The generated changelog is only as good as your commit messages; review it for clarity.
- Pin your version. Lock
standard-versionto a specific version indevDependenciesto avoid unexpected behavior changes. - Tag consistently. Stick with one
tagPrefixacross the project's lifetime to keep release history clean.
Common Pitfalls
Missing Commit History
If standard-version reports no changes, ensure you have committed work after the last tag. Shallow clones in CI also cause this โ always use fetch-depth: 0.
Incorrect Version Bumps
If a feat commit only triggers a patch bump, check that your commit messages strictly follow the Conventional Commits format. A missing colon or wrong type keyword causes the parser to treat the commit as a non-release commit.
Changelog Not Updating
Verify that skip.changelog is not set to true in your configuration. Also confirm that CHANGELOG.md is writable and not ignored by Git in a way that prevents commits.
Example: Full Project Setup
Here is a complete package.json snippet combining scripts, commitlint, and standard-version:
{
"name": "my-awesome-lib",
"version": "1.0.0",
"scripts": {
"test": "jest",
"lint": "eslint .",
"release": "standard-version",
"release:alpha": "standard-version --prerelease alpha",
"release:dry": "standard-version --dry-run"
},
"devDependencies": {
"@commitlint/cli": "^19.0.0",
"@commitlint/config-conventional": "^19.0.0",
"husky": "^9.0.0",
"standard-version": "^9.5.0"
},
"standard-version": {
"types": [
{ "type": "feat", "section": "Features" },
{ "type": "fix", "section": "Bug Fixes" },
{ "type": "perf", "section": "Performance" },
{ "type": "refactor", "section": "Refactoring" },
{ "type": "docs", "section": "Documentation" },
{ "type": "chore", "hidden": true },
{ "type": "test", "hidden": true }
],
"scripts": {
"prerelease": "npm run lint && npm test"
}
}
}
Pair this with a commitlint.config.js:
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'perf', 'refactor', 'docs', 'test', 'chore', 'style', 'ci']
]
}
};
And a Husky hook to enforce it:
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'
Conclusion
standard-version brings discipline and automation to the release process by tying version bumps and changelog generation directly to your commit history. With a well-crafted .versionrc configuration, enforced Conventional Commits through commitlint, and a CI pipeline that runs releases automatically, your team can ship confidently and consistently. The upfront investment in commit conventions pays off every release cycle โ you get accurate changelogs, predictable semantic versions, and a fully auditable release trail without manual bookkeeping. Whether you are maintaining a small library or a large application, mastering standard-version's configuration options gives you a release workflow that scales with your project.