If you've ever been the person manually bumping version numbers, writing changelogs, and tagging releases, you know the drill. It's tedious, error-prone, and the kind of work that quietly eats into your team's focus. For our micro-frontend team running on Azure DevOps with Azure Artifacts, this was a real bottleneck. Releases that should have taken minutes were stretching into hours.
This is the story of how I proposed and implemented semantic-release to automate the whole thing.
The Problem: Manual Releases Don't Scale
We were managing multiple micro-frontend packages published to Azure Artifacts. Every release meant someone had to:
- Manually decide the version bump (patch? minor? major?)
- Update
package.jsonby hand - Write or update
CHANGELOG.md - Create a git tag
- Manually trigger the pipeline to publish to Azure Artifacts
- Hope nobody forgot a step
With multiple developers and multiple packages, this process was inconsistent. Sometimes changelogs were skipped. Sometimes the wrong version was bumped. And every release required a human to stop, context-switch, and babysit the pipeline.
The fix? Stop relying on humans for decisions that should be deterministic.
Why Semantic Release Over the Alternatives?
Before committing, I evaluated three tools. Here is a quick comparison:
| semantic-release | Changesets | release-it | |
|---|---|---|---|
| Version decision | Fully automatic from commits | Manual per-change file | Semi-manual |
| Changelog generation | Automatic | Automatic | Automatic |
| Azure DevOps integration | Clean, first-class | Workable but extra steps | Workable but extra steps |
| Human input required | Zero (after setup) | Yes, per PR | Yes, per release |
| Best for | CI-driven teams with commit discipline | Monorepo with manual control preference | Teams wanting a guided CLI flow |
Why we picked semantic-release: The Azure DevOps pipeline integration was the cleanest of the three. Once configured, it requires zero human decisions at release time. You merge your PR, the pipeline does the rest. For a team already writing structured commits, it felt like a natural extension of the workflow rather than an extra step bolted on.
Changesets is excellent, especially for monorepos where different packages need independent changelogs and a human wants final say. But for our use case, that manual control was exactly what we were trying to eliminate.
How It Works
Semantic-release reads your commit messages. If your commits follow the Conventional Commits spec, it determines the correct version bump, generates the changelog, publishes the package, and commits everything back to the repo, all without human intervention.
| Commit type | Version bump |
|---|---|
feat: add new filter component |
Minor (1.0.0 → 1.1.0) |
fix: correct button alignment |
Patch (1.1.0 → 1.1.1) |
feat!: or BREAKING CHANGE: in footer |
Major (1.1.1 → 2.0.0) |
PROJ-123: some work (JIRA ticket) |
Minor |
Implementation
1. Install the packages
npm install --save-dev semantic-release \
@semantic-release/commit-analyzer \
@semantic-release/changelog \
@semantic-release/npm \
@semantic-release/git \
conventional-changelog-conventionalcommits
2. Configure .releaserc.js
This is the heart of the setup. A few things worth highlighting before you read the config:
The headerPattern regex handles three real-world commit formats your team might use:
- Standard:
type(scope): description - JIRA-prefixed:
TICKET-123 type(scope): descriptionorTICKET-123: type: description - Merge commits:
Merged PR 123: TICKET-123 type: description
This matters a lot in practice. Developers often prefix commits with ticket IDs, and without this pattern, semantic-release would silently skip those commits when deciding the version bump.
The JIRA rule (subject: ".*[A-Z]+-[0-9]+.*") treats any commit referencing a ticket as a minor bump. Adjust this to "patch" if that better fits your team's conventions.
The same parserOpts block is intentionally repeated in both commit-analyzer and release-notes-generator. Both plugins parse commits independently, so both need the custom header pattern to work correctly.
module.exports = {
branches: ["main"],
plugins: [
[
"@semantic-release/commit-analyzer",
{
preset: "conventionalcommits",
releaseRules: [
{ type: "feat", release: "minor" },
{ type: "fix", release: "patch" },
{ type: "perf", release: "patch" },
{ type: "revert", release: "patch" },
{ type: "refactor", release: "patch" },
{ type: "docs", release: false },
{ type: "style", release: false },
{ type: "chore", release: false },
{ type: "test", release: false },
{ type: "build", release: false },
{ type: "ci", release: false },
{ breaking: true, release: "major" },
// Treat any JIRA ticket reference as a minor release
{ subject: ".*[A-Z]+-[0-9]+.*", release: "minor" },
],
parserOpts: {
noteKeywords: ["BREAKING CHANGE", "BREAKING CHANGES"],
// Handles: standard, JIRA-prefixed, and merge commits
headerPattern:
/^(?:Merged PR \d+: )?(?:[A-Z]+-\d+: )?(?:(\w+)(?:\(([^\]]*)\))? *: *(.*))$/,
headerCorrespondence: ["type", "scope", "subject"],
},
},
],
[
"@semantic-release/release-notes-generator",
{
parserOpts: {
noteKeywords: ["BREAKING CHANGE", "BREAKING CHANGES"],
headerPattern:
/^(?:Merged PR \d+: )?(?:[A-Z]+-\d+: )?(?:(\w+)(?:\(([^\]]*)\))? *: *(.*))$/,
headerCorrespondence: ["type", "scope", "subject"],
},
},
],
[
"@semantic-release/changelog",
{
changelogFile: "CHANGELOG.md",
},
],
[
"@semantic-release/npm",
{
npmPublish: true,
},
],
[
"@semantic-release/git",
{
assets: ["CHANGELOG.md", "package.json"],
message:
"chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}",
},
],
],
};
3. Add the script to package.json
"scripts": {
"semantic-release": "semantic-release"
}
4. Point npm to Azure Artifacts
If you are publishing to Azure Artifacts instead of the public npm registry, you need an .npmrc file that points to your feed:
registry=https://pkgs.dev.azure.com/<your-org>/<your-project>/_packaging/<your-feed>/npm/registry/
always-auth=true
In the pipeline, authenticate using the built-in $(System.AccessToken). No separate npm token management needed since Azure Artifacts and Azure DevOps share the same auth system.
5. Update your Azure DevOps pipeline
A few important things here:
- Configure git identity before running semantic-release, otherwise the git push step will fail
- Pass
System.AccessTokenfor both npm (Azure Artifacts auth) and git (to push the version commit back to the repo) - Run semantic-release before your build step so
package.jsonalready has the updated version when you build
# Configure git so semantic-release can push the version commit
- script: |
git config --global user.email "build@your-org.com"
git config --global user.name "Azure DevOps Build"
displayName: "Configure git for semantic-release"
# Run semantic-release before building
- script: |
npm run semantic-release
displayName: "Run semantic-release"
env:
NPM_TOKEN: $(System.AccessToken)
GIT_CREDENTIALS: $(System.AccessToken)
# Build and publish with the now-updated version
- script: |
npm run build
displayName: "Build and publish"
6. Repository permissions (the part nobody tells you about)
This one catches most people on Azure DevOps. Since semantic-release needs to push a commit back to a protected branch (main), the pipeline service account needs the right permissions in Azure Repos:
-
Bypass policies when pushing: set to
Allow -
Force push: set to
Deny(keep this locked down)
This lets the pipeline commit the version bump and updated CHANGELOG.md without disabling branch protection entirely. Your normal PR policies stay intact for everyone else.
The Workflow After Implementation
Once this is in place, the developer experience becomes:
- Write commits using conventional format (
feat:,fix:,PROJ-123: feat:, etc.) - Open a PR, get it reviewed, merge to
main - Done. Everything else is automatic.
The pipeline analyzes commits, bumps the version, updates CHANGELOG.md, creates a git tag, and publishes the package to Azure Artifacts. No human decisions. No manual pipeline triggers.
Common Gotchas
Lock file out of sync errors
rm -rf node_modules package-lock.json
npm install
Node.js version mismatch
Make sure the Node.js version in your pipeline matches your local .nvmrc. Mismatches can cause subtle package resolution issues.
Commits not triggering a release
Run the dry-run first to see exactly what semantic-release sees:
npm run semantic-release -- --dry-run
If your JIRA-prefixed commits are not being picked up, double-check the headerPattern regex. This is the most common configuration issue when teams have non-standard commit formats.
Azure Artifacts 401 errors
If the pipeline fails to publish with an auth error, make sure the feed's contributor role is granted to the pipeline service account in your Azure Artifacts feed settings.
The Results
After rolling this out, our release process went from something that could drag on for hours (tracking down who needs to bump what, writing changelog entries, manually triggering pipelines) to something that completes in minutes, automatically, every single time a PR merges.
That is a 50%+ reduction in release overhead, and the number undersells the qualitative improvement. The cognitive load of "did someone remember to release this?" just disappeared. Releases stopped being a task on someone's to-do list and became a side effect of merging good code.
Final Thoughts
If you are running micro-frontends (or any npm packages) on Azure DevOps with Azure Artifacts and still doing releases manually, I would strongly encourage giving semantic-release a try. The initial setup is an hour or two of work. The return starts immediately.
One thing worth saying to anyone proposing this to their team: the commit discipline pays off beyond just releases. Once your team writes structured commits, your git history becomes genuinely useful for debugging, onboarding, and understanding the context behind changes.

Top comments (0)