release-itとGitHub ActionsによってNode.jsパッケージのリリース作業を自動化する

# 前置き

## 導入のきっかけ

Node.jsパッケージのリリース作業を何度も手動で実施しているうちに「この手順を自動化できないだろうか」と思うようになりました。
手順はバージョンを書き替えて、Gitにタグを付けて、リリースノートを書いて、`npm login`して、`npm publish`して...。
少し面倒なので、プログラマの3大美徳の一つ"怠惰"の実現を図ります。

## 記事を書くことにした理由

調査したところ`release-it`によって自動化が実現できることはわかりました。
公式ドキュメントも豊富でした。
更新も頻繁に行われており、安心して使うことができそうでした。

しかし実際に導入しようとすると、詰まるポイントがいくつかあり苦戦しました。
日本語ドキュメントも少ない状況でした。
私が実施したことの紹介が何かの役に立つのではと思い、記事を書いていきます。

## この記事で紹介すること

- release-itの紹介
- リリース自動化の設定例
  - [npmjs.com](https://www.npmjs.com/)のアクセストークンの取得方法
  - release-itの設定例
  - GitHub Actionsの設定例
- 実際に作ったサンプルの紹介
  - [ljourm/sample-release-it (GitHub)](https://github.com/ljourm/sample-release-it)
  - [ljourm-sample-release-it(npmjs.com)](https://www.npmjs.com/package/ljourm-sample-release-it)

## この記事で紹介しないこと

- release-itの詳細な解説
  - 自分が試したことだけを記載する
  - 自分が試していないことは主要機能であっても記載しない
- `package.json`の書き方
- npmの解説、npmコマンドの使い方
- GitHub Actionsの解説

## バージョン情報

- release-it: 14.4.1
- release-it conventional-changelog: 2.0.1

## 注意点

(当然のことですが) 公開してはならない情報をうっかり公開しないように注意してください。
公開から72時間以内であれば簡単に `npm unpublish <package_name> --force` で取り下げられるようですが、72時間を超えるといろいろ制約があるようです。
[npm Unpublish Policy (公式ドキュメント)](https://www.npmjs.com/policies/unpublish)

# release-itとは

[release-it (GitHub)](https://github.com/release-it/release-it)

従来のnpmコマンドによって実施していたリリース作業を自動化するためのツールです。
`npm publish`だけでなく、同時に行うであろう操作や修正もサポートしています。

release-itによって自動化できることは主に以下です。
詳細な情報や正確な情報は公式を参照してください。

- 公開時の事前スクリプト実行
- バージョンアップ　(`package.json`に記載されているバージョンの更新)
- npm registryへの登録 ([npmjs.com](https://www.npmjs.com/)やその他プライベートregistryなどへの登録)
- Gitタグの付与
- GitHub Releaseの生成
- `CHANGELOG.md`の生成

# Access Tokenの取得と設定

自動化を実現するためにはnpmアカウントの手動入力を省略する必要があります。
ここではnpm registryからトークンを取得し、GitHubに設定するところまで進めます。

## npmリポジトリ側の操作

### 前提

npmのアカウントが作成済みであること。

### 手順

1. [npmjs.com](https://www.npmjs.com/)にアクセスする
1. `Access Tokens` -> `Generate New Token`を順に押下する
1. `Publish` -> `Generate token`を順に押下する
1. 生成されたトークンを控えておく

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231426/cc13e8df-7538-1107-7f28-260faec39357.png" width="400px"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231426/47450a8a-1c41-2079-e45c-c290e97ca985.png" width="400px"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231426/d82a8dcc-eb86-529c-4fdf-8e781adb03a4.png" width="400px">


## GitHub側の操作

### 前提

GitHubにリポジトリが作られていること。

### 手順

1. GitHubのリポジトリにアクセスする
1. `Settings` -> `Secrets` -> `New repository secret`を順に押下する
1. 以下を入力して`Add secret`を押下する
  - Name: `NPM_TOKEN` (名称は任意。後述のGitHub Actionsで使用する）
  - Value: 先ほど取得したトークン

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231426/15e67004-e80d-8e02-9a55-a3ae9653984b.png" width="400px"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231426/c2e576e0-4f95-4265-b460-5d3f9167f3f1.png" width="400px">

## release-itの設定例

```js:.release-it.js
module.exports = {
  "npm": {
    "publish": true 
  },
  "github": {
    "release": true
  },
  "git": {
    "requireCleanWorkingDir": false,
    "addFiles": ["package.json", "CHANGELOG.md"],
    "commitMessage": "chore: release ${version}"
  },
  "plugins": {
    "@release-it/conventional-changelog": {
      "preset": "angular",
      "infile": "CHANGELOG.md"
    }
  }
}
```

設定ファイルは `json` や `yaml` でも書けますが、ここでは `js` を採用します。
[公式ドキュメント](https://github.com/release-it/release-it#configuration)

### `npm.publish`
`true`の場合、パッケージが自動的に公開されます。
前述のトークンの設定が事前に必要です。

公開しない場合は`false`を設定してください。

成功すると以下のようにregistryに登録されたことが確認できます。
[ljourm-sample-release-it (npmjs.com)](https://www.npmjs.com/package/ljourm-sample-release-it)

[公式ドキュメント](https://github.com/release-it/release-it/blob/master/docs/npm.md)

### `github.release`
`true`の場合、GitHubのReleaseが生成されるようになります。
[実際に生成されたRelease](https://github.com/ljourm/sample-release-it/releases/tag/0.1.1)

[公式ドキュメント](https://github.com/release-it/release-it/blob/master/docs/github-releases.md)

### `git.requireCleanWorkingDir`

後述の手順で`.npmrc`を一時的に作るため、git差分が発生してエラーとなりました。
これを回避するために`false`を設定します。

### `git.addFiles` `git.commitMessage`

以下の更新がコミットされるようになります。

- `package.json` (自動でバージョンが更新される)
- `CHANGELOG.md`

デフォルトで`git push`まで実行されます。
[実際にpushされたコミット](https://github.com/ljourm/sample-release-it/commit/fc2f0e907b8a938a12671137a1483e011354688d)

[公式ドキュメント](https://github.com/release-it/release-it/blob/master/docs/git.md)

### `plugins.@release-it/conventional-changelog`

せっかくなので`CHANGELOG.md`の更新まで実行したいと思い、公式のプラグインを導入しました。
[@release-it/conventional-changelog (GitHub)](https://github.com/release-it/conventional-changelog)

前回リリース以降のコミットメッセージのうち、条件に合ったものが`CHANGELOG.md`に追記されます。
そしてそれがGitHubのリリースにも反映されます。

`CHANGELOG.md`の記述ルールには複数の選択肢が用意されています。
[選択肢一覧](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages)

#### `"preset": "angular"`
ここでは`angular`を採用しました。
コミットメッセージのルールはプロジェクトの事情に合わせればいいと思いますがが、`type`をプレフィックスに採用するのは多くのプロジェクトに合うのではと思っています。
一方で`scope`は必須でないと思ったので今回のサンプルでは省略しています。
[Angular Commit Message Format (GitHub)](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-format)
[僕が考える最強のコミットメッセージの書き方 Prefixをつける (Qiita)](https://qiita.com/konatsu_p/items/dfe199ebe3a7d2010b3e#1-prefix%E3%82%92%E3%81%A4%E3%81%91%E3%82%8B)

全てのコミットが`CHANGELOG.md`に反映されるのではなく、`feat` `fix` `perf` のみが反映されるとのことでした。
[Angular Convention type (GitHub)](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular#type)

なお、柔軟に`CHANGELOG.md`の内容を変えていくこともできるようです。
詳細は公式を参照ください。
[公式ドキュメント config (GitHub)](https://github.com/release-it/conventional-changelog#config)

#### `CHANGELOG.md`の自動生成を上手く運用できるのか

ほとんど運用していない状態でこの記事を書いています。

`CHANGELOG.md`の生成を自動化しましたが、コミットメッセージを正確に書いてもらない場合もありそうで、`CHANGELOG.md`も不正確な状態になるのではと懸念しています。
今回はとりあえず導入してみて、しばらく様子を見てみたいと考えています。

## GitHub Actionsの設定例

```yml:.github/workflows/release.yml
name: auto-release
on:
  push:
    branches:
      - master
jobs:
  auto-release:
    runs-on: ubuntu-latest
    env:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      RELEASE_IT_VERSION: 14.4.1
      RELEASE_IT_CHANGELOG_VERSION: 2.0.1
    steps:
      - uses: actions/checkout@v2
        with:
		  # ポイント1
          fetch-depth: 0
      - uses: actions/setup-node@v2
      - name: Set releaser settings
        run: |
          git config --global user.name release-bot
          git config --global user.email release-bot@example.com
      - name: Set .npmrc
	    # ポイント2
        run: echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc
      - name: Major release
        id: major
        run: npx --package release-it@${RELEASE_IT_VERSION} --package @release-it/conventional-changelog@${RELEASE_IT_CHANGELOG_VERSION} release-it major --ci
        if: contains(toJSON(github.event.commits.*.message), 'bump up version major')
      - name: Minor release
        id: minor
        run: npx --package release-it@${RELEASE_IT_VERSION} --package @release-it/conventional-changelog@${RELEASE_IT_CHANGELOG_VERSION} release-it minor --ci
        if: steps.major.conclusion == 'skipped' && contains(toJSON(github.event.commits.*.message), 'bump up version minor')
      - name: Patch release
	    # ポイント3
        run: npx --package release-it@${RELEASE_IT_VERSION} --package @release-it/conventional-changelog@${RELEASE_IT_CHANGELOG_VERSION} release-it patch --ci
		# ポイント4
        if: "!(steps.major.conclusion == 'success' || steps.minor.conclusion == 'success')"
```

### ポイント1 `fetch-depth: 0`

`CHANGELOG.md`に最新コミットしか反映されない問題の対策です。

```bash
# `fetch-depth: 0`が付いていない場合
git commit -m "feat: change 1" # これは反映されない
git commit -m "feat: change 2" # 最新のコミットメッセージのみ反映される
git push origin master
```

`actions/checkout@v2` はデフォルトでGitの履歴を取らない (`git log`で何も返ってこない)ことが原因とのことでした。
対策として `fetch-depth: 0` を付けてGitの履歴を取得可能にします。
[公式ドキュメント](https://github.com/release-it/conventional-changelog#github-actions)

### ポイント2 `.npmrc`

手動でnpmコマンドを実行してリリースする場合、以下の手順になると思います。

```bash
$ npm login # ユーザ情報を対話式で入力する。これが自動化で問題となる。
$ npm publish
```

事前に登録した`NPM_TOKEN`をここで使用します。

### ポイント3 `release-it patch --ci`

ここでは`patch`について解説しますが、`majar`や`minor`も基本的に同じです。

利用したいパッケージは通常`package.json`内の`devDependencies`に追加すると思いますが、
今回はリリースでしか使用しないパッケージを開発環境に導入したくなかったため `npx` による解決を採りました。

また、今回必要となるパッケージは二つあったため、`-p`または`--package`を使用します。

読みやすいように改行したものを以下に記載します。

```bash
npx \
  --package release-it@${RELEASE_IT_VERSION} \ # 使用するパッケージ その1
  --package @release-it/conventional-changelog@${RELEASE_IT_CHANGELOG_VERSION} \ # 使用するパッケージ その2
  release-it patch --ci # 実行するコマンド。
```

### ポイント4 `if`

コミットメッセージによってバージョンアップの対象を制御します。
これは後述の"参考にした記事"と同じ設定です。

- コミットメッセージに `bump up version major` が含まれていた時
  - -> Major version up (e.g. `1.0.0` -> `2.0.0`)
- コミットメッセージに `bump up version minor` が含まれていた時
  - -> Minor version up (e.g. `1.0.0` -> `1.1.0`)
- 上記に該当しない場合
  - -> Patch version up (e.g. `1.0.0` -> `1.0.1`)

### GitHub Actionsの実行

ここまでの設定が完了した状態でmasterブランチに変更が入るとGitHub Actionsが実行されます。
[実行されたAction](https://github.com/ljourm/sample-release-it/runs/1995451223)

完了すると公開されたことが確認できるようになります。
[実際にリリースされたパッケージ](https://www.npmjs.com/package/ljourm-sample-release-it)

# 1回のバージョンアップに複数のPull Requestをまとめたい場合

ここからは設定でなく運用テクニックの紹介です。

運用していると、例えばメジャーバージョンアップする時に「AとBとCを同時にリリースしたい」といったケースが発生すると思います。
そのような場合はバージョンアップ用の集約ブランチを作り、そこに各Pull Requestをマージし、最後に集約ブランチをmasterブランチにマージするのがよいと思います。

## 実際にシミュレーションしてみる

サンプルとして作成したリポジトリで実施してみました。

### 状況

マイナーバージョンアップを予定している。
その際に[#2](https://github.com/ljourm/sample-release-it/pull/2)、[#3](https://github.com/ljourm/sample-release-it/pull/3)、[#5](https://github.com/ljourm/sample-release-it/pull/5)を同時にリリースしたい。

### 手順

1. 集約ブランチ [#4](https://github.com/ljourm/sample-release-it/pull/4) を作る。
  - 差分がないブランチからPull Requestを作ることはできないため、空コミットを作ってからPull Requestを作成する。
  - e.g. `git commit --allow-empty -m "bump up version minor"`
1. 各修正のマージ先をmasterブランチでなく集約ブランチに変更してからマージする。
1. 集約ブランチをmasterブランチにマージする。

この操作により複数のPull Requestを一つのリリースにまとめることができました。
[release-itによって生成されたコミット](https://github.com/ljourm/sample-release-it/commit/e6e5f36d049e253aa0ebf47de4427aa4f5a79e5e)

# 参考にした記事

[GitHub Actionsとrelease-it npmでリリース作業を自動化する](https://devblog.thebase.in/entry/automatic-release-on-github-actions)
こちらの記事がとても参考になりました。
上述のコミットメッセージによるMajor、Minor、Patchの切替もこちらを元にしています。
また、本記事と目指している方向が少し違うので、そういった意味でも参考になると思います。