10

I try to figure out how to manage versions in an monorepo where one of the packages is an application that needs to support multiple releases.

Assume we have a monorepo with just two packages: lib and app:

repo:
- lib@2.0
- app@1.0 (depends on lib@2.0)

app@1.0 is released release-app-1.0 tag is created.

We continue to make changes to the code and the repo becomes:

repo:
- lib@2.1
- app@2.0-beta (depends on lib@2.1)

Now a customer discovers a bug in app@1.0 caused by code in lib@2.0.

Where should I fix this bug?

Ideally, since lib currently is in 2.1, I should fix the bug in master and release lib@2.1.1, and update app@1.0 to use lib@2.1.1.

But in practice, it poses a few challenges:

  1. We need to first checkout release-app-1.0 first to figure out app@1.0 is using a SemVer compatible lib in order to determine we can implement the fix in master.
  2. After we make the changes in master and release lib@2.1.1, should we (and how) back port all those changes to the release-app-1.0? It would be close to impossible to cherry pick all changes only related to lib from master to release-app-1.0. But if we don't do that, then when we release app@1.0.1 and create tag release-app-1.0.1, the monorepo link between app and lib is broken (app@1.0.1 depends on lib@2.1.1, but in that tag lib is still in 2.0)

On the other hand, if we fix lib@2.0 directly and release lib@2.0.1, we do not get any benefit of using SemVer and get all the new features/bug fixes in lib@2.1.

Any suggestion is greatly appreciated. Thanks.

unional
  • 209

3 Answers3

5

Here is my own take on this, after collecting ideas from other people.

First of all, let's state the benefits of monorepo:

  • reduce the need of publish chaining:
    • package-a: change and publish
    • package-b: update package-a, change and publish
    • package-c: update package-b, change and publish
    • ...and so on
  • symbolic link or direct reference to source code (for scripting language such as JavaScript):
    • changes are immediately available for test runner to use

For this question, we are not going to talk about ultra large repo such as Google. We will focus on small to large monorepo, for example:

The only difference is that we need to support previous releases for a substantial period of time (1-3 years).

Let's use the same example:

repo:
- lib@2.0
- app@1.0 (depends on lib@2.0)

Before releasing app@1.0, there is essentially just one branch: master. The version tags for lib@1.0, lib@2.0 and app@1.0 are all under master.

as we continue to develop, the repo becomes:

repo:
- lib@2.1
- app@2.0-beta (depends on lib@2.1)

where version tags for lib@2.1 and app@2.0-beta continues the trend and every thing are linear under master, something like this:

lib@1.0 ... lib@2.0 ... app@1.0 ... lib@2.1 ... app@2.0-beta (master, HEAD)

Now the customer issue occurs. We check out the app@1.0 tag, and discover the issue is inside lib@2.0-dev. It is lib@2.0-dev because there might be some changes for lib since the lib@2.0 tag (e.g. chore changes, does not trigger a version bump).

So at this point, we can do two things:

  1. fix the problem in lib and publish it as lib@2.0.1
  2. checkout master, fix the bug there, and publish it as lib@2.1.1

The benefits of (1) is that it is simple, and the changes are minimal, so if you do not have a strong test suite, you can do this approach. But the disadvantage is that if there are other bug fixes in the latest lib, you will not get them (e.g. the new lib is actually lib@2.1.15).

If we choose (2), we will need to update the lib dependency in app@1.0 to lib@2.1.1 (whether explicitly by specifying "lib": "^2.1.1", or rely on lock file to get the latest version).

The problem then becomes should we somehow update the lib package in the app@1.0.1 branch so that we can still benefit from using monorepo.

There is where the opinion really split:

a) If you don't update, then your process is treating all older releases and separate silos and disregard all benefits monorepo brings for them.

This is a reasonable approach if your releases are short lived, and/or you value workflow simplicity over development convenience.

b) If you update, then you will follow the following process:

  1. cherry pick (or rather, straight copy) the latest lib code to the app@1.0 branch, and do a commit.
  2. implement the fix needed in lib, commit (and optionally bump version, but don't tag), but do not publish.
  3. cherry pick the changes you made on lib to master, tag the lib@2.1.1 version, and publish
  4. publish app@1.0.1

Step 3 and 4 can be swapped to make the workflow more straight forward. It is listed this way for logic reasoning.

Note that the version tag for lib@2.1.1 is made on master instead of under the app@1.0 branch, so that the version tracking and history is easier to maintain.

unional
  • 209
4

Theres no solution to this problem where you automatically get the fixes you apply to earlier versions in later versions.

You have to fix v2 and release v2.0.1 and seperately fix v2.1 releasing 2.1.1

For all you know v2.1 has been completely refactored internally. The code change you did to fix 2.1 might not even compile in 2.1

The Cherry Pick is a Lie!

Ewan
  • 83,178
1

A bug should be fixed in the earliest release that is affected, in this case release-app-1.0, bumping lib to 2.0.1. This fix can then be merged or cherry-picked into master, bumping lib to 2.1.1.

Ozan
  • 370