Skip to main content
Semantic Versioning Is Not a SuggestionGeneral
4 min readFor Developers

Semantic Versioning Is Not a Suggestion

On April 25, 2020, a single-line change in is-promise version 2.2.0 disrupted build pipelines across 12,000,000 weekly downloads. The maintainer fixed it three hours later, but the incident highlighted a critical misunderstanding: semantic versioning is a contract, not a style preference.

This guide provides a framework to prevent similar issues in your packages and protect your organization from upstream dependency failures.

Guide Scope

This guide covers:

  • Implementing semantic versioning as a system for detecting breaking changes
  • End-to-end testing strategies for package releases
  • Dependency pinning strategies that balance security and stability
  • Incident response when dependencies fail unexpectedly

This guide does not cover supply-chain attacks or malicious code injection. The is-promise incident was a configuration error, not a security compromise.

Key Concepts and Definitions

Semantic Versioning (SemVer): Version format MAJOR.MINOR.PATCH where:

  • MAJOR = breaking changes (incompatible API changes)
  • MINOR = new functionality, backward compatible
  • PATCH = backward-compatible bug fixes

Breaking Change: Any modification requiring downstream users to change their code, such as:

  • Removing exported functions or properties
  • Changing module export mechanisms (CommonJS vs ES modules)
  • Altering function signatures
  • Modifying default behaviors that code depends on

Dependency Depth: is-promise had 500 direct dependencies, each with its own dependents, creating potential for cascade failures.

Requirements Breakdown

For Package Maintainers

1. Version Bumps Must Match Change Scope

If you modify the exports field in package.json, you change how consumers import your code. This requires a MAJOR version change, not a PATCH. The is-promise incident occurred because version 2.2.0 introduced an exports field that broke existing import patterns. This should have been version 3.0.0.

2. Pre-Release Testing Requirements

Before publishing:

  • Test against your own direct dependents (sample at least 50 if you have 500)
  • Run integration tests in both CommonJS and ES module contexts
  • Verify all documented import patterns still work
  • Check that tree-shaking behavior hasn't changed

3. Deprecation Path for Breaking Changes

When breaking compatibility:

  • Release a MINOR version that warns about deprecated patterns
  • Wait at least 3 months before the MAJOR version
  • Document the migration path in both CHANGELOG and README

For Package Consumers

1. Pin Major Versions Only

Your package.json should specify:

"is-promise": "^2.0.0"

NOT:

"is-promise": "*"
"is-promise": "latest"

The caret (^) allows MINOR and PATCH updates but blocks MAJOR changes.

2. Lock File Discipline

Commit your package-lock.json or yarn.lock. This ensures:

  • CI/CD uses identical versions to local development
  • You control when updates occur
  • Rollback is possible when upstream breaks

3. Update Cadence

Run npm outdated weekly. Review and test updates in a branch before merging to main. This provides a controlled update window instead of emergency fixes when builds break.

Implementation Guidance

Setting Up Pre-Release Testing

Create a pre-publish.sh script:

#!/bin/bash
# Build the package
npm run build

# Pack it locally
npm pack

# Install in test project
cd ../test-consumer
npm install ../your-package/your-package-1.2.3.tgz

# Run consumer's test suite
npm test

Run this script before every npm publish.

Automated SemVer Validation

Use semantic-release or standard-version to enforce version bumps based on commit messages:

  • fix: commits → PATCH bump
  • feat: commits → MINOR bump
  • BREAKING CHANGE: in commit body → MAJOR bump

This removes human judgment errors from versioning decisions.

Monitoring Dependency Health

Set up alerts for:

  • New major versions of your direct dependencies
  • Deprecation notices in npm audit
  • Packages with no commits in 24+ months (abandonment risk)

GitHub's Dependabot can automate PATCH and MINOR update PRs. Review these weekly, but test them before merging.

Common Pitfalls

Pitfall 1: "It's just a config change"

Changing package.json fields like exports, main, or module is NOT a config change. These fields control how your code is loaded. Treat them as API surface.

Pitfall 2: Testing only in your own environment

The is-promise maintainer likely tested the change locally. But downstream consumers had different Node.js versions, different bundlers, different import patterns. Test across environments, not just your laptop.

Pitfall 3: Assuming PATCH means "safe"

Even PATCH versions can break if the maintainer misunderstands SemVer. Your CI pipeline should run full test suites on dependency updates, not just install them blindly.

Pitfall 4: Pinning to exact versions

Exact pinning ("is-promise": "2.1.0") prevents you from receiving security patches. Pin the MAJOR version, not the full version string.

Pitfall 5: No rollback plan

When a dependency breaks, you need to:

  • Revert to the last known-good lock file
  • Pin the working version explicitly
  • File an issue upstream
  • Monitor for a fix

Document this process before you need it.

Quick Reference Table

Scenario Version Bump Example
Fix a bug, no API changes PATCH 2.1.0 → 2.1.1
Add new function, old code still works MINOR 2.1.0 → 2.2.0
Remove a function MAJOR 2.1.0 → 3.0.0
Change module export mechanism MAJOR 2.1.0 → 3.0.0
Modify function signature MAJOR 2.1.0 → 3.0.0
Update dependencies (no API change) PATCH 2.1.0 → 2.1.1
Change default behavior code relies on MAJOR 2.1.0 → 3.0.0
Consumer Strategy Risk Level When to Use
Pin exact version (2.1.0) High (no patches) Never for production
Pin major (^2.0.0) Low Default for all deps
Pin minor (~2.1.0) Medium Unstable upstream
Use latest Critical Never
Testing Checkpoint Frequency Automation
Run npm outdated Weekly Dependabot
Review and test updates Weekly Manual
Full integration test on updates Per PR CI/CD
Test package before publish Every release pre-publish hook
Verify in consumer projects Every release Manual sampling

The is-promise incident was resolved in three hours because the maintainer responded quickly. Your response time depends on having these processes in place before an incident occurs.

Topics:General

You Might Also Like