Movatterモバイル変換


[0]ホーム

URL:


Sitemap
Open in app

Valtech Switzerland

We are Valtech, a global digital agency focused on business transformation.

the opinionated way

Press enter or click to view image in full size
Photo by Tom Wilson on Unsplash

Maybe the one of the reasons you reading this lines is that you already tried to setup semantic releases in the monorepo. Googled, crawled Stack Overflow, maybe asked ChatGPT but lots of questions remained unanswered. At least that’s what my experience was…

🤔 Having the following requirements

  • Monorepo — all libraries and applications are living in the same git repository
  • MeaningfulCHANGELOG.md — we want to know why and what’s being released.
  • Every monorepo member is releasable — we create a release for every workspace (lib, config or an app) when its changed
  • Changes of up-stream dependency should cause a release down-stream dependents (domino effect)

🛠️ Tooling

👀 Monorepo schema

Press enter or click to view image in full size
Monorepo schema. Arrows represent dependencies.

📚 Very minimalist setup

You can find a demo repository here —github.com/b12k/monorepo-semantic-releases

Root package.json

{
"name": "mono",
"version": "0.0.0",
"private": true,
"license": "UNLICENSED",
"workspaces": [
"apps/app-a",
"apps/app-b",
"libs/lib-a",
"libs/lib-b",
"libs/lib-c",
"configs/config-release-it"
],
"scripts": {
"start": "turbo start",
"prepare": "husky install",
"release": "turbo release --concurrency=1"
},
"devDependencies": {
"@commitlint/cli": "17.6.1",
"@commitlint/config-conventional": "17.6.1",
"husky": "8.0.3",
"turbo": "1.9.3"
},
"packageManager": "yarn@1.22.19"
}

workspaces:

Explicitly defined monorepo members. Yarn tend to initialize explicit references faster, and no “false-positive” workspaces (in case of nestedpackage.json).

scrips:

  • start: used to run our apps
  • prepare: used to install git hooks
  • release: name speaks for itself

devDependencies:

  • @commitlint/cli +@commitlint/config-conventional — used to ensure conventional commits for furtherCHANGELOG.md generation.
  • husky — used to run commitlint on commit message hook.
  • turbo — used to manage the monorepo and execute npm scripts defined in memberspackage.json files.

turbo.json

{
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"start": {
"cache": false
},
"release": {
"dependsOn": ["^release"],
"outputMode": "new-only"
}
}
}

Configuration of “apps” and “libs”

All of our monorepo members inapps andlibs folders have very similar configuration.

member's package.json:

{
"name": "@mono/app-a",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"start": "node index.js",
"release": "release-it --ci"
},
"dependencies": {
"@mono/lib-a": "*"
},
"devDependencies": {
"@mono/config-release-it": "*"
}
}

The only lines differ from member to member is:

  • name: in the example repo all of the members package names are prefixed with@mono for convenience.
  • release: the script to be executed usingturbo.
  • dependenciesanddevDependencies: they vary, but all of them depend on the same sharedrelease-it configuration.

member’s .release-it.js

module.exports = require('@mono/config-release-it');

In order to be “released” every monorepo member should have this configuration file, and the only thing it does — re-exports the shared config.

⚙️ Release-it configuration

The tool used for release process is “release-it”. It was selected due to simplicity and at the same time flexibility of release process setup.

shared configuration — .release-it.js

const version = '${version}';
const packageName = process.env.npm_package_name;
const scope = packageName.split('/')[1];

module.exports = {
plugins: {
'@release-it/conventional-changelog': {
path: '.',
infile: 'CHANGELOG.md',
preset: 'conventionalcommits',
gitRawCommitsOpts: {
path: '.',
},
},
},
git: {
push: true,
tagName: `${packageName}-v${version}`,
pushRepo: 'git@github.com:b12k/monorepo-semantic-release.git',
commitsPath: '.',
commitMessage: `feat(${scope}): released version v${version} [no ci]`,
requireCommits: true,
requireCommitsFail: false,
},
npm: {
publish: false,
versionArgs: ['--workspaces false'],
},
github: {
release: true,
releaseName: `${packageName}-v${version}`,
},
hooks: {
'before:git:release': [
'mvm-update',
'git add --all',
],
}
};
  • version:release-it has internal string interpolation mechanism to replace provided placeholder with calculated version.
  • packageName: the samenameproperty present in the workspacepackage.json which properties nodejs injects into process environment.
  • scope: this is optional. Used to make release commit message more explicit. Based on workspace naming convention@mono/workspace-name only second part is an actual application/library name.
  • plugins: responsible for semantic version calculation based on conventional commits. ⚠️ Importantpathproperty which tells plugins to only consider commits which affect current. directory (workspace).
  • git.tagName: contains configuration for tag name —@mono/workspace-name-v0.0.0.
  • git.pushRepo mostly needed for inGitHub Actions execution, to force usingssh instead ofhttps in order to push backpackage.json version bumps and updatedmvm.lock andCHANGELOG.md files.
  • git.commitsPath: same purpose as inplugins.
  • git.commitMessage: besides providingscope for explicitness[no ci] is added as it disablesGithub Actions execution not to get into infinite loop statecommit to master => run release action => commit back to master ... .
  • git.requireCommits:release-it will execute release process only if there commits available (rel. togit.commitsPath).
  • git.requireCommitFail: disables non zero process termination (erroring) if there no commits for workspace. That’s a standard situation in monorepo, and we do not want to generate errors and terminateturbo release process.
  • npm.publish: this article do not cover publishing of monorepo packages. Considering that it may contain applications and libraries written in other then JS/TS languages publishing can be handled using respective GitHub action triggered by “on release” event.
  • npm.versionArgs: by default while bumping members version in respectivepackage.json NPM will try to update version in dependents as well. This will break monorepo internal dependency linking.--worspaces false disable this functionality.
  • github.releaseName: making release name to match tag name pattern defined previously.
  • hooks: ⚠️ important part of current setup. Will be further explained in details…

🚀 Releasing

Now when monorepo is configured we can try releasing it.

Press enter or click to view image in full size
Configured monorepo initial state

> git add . && git commit -am “feat(repo): initial”

All versions of monorepo members are initially0.0.0 so after release process we may expect them to be0.1.0 because of semantic versioning and conventional commits

> yarn release

Turborepo will:

  • find “touched” monorepo members (initially all of them).
  • execute release scripts, according to dependency tree — upstream first.
  • cache the outputs of every release action.
Press enter or click to view image in full size
Expected output of release process
Press enter or click to view image in full size
Post-release state of monorepo
Tags created

A bunch of new files were generated and updated:

  • CHANGELOG.md: every monorepo member now has a source of changes file
  • package.json: version was updated
  • mvm.lock: file with contains actual versions of member dependencies which are present in monorepo.* should be kept as dependency version to keep monorepo working, for that reason actual versions are stored in a different file.

Cascading releases is also a desired feature (aka domino effect). You may have noticed that change log on the image contains more commits then we did initially.MVM library did that. In order to invalidateturbo cache for release action monorepo member should be “touched” (contain changes). Every time something is released there ishooks section in sharedrelease-it config which executesmvm-update.MVM searches for all ofpackage.json files present and gerenerates/updatesmvm.lock with latest version of the currently released dependency.

⚠️Turbo cache issue and a workaround

Because of howturbo cache works after initial release cache is invalidated because all of the members received changes.turbo generates cache key before the task executed. Meaning that the second execution of> yarn release will run the whole process again.

release-it got us covered.

requireCommits andrequireCommitFail will just skip the whole process (successfully fail 😂) and results will be now properly cached.

Press enter or click to view image in full size
Second run with no changes
Press enter or click to view image in full size
Third run with successful cache hit

🁠 Domino effect

As soon as something within monorepo will be “touched” (changed)

  • it will be commited
  • turbo cache will be invalidated
  • release action executed
  • mvm will update dependents mvm.lock — invalidating turbo cache and producing a reason for release
  • entire process will be repeated…
Press enter or click to view image in full size
Expected console output
Press enter or click to view image in full size
lib-b & app-b updated CHANGELOG.md

🏁 That’s it

Everything is working according to requirements. More details can be found in the example repository, with a bonus 😎 — example GitHub Action which does a release on every merge to master.

--

--

Valtech Switzerland
Valtech Switzerland

Published in Valtech Switzerland

We are Valtech, a global digital agency focused on business transformation.

Bogdan Kolesnyk
Bogdan Kolesnyk

Written by Bogdan Kolesnyk

Principal Software Architect @ Valtech Switzerland

Responses (1)


[8]ページ先頭

©2009-2025 Movatter.jp