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:
- fix the problem in
lib and publish it as lib@2.0.1
- 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:
- cherry pick (or rather, straight copy) the latest
lib code to the app@1.0 branch, and do a commit.
- implement the fix needed in
lib, commit (and optionally bump version, but don't tag), but do not publish.
- cherry pick the changes you made on
lib to master, tag the lib@2.1.1 version, and publish
- 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.