pnpm + Github Actionsだけでモノレポパッケージ配信を始める
はじめに
これは STORES Advent Calendar 2024 の6日目の記事です。
こんにちは。STORES で働いているエンジニアの野老です。
今は社内でのプロジェクト横断的なフロントエンド領域に関わるプロジェクトに関わっています。
それぞれ独立して運用されていた複数のprivateパッケージを、一つのモノレポから配信するようにするということについて、実際にその環境を構築した体験を交えつつ解説します。
背景
関連性が高いコードがnpmパッケージ単位で4つほどに分けられており、それぞれの個別のリポジトリでパッケージ配信の仕組みが実装と運用されていました。
これらのライブラリを一つのモノレポで管理下に置くことでパッケージ配信の仕組みを一本化して、運用の管理を楽にできるようにしたいというモチベーションがありました。
今回関連ライブラリを新しく追加する必要が出たタイミングということでその移行に着手しました。
モノレポ化に向けたツール選定
社内パッケージ管理という限られたスコープであるということもあり、とにかく早くモノレポ化してpackage配信したいという観点から、高機能なモノレポ系のツール(Lernaなど)は利用せず、pnpm + github actionsだけでいけるところまでやるという方針になりました。
pnpmはパッケージマネジャーながらworkspaceという機能もありモノレポツールとしても相当に有能です。
Github Actionsはとても便利なCI環境でサクッと構築できます。また既存のパッケージの配信の仕組みもGithub Actionsを利用していたものだったため移行が容易になることも想像できました。
モノレポのセットアップ
最終的なファイル構成は以下のようになります。
/packages
/package-a
package.json
/src
...
/package-b
package.json
/src
...
pnpm-workspace.yaml
package.json
pnpmのセットアップ
pnpmをドキュメントに従いセットアップします。
rootのpackage.json
はこのようになります。
{
"private": true,
"name": "multi-package-example",
"version": "0.0.0",
"description": "",
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@version",
"scripts": {
"build": "pnpm -r run build"
}
}
モノレポでrootのpackage.json
はパッケージとして配布する意図がないという意味もつけてprivateをつけておきます。
唯一のscriptであるpnpm -r run build
は、それぞれのpackageでbuildコマンドを実行します。
また今回はpnpm workspaceを利用しますので、.pnpm-workspace.yamlも準備します
packages:
- "packages/*"
pnpmではこのように記述することで、packages配下に各パッケージがあるということを設定できます。
また合わせてGithubにもモノレポ用のリポジトリを作成しておきます。
ソースコードの移管
既存のリポジトリからソースコードを移管する必要があります。
単純にコードをコピー/ペーストすることもできますが、過去のcommitヒストリーも消えてしまうのは惜しかったので、Githubのcommitヒストリーもmergeする方法が必要でした。
これはgit-filter-repoなどを利用することで可能です。
ただし、このツールはgitのcommitヒストリーは移管するのには便利ですが、Githubがcommitに付与するPRの参照などは残されません。
これに対してのワークアラウンドは見つからなかったので自作ツールを作りました。
gh-monorepo-migrator
詳細についてはリポジトリのREADMEをご確認ください。
上記のプロセスを経て、最終的にpackages
配下にそれぞれ既存のソースコードを移管します。
各パッケージの対応
移管した各パッケージについて、モノレポに合うように必要であれば変更を加えます。
{
"name": "@orgname/package-a",
"repository": "github:orgname/multi-package-example",
"version": "1.0.0",
"license": "UNLICENSED",
"publishConfig": {
"access": "restricted",
"registry": "https://npm.pkg.github.com/"
},
"main": "index.ts",
"scripts": {
"version:ci": "pnpm version --no-git-tag-version",
"build": "build.sh"
},
"dependencies": {
....
}
}
また、このとき各パッケージの.github配下の古いaction類は利用できなくなるので消してしまいます。
今回のセットアップでは、各パッケージは以下の2つのnpm scriptが必要です。
version:ci
このコマンドは後で設定するバージョン更新を発火するGithub Actionによって実行され、1つのパッケージのpackage.jsonのバージョンを更新するのに使われます。
build
このコマンドは後で設定するパッケージをpublishするGithub Actionによってpublishする前に実行されます。
これでモノレポのソースコード自体のセットアップは完了です。
GitHub ActionsでのCI/CDの設定
以下の2ファイルを作成します。
- commit-and-push-version-bump.yml
- publish-package.yml
リリースフローの概要
リリースフローの概要を説明します。
リリースはGithub Actionsのworkflow dispatchから手動で実行します。
この時以下の入力を要求します。
bump_type
major
, minor
, patch
のいずれか
package
commaで分離されたパッケージ名
ex. package-a,package-b
このactionが手動実行されると、入力したパッケージを該当のバージョンにあげmainブランチにpushします。
ここでバージョンのtagは付与されず、ただpackage.jsonのversionが更新されるだけというのがポイントになります。
その後mainへのpushをトリガーにpublish-package.ymlが実行されます。
このactionは存在するtagと各package.jsonのversionを比較し、新しいバージョンを検知するとそのpackageをpublishし、git tagを付与します。
commit-and-push-version-bump.yml
一部省略しています。
name: Commit And Push Version Bump
on:
workflow_dispatch:
inputs:
bump_type:
description: "Type of version bump"
required: true
type: choice
options:
- patch
- minor
- major
package:
description: "Package(s) to update (e.g., 'package-a' or 'package-a,package-b' etc.)"
required: true
type: string
jobs:
version-bump:
name: Version Bump
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- checkout, install node, pnpm etc..
- name: Determine bump type and packages
run: |
BUMP_TYPE="${{ github.event.inputs.bump_type }}"
PACKAGE="${{ github.event.inputs.package }}"
echo "Bump type: $BUMP_TYPE"
echo "Package(s) to update: $PACKAGE"
declare -A PACKAGE_MAP=(
[package-a]='@orgname/package-a'
[package-b]='@orgname/package-b'
)
EXPANDED_PACKAGE=""
IFS=',' read -ra SHORTHAND_ARRAY <<< "$PACKAGE"
for shorthand in "${SHORTHAND_ARRAY[@]}"; do
if [ -n "${PACKAGE_MAP[$shorthand]}" ]; then
EXPANDED_PACKAGE+="${PACKAGE_MAP[$shorthand]},"
else
echo "Error: No mapping found for shorthand '$shorthand'. Exiting."
exit 1
fi
done
EXPANDED_PACKAGE="${EXPANDED_PACKAGE%,}"
echo "EXPANDED_PACKAGE=$EXPANDED_PACKAGE" >> $GITHUB_ENV
echo "BUMP_TYPE=$BUMP_TYPE" >> $GITHUB_ENV
- name: Bump version in specified packages
env:
BUMP_TYPE: ${{ env.BUMP_TYPE }}
PACKAGE: ${{ env.EXPANDED_PACKAGE }}
run: |
IFS=',' read -ra PKG_ARRAY <<< "$PACKAGE"
for pkg in "${PKG_ARRAY[@]}"; do
echo "Bumping version in $pkg"
pnpm --filter $pkg version:ci $BUMP_TYPE
done
- name: Commit and push changes to main
env:
BUMP_TYPE: ${{ env.BUMP_TYPE }}
PACKAGE_NAMES: ${{ env.EXPANDED_PACKAGE }}
run: |
git config user.name github-actions
git config user.email github-actions@github.com
git commit -am "Bump version(s) for: $PACKAGE_NAMES to $BUMP_TYPE"
git push origin main
publish-package.yml
一部省略しています。private packageなどの場合はここでauthトークンが必要だったりします。
name: Publish to GitHub Packages
on:
push:
branches:
- 'main'
jobs:
get-new-version:
name: Check new version exists
outputs:
new_versions: ${{ steps.check-for-new-versions.outputs.new_versions }}
steps:
- checkout, setup etc...
- name: Check for new versions
id: check-for-new-versions
run: |
NEW_VERSIONS=""
for PACKAGE_DIR in packages/*; do
PACKAGE_NAME=$(jq -r '.name' "$PACKAGE_DIR/package.json")
PACKAGE_VERSION=$(jq -r '.version' "$PACKAGE_DIR/package.json")
TAG="${PACKAGE_NAME}@${PACKAGE_VERSION}"
if ! git tag | grep -q "^${TAG}$"; then
NEW_VERSIONS+="${PACKAGE_NAME}=${PACKAGE_VERSION},"
fi
done
NEW_VERSIONS="${NEW_VERSIONS%,}"
echo "New Versions: $NEW_VERSIONS"
echo "new_versions=$NEW_VERSIONS" >> $GITHUB_OUTPUT
publish:
name: Publish to GitHub Packages
needs: get-new-version
if: ${{ needs.get-new-version.outputs.new_versions != '' }}
steps:
- checkout, install node, pnpm etc..
- name: build
run: pnpm build
- name: publish
run: pnpm -r publish --access restricted
- name: tag each packages
run: |
git config user.name github-actions
git config user.email github-actions@github.com
IFS=',' read -ra NEW_VERSIONS_ARRAY <<< "${{ needs.get-new-version.outputs.new_versions }}"
for VERSION_INFO in "${NEW_VERSIONS_ARRAY[@]}"; do
PACKAGE_NAME="${VERSION_INFO%%=*}"
PACKAGE_VERSION="${VERSION_INFO##*=}"
git tag "${PACKAGE_NAME}@${PACKAGE_VERSION}"
done
git push --tags
他ツールとの比較
この構成を始める際に考慮した他のツールなど紹介と比較します。
lerna-lite
歴史のあるmonorepoツールlernaのフォークです。
pnpmのworkspaceに対応していたり、機能単位だけで利用できたりでき便利そうでした。
この記事の構成のリリースフローは旧パッケージのリリースフローを一部踏襲したりしているので、そう考えるとpnpm + Github Actionで十分でその方が簡単と判断し利用しませんでした。
おそらく今回の解説している内容と同じようなことができるし多くのケースではそちらの方が向いているような気もします。
changeset
これはかなり個人的に気になるプロジェクトです。
ただしコントリビューションの方法などもchangesetに沿った方法にする必要があるため(それが良いところでもあるのですが)、今回は利用しませんでした。
実際に運用してみる
実際にこの構成で運用を始めており大きな障害は今のところありませんが、一つ気になる点は出ています。
このモノレポ構成ではあるパッケージが他のパッケージのビルド成果物に依存するような構成になっているのですが、その際単純にpnpm -r run build
をするだけでは依存関係を考慮したビルド順が担保できないのでは、という懸念があります。
いまのところ問題なくパッケージはリリースされていますが、そのうち対応が必要になるかもしれません。
ただ、これは知る限りではlerna-lite等を利用していても起こりうる問題という認識をしています。
まだ旧パッケージからの切り替えなどされていないプロジェクトも残っているので、これから利用者が増えるにあたって改善やモノレポツールの必要性などについて考えることになりそうです。
まとめ
いかがでしたでしょうか。
実際に運用してみると他ツールの良いところなどもわかりました。
ただGithub Actionsだけでも短期間で十分な結果を得ることができました。
スコープの限られるモノレポのプロジェクトではカスタマイズ性も高くおすすめできます。
読んでいただきありがとうございました🌞
Discussion