【Deprecated】pnpm+Changesets+GitHub Actionsでmonorepo内のnpmパッケージのリリースを半自動化する

:::note warn
増補改訂版の以下の記事をご利用ください。
:::

https://qiita.com/macropygia/items/c74d3c1f27addb70b280

# 概要

[Changesets](https://github.com/changesets/changesets) と [GitHub Actions](https://docs.github.com/ja/actions) を使用し、monorepoに含まれる複数のnpmパッケージの更新履歴とバージョンの管理およびリリース作業を省力化する。
以下のリリースフローを構築する。

1. ローカルでは開発の区切り毎に `$ pnpm changeset` でChangesetsの対話式CLIからバージョンアップ種別の選択とCHANGELOGの入力を行う
    - 一時ファイル（メタデータ入りの `CHANGELOG.md` の断片）が生成される
2. 一時ファイルをソースと一緒にGitHubにPushする
3. 一時ファイルがGitHubのmainブランチに到達すると [Changesets Release Action](https://github.com/changesets/action) が「その時点でリリース対象となるnpmパッケージを一括リリースするためのPR」を自動生成する
    - PRの内容は「 `CHANGESET.md` の更新」「 `package.json` の `version` フィールドのインクリメント」「一時ファイルの削除」
    - PRをマージする前に別の一時ファイルを追加するとリリース用PRの内容は自動で更新される
    - PRには全ての一時ファイルの内容がパッケージ毎に整理されて表示される
4. 任意のタイミングでリリース用PRをマージするとリリースされる


<details><summary>自動生成されるリリース用PRのサンプル（画像）</summary>

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63934/05afdce6-fe14-3da5-f3d4-795667ff3d68.png)

</details>

## 要件

- 単独または少人数による開発を想定
- [pnpm](https://pnpm.io/ja/) Workspaceによるmonorepo環境
- npmパッケージのバージョンは個別管理とし、CommitやPRには紐付けない
    - npmパッケージ間の相互依存はないものとする
- `CHANGELOG.md` の管理と `package.json` 上のバージョンのインクリメントはChangesetsに一任する
- GitHub ActionsでChangesets Release Actionを使用してリリース用PRを自動生成する
- リリース用PRをマージしたら自動でnpmjs.comにデプロイする
- [changeset-bot](https://github.com/apps/changeset-bot) は扱わない

ベースとなる開発環境は以下の記事の通り。

https://qiita.com/macropygia/items/7d9a06cf33ec4997dcfa

# 構築

## リポジトリ

### Changesets

導入は[開発環境構築記事のChangesetsの節](https://qiita.com/macropygia/items/7d9a06cf33ec4997dcfa#changesets)を参照のこと。
当該記事における「アップデート対象パッケージの選択とサマリーの入力」を実施した後、生成された一時ファイルのMarkdownをそのままリポジトリにCommitする。
「CHANGELOG.mdの更新とバージョンのインクリメント」相当の手順はChangesets Release ActionがGitHub上で自動実行する。

#### Tips

概要に載せたスクリーンショットの通り、自動生成されるリリース用PRには「一時ファイルを処理したコミットへのリンク」が設置される。
そのため、個別の開発用ブランチ（いわゆるfeatureブランチ）からリリース用のブランチに直接マージするGitHub Flow的なブランチモデルとの親和性が高い。
開発用ブランチ毎に `$ pnpm changeset` を実行することでCHANGELOGの項目とマージコミットが紐付く形になるため、後から「GitHub上で `パッケージ名@バージョン` で検索 → PR → ソース/差分/Issue」と簡単に辿ることができる。

### npm-scripts

`pnpm publish` コマンドで自動実行される `prepublishOnly` タスクで必要な処理が行われるように各npmパッケージの `package.json` を設定する。

```json-doc:package.json
// 例
"scripts": {
  "clean": "rimraf ./dist",
  "test": "vitest run",
  "coverage": "vitest run --coverage",
  "tsc": "tsc -p .",
  "build": "pnpm clean && pnpm tsc",
  "prepublishOnly": "pnpm test && pnpm build"
}
```

## GitHub

### Secrets追加

npmjs.comにデプロイする際に必要なアクセストークンをGitHubのSecretsに追加する。

- npmjs.comにログインしユーザーメニューから「Access tokens」に移動
- 「Generate new token」ボタンから「Automation」トークンを作成する
- GitHubのリポジトリで `Settings -> Secrets -> Actions -> New repository secrets` と移動
- トークンを `NPM_TOKEN` として追加する

### GitHub Actions

リポジトリのルートに `.github/workflow` ディレクトリを作成して設定用のYAMLを配置する。
ファイル名・Workflow名・Job名は適宜変更のこと。

```yaml:.github/workflow/changesets.yml
name: Changesets

# mainブランチに対するPush時に動作
on:
  push:
    branches:
      - main

jobs:
  changesets:
    runs-on: ubuntu-latest
    steps:
      # [1] リポジトリをチェックアウト
      - name: Checkout
        uses: actions/checkout@v3

      # [2] Node.js 16系をインストール
      - name: Install Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 16

      # [3] pnpm 7系をインストール
      - name: Install pnpm
        id: pnpm-install
        uses: pnpm/action-setup@v2
        with:
          version: 7

      # [4] pnpmのストアディレクトリを取得
      # pnpmは依存関係の実体をnode_modulesではなく独自のストアで管理している
      - name: Get pnpm store directory
        id: pnpm-cache
        run: |
          echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"

      # [5] pnpmのストアディレクトリをキャッシュとして定義
      # 一致するものがあれば復元される
      - name: Setup pnpm cache
        uses: actions/cache@v3
        with:
          path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
          # キーは実行環境のOSとpnpm-lock.yamlのハッシュ
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
          # ハッシュが一致するものがない場合はOSが一致するものを使用する（step6で更新される）
          restore-keys: |
            ${{ runner.os }}-pnpm-store-

      # [6] 依存関係のインストール
      - name: Install dependencies
        run: pnpm install

      # [7] 都度指定しなくていいようにnpmのtokenを .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:
          # リリース用のコマンドは `pnpm publish` の再帰実行とする
          # npmjs.com上のバージョンとpackage.json内のバージョンを比較して更新があったもののみリリースされる
          # デバッグ用にドライランにしたい場合は `pnpm -r publish --dry-run` とする
          publish: pnpm -r publish
          # GitHubのReleaseは自動生成しない（既定では自動生成される）
          # （2022-08-01現在、有効にしても最新のChangesets Release Actionとpnpm7系の組み合わせでは正常にトリガされない）
          createGithubReleases: false
        env:
          # GITHUB_TOKENは自動生成されるためなにもしなくても使用可能
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

#### Linter

基本的な文法チェックには [actionlint](https://github.com/rhysd/actionlint) が便利。

https://rhysd.github.io/actionlint/

#### 動作テスト

環境変数やoutputsの処理などは [nektos/act](https://github.com/nektos/act) を使ってローカルでテストできる。

https://github.com/nektos/act

PRの生成などGitHubの機能を使用するものは、テスト用のブランチないしリポジトリを作成し、デプロイ系の動作をdry-runに設定して確認する。

# 実行

基本的に概要の通り。
実際に運用中のリポジトリにおける一連のフローをサンプルとして掲載する。

1. [changsetコマンドを実行](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63934/4959d326-bd64-d5cb-8169-9f8bc09325bf.png) ※画像
    - この例ではPatchレベルのサマリを2件入力している
2. [1で生成された一時ファイルをCommit/Push](https://github.com/macropygia/static-site-stack/commit/c60881e569a4d9ebc2a70cb53d303a713d051d59)
    - 他のファイルと一緒でも構わない
3. [GitHub Actionsが2を処理してPRを生成する](https://github.com/macropygia/static-site-stack/runs/7604752385?check_suite_focus=true)
    - Step名 `Create release PR or publish to npm` 参照
4. [3で生成されたPR](https://github.com/macropygia/static-site-stack/pull/3)
5. [4をマージするとGitHub Actionsが自動でnpmjs.comにデプロイする](https://github.com/macropygia/static-site-stack/runs/7604807946?check_suite_focus=true)
    - Step名 `Create release PR or publish to npm` 参照

※各所のCodecov関連の記述はテスト用のため無視のこと

# 留意事項

- 本稿で行った設定では、経緯に関わらずnpmjs.comに公開されているバージョンより新しいバージョンが指定された `package.json` がChangeset Releace Actionの動作するリモートブランチに到達すると即座にリリースされる
    - 動作としてはリリース用PRがマージされた時と同じ
    - `package.json` を手作業で編集する場合は予期しないリリースが行われないよう注意が必要
    - 逆にこの仕様を利用して `$ pnpm changeset version` コマンドをローカルで実行する運用にすることも可能

# 参考リンク

- [Use cache to reduce installation time - Setup pnpm · Actions · GitHub Marketplace](https://github.com/marketplace/actions/setup-pnpm#use-cache-to-reduce-installation-time)
- [Registry & Authentication Settings - .npmrc | pnpm](https://pnpm.io/npmrc#registry--authentication-settings)
- [Using changesets with pnpm - pnpm](https://pnpm.io/using-changesets)
- [Changesets Release Action - GitHub](https://github.com/changesets/action)