概要
pnpm Workspace, Changesets, GitHub Actionsを組み合わせて以下のリリースフローを構築する。
- 開発の区切り毎に
$ pnpm changeset
でChangesetsの対話式CLIからバージョンアップ種別の選択とCHANGELOGの入力を行う- 一時ファイル(メタデータ入りの
CHANGELOG.md
の断片)が生成される
- 一時ファイル(メタデータ入りの
- 一時ファイルをソースと一緒にGitHubにPushする
- 一時ファイルがGitHubのmainブランチに到達すると Changesets Release Action が「その時点でリリース対象となるnpmパッケージを一括リリースするためのPR」を自動生成する
- PRの内容は「
CHANGESET.md
の更新」「package.json
のversion
フィールドのインクリメント」「一時ファイルの削除」 - PRをマージする前に別の一時ファイルを追加するとリリース用PRの内容は自動で更新される
- PRには全ての一時ファイルの内容がパッケージ毎に整理されcommitハッシュと紐付けて表示される
- PRの内容は「
- 任意のタイミングでリリース用PRをマージすると npmjs.com に対象パッケージがデプロイされる
- デプロイに成功したパッケージのカバレッジデータを Codecov GitHub Action でCodecovにアップロードする
要件
- pnpm Workspaceによるmonorepo環境
- リポジトリ内の複数のnpmパッケージをnpmjs.comにデプロイする
- デプロイにはChangesets Release Actionを使用する
- デプロイ対象のパッケージはサブプロジェクトとして
packages/*
に配置する - パッケージ名は名前空間の有無を問わない
- パッケージのバージョンは個別管理とし、CommitやPRには紐付けない
- パッケージ間の相互依存はないものとする
-
CHANGELOG.md
の更新とpackage.json
上のバージョンのインクリメントはChangesetsに一任する - GitHub ActionsでChangesets Release Actionを使用してリリース用PRを自動生成する
- リリース用PRをマージしたら自動でnpmjs.comにデプロイする
- デプロイは内部的には
npm publish
で行われる
- デプロイは内部的には
- カバレッジデータを Codecov GitHub Action を使用して Codecov にアップロードする
- パッケージ毎にflagに紐付ける
※本稿では changeset-bot および Codecov GitHub App は扱わない
ベースとなる開発環境は以下の記事の通り。
構築
1. ルートプロジェクト
npm-scriptsにChangesets Release Actionが使用するタスクを追加し、必要なパッケージをインストールする。
// ※抜粋
{
"scripts": {
"ci:version": "changeset version",
"ci:publish": "changeset publish"
},
"devDependencies": {
"@changesets/cli": "^2.26.0",
"@vitest/coverage-c8": "^0.28.3",
"c8": "^7.12.0",
"tsup": "^6.5.0",
"typescript": "^4.9.5",
"vitest": "^0.28.3"
}
}
-
ci:version
- リリース用PRを作成する際に使用するコマンド
- 現時点ではChangesetsの標準コマンドとする
- CHANGELOGの更新やバージョンのインクリメントを行う
- デプロイ用ビルドの前にリポジトリ内のファイルを編集する必要がある場合はここで行う
-
ci:publish
- npmへのデプロイ時に使用するコマンド
- 現時点ではChangesetsの標準コマンドとする
- 内部的には
npm publish
を使用する - ここを
pnpm -r publish
で置き換えてもデプロイ自体は行われるが、デプロイしたパッケージの情報をChangesets Release Actionが取得できなくなる
- 内部的には
- タスク名は任意
- GitHub Actions側の指定と一致していればよい
- この例の
devDependencies
はTypeScriptを使用し Vitest で C8 を使ってcoverageデータを生成する場合の構成- サブプロジェクトから利用する
2. サブプロジェクト
npm-scriptsにビルド及びcoverageデータ生成のタスクを追加し、publish時に実行されるように設定する。
例として以下の2つのパッケージが含まれているものとする。
// ※抜粋
{
"name": "@namespace/first-package",
"scripts": {
"clean": "rimraf ./dist",
"tsc": "tsc -p tsconfig.build.json",
"coverage": "vitest run --coverage",
"build": "pnpm clean && pnpm tsc",
"prepublishOnly": "pnpm coverage && pnpm build"
},
}
// ※抜粋
{
"name": "second-package",
"scripts": {
"clean": "rimraf ./dist",
"tsup": "tsup src/index.ts src/cli.ts --dts --shims --format cjs,esm",
"coverage": "vitest run --coverage",
"build": "pnpm clean && pnpm tsup",
"prepublishOnly": "pnpm coverage && pnpm build"
},
}
- パッケージ名は名前空間(
name
の@namespace/
の部分)の有無が混在していても問題ない -
prepublishOnly
はChangesets Release Actionがnpm publish
を実行した際に自動で実行されるタスク- coverageデータの生成とビルドが行われるように設定する
- 参考: Life Cycle Scripts - scripts | npm Docs
- Codecovにアップロードするデータとして
packages/<package_name>/coverage/coverage-final.json
にCodecovの対応する形式のJSONが出力されるように設定する - Vitestは指定しない場合
./coverage
にcoverageデータを出力する- つまり
packages/<package_name>/coverage
に出力される -
@vitest/coverage-c8
を使用する場合、標準でcoverage-final.json
も出力される - 参考: Coverage | Guide | Vitest
- つまり
3. Changesets
3-1. 初期設定
changeset init
コマンドで .changeset/config.json
を生成し、必要に応じて内容を編集する。
{
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
- 公開パッケージの場合は
access
キーをpublic
とする
3-2. 使用手順
開発環境構築記事のChangesetsの節を参照のこと。
- 当該記事における「アップデート対象パッケージの選択とサマリーの入力」を実施した後、生成された一時ファイルのMarkdownをそのままリポジトリにCommitする。
- 「CHANGELOG.mdの更新とバージョンのインクリメント」相当の手順はChangesets Release ActionがGitHub上で自動実行する。
Tips
概要に載せたスクリーンショットの通り、自動生成されるリリース用PRには「一時ファイルを処理したコミットへのリンク」が設置される。そのため、個別の開発用ブランチ(いわゆるfeatureブランチ)からリリース用のブランチに直接マージするGitHub Flow的なブランチモデルとの親和性が高い。
開発用ブランチ毎に $ pnpm changeset
を実行することでCHANGELOGの項目とマージコミットが紐付く形になるため、後から「GitHub上で パッケージ名@バージョン
で検索 → PR → ソース/差分/Issue」と簡単に辿ることができる。
4. Codecov
リポジトリのルートに設定ファイルを設置する。
flag_management:
default_rules:
carryforward: true
individual_flags:
- name: first-package
paths:
- packages/first-package
- name: second-package
paths:
- packages/second-package
ignore:
- '**/coverage'
- '**/__tests__'
- '**/dist'
- '**/docs'
- 各パッケージ用のflagを定義する
- flag毎にcoverageデータを更新するため
carryforward: true
としておく - ignoreは適宜
サインアップや連携については本稿では扱わないが、GitHubの公開リポジトリで使う分には、GitHubアカウントでサインアップするだけで他の操作は必要ないと思われる。
5. GitHub
5-1. Secretsの登録
npmjs.comにデプロイする際に必要なアクセストークンをGitHubのSecretsに追加する。
- npmjs.comにログインしユーザーメニューから「Access tokens」に移動
- 「Generate new token」ボタンから「Automation」トークンを作成する
- 生成画面を離れると再表示できない点に留意する
- GitHubのリポジトリで
Settings -> Secrets -> Actions -> New repository secrets
と移動 - トークンを
NPM_TOKEN
として追加する
※公開リポジトリの場合Codecovのtokenは登録不要
5-2. Workflow permissionsの設定
- GitHubでリポジトリを開き
Settings -> Actions -> General
- 最下部の
Workflow permissions
に移動 - ラジオボタンで
Read and write permissions
を選択 -
Allow GitHub Actions to create and approve pull requests
チェックボックスをON -
Save
で適用する
6. GitHub Actions
リポジトリのルートに .github/workflows
ディレクトリを作成して設定用のYAMLを配置する。
name: Release
# mainブランチに変更が発生したら動作する
on:
push:
branches:
- main
# 標準でbashを使用する
defaults:
run:
shell: bash
jobs:
release:
runs-on: ubuntu-latest
steps:
# [1] リポジトリをチェックアウト
- name: Checkout
uses: actions/checkout@v3
# [2] Node.js 18系を使用
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
# [3] pnpm 7系を使用
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 7
# [4] pnpmのストアディレクトリを取得
# - pnpmは依存関係の実体をnode_modulesではなく独自のストアで管理している
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "PNPM_CACHE_DIR=$(pnpm store path)" >> $GITHUB_OUTPUT
# [5] Github Actionsのキャッシュとpnpmのキャッシュの繋ぎ込み
# - pnpm-lock.yamlのハッシュが一致するものが存在していれば復元する
# - 存在していなければ直近のものを復元する
- name: Setup pnpm cache
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache.outputs.PNPM_CACHE_DIR }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
# [6] 依存関係のインストール
- name: Install dependencies
run: pnpm install
# [7] npmjs.comのデプロイ用トークンをgithubのsecretsから取得して `.npmrc` に追記
- name: Setup npmrc
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
cat << EOF > "$HOME/.npmrc"
//registry.npmjs.org/:_authToken=$NPM_TOKEN
EOF
# [8] Changesets Release Action
- name: Create release PR or publish to npm
id: changesets
uses: changesets/action@v1
with:
# Changesets標準のコマンドの代わりに
# ルートプロジェクトのpackage.jsonのnpm-scriptsを実行する
version: pnpm ci:version
publish: pnpm ci:publish
# PRのタイトルを指定
title: '[ci] release'
# PRのコミットメッセージを指定
commit: '[ci] release'
# GitHubのReleaseを作成しない(任意)
createGithubReleases: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# [9] jqでChangesets Release Actionの出力を加工する
- name: Convert publishedPackages to comma separated string using jq
id: jq
if: steps.changesets.outputs.published == 'true'
run: |
flags=`echo '${{ steps.changesets.outputs.publishedPackages }}' | jq '.[].name' | jq '. | if(startswith("@")) then split("/") | .[1] else . end' | jq -s -j 'join(",")'`
echo "CODECOV_FLAGS=$flags" >> $GITHUB_OUTPUT
files=`echo '${{ steps.changesets.outputs.publishedPackages }}' | jq '.[].name' | jq '. | if(startswith("@")) then split("/") | .[1] else . end' | jq -s -j 'map("packages/" + . + "/coverage/coverage-final.json") | join(",")'`
echo "CODECOV_FILES=$files" >> $GITHUB_OUTPUT
# [10] Codecov GitHub Action
- name: Upload coverage to Codecov
if: steps.changesets.outputs.published == 'true'
uses: codecov/codecov-action@v3
with:
flags: ${{ steps.jq.outputs.CODECOV_FLAGS }}
files: ${{ steps.jq.outputs.CODECOV_FILES }}
# verbose: true
# dry_run: true
- ファイル名・Workflow名・Job名・Step名は適宜変更のこと
- pnpm Workspace+Changesets構成のリポジトリとして Astro, SvelteKit, Chakra UI などが参考になる
jqの用途
Changesets Relase Actionはnpmjs.comにデプロイしたパッケージの情報を以下のフォーマットで publishedPackages
に入れて返す。
[
{ "name": "@nameaspace/first-package", "version": "0.1.0" },
{ "name": "second-package", "version": "0.1.0" }
]
一方Codecov GitHub Actionは更新されたflagとアップロードするファイルの情報を「カンマ区切りテキスト」で要求する。
つまり上記のオブジェクト配列を以下の形式に変換する必要があり、jqでこれを行っている。
with:
flags: 'first-package,second-package'
files: 'packages/first-package/coverage/coverage-final.json,packages/second-package/coverage/coverage-final.json'
なお、本稿の設定ではパッケージ名は「 @
から始まる場合は /
で分割して2個目の要素を使用」「 @
から始まらない場合はそのまま使用」となっている。
パッケージ名に /
が2個以上含まれる場合は適宜修正のこと。
Linter
基本的な文法チェックには actionlint が便利。
動作テスト
環境変数やoutputsの処理などは nektos/act を使ってローカルでテストできる。
PRの生成などGitHubの機能を使用するものは、テスト用のブランチないしリポジトリを作成し、デプロイ系の動作をdry-runに設定して確認する。
実行
基本的に概要の通り。
PushないしPRでGitHub上のmainブランチにコミットが到達するとChangesets Release Actionが作動し、デプロイが必要かつ可能な状態であればリリース用PRが作成される。
PRをマージするとnpmjs.comへのデプロイとCodecovへのアップロードが行われる。
実際に得られる出力をサンプルとして掲載する。
-
pnpm changset
を実行- 参考画像
- この例ではPatchレベルのサマリーを2件入力している
- 手順1で生成された一時ファイルをCommit/Push
- 参考リンク
- 他のファイルと一緒でも構わない
- GitHub Actionsが2を処理してPRを生成する
- 手順3のPRをマージするとGitHub Actionsが自動でnpmjs.comにデプロイする
- デプロイに成功したパッケージのカバレッジデータがCodecovにアップロードされる
- 手順5の参考画像の下の方
留意事項
Changeset Releace Actionの動作対象となるリモートブランチ(本稿では main
)に上記構成のWorkflowのYAMLがPushされた時点から、各パッケージがデプロイ可能な状態の場合には即座にデプロイされるようになる。
「パッケージがデプロイ可能な状態」とは以下の通り
-
.changeset
ディレクトリ内にCHANGELOG.mdの断片が存在しない - 各種tokenが正しく設定されている
- リリース済のバージョンよりpackage.json上のバージョンが新しい
- パッケージがnpmjs.com上に存在しない場合は無条件1で「新しい」と判定される
- ビルドが通る
よって、リポジトリ立ち上げ直後や package.json
を手作業で編集する場合は予期しないリリースが行われないよう注意が必要になる。
逆にこの仕様を利用して $ pnpm changeset version
コマンドをローカルで実行する運用にすることも可能。
参考リンク
- Use cache to reduce installation time - Setup pnpm · Actions · GitHub Marketplace
- Registry & Authentication Settings - .npmrc | pnpm
- Using changesets with pnpm - pnpm
- Changesets Release Action - GitHub
-
少なくとも
0.0.1
は対象になる(0.0.0
は未確認だが判定ロジックが存在しないため恐らくこちらも対象になる) ↩