DEV Community

Cover image for Monorepo Versioning: Stop the Chaos
jellyfith
jellyfith

Posted on

Monorepo Versioning: Stop the Chaos

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:

Doctor Victor Frankenstein yells

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:

  1. 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).
  2. Clear Branching Strategy: Define distinct lanes for different types of work. Consistency is paramount to avoid chaos.
  3. Automation is Key: At scale, manual steps lead to errors. Embrace CI/CD pipelines for builds, tests, releases, and propagation.
  4. 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 (or trunk)
    • 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 (or v1)
    • 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 to 1.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)
    1. Branch: Create a new feature branch from main.
    2. Develop: Implement the feature leveraging 2.x.x capabilities.
    3. Test: Write and run tests tailored for the 2.x.x environment.
    4. Merge: Open a Pull Request (PR) to main, get approvals, and merge.
  • Scenario 2: Bug Fix (Only for 1.x.x)
    1. Branch: Create a hotfix branch from release/1.x.x.
    2. Fix: Implement the bug fix, ensuring no 2.x.x-specific code is introduced.
    3. Test: Run tests for the 1.x.x environment.
    4. Merge: Open a PR to release/1.x.x, get it reviewed, and merge. This leads to a new 1.x.x patch release.

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:

  1. 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
    
  2. 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.

  3. 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.

  4. 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.
Enter fullscreen mode Exit fullscreen mode
  1. 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 and main. 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)