The Git Juggling Act: How to Gracefully Support Legacy & New Major Versions in Your Monorepo
The Inevitable Fork in the Road
You've done it. You launched your shiny 2.x.x version. Months of work, through CI/CD, and then:
It's published.
But reality quickly sets in. A significant portion of your users are still on 1.x.x. They have their reasons: legacy integrations, "if it ain't broke" mantras, or simply phased rollouts. The truth is, you still need to support both. A critical 1.x.x bug emerges. A non-breaking feature is requested for both versions.
How do you manage two diverging product lines within a single monorepo? This is a common challenge for growing products. This guide provides a sustainable strategy to keep both your legacy users and your forward momentum on track.
The Bedrock: Principles for Peaceful Coexistence
First of all, this message is for those of us who adhere to a few important source control principles. If we can't agree on the following, this might not be the message for you:
-
Strict Semantic Versioning (SemVer): This is non-negotiable. Your 1.x.x vs. 2.x.x releases must have clear meaning.
- Breaking changes = major bumps (2.x.x).
- New features = minor bumps (1.1.0, 1.2.0).
- Bug fixes = patch bumps (1.0.1, 1.0.2).
- Clear Branching Strategy: Define distinct lanes for different types of work. Consistency is paramount to avoid chaos.
- Automation is Key: At scale, manual steps lead to errors. Embrace CI/CD pipelines for builds, tests, releases, and propagation.
- Transparent Communication: Inform users about version lifecycles, 1.x.x support timelines, and migration paths to 2.x.x.
The Juggling Mechanics: Your Git Branching Playbook
For a monorepo supporting distinct major versions, a dedicated release branch for each actively supported major version is essential.
-
The Main Stage:
main
(ortrunk
)- This is the home for 2.x.x development. All new, potentially breaking features, architectural shifts, and forward-looking innovations land here. This branch represents your product's future.
-
The Legacy Lane:
release/1.x.x
(orv1
)- This is a long-lived, dedicated branch for the 1.x.x line.
- Purpose: It's for critical bug fixes, security patches, and non-breaking new features that enhance but don't disrupt existing 1.x.x functionality.
-
Releases: Urgent 1.x.x patches originate here (e.g.,
1.0.x
release). Non-breaking features lead to1.x.0
releases. Changes here apply only to the 1.x.x world unless propagated.
Your Standard Workflow: How Features & Fixes Flow
Here are the predictable scenarios:
-
Scenario 1: New Feature (Only for 2.x.x)
-
Branch: Create a new feature branch from
main
. - Develop: Implement the feature leveraging 2.x.x capabilities.
- Test: Write and run tests tailored for the 2.x.x environment.
-
Merge: Open a Pull Request (PR) to
main
, get approvals, and merge.
-
Branch: Create a new feature branch from
-
Scenario 2: Bug Fix (Only for 1.x.x)
-
Branch: Create a hotfix branch from
release/1.x.x
. - Fix: Implement the bug fix, ensuring no 2.x.x-specific code is introduced.
- Test: Run tests for the 1.x.x environment.
-
Merge: Open a PR to
release/1.x.x
, get it reviewed, and merge. This leads to a new 1.x.x patch release.
-
Branch: Create a hotfix branch from
The Dual-Version Dilemma: Feature/Fix for Both
A common challenge is when a non-breaking feature or critical fix needs to be applied to both 1.x.x and 2.x.x. The core principle:
Develop the change on the oldest relevant branch first, then propagate it forward.
❓ Why? |
---|
Starting on release/1.x.x forces backward compatibility. It's easier to integrate older, simpler changes into a newer, more complex codebase than vice-versa. TLDR: Cherry-picking "backward" is prone to conflicts. |
Here's the step-by-step workflow:
-
Branch off
release/1.x.x
:
git checkout release/1.x.x git pull origin release/1.x.x # Get latest git checkout -b feature/dual-purpose-change
Develop & Test for 1.x.x:
Implement the change, ensuring it adheres to 1.x.x constraints. Thoroughly test in the 1.x.x environment.-
Commit & Merge to
release/1.x.x
:
git add . git commit -m "feat(my-module): Add dual-purpose feature (for 1.x.x and 2.x.x)"
Push, open a Pull Request (PR) targeting
release/1.x.x
, get approvals, and merge. -
Cherry-Pick to main:
Once merged into release/1.x.x, get the commit hash(es).-
Switch to
main
:
git checkout main git pull origin main # Get latest git cherry-pick <commit-hash-from-1.x.x>
-
- **Address Conflicts:** `main` may have diverged. Resolve any conflicts manually, adapting the 1.x.x code to fit the 2.x.x architecture.
- After resolving, `git add .` and `git cherry-pick --continue`.
- **Test on `main` (Essential):** Even if it passed on 1.x.x, **test rigorously** in the 2.x.x environment.
-
Push to main:
Once verified on main, push your changes (or open a PR for final review).
git push origin main
💡 When Re-Implementation is Necessary |
---|
Sometimes, the divergence between release/1.x.x and main is too great. If a feature's underlying architecture has been completely reworked in 2.x.x, a cherry-pick might be a fool's errand, leading to endless conflicts and a Frankenstein's monster of code. In those rare but real cases, a careful re-implementation of the feature from scratch on main is the more pragmatic (though more time-consuming) path. It ensures the feature is truly "native" to 2.x.x. |
Beyond the Branches: Scaling Your Sanity
Managing multiple major versions at scale in a monorepo requires robust support systems.
-
CI/CD Magic: Implement distinct, robust CI/CD pipelines for both
release/1.x.x
andmain
. Automate linting, static analysis, comprehensive testing (unit, integration, E2E), builds, versioning, and deployments. - Feature Flags: For strategic feature rollouts, feature flags allow you to deploy code to both versions (a "dark launch") and enable it independently. This reduces deployment risk and provides granular control.
- Monitoring & Telemetry: Implement robust monitoring for both versions in production. Quickly identify issues unique to 1.x.x and track user adoption of 2.x.x.
- Documentation: Maintain clear, up-to-date documentation on your branching strategy, release process, and guidelines for changes affecting multiple versions.
Conclusion: Embrace the Versioning Dance
Maintaining multiple major versions in a monorepo can seem daunting, but with a clear strategy, disciplined execution, and automation, it becomes a manageable process. This guide provides a blueprint for navigating these complexities with confidence and less stress.
Remember, you're not just managing code; you're ensuring a smooth, reliable experience for all users.
Top comments (0)