Manage software releases. Covers semver, changelog automation, release branching strategies, monorepo versioning, and coordinating releases across distributed teams.
Semantic Versioning & Release Management is the discipline of aligning version numbers with release cycles, change intent, and deployment strategy. It matters when teams must coordinate across services, libraries, or platforms, and when consumers—including other teams, clients, or CI/CD pipelines—depend on predictable upgrade paths. This is the foundation of interoperability in distributed systems.
Semantic Versioning (SemVer) defines a three-part version number: MAJOR.MINOR.PATCH. Each component reflects a change in stability and compatibility:
PATCH (e.g., 1.2.3 → 1.2.4): backward-compatible bug fixes. No breaking changes.MINOR (e.g., 1.2.3 → 1.3.0): backward-compatible new features. All existing APIs work without change.MAJOR (e.g., 1.2.3 → 2.0.0): breaking changes. Clients may need updates to work with the new version.A release is a tagged artifact—typically a Git tag, a container image, or a binary archive—associated with a specific version. The version string is the primary identifier for a release, used in dependency declarations, deployment manifests, and changelogs.
When a library team releases a new version, they follow a process:
develop, main, or release/v1.3).main, ensuring CI passes.git tag -a v1.3.0 -m "Add support for WebSockets in API client"
git push origin v1.3.0
.npm package, a Docker image, or a ZIP file—based on that tag.Added, Changed, Deprecated, Removed, Fixed).The version number 1.3.0 now represents a stable, tested, and documented artifact, ready for consumption.
For an API, versioning is not just a number—it’s a contract. A service exposes endpoints under a versioned path, header, or query parameter. The most common approach is URL path versioning:
GET /api/v1/users
GET /api/v2/users
Clients can opt into a version by setting a header:
Accept: application/vnd.myapp.v2+json
The server responds with a Content-Type including the version:
Content-Type: application/vnd.myapp.v2+json
A versioned API must maintain backward compatibility across versions. A client using v1 should continue to work when v2 is deployed, even as new features are added.
A v2 release may introduce breaking changes:
user.name → user.full_name{"data": [...]} → {"results": [...]}To support a smooth transition, the API server must:
v1 and v2 endpoints concurrently.Deprecation headers when a v1 client calls a v2 endpoint:Deprecation: true
Sunset: 2024-12-31
Link: <https://docs.example.com/deprecation/v1>; rel="deprecation"
Clients can monitor Sunset and plan upgrades before the v1 API is retired.
A release is not just a version—it is a coordinated event across environments: development, staging, production.
In a microservices architecture, teams often use a release train model. A new release is scheduled every two weeks. All services are updated to the same version of shared libraries and APIs.
Each service defines its release schedule in a release.yml file:
release_cycle: biweekly
release_date: 2024-07-15
dependencies:
- name: auth-service
version: 2.1.0
source: git
branch: release/v2.1
- name: payment-api
version: 1.5.3
source: docker
image: registry.example.com/payment-api:1.5.3
On release day, all services are deployed together. The release is tested in a staging environment before rollout.
A production deployment uses versioned artifacts to enable fast rollbacks and canary releases.
A service uses Docker images with version tags:
docker build -t registry.example.com/user-service:1.3.0 .
docker push registry.example.com/user-service:1.3.0
Deployment manifests (e.g., Kubernetes Deployment YAML) reference the image:
# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 4
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: registry.example.com/user-service:1.3.0
ports:
- containerPort: 8080
env:
- name: VERSION
value: "1.3.0"
When a new version is deployed, the system rolls out updates in batches:
1.3.0.1.3.0 pods.1.3.0 and scale down 1.2.0.If the new version fails, rollback to 1.2.0 with a simple kubectl set image command:
kubectl set image deployment/user-service \
user-service=registry.example.com/user-service:1.2.0
The version number becomes the unit of deployment and rollback.
When a project depends on external libraries, it must lock down the versions used in production.
A package.json file in a Node.js project declares dependencies:
{
"name": "api-gateway",
"version": "2.0.0",
"dependencies": {
"express": "^4.18.0",
"redis": "~1.0.0",
"pg": ">=8.0.0"
},
"devDependencies": {
"jest": "^29.0.0"
}
}
The ^4.18.0 means: compatible with 4.18.0 and any 4.x release, but not 5.0.0 or higher.
The ~1.0.0 means: compatible with 1.0.0, 1.0.1, ..., up to 1.1.0, but not 1.1.1 or 2.0.0.
Running npm install generates a package-lock.json file that freezes all transitive dependencies:
{
"name": "api-gateway",
"version": "2.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "api-gateway",
"version": "2.0.0",
"dependencies": {
"express": "4.18.2",
"redis": "1.0.1",
"pg": "8.7.1"
}
},
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5B8m85Ym2YgM1ZCqP45D973Kx21n+3j7i3bX3e6iY8x8xH6T35c31j1X7gA5kWwE5KJ2",
"dependencies": {
"body-parser": "1.20.2",
"cookie-parser": "1.4.6"
}
}
}
}
This package-lock.json ensures that every deployment uses the *exact same* versions of every dependency. A npm ci (continuous integration) command installs dependencies from the lock file and verifies consistency:
npm ci
# Runs in a fresh environment with no node_modules
# Fails if any dependency version is missing or mismatched
When two dependencies require different versions of the same library, the resolver must pick one.
For example:
express requires lodash@^4.17.0react requires lodash@^4.18.0The resolver picks [email protected], satisfying both.
But if a new version 4.19.0 is released, and express is updated to 4.18.3, which requires lodash@^4.18.0, the resolver may choose [email protected]—but react expects 4.18.0 or later.
The resolution is not always optimal. A package.json may contain a resolutions section to override the default:
{
"resolutions": {
"lodash": "4.18.1"
}
}
This forces the version to 4.18.1 across the entire project, even if it’s not the best fit.
When a service uses [email protected] but the package-lock.json has [email protected], the deployment may appear to work—but a bug only appears when express introduces a subtle change in 4.18.3.
The mismatch is silent: no error, no log entry, but a subtle behavior difference. This is common when multiple teams manage dependencies independently.
A PATCH release (1.2.3 → 1.2.4) is expected to be safe. But if a library includes a new dependency in a PATCH release—say, fs-extra@^10.0.0 in 1.2.4—and fs-extra introduces a breaking change in 10.1.0, the client may break unexpectedly.
The root cause is not the PATCH version but a transitive dependency. The consumer must audit their package-lock.json to see which PATCH releases introduced breaking changes.
Teams often treat MINOR and MAJOR as interchangeable. A MINOR release may include breaking changes—e.g., renaming a property or changing a required field in a request body—but the version number does not reflect the impact.
This leads to version numbers that do not match expectations. A client updating from 1.2.0 to 1.3.0 may break, even though 1.3.0 is a MINOR release.
To avoid this, teams must define clear criteria for each release type:
PATCH: All changes are bug fixes, no new features, no breaking changes.MINOR: At least one new feature, all existing APIs work unchanged.MAJOR: At least one breaking change, no backward compatibility.A release without a changelog is a release that fails to communicate its value. The changelog is the primary documentation for consumers.
A well-maintained changelog uses a standard format:
## [1.3.0] - 2024-07-10
### Added
- Support for WebSocket connections in the `UserClient` class.
- New `fetchUserWithRoles()` method.
### Changed
- Renamed `User.getProfile()` to `User.getDetails()`.
- Updated `User` JSON schema: `first_name` → `firstName`.
### Deprecated
- `User.getProfile()` (since 1.2.0)
### Removed
- `User.getMetadata()` method.
### Fixed
- Correctly serializes nested `Address` objects.
The changelog must be published alongside the release, and version tags should link to it.
Versioning is not a step in the release process—it is the heart of it. A version number is a contract between producer and consumer. It communicates stability, compatibility, and intent.
A MAJOR version means: “You can upgrade, but expect to change your code.” A MINOR means: “You can upgrade with confidence, and you’ll get new features.” A PATCH means: “You can upgrade with no risk.”
The release process ensures that this contract is honored. With versioned artifacts, clear release schedules, and precise dependency management, teams can evolve systems in parallel, with minimal coordination and high reliability.
The version number is not just a label—it is a signal, a commitment, and a guarantee.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy