DEV Community

Cover image for How I Cut Our Micro-Frontend Release Time by 50%+ with Semantic Release on Azure DevOps
Nikhil Raj A
Nikhil Raj A

Posted on

How I Cut Our Micro-Frontend Release Time by 50%+ with Semantic Release on Azure DevOps

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.json by 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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Standard: type(scope): description
  2. JIRA-prefixed: TICKET-123 type(scope): description or TICKET-123: type: description
  3. 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}",
      },
    ],
  ],
};
Enter fullscreen mode Exit fullscreen mode

3. Add the script to package.json

"scripts": {
  "semantic-release": "semantic-release"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.AccessToken for 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.json already 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"
Enter fullscreen mode Exit fullscreen mode

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:

  1. Write commits using conventional format (feat:, fix:, PROJ-123: feat:, etc.)
  2. Open a PR, get it reviewed, merge to main
  3. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)