lernaでのmonorepoにおけるリリースフロー(Fixed/Independent)
一つのリポジトリで複数のパッケージを管理する際にはLernaとYarnのワークスペースを組み合わせて運用するmonorepoにすることが多いです。
LernaにはFixed(すべてのパッケージが同じバージョン)とIndependent(パッケージごとに異なるバージョン)のモードがあります。
基本的にはFixedの方が運用は簡単ですが、不自然なバージョンの上がり方を避けたい場合などはIndependentのmodeを使うことになります。
この記事では、Fixed modeとIndependent modeでのパッケージのリリースフローについて見ていきます。
目次
modeの違い
Fixed mode
Pros
- 簡単でわかりやすい
- リリースフローは通常のパッケージとほぼ同じ
Cons
- すべてのパッケージのバージョンを同時に上げないといけない
- majorをあげる時にmonorepo全部のパッケージのmajorがあがる(巻き込まれる)
- 一つのリポジトリに複数の役割のパッケージを混ぜるのは難しい
Independent mode
Props
- 異なる役割のパッケージを1箇所で管理できる
Cons
- バージョン管理がやや複雑
FixedからIndenpendent への移行は簡単なので、Fixedで問題ない場合はFixedでスタートした方が楽だと思います。(Independentへの移行はlerna.json
のversion
をindependent
に変更するだけです)
それぞれのパターンにおけるmonorepoのパッケージリリースフローやCHANGELOG.md
などのリリースノートの扱い方について書いていきます。
lerna 3から@lerna/versionと@lerna/publishにコマンドが分離されたので、npmコマンドとコマンド体系は大体同じになりました。
コマンドは次のような対応になっていると考えておけば大体OKです。
npm version
==lerna version
npm publish
==lerna publish from-package
npm version
+npm publish
==lerna publish
(lerna 2の挙動と同じ)
共通のコミットメッセージ規約
lerna version
は –conventional-commits オプションをサポートしているので、基本的にはこれをベースに考えています。これはFixedとIntependentどちらも同じです。
Conventional Commitsはコミットメッセージから自動的にバージョンを推論したり、CHANGELOG.mdを書き出す時に役立つコミットメッセージの規約です。
このコミットメッセージの規約でコミットしておくと、lerna version --conventional-commits
で次のバージョンを自動的に推論してくれます。Fixedの場合は手動でも大丈夫ですが、Independent modeで全部手動は厳しいので、どちらの場合でも基本的にこのルールを使っています。
Conventional Commitsは、AngularJSのコミットメッセージ規約をsemverに絞ってもっとシンプルにしたものです。
コミットメッセージを以下のような形で
- 1行目に概要
- 2行目は空改行
- 3行目から本文
最後に関連するIssue(任意)を書きます。
feat(ngInclude): add template url parameter to events
The `src` (i.e. the url of the template to load) is now provided to the
`$includeContentRequested`, `$includeContentLoaded` and `$includeContentError`
events.
Closes #8453
Closes #8454
scope commit title
commit type / /
\ | |
feat(ngInclude): add template url parameter to events
body -> The 'src` (i.e. the url of the template to load) is now provided to the
`$includeContentRequested`, `$includeContentLoaded` and `$includeContentError`
events.
referenced -> Closes #8453
issues Closes #8454
commit type
としては次のようなものがあります。
- feat
- CHANGELOGに載せる新しい機能の追加
- minorな変更
- fix
- CHANGELOGに載せる機能などの修正
- patchな変更
- その他:
- chore: , docs:, style:, perf: なども使いますが、semverの推論には関係ありません
- 基本的にpatchの変更となります。
BREAKING CHANGEをしたい場合は、3行目をBREAKING CHANGE:
から開始します。
BREAKING CHANGEはcommit type
と併用できます。
feat(api): change api interface
BREAKING CHANGE: rename `getLevel` to `get level`
破壊的な変更をしました。
Closes #234
Conventional Commitsではバージョンに推論に関係ある部分しか決まっていません。 また、–changelog-presetでこのルールセットは変更できます。
Fixed modeのリリースフロー
mdlineはFixed modeでのリリースフローを採用しています。
Fixed modeはnpm
の代わりにlerna
コマンドを使う程度で通常のパッケージとそれほど違いはありません。
package.jsonには次のようなスクリプトが定義されています。
"scripts": {
"bootstrap": "lerna bootstrap",
"build": "lerna run build",
"test": "lerna run test",
"updateSnapshot": "lerna run updateSnapshot",
"versionup": "lerna version --conventional-commits",
"versionup:patch": "lerna version patch --conventional-commits",
"versionup:minor": "lerna version minor --conventional-commits",
"versionup:major": "lerna version minor --conventional-commits",
"release": "lerna publish from-package",
"prettier": "prettier --write \"**/*.{js,jsx,ts,tsx,css}\"",
"netlify": "node public/update-index.js"
},
Tips:スクリプト名に version
やpublish
は避ける
yarn run version
のように実行するためにversion
というnpmが本体使ってる名前を使うと、挙動がコンフリクトすることができます。
基本的に次のような形でリリースを行います。(yarn
はnpm
に置き換えても問題ないです。このプロジェクトではuseWorkspace
を使っているのでyarn
で統一しています)
1. yarn run versionup
でアップデートするバージョンを更新する
yarn run versionup
# pacakgeのversion更新とgit tagとCAHNGELOG.mdを更新
yarn run versionup
を実行すると、コミット規約から自動的に次のバージョンを推論して決めてくれます。
コミットメッセージがちゃんと書かれている場合はそれで問題ないですが、手動で選ぶ場合は yarn run versionup:{patch, minor, major}
を使います。
このコマンドは次の3つのことをしてくれます
- 各パッケージの
version
を更新 - ルートディレクトリに
CHANGELOG.md
を出力(更新) - バージョンを
git tag
してくれる
2. yarn run release
でパッケージを公開
yarn run release
# npm publishをする
yarn run release
はlerna publish from-package
をしています。
これは、現在のバージョンがnpm registryに公開されていなければ、npm publish
するという形になります。
なので、npm publish
を順番にやっていく形になります。
(適当な順番でnpm publish
をすると依存関係から数秒パッケージが取得できない問題が発生しますが、lerna publish
は依存関係をみてpublish順を決めます)
Notes: scoped packageの場合はpublishConfigを設定する
scoped packageをpublicに公開するときはpublishConfig
をそれぞれのpackage.json
に設定しないとエラーとなります。(初回だけ)
これはnpmの制約なので、publishに失敗してるときは各パッケージの設定を確認してください
- https://github.com/azu/mdline/blob/master/packages/types/package.json
- npmで名前空間を持ったモジュールを公開する方法(scoped modules) | Web Scratch
"publishConfig": {
"access": "public"
},
3. GitHub Releaseにリリースノートを書く
1.のyarn run versionup
で既にCHANGELOG.md
が生成されているので、コピペでGitHub Releaseにリリースノートを更新もできます。
また、Fixed modeではconventional-github-releaser - npmが利用できます。
リリースごとに次のコマンドを実行すると、前回のバージョンからのCHANGELOGをGitHub Releaseのリリースノートとして更新してくれます。
これもコミットメッセージ規約としてConventional Commitsを使っていれば、同じ仕組みでCHANGELOGを作ってくれます。
conventional-github-releaser -p angular
また、CHANGELOG.mdから指定したバージョンのCHANGELOGを取り出したい場合は@monorepo-utils/collect-changelogが利用できます。
次のようにCHANGELOG.md
から指定したバージョンの内容を取得して標準出力に流してくれます。
collect-changelog-from-tag --changelog ./CHANGELOG.md "version"
最新のコミットからgit tagを取り出すには、git tag --points-at HEAD
が使えるので、次のようにすれば現在のバージョンのCHANGELOGを取得できます。
git tag --points-at HEAD | xargs -I{} monorepo-utils-collect-changelog {} | pbcopy
Independent modeのリリースフロー
ここからはIndependent modeでのリリースフローです。
次のリポジトリはIndependent modeを採用しています。
- textlint/textlint: The pluggable natural language linter for text and markdown.
- almin/almin: Client-side DDD/CQRS for JavaScript.
たとえばalminでは次のようなスクリプトが定義されています(一部抜粋)
"scripts": {
"bootstrap": "lerna bootstrap",
"versionup": "lerna version --conventional-commits",
"versionup:patch": "lerna version patch --conventional-commits",
"versionup:minor": "lerna version minor --conventional-commits",
"versionup:major": "lerna version major --conventional-commits",
"release": "lerna publish from-package",
"release:canary": "lerna publish --canary"
},
基本的にFixed、Independentどちらも同じです。 また、あえて分けて書いてますが、基本的にはIndependentでも”3. GitHub Releaseにリリースノートを書く”以外は全く同じ方法を取れます。
1. yarn run versionup
でアップデートするバージョンを更新する
yarn run versionup
# pacakgeのversion更新とgit tagとCAHNGELOG.mdを更新
yarn run versionup
を実行すると、コミット規約から自動的に次のバージョンを推論して決めてくれます。
コミットメッセージがちゃんと書かれている場合はそれで問題ないですが、手動で選ぶ場合は yarn run versionup:{patch, minor, major}
を使います。(また、lerna version
とオプションなしで実行すれば、一つずつパッケージごとにバージョンを選択する対話的に決定できますが、CHANGELOG.mdは生成されません)
このコマンドは次の3つのことをしてくれます
- 各パッケージの
version
を更新 - 各パッケージのディレクトリに
CHANGELOG.md
を出力(更新) - バージョンを
git tag
してくれる
CHANGELOG.mdが各パッケージ以下に生成される以外はFixed modeと同じです。
2. yarn run release
でパッケージを公開
yarn run release
# npm publishをする
yarn run release
はlerna publish from-package
をしています。
これは、現在のバージョンがnpm registryに公開されていなければ、npm publish
するという形になります。
これはFixedと同じです。
3. GitHub Releaseにリリースノートを書く
1.のyarn run versionup
で既にCHANGELOG.md
が生成されているので、コピペでGitHub Releaseにリリースノートを更新もできますが、各パッケージからかき集める必要があります。・
@monorepo-utils/collect-changelogはIndependent modeにも対応しています。
次のようにmonorepoでtag@version
を指定すると、そのタグに関係するパッケージのCHANGELOG.md
から指定したバージョンの内容を取得して標準出力に流してくれます。
collect-changelog-from-tag --directory ./monorepo-root/ "tag@version"
例えば、Independentではパッケージ名@バージョン
となるので@textlint/types
の1.1.2
は次のようなタグが付きます。
このタグに関連するCHANGELOGを取り出すには、次のようにコマンドを実行するだけです。(directoryを省略した場合はcurrent = monorepot rootとなる)
collect-changelog-from-tag "@textlint/[email protected]"
最新のコミットからgit tagを取り出すには、git tag --points-at HEAD
が使えるので、次のようにすれば現在の各バージョンのCHANGELOGを取得できます。
git tag --points-at HEAD | xargs -I{} monorepo-utils-collect-changelog {} | pbcopy
これで、Independentでもバージョンの更新からパッケージの公開までできます。 しかし、Independentではパッケージごとにバージョンが異なるため、パッケージを公開する前にバージョンがあってるかを確認したいことが多いです。
そのため、自分のプロジェクトのIndependent modeではリリースレビューを行うフローを使っています。
Notes: 現時点ではconventional-github-releaser - npmはlernaをサポートしていない
Independent modeのリリースレビューフロー
https://github.com/almin/almin/blob/master/.github/CONTRIBUTING.md#release-workflow にもこのリリースフローが書かれています。
具体的なスクリプトは同じです。パッケージを公開する前にPull Requestを出して、それをチェックするという違いがあります。
1. リリースブランチをcheckout
Pull Requestをするためのリリースブランチをチェックアウトして、それをpushします。(lernaはremote branchがないとエラーとなるため)
git checkout -b release-date
git push origin HEAD -u
2. yarn run versionup
でバージョンを更新
ここは同じでyarn run versionup
を使ってバージョン更新とCHANGELOGファイルの更新を行います。
# automatic versioning by commit message
$ yarn run versionup
# major update for all
$ yarn run versionup:major
# minor update for all
$ yarn run versionup:minor
# path update for all
$ yarn run versionup:patch
3. CHANGELOGから変更点をコピーする
先ほどの@monorepo-utils/collect-changelogを使ってCAHNGELOGから変更点をコピーします。
たとえば、Alminではcopy-changelogというスクリプトを定義しています。(Windowsだと動かないけど…)
"copy-changelog": "git tag --points-at HEAD | xargs -I{} monorepo-utils-collect-changelog {} | pbcopy",
yarn run copy-changelog
と実行すれば主要な変更点をコピーできます
4. Pull Requestを出してレビューする
ここまでのコミットをpushしてPull Requestを出します。 先ほどコピーしておいた変更点をPull RequestのDescriptionに入れて、意図してないバージョンの更新がないかをチェックします。
また、GitHub Releaseに貼り付けるリリースノートのドラフトもこのPull Requestで作成します。 他にもリリースノートをブログとして書く場合には、このブランチでブログ記事を書いています。
AlminとtextlintではDocusaurusを使ったドキュメント + ブログがmonorepoに含まれているので、リリースするためのPull Request上でブログを書いたりします。
Prettierとかも似たような仕組みをもっています。
textlintのリリースPull Requestの例
レビューやリリースノートが完了したら、実際にパッケージを更新します。
5. yarn run release
でパッケージを公開
実際にユーザー影響がでるのはここからなので、ここまでは間違っていてもrevertできるので、このリリースフローの利点です。
リリース準備が完了したら実際にpublishします。
yarn run release
後は、GitHub Releaseを更新したりすれば完了です。
おわりに
LernaでのFixed modeとIndependent modeでのリリースフローについて書きました。
lernaを使ったmonorepoではYarnのワークスペースと組み合わせた方がlockファイルの管理が楽です。 またyarn upgrade-interactiveがmonorepoに対応しているのでとても便利です。 そのため、基本的にはnpmではなくyarnを使っています。
Lerna + YarnでWorkspaceを有効化する場合には、lerna.jsonの設定でuseWorkspace
を有効化して、pacakge.json
でYarnの設定をする必要があります。
lerna.json
:
{
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true,
"packages": [
"packages/*"
]
}
package.json
:
{
"private": true,
"workspaces": [
"packages/*"
]
}
monorepoでパッケージを管理することで、関連するパッケージが一箇所に集まるのでバージョンアップが簡単になります。また、monorepo内でコア部分をパッケージとして切り出したり、@mdline/typesのように型定義だけを切り出して共有したり、@proofdict/domainのようにドメインだけを切り出すといった運用が現実的な範囲でできます。
一方でなんでもかんでも詰め込むと、CIが重くなったり更新に不安を持つような作りになったりします。 そのため、基本的にはライフサイクルが同じものをmonorepoでまとめるのがいいとは思います。
リリースフローが単独のリポジトリと若干異なることはありますが、Fixed modeだとそれが最小限なので最初はFixed modeのほうが楽です。Independent modeでもだいぶ似たようなフローを取れるようになってきています。
lernaではlerna importによって既存のリポジトリをmonorepoへ取り込めるので、monorepoへの移行自体はそれほど大変ではありません。(大体import時点ではバージョンがバラバラになるのでIndependent modeになりやすい)
TypeScriptもmonorepo向けのProject Referencesなどが入ってきています。
単独のリポジトリをmonorepoに集めたからといってなんでも解決するわけではないですが、楽になったりする部分はあるので、興味がある人は試してみるといいかもしれません。
逆に今回の記事で書いているように開発フローなどが少し異なったり、複数のパッケージを同時に開発するという事実は変わりませんが、LernaやYarnによってその部分はだいぶ楽になっていると思います。
参考
- textlint/textlint: The pluggable natural language linter for text and markdown.
- Independent
- azu/mdline: Markdown timeline format and toolkit.
- Fixed
- almin/almin: Client-side DDD/CQRS for JavaScript.
- Independent
- azu/immutable-array-prototype: A collection of Immutable Array prototype methods(Per method packages).
- Fixed
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。