Lerna Monorepo Workflow

A workflow for monorepos that is working out fine for my personal projects.

I recently started a monorepo for a JS-based project called Piral. Since I heard about Lerna for easily managing a monorepo I tried it out and found it quite straight forward and efficient. Yet, like many great tools, Lerna is already biased towards a certain workflow that seems to be not 100% compatible to my desired workflow.

The workflow that I desire is:

  • Have two base branches (e.g., master and develop)
  • First base branch to be used for releases / hotfixes
  • Second base branch to be used for feature aggregation / pre-releases
  • An independent CHANGELOG (independent of VCS log entries; these should be quite technical and feature independent, while the CHANGELOG entries should be more on user / business-feature level)
  • Authors determine the semantic versioning, i.e., they see that some addition is a breaking change and alter the next version number accordingly)
  • The CHANGELOG's version is taken as the upcoming (i.e., prerelease) and current (i.e., release) version

As a result a standard work log would look like:

  1. Author pulls latest from develop (second base branch, used for feature aggregation)
  2. Author starts a new feature branch, commits at every technical step, in the end makes required changes to the CHANGELOG (e.g., adding the new feature they have been working on)
  3. The pull request (with target develop) has a validation build (and potentially other policies) which need to pass besides a required code review
  4. Upon merge the aggregation branch has changed which invokes a CI/CD build which publishes all packages as a semantic preview (tagged next) using the version mentioned in the CHANGELOG
  5. At some point there are enough changes in the aggregation branch, such that a PR to master is performed
  6. When this PR is accepted the CI/CD system publishes the packages versioned according to the CHANGELOG; also a proper Git tag (at the given version) is created

Always when a version changes the package.json file(s) reflect it. Now, while this is done nicely by Lerna, some of the mentioned steps are not. Actually, respecting CHANGELOG.md is not done at all by Lerna! There are ways to update CHANGELOG.md, but these ways use the git commit message log as source (exactly what I don't want).

As a consequence I wrote a little script to work with my definition of a CHANGELOG. This script extracts the last version number:

const { join } = require('path');
const { readFileSync } = require('fs');

const defaultPath = join(__dirname, '..', 'CHANGELOG.md');

function getChangelogVersion(changelogPath = defaultPath) {
  const CHANGELOG = readFileSync(changelogPath, 'utf8');
  const matches = /^\#\# (\d+\.\d+\.\d+) .*/gm.exec(CHANGELOG);

  if (!matches) {
    throw new Error('Invalid CHANGELOG format found. Need to fine line starting with "## x.y.z" to get the latest version.');
  }

  const version = matches[1];
  return version;
}

if (require.main === module) {
  const version = getChangelogVersion();
  console.log(version);
} else {
  module.exports = getChangelogVersion;
}

Note: This tool can be called from command line or used as a library. By default it assumes that the CHANGELOG.md file is in the upper directory. You can change this easily.

Now together with this tool we can use some Lerna commands. I played around with lerna version and also with lerna publish directly. In the end it turns out I only need the latter to achieve what I want to achieve.

I needed two commands:

# Publish the packages with the desired version
lerna publish $(node tools/version.js) --yes --force-publish

# Publish a preview of the packages with the desired version
lerna publish $(node tools/version.js)-pre.$BUILD_BUILDID --yes --force-publish --no-git-tag-version --no-push --dist-tag next && git checkout -- .

Note: The $BUILD_BUILDID variable is something I get from the CI/CD system. Locally, you should just increase some number. The only important factor is that you do not re-use the same number for the same version twice.

How does this work? Well, for the publish we discard any command line interaction (--yes) and definitely force a publish (we do not want to risk unpublished packages). For the preview we need to work harder. We don't want to tag this version (as its a preview) and we don't want any changes to be committed (e.g., changing all the package.json files), so we use --no-git-tag-version --no-push in addition to the parameters already seen. Finally, we use --dist-tag next to avoid labelling the release as latest.

The only limitation of this method is that for a release we need a new CHANGELOG.md entry / version. However, this is a requirement that we should have anyway, so it perfectly fits into my desired workflow!

Created .

References

Sharing is caring!