Understanding Version Management
Software changes constantly. New features are added, bugs are fixed, APIs evolve, and sometimes things break. Version numbers are how we communicate these changes to the world. But versioning is more than just incrementing numbers—it’s a language for expressing compatibility, stability, and change.
This document explores why we version software the way we do at EBRAINS.
The Problem Versioning Solves
Imagine you’re using a library for data visualization. One day you update it, and your entire application breaks. The library changed how a core function works, but you had no warning. You’re now stuck: revert the update and miss security fixes, or spend days rewriting your code?
This is the dependency hell that versioning aims to prevent.
Communication Through Numbers
A version number tells users:
- Can I safely upgrade? If you’re on version 1.2.3, can you upgrade to 1.2.4 without anything breaking?
- What changed? Did this update fix bugs, add features, or redesign the entire API?
- What risks exist? Is this a stable release or an experimental beta?
Without standardized versioning, every update is a gamble. With it, you can make informed decisions about when and how to upgrade.
Why Semantic Versioning
At EBRAINS, we use Semantic Versioning (SemVer).
The Three-Number Contract
SemVer uses three numbers: MAJOR.MINOR.PATCH (e.g., 2.5.1)
PATCH (the rightmost number): Bug fixes only. Nothing new. Nothing breaks. If you’re on 2.5.0 and upgrade to 2.5.1, your code works exactly the same, just with fewer bugs.
MINOR (the middle number): New features, but backward-compatible. If you’re on 2.5.0 and upgrade to 2.6.0, everything that worked before still works. New capabilities are available, but you don’t have to use them.
MAJOR (the leftmost number): Breaking changes. Something that worked in 2.5.0 might not work in 3.0.0. APIs changed. Functions were removed. Behavior differs. Upgrading requires reading changelogs and potentially modifying code.
Why This Matters
This three-part structure encodes risk:
- PATCH updates are safe. Apply them freely.
- MINOR updates are low-risk. You gain features without losing compatibility.
- MAJOR updates require caution. Plan time to test and adapt.
Without this convention, you’d need to read full changelogs for every update to assess risk. With SemVer, the version number itself conveys the risk level.
The Zero-Version Exception
Version 0.x.y is special. It means “under active development, anything can change.” Projects in 0.x versions are allowed to make breaking changes in MINOR updates because the API isn’t stable yet.
This communicates to users: “This is experimental. Don’t build production systems on this yet.” Once the project hits 1.0.0, it signals “This API is stable. We’ll respect SemVer from now on.”
The Breaking Changes
Deciding whether a change is breaking isn’t always obvious.
Obvious Breaking Changes
- Removing a public function
- Changing a function signature (adding required parameters, removing parameters)
- Changing return types
- Renaming public APIs
Why do so many projects spend years in 0.x versions?
The Fear of 1.0
Releasing 1.0.0 is a commitment. It says “this API is stable.” Once you do that, breaking changes require incrementing the MAJOR version, which creates pressure to avoid them.
Some teams fear this commitment. What if they got the API wrong? What if they need to redesign something? Staying in 0.x preserves flexibility.
The Cost of Perpetual Beta
But staying in 0.x forever has costs:
Users don’t trust it: A project at 0.8.3 after three years signals instability. Users worry it might never stabilize.
No compatibility guarantees: Every update could break things. Users pin to exact versions, fragmenting the ecosystem.
It’s a false safety net: Delaying 1.0 doesn’t prevent you from having to support backward compatibility. Users still build on your 0.x versions and complain when you break them.
When to Release 1.0
At EBRAINS, we release 1.0.0 when:
- The core API is stable and we’re confident in its design
- We’re willing to commit to backward compatibility
- The project is being used in production by real users
Version 1.0 isn’t about perfection. It’s about maturity and commitment.
The latest Tag Paradox
Docker images and similar artifacts often use a latest tag to indicate “the newest version.” This seems convenient but creates problems.
Why latest Exists
Developer convenience: Running docker pull myapp:latest gives you the newest version without looking up version numbers.
Default behavior: If you don’t specify a tag, Docker assumes :latest.
Continuous deployment: Some teams deploy from :latest so new builds automatically propagate.
Why latest Is Dangerous in Production
Ambiguity: What does “latest” mean? Latest stable release? Latest build from main? Latest including betas?
Non-reproducibility: docker pull myapp:latest might give you different results today versus tomorrow. If a deployment breaks, you can’t reliably roll back to “the version that worked yesterday” because you don’t know which version that was.
Silent breaking changes: If :latest points to a new MAJOR version with breaking changes, services that pull it automatically will break without warning.
Our Philosophy
At EBRAINS:
- Use
:latestin development environments where convenience matters and breakage is tolerable - Never use
:latestin production; always pin to specific versions (:1.2.3) - Update
:latestto point to the newest stable release, not pre-release versions
This balances convenience for development with safety for production.
Environment Tags and Build Metadata
Beyond semantic versions, tags can convey additional context.
Environment Tags
Adding environment identifiers (1.2.3-dev, 1.2.3-prod) serves different purposes:
Configuration differentiation: The same code might use different configuration for development versus production.
Promotion workflow: An image might be deployed to dev, then tested and promoted to prod. Tags track this progression.
Security boundaries: Production images might undergo additional scanning or review that dev images skip.
However, if the code is identical, some argue you should use the same tag and control environment differences through configuration, not versioning. This prevents drift where dev and prod run subtly different code.
Our approach: use environment tags when the build itself differs (different build flags, different dependencies). Use configuration when only runtime settings differ.
Pre-release Versions
SemVer supports pre-release identifiers: 1.0.0-alpha.1, 1.0.0-beta.2, 1.0.0-rc.1
Why Pre-releases Matter
You want to release version 2.0.0 with major changes. But you need testing before declaring it stable. Pre-releases let you:
- Share the new version with early adopters
- Gather feedback without committing to stability
- Iterate rapidly without burning through MAJOR version numbers
Pre-release Terminology
Common pre-release stages (not standardized, but conventional):
Alpha: Early, incomplete, likely unstable. For internal testing or brave early adopters.
Beta: Feature-complete but not fully tested. For broader testing. Bugs expected.
Release Candidate (RC): “We think this is ready to release.” If no major issues are found, this becomes the stable release.
Pre-releases are sorted lexicographically: 1.0.0-alpha.1 < 1.0.0-alpha.2 < 1.0.0-beta.1 < 1.0.0-rc.1 < 1.0.0
Versions Across Different Artifact Types
EBRAINS produces different types of artifacts: npm packages, Docker images, Git tags, Helm charts. How do versions relate across these?
Git Tags vs. Package Versions
When you tag a commit with v1.2.3 in Git, and publish an npm package as 1.2.3, they should represent the exact same code. The Git tag records the source, the npm version identifies the built artifact.
Keeping these in sync creates traceability: given a package version, you can find the source code. Given a commit, you can find the published artifact.
Docker Tags vs. Application Versions
A Docker image contains an application. The image tag should reflect the application version, not the container structure.
For example: if you’re building an image for Forms Builder 2.3.1, tag it forms-builder:2.3.1 even if the Dockerfile changed. The tag represents what’s inside, not the container itself.
If you need to rebuild the same application version with a different Dockerfile, you might use build metadata: forms-builder:2.3.1-build2
Further Reading
- For hands-on version management, see Version Your Project Tutorial
- For changelog practices, see Maintain Your Changelog Tutorial
- For Docker tagging in practice, see Publish Your First Docker Image Tutorial