一つのリポジトリで複数のパッケージを管理する際にはLernaYarnのワークスペースを組み合わせて運用する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でスタートした方が楽だと思います。(lerna.jsonversionindependentに変更するだけです)

それぞれのパターンにおける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

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:スクリプト名に versionpublishは避ける

yarn run versionのように実行するためにversionというnpmが本体使ってる名前を使うと、挙動がコンフリクトすることができます。


基本的に次のような形でリリースを行います。(yarnnpmに置き換えても問題ないです。このプロジェクトでは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つのことをしてくれます

  1. 各パッケージのversionを更新
  2. ルートディレクトリCHANGELOG.mdを出力(更新)
  3. バージョンをgit tagしてくれる

2. yarn run releaseでパッケージを公開

yarn run release
# npm publishをする

yarn run releaselerna 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に失敗してるときは各パッケージの設定を確認してください

  "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を採用しています。

たとえば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つのことをしてくれます

  1. 各パッケージのversionを更新
  2. 各パッケージのディレクトリにCHANGELOG.mdを出力(更新)
  3. バージョンをgit tagしてくれる

CHANGELOG.mdが各パッケージ以下に生成される以外はFixed modeと同じです。

2. yarn run releaseでパッケージを公開

yarn run release
# npm publishをする

yarn run releaselerna 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/types1.1.2は次のようなタグが付きます。

このタグに関連するCHANGELOGを取り出すには、次のようにコマンドを実行するだけです。(directoryを省略した場合はcurrent = monorepot rootとなる)

collect-changelog-from-tag "@textlint/types@1.1.2"

最新のコミットから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を使っています。

interactive

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に集めたからといってなんでも解決するわけではないですが、楽になったりする部分はあるので、興味がある人は試してみるといいかもしれません。

逆に今回の記事で書いているように開発フローなどが少し異なったり、複数のパッケージを同時に開発するという事実は変わりませんが、LernaYarnによってその部分はだいぶ楽になっていると思います。

参考