Ship.jsでリリースフローを改善する

PLAID で i18n おじさんエンジニアしてる kazupon です。

この記事は [plaid advent calendar 2019](https://qiita.com/advent-calendar/2019/plaid) の 17日目の記事です。

# はじめに
筆者は、PLAID のプロダクト開発以外にも、オープンソースソフトウェア (以下 OSS )開発者として [vue-i18n](https://github.com/kazupon/vue-i18n) といったオープンソースプロジェクトを持っており、Node.js においては [npm](https://www.npmjs.com/) または [yarn](https://yarnpkg.com/) といったパッケージマネージャーと呼ばれるもので、Node.js そして必要に応じてブラウザ向けに動作するコードをパッケージにして OSS として配布しています。

npm / yarn で配布する OSS は、一般的には [semver](https://semver.org/) のようなセマンティックバージョンニングの仕様に沿った形で、バージョンをリリース毎に発行して管理して、`npm publish` や `yarn publish` によって配布します。

OSS をリリースする際には、何が変更されたのかどうかを利用者に情報として提供するために、一般的には changelog[^1] [^2] といういわゆる「変更履歴」を書いたドキュメントも OSS のリリースとともにいっしょに提供します。

# この記事で紹介すること
この記事の前半では、OSS のリリースについて紹介します。OSS をリリースしたことが無い人もいると思うので、リリースの雰囲気を感じてもらえればと思います。

この記事の後半では、本記事の本題である、筆者が改善した OSS の リリースフローについてです。これまでどのようにしてリリースしていて、なぜ改善する理由に至ったのか、そして[lerna-changelog](https://github.com/lerna/lerna-changelog) と [Ship.js](https://github.com/algolia/shipjs) を使って、どうやって改善したのか。これから OSS を公開したいと思っている人、そして OSS のリリース方法について何かモヤモヤしている人に参考になればと思います。

# 前提
以降では、パッケージマネージャーは  npm  を前提に紹介していきます。yarn を使っている方は、そこは適宜それに置き換えてイメージしてもらえればと。

またバージョン管理システムは Git、そしてコード共有は GitHub を使っているものとして前提としておきます。

# OSS のリリースについて
普段業務でプロダクトやサービスをリリースされたことがあっても、OSS をリリースしたことがない方もいるでしょう。以降は、OSS のリリースについて紹介します。

## 素朴にリリースする場合
OSS 開発者は開発が終わった後、一般的には以下の流れでリリースします。

- `npm version` でバージョン採番
- changelog に変更内容を書く
- バージョンニングされた`package.json`と changelog を `git commit`
- `npm publish` でリリース
- `git tag` で semver に対応するタグを発行
- GitHub にコードとタグを `git push` 
- タグによって作られた GitHub Releases にリリース情報内容を changelog の内容で更新

上記リリースステップのとおり、特に何もツールを使わずに、OSS をリリースする場合は、いくつものステップを手で対応しないといけないため結構に大変です。

changelog の変更内容は、Git でコミットした内容を `git log` 等で確認しながら、書かないといけないので、地味に大変です。そして、特に GitHub には GitHub Releases というリリース情報を提供できる機能がありますが、これも changelog と同じ内容を手で更新しないといけないので、コピペ作業のような手間があって面倒いです...[^3]

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7988/fb8b9697-592d-d207-11e1-2731824e32af.png)
(GitHub Releases はいろいろと入力しないといけない...)

## conventional commits + ツールを使ってリリースする場合
先に説明したように、素朴にリリースする場合は、手で対応するステップが多いため大変だったと思いますが、

- **conventional commits** というコミットメッセージ規約で Git でコミット
- changelog 生成ツール、自動化ツールを使う

を複数組み合わせることで解決することができます。

[conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) (コンベンショナル・コミット) という Git のコミットメッセージを規約化してるものがあります。conventional commits ではフォーマットは以下にようになっています。

```
<type>[optional scope]: <description>

[optional body]

[optional footer(s)]
```

上記フォーマットの詳細は、こちらの[公式サイト](https://www.conventionalcommits.org/en/v1.0.0/#summary)で確認できるので、ここでは説明割愛します。このフォーマットによるコミットメッセージの具体例としては、 Vue.js 本体のプロジェクトをケースにあげると、以下のような感じになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7988/14840aab-e1d8-65e4-3332-f3c93b8296fd.png)

このように、conventional commits の規約に従ってメッセージをコミットしておくと、配布する changelog を自動生成、そして GitHub Relelese へのリリース情報の反映も、同じ changelog の内容ですることができます。

さらに、ツールを利用することで、changelog の生成、npm へのパッケージ化された OSS の公開、そして、GitHub Releases でリリースするための `git tag` そしてリリース情報内容更新まで、手でやっていたことを自動化することができます。

### changelog を自動生成する
conventional commits の規約に従ってコミットされたメッセージから changelog を自動生成するには、[conventional-changelog-cli](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-cli) というCLI ツールを利用します。

```bash
$ npm install -g conventional-changelog-cli
$ cd my-project
$ conventional-changelog -p angular -i CHANGELOG.md -s
```

上記のようにコマンドを実行すると、conventional-changelog-cli は Git でコミットされたコミットログと Git のタグ情報からリリース対象となる changlog を生成して markdown 形式でファイルに出力します。

以下は、Vue Router の changelog である `CHANGELOG.md` の内容の抜粋です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7988/042c9a35-0783-e380-ddcd-4dcd2b6dd4e2.png)

このように conventional commits の規約でコミットしておくと、自動生成させた際にツール側が GitHub の issue 番号と URL が紐付かれたものを changelog に出力してくれます。

### GitHub Releases のリリース情報に反映する
conventional-changelog-cli という CLI を使って conventional commits の規約に従った Git コミットログから changelog を同じ内容を、GitHub Releases のリリース情報にも反映することができます。

以下のように、[conventional-github-releaser](https://github.com/conventional-changelog/releaser-tools/tree/master/packages/conventional-github-releaser) という CLI ツールを利用します。

```bash
$ npm install -g conventional-github-releaser
$ cd my-project
$ conventional-github-releaser -p angular
```

conventional-github-releaser に指定するパラメータはほぼ、conventional-changelog-cli と同じです。

以下は Vue Router の GitHub Releases の内容です[^4]。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7988/d65ace97-a3c0-0613-debe-0924344b777b.png)

先程紹介した Vue Router の `CHANGELOG.md` とリリース内容が同じであることに気がついたでしょう。これは conventional-github-releaser は conventional-changelog-cli を提供している [conventional-changelog](https://github.com/conventional-changelog) が提供していて、内部では同じロジックを使っているためです。

このように、converntional commits の規約に従って Git でメッセージをコミットしていると、changelog の生成と GitHub Releases のリリース情報を生成が楽であることに分かったと思います。

### 一連のリリースステップを自動化する
リリースステップを自動化するには以下のツールで実現することができます。

- standard-version を利用する
- semantic-release を利用する
- monorepo の場合は lerna を利用する
- Ship.js を利用する

以降では、standard-version と semantic-release について簡単に紹介しておきます。lerna を使ったリリースについては、[こちら](https://efcl.info/2019/01/26/monorepo-release-flow/)の記事を読んで雰囲気がわかるはずです。Ship.js については、後半で話します。

#### standard-version を利用する
[standard-version](https://github.com/conventional-changelog/standard-version) という CLI ツールを以下のようにして利用します[^5]。

インストール:

```bash
$ npm i --save-dev standard-version
```

`npm run script` で実行できるよう `package.json` を以下のように編集:

```json
{
  "scripts": {
    "release": "standard-version"
  }
}
```

これで、リリースする際に、`npm run release` とするだけで、changelog の生成から、`npm publish`、そして `git tag` までしてくれます。

ちなみに、バージョンの採番ですが、これは standard-version は conventional commits  の[規約](https://www.conventionalcommits.org/en/v1.0.0/#how-does-this-relate-to-semver)に従っているので自動的に採番されるようになっています。

standard-version は GitHb Releases まではやってくれないので、そこは先に説明した、conventional-github-releaser を以下のように組み合わせる必要があります。

```json
{
  "scripts": {
    "release": "standard-standard && conventional-github-releaser -p angular"
  }
}
```

あとは、CircleCI や GitHub Actions を利用することで、CI / CD によって自動化することができます。

#### semantic-release を利用する
[semantic-release](https://semantic-release.gitbook.io/semantic-release/) も standard-version と同様に自動化することができます。

Git コミットメッセージも [conventional commits ベース](https://semantic-release.gitbook.io/semantic-release/#commit-message-format)なので、もちろん、changelog は自動生成することができ、バージョンの採番も自動で行われます。

semantic-release が standard-version と大きく違うのは以下の点です。

- 最初から完全に自動化を念頭に作られていること 
    - 人間が介入しないCIでの利用を想定 ([思想が過激！](https://www.youtube.com/watch?v=ZXyx_1kN1L8&t=2s))
    - `git push` 、リリースブランチへのPR のマージ等のトリガーとして CI を通してリリース
    - [ローカルでも設定次第でリリースできる模様](https://semantic-release.gitbook.io/semantic-release/support/faq#can-i-run-semantic-release-on-my-local-machine-rather-than-on-a-ci-server)
- リリースステップが定義されていて、[プラグインでリリースを制御することができる](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/plugins.md)
    - 必要に応じてリリースステップにプラグインで介入したりスキップしたり制御可能っぽい

このような違い、特徴から semantic-release はまさに semver  に従ったセマンティックバージョンニングによる完全に自動化を念頭化した、そしてリリースフローを制御できるリリースツールといえます。

semantic-release の導入については、[こちら](https://semantic-release.gitbook.io/semantic-release/usage/getting-started) ドキュメントがあるので、ドキュメント通りに設定すれば動作するのでここでは割愛します。

# これまでのリリース、生じた問題、そしてリリースの改善
ここまで長々と OSS のリリース方法について紹介してきました。お待たせしました。そろろろ本記事の本題に入っていきましょう！

## どのようなリリースを実践していたのか
まず、筆者がどのようにこれまにで OSS のリリースをしていたのかを説明しておきます。

### 独自拡張した conventional commits を利用して changelog を生成
流石に **changelog を手書きするのはキツかった**ので、conventional commits を導入して changelog は自動生成していました。

ただ、筆者が導入した conventional commits は、いわゆる標準のものではなく、[自分で拡張した emoji に対応](https://github.com/kazupon/git-commit-message-convention)した conventional commits です。そうです、オレオレ conventional commits です。

フォーマットは以下のように、標準の conventional commits に対して  `<emoji>:` を追加しただけです。

```
[:<emoji>: ]<type>[optional scope]: <description>

[optional body]

[optional footer(s)]
```

type に指定できる種類も以下のように独自に定義し直しています。

```
- new
- feature
- update
- bug
- security
- performance
- improvement
- breaking
- deprecated
- i18n
- a11y
- refactor
- docs
- example
- test
- deps
- config
- build
- release
- wip
- chore
```

これまでの説明で紹介した conventional-changelog-cli は、実は自分で拡張した conventional commits の規約を、以下のようにパラメータにプリセットとして指定することで、カスタマイズした changelog を自動生成することができます。

```bash
$ conventional-changelog -i CHANGELOG.md -s -n /path/to/git-commit-message-convention/convention.js
```

また、GitHub Releases のリリース情報を更新する  conventional-github-releaser も同様に独自定義したプリセットを以下のようにパラメータで指定することができます。

```bash
$ conventional-github-releaser -n /path/to/git-commit-message-convention/convention.js
```

独自拡張した conventional commits で、筆者は conventional-changelog-cli 、conventional-github-releaser  を使って、公開している OSS の  changelog の生成、GitHub Releases のリリース情報を更新して運用してきました。以下は、 vue-i18n での changelog 、GitHub Releases のリリース情報の抜粋したところです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7988/11c82c9f-929c-c54f-320c-15e984b2ed1a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7988/cbfe86c2-2c9e-f2b4-6e5c-9109062ea71e.png)

### 一連のリリースは基本手動
OSS は手動でリリースしています。これまでに紹介した standard-version や semantic-release で自動化は考えましたが、以下の理由により changelog 生成、GitHub Releases のリリース情報の更新のみ自動にした、半自動でリリースを行ってきました。

- 自動生成した changelog 、そして GitHub Releases のリリース情報に対して時々カスタマイズしたいため
- 独自拡張した conventional commits のため、standard-version や semantic-release といったツールにカスタマイズが必要

自動生成した changelog 、そして GitHub Releases のリリース情報の更新ですが、基本的にはそのままでいいのですが、時々、例えば、メジャーリリースの場合などは、**example コード、図といった画像、そして URL といった情報を盛り込みたい**ときがあります。

リリースを自動化していると、その情報をカスタムマイズすることが出来ないため、あえて手動でリリースしていました。

また、筆者は独自拡張した conventional commits の規約で日々の Git のコミットメッセージは運用していますが、standard-version や semantic-release といったツールで、独自拡張された conventional commits の場合は、そういったツールのカスタマイズが必要になり、そういったツールのバージョンアップに伴うツールの仕様変更に対応しながら、追随するのが大変です。

例えば、semantic-release の場合は、プラグインを組み合わせてリリースフロー構築して自動化をしますが、そのプラグインを作る必要があり、そしてメンテナンスもしなければなりません。

## これまでリリースの運用で生じてきた問題
このリリース方法を始めてから4年近くOSS をリリースしてきましたが、時とともに筆者は様々な OSS を開発してリリースするに従い問題がでてきました。

### conventional commits が辛くなってきた
筆者は conventional commits を利用して OSS 開発において Git でコミットメッセージをしてきました。conventional commits、最初なれるまでは大変でしたが、changelog の自動生成の快適さを一度経験してしまうと、 conventional commits の規約を使ったメッセージコミットはもう手放せません。

なので便利ではあるのですが、conventional commits は以下の点でちょっと不便です。

1. scope、type の分類に悩むことがある
1. 間違った状態でコミットしまった状態で、後で間違いに気がついても、コミットメッセージの変更することがほぼ無理
1. conventional commits は学習コストが発生する

scope、type の分類は今でも悩みます。

scope は monorepo になっている レポジトリ、ディレクトリ構成でモジュール化されているレポジトリの場合は、それに従えばいいんですが、ない場合は考えてしまいそこで時間を取られてしまうことがあります。scope は必須ではないので、最近は考えるの面倒な場合は、省略前提でコミットしてしまいます。type については必須で分類は避けられないので、**コミットするときに分類する負荷が頭にのしかかる**ので結構きついです。

conventional commits の規約に従わないケースでコミットされてしまった場合は、メッセージの修正変更はほぼ無理に等しいんではないでしょうか。`git push --force` はご法度なので。

コミットメッセージが conventional commits の規約に従っているかどうか、[coommitlint](https://github.com/conventional-changelog/commitlint) 等があるので、Git でコミットする際に hook して lint すれば防ぐことができるんですが、GItHub の PR のマージは、PR のタイトルがコミットメッセージになるので、メンテナが気をつけていないとすり抜けてしまうので、完全に防ぐことはできないです。実際、筆者の OSS の vue-i18n で conventional commits ではない PR のタイトルがマージされてしまい、changelog の自動生成で生成されないため、changelog に手で修正入れたことがありました。

conventional commits は規約なので、それを覚える学習コストはさけられないです。フォーマットはそんなに難しくないですが、type は標準化されているものでも 10 種類あるので、頭脳の負荷になってしまいます。

こうして、OSS 開発が長年やっていくうちに、conventional commits に対する辛さのお気持ちが強くなっていった次第です。

### チーム体制になってくると自動リリースが必要
筆者 は、vue-i18n という Vue.js 向けのプラグインを OSS として公開して以降これまで一人で開発してリリースしてきました。

Vue.js のエコシステムの発展とともに、必要になったいくつかの OSS を開発してリリースするようになりました。そして、vue-i18n を始めとして issue & PR をトリアージする人がでてきて、チーム体制[^6]が必要になってきました。

チーム体制になってくると、他の人でも簡単に OSS をリリースするためには、やはりリリースの自動化は必須です。ちなみに、[The DevOps Handbook](https://www.oreilly.com/library/view/the-devops-handbook/9781457191381/) によると Etsy では犬でもデプロイできるそうです。動物でも OSS リリースできたら面白いですね！

## changelog の生成とリリースを自動化するツールの選定
というわけで、問題改善に向けて動きだしたのですが、いろいろ調べて試した結果、 lerna-changelog というchangelog 自動生成のためのツールと、Ship.js というここ最近新しく OSS として公開されたリリース向けのツールを選定しました。

### lerna-changelog 
lerna-changelog は、GitHub の PR ベースで changlog を生成するツールです。

lerna-changelog は conventional commits とは違い規約に従った Git でメッセージにコミットは必要はなく、PR に対してラベル付けするだけです。changelog 生成できるための設定が出来ている状態で、かつ PR に対してラベル付けされていれば、CLI を実行するだけで、changelog 生成できてしまいます。

conventional commits のときは、changelog を生成するために普段から意識して、Git でメッセージのフォーマットが規約に従っているかどうか OSS 開発チームのメンテナだけでなく、OSS をコントリビュートする側も意識する必要がありました。それに対し lerna-changelog は OSS メンテナだけが changelog を自動生成するためにラベル付けを意識していればよく、OSS をコントリビュートをする側は特に Git のコミットにおいても意識する必要はありません。

lerna-changelog 、**OSS コントリビュートする側は特に何もすることがなく、そして OSS メンテナ側も conventional commits を覚える必要がなく、changelog 生成する際のカテゴリ分けする際のラベル付けのルールだけを意識すればよい**ので、かなり良いです。

#### 導入方法
CLI ツールが必要なのでインストールします。

```bash
$ npm install -g lerna-changelog
```

GitHub の対象 repository から API 経由で PR 情報を取得するので、[こちら](https://github.com/settings/tokens)で GitHub Token を取得して以下のように環境変数への設定が必要です。

```bash
export GITHUB_AUTH="..."
```

導入の最後として、changelog で PR を分類するために GitHub のラベルをマッピングを定義します。マッピングは `package.json` に以下のように`changelog.labels` に設定する必要があります[^7]。

```json
{
  // ...
  "changelog": {
    "labels": {
      "feature": "New Feature",
      "bug": "Bug Fix"
    }
  }
}
```

このラベル設定のマッピングを忘れると、lerna-changelog を実行しても changelog 何も生成されないので注意が必要です。

筆者は[こちらのレポジトリでラベルを定義](https://github.com/kazupon/lerna-changelog-label-schema#notebook-label-schema)し、手でラベルを package.json に設定するの面倒なので、ラベルを定義したレポジトリで lerna-changelog-label-schema というCLI を作り、以下のようにコマンドを叩くだけで設定を自動化しています。

```bash
$ npm install -g lerna-changelog-label-schema
$ cd /path/to/project
$ lerna-changelog-label-schema define --preset=full
apply lerna-changelog label schema 'full' to package.json
```

ちなみに、ラベルは、レポジトリに対するラベルのセットアップも、[azuさんのこちらのツール](https://github.com/azu/github-label-setup)を使って、そのツール向けに自分が用意したプリセットを使って、以下のようにしてコマンドだけでセットアップできるようにしています。

```bash
$ npm install -g @azu/github-label-setup
$ npm install -g @kazupon/github-label-preset
$ cd /path/to/project
$ github-label-setup --token xxx --labels @kazupon/github-label-preset
```

github-label-setup も`--token` で GitHub Token が必要なので設定が必要です。毎回設定が面倒な場合は、環境変数 `GITHUB_ACCESS_TOKEN` に設定しておくとよいでしょう。

筆者の lerna-changelog-label-schema を `git clone` してきて、 lerna-changelog を以下のように実行すると標準出力に changelog が出力されます。

```bash
$ git clone git@github.com:kazupon/lerna-changelog-label-schema.git
$ cd lerna-changelog-label-schema
$ lerna-changelog --from=v2.0.3 --to=v3.0.0

## v3.0.0 (2019-12-13)

#### :star: Features
* [#38](https://github.com/kazupon/lerna-changelog-label-schema/pull/38) feat: lernal changelog label schema  ([@kazupon](https://github.com/kazupon))

#### :boom: Breaking Change
* [#38](https://github.com/kazupon/lerna-changelog-label-schema/pull/38) feat: lernal changelog label schema  ([@kazupon](https://github.com/kazupon))

#### :pencil: Documentation
* [#40](https://github.com/kazupon/lerna-changelog-label-schema/pull/40) update readme ([@kazupon](https://github.com/kazupon))

#### Committers: 1
- kazuya kawaguchi ([@kazupon](https://github.com/kazupon))
```

上記の lerna-changelog のパラメータは、今回の説明用に指定していますが、実際には、`lerna-changelog --next-version=vX.Y.Z` のように実行すれば、常に最新のバージョンの changelog が標準出力されるので、その changelog をよしなに `CHANGELOG.md` 、そして GitHub Releases のリリース情報に反映できるようにすればよいだけです。

### Ship.js 
Ship.js は名前示しているとおり、JavaScript 向けパッケージのリリースを制御するためのツールです。

このリリースツールは、SaaS 型の検索サービスを提供している最近イケイケの [algolia](https://www.algolia.com/) という会社が OSS で提供しています。

Ship.js は、自動化はもちろん、ローカルマシン、CI 環境、そして monorepo 構成のレポジトリでも、リリースすることもできます。standard-version、semantic-release といったリリースツールと大きく異るのは、**開発に関わるメンバーでリリースをコラボレーションできる**のが他にはない特徴です。

また、Ship.js は[いくつかリリースにおけるフックを公開](https://shipjs.netlify.com/reference/all-config.html)しているので、 `ship.config.js` という設定ファイルで、リリースフローを柔軟に制御できます。

Ship.js はリリースする前に、GitHub で PR を作成して、チームメンバーで以下の内容をレビューしてリリースすることができます。

- リリースするバージョンが正しいかどうか
- リリースする際の内容の確認
- リリースする際の changelog の内容が、使い手に分かりやすい内容かどうか

ちょうど自分が問題意識していたところを解決できそうなリリースツールだったので、検証してフィットしたので、筆者の OSS のプロジェクトに導入して使っています。

#### Ship.js のリリースプロセス
Ship.js ではリリースプロセスが以下の項目で定義されています。

1. 準備プロセス (Preparation)
1. レビュープロセス (Review)
1. リリースプロセス (Release)

#### 準備プロセス
準備プロセスでは、リリースするための準備するプロセスです。

`shipjs prepare` というコマンドを実行すると以下の内容を実行します。

- リリースするバージョンを確認
- リリースするバージョンの決定と changelog の生成
- GItHub で PR を作成する

これらについては gif 動画で見たほうが理解しやすいので、公式リポジトリで掲載されるものの引用 gif 動画ですが、以下に掲載しておきます。

![preview.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7988/282865c8-cd6b-2974-f11b-f719618262bb.gif)

`shipjs prepare` を実行すると GitHub で PR は実際にはこんな感じ PR が作成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7988/40e6aea2-6f58-a30c-b689-829fefaf2c30.png)

#### レビュープロセス
レビュープロセスは、特に Ship.js のコマンド叩く必要はなく、準備プロセスで作成された PR をレビューするのみです。

レビューで changelog の変更が必要なら、この PR に対して追加コミットだけで、もしリリースをキャンセルするなら PR をクローズするだけです。

個人的には、Ship.js のこのフローが気に入っています。 **自動生成した changelog に対して、カスタマイズできる**プロセスなので。

#### リリースプロセス
レビューが通ったら、`shipjs trigger` というコマンドを実行してリリースするだけです。リリースは、ローカルマシンでも実行はもちろん、[こちらのドキュメント](https://shipjs.netlify.com/guide/getting-started.html#automate-part-3-trigger)にある通り、先の PR がマージされたタイミングで Circle CI を使ってリリースを自動化することができます[^8]。

リリースプロセスでは、リリース直前に、リリースで配布すべきパッケージのビルド、単体テスト、E2E テストを実行してから、npm のレジストリにリリースすることができます。

リリースが完了後に、 GitHub Releases のリリース情報にも準備プロセスの PR でマージされた changelog の内容でリリースすることができます。

#### 導入方法
Ship.js の導入方法は簡単です。

```bash
$ npm install --save-dev shipjs
```

で、対象プロジェクトにインストールしたあと、

```bash
$ npx shipjs setup
```

を実行すると、以下のようにインタラクティブに答えていくだけで、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7988/bc0b31fa-e1e3-e6dd-664b-5fda5ad86d0a.png)

`pakcage.json` の npm-scripts に設定したり、必要に応じて設定ファイル `ship.config.js` をプロジェクトにセットアップしてくれます。

## Ship.js + lerna-changelog でリリース環境を構築
試しに新しく作り上げた OSS プロジェクトは、先に lerna-changelog のラベルのセットアップで紹介した[ lerna-changelog-label-schema](https://github.com/kazupon/lerna-changelog-label-schema) というものです。

Ship.js で自動生成する changelog は、デフォルトで conventional commits に従ったコミットメッセージから conventional-changelog-cli で生成します。

筆者は、今後は lerna-changelog で changelog を生成していきたいので、lerna-changelog を組み込むために、`ship.config.js` で以下のようにカスタマイズしました。

```js
const execa = require(require.resolve('execa'))
const { promisify } = require('util')
const fs = require('fs')
const path = require('path')
const read = promisify(fs.readFile)
const write = fs.writeFileSync

function extractSpecificChangelog (changelog, version) {
  if (!changelog) {
    return null
  }
  const escapedVersion = version.replace(/\./g, '\\.')
  const regex = new RegExp(
    `(#+?\\s\\[?v?${escapedVersion}\\]?[\\s\\S]*?)(#+?\\s\\[?v?\\d\\.\\d\\.\\d\\]?)`,
    'g'
  )
  const matches = regex.exec(changelog)
  return matches ? matches[1] : null
}

async function commitChangelog (current, next) {
  const { stdout } = await execa('npx', ['lerna-changelog', '--next-version', `v${next}`])
  const escapedVersion = next.replace(/\./g, '\\.')
  const regex = new RegExp(
    `(#+?\\s\\[?v?${escapedVersion}\\]?[\\s\\S]*?)(#+?\\s\\[?v?\\d\\.\\d\\.\\d\\]?)`,
    'g'
  )
  const matches = regex.exec(stdout.toString())
  const head = matches ? matches[1] : stdout
  const changelog = await read('./CHANGELOG.md', 'utf8')
  return write('./CHANGELOG.md', `${head}\n\n${changelog}`)
}

module.exports = {
  mergeStrategy: { toSameBranch: ['master'] },
  monorepo: undefined,
  updateChangelog: false,
  beforeCommitChanges: ({ nextVersion, exec, dir }) => {
    return new Promise(resolve => {
      const pkg = require('./package.json')
      commitChangelog(pkg.version, nextVersion).then(resolve)
    })
  },
  formatCommitMessage: ({
    version,
    releaseType,
    mergeStrategy,
    baseBranch
  }) => `${releaseType} release v${version}`,
  formatPullRequestTitle: ({
    version,
    releaseType
  }) => `${releaseType} release v${version}`,
  shouldRelease: () => true,
  releases: {
    extractChangelog: ({ version, dir }) => {
      const changelogPath = path.resolve(dir, 'CHANGELOG.md')
      try {
        const changelogFile = fs.readFileSync(changelogPath, 'utf-8').toString()
        const ret = extractSpecificChangelog(changelogFile, version)
        return ret
      } catch (err) {
        if (err.code === 'ENOENT') {
          return null
        }
        throw err
      }
    }
  }
}
```

Ship.js の準備プロセスで 、Git でコミットされる前にフックされる `beforeCommitChanges` で、lerna-changelog を実行して生成した changelog を、`CHANGELOG.md` に書き込むことで、changelog を lerna-changelog に対応しています。

また、Ship.js のリリースプロセスでは、GitHub Release においてリリース情報を lerna-changelog で生成した changelog を同じ内容にするために、`extractChangelog` フックで、`CHANGELOG.md` ファイルから対象となる changelog を抽出しています。これにより、Ship.js の以降の処理で、GitHub Releases のリリース情報に反映されるようになっています。

`ship.config.js` を使って lerna-changelog を組み込むことができましたので、conventional commits の規約を使ったメッセージのコミットからは開放され、lerna-changelog-label-schema のプロジェクトでは、今ではラベル付けベースで changelog の生成を管理しています。

changelog の生成を対応しましたが、`ship.config.js` 使いこなせばリリースのときに slack に通知したり、いろいろと柔軟にカスタマイズできます。

# さいごに
**長々とした本記事を最後まで読んで頂いて、本当にありがとうございました！**

前半では、OSS のリリースについてどのような感じなのかイメージをつかむことができたのではないかと思っています。

筆者は Node.js ベースの OSS を公開しているため、そちらよりになってしまいましたが、他の言語、プラットフォーム環境のパッケージマネージャーを使ってリリースしている方にも参考になったのではないかと思っています。

後半では、筆者が実践しているリリース方法、それに対する抱える問題、そして改善について紹介しました。

リリースを改善するまでの過程で、changelog を生成するためのいろいろなツール、そしてリリースの自動化ツールを紹介したので、これから OSS を公開していきたい方に参考になったのではないかと思っています。

そして、筆者自身、 lerna-changelog と Ship.js を組み合わせることで、満足のいくリリース方法を新たに確立できたと思っています。リリースの自動化はまだ完全にはしていませんが、対応方法はサポートしているので後は CI/CD をサポートするサービスを利用して環境を整えるだけです。

Ship.js はかなりのリリースの面倒くさい部分をツール自体がやってくれるので、導入するだけリリースの労力をさいてくれる大変便利です！ リリースで悩んでいる方は一度検証してみてはいかがでしょうか。

Ship.js はまだバージョンが 0.x 系です。今後も進化していくので今後も動向をウォッチしつつ、コントリビューションも可能ならしていきたいと思います。

[^1]: ファイル名は、GNU Emacs プロジェクトのフォーマット形式の場合はファイル拡張子がない `CHANGELOG`、フォーマットが markdown なら `CHANGELOG.md` のケースでファイル名大文字がほとんどです。
[^2]: 筆者が新卒入社した会社では MS Word で changelog していたため、非常辛い思い出があります... 
[^3]:  GitHub Release は特に、npm のようなパッケージマネージャーで提供している場合は、npm 経由でインストールできるので特に必要ないと思いますが、世の中にはそのようなパッケージマネージャーを使わない人(CLI 使わない人)もいるため、GitHub Releases 経由によるオープンソースソフトウェアの提供も必要だと筆者は考えています。
[^4]: ちなみに、このリリース情報は Vue Router の GitHub Releases の[こちら](https://github.com/vuejs/vue-router/releases/tag/v3.1.3)で確認できます。
[^5]: ちなみに `standard-version` も `conventional-changelog-cli` そして `conventional-github-releaser` と同じ提供元である conventional-changelog が提供しています。
[^6]: 筆者は来年以降 [Intlify](https://github.com/intlify) という i18n 向けの新たな OSS プロジェクトを本格稼働を考えています。 
[^7]: lerna-changelog は他にも設定項目があります。詳しくは[こちら](https://github.com/lerna/lerna-changelog#configuration)で。
[^8]: [こちらの issue](https://github.com/algolia/shipjs/pull/502) が対応されれば 近々 GitHub Actions でも利用可能になりそうです。