GitHub Actionsとsemantic-releaseでnpmパッケージのバージョン管理・リリースノート作成を自動化してみた

GitHub Actionsとsemantic-releaseを利用したnpmパッケージのバージョン管理・リリースノート作成の自動化を試してみました
2020.12.16

こんにちは、CX事業本部のうらわです。

本記事では最近触ったGitHub Actionsとsemantic-releaseの連携についてご紹介します。

semantic-releaseとは

npmパッケージのバージョン管理やnpm repositoryへのpublishを自動で行うためのツールです。GitHubと連携すると、タグとリリースノートを自動で作成してくれます。

semantic-releaseはパブリックに公開するnpmパッケージだけでなく、プライベートなnpmパッケージ・npm以外のライブラリやツールでも利用することができます。

今回はプライベートなnpmパッケージの想定で、semantic releaseとGitHub Actionsを組み合わせたバージョン管理・リリースノート作成の自動化を試してみます。

なお、本記事で作成するサンプルプロジェクトのソースコードは以下に格納してあります。

https://github.com/urawa72/semantic-release-sample

準備

以下の環境で実施します。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.6
BuildVersion:   19G2021

$ node -v
v12.18.3

$ npm -v
6.14.6

まずはサンプルプロジェクトを作成します。

mkdir semantic-release-sample
cd semantic-release-sample
git init
echo "node_modules" > .gitignore
npm init -y

必要なnpmパッケージを追加します。各パッケージに関する設定は後述します。

npm i -D typescript semantic-release husky @commitlint/cli @commitlint/config-conventional

semantic-releaseを試すにあたりTypeScriptを使用する必要はないのですが、今回は私の好みでTypeScriptも導入しておきます。

npx tsc --init

README.mdとsrc/index.tsを追加しておきます。

echo "# semantic-release-sample" > README.md
mkdir src
echo "console.log('hello world')" > src/index.ts

各種設定

private: true

パッケージを非公開設定とするために、package.json"private: true"を追記します。

ただし、今回はnpm resistoryへpublishするためのsemantic-releaseの設定は実施しないためnpm resistoryへ公開されることはありませんが、プライベートなパッケージはこの設定を追記しておく方が良いかと思います。

  "private": true

semantic-release

package.jsonに以下を追記します。これはmasterブランチにマージされたコミットのメッセージを分析してGitHubにタグとリリースノートを作成するための設定です。

  "release": {
    "plugins": [
      "@semantic-release/commit-analyzer",
      "@semantic-release/release-notes-generator",
      "@semantic-release/github"
    ],
    "branches": [
      "master"
    ]
  },

ちなみに、"plugins"の配列の末尾に@semantic-release/npmを追加すると、npm publish関連のリリースステップが実行されます。今回はプライベートなnpmパッケージの想定なので、記述していません。

commitlint & husky

semantic-releaseではコミットメッセージが重要になるため、コミット時にメッセージが形式通りになっているかチェックするようにします。package.jsonに以下を追記します。

  "commitlint": {
    "extends": [
      "@commitlint/config-conventional"
    ]
  },
  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  }

なお、これらの設定はpackge.jsonではなく、.huskyrc.json, .commitlinrc.jsonを作成して記載してもOKです。semantic-releaseの設定も同様に.releaserc.jsonに記載してもOKです。

GitHub Actions

最後に、GitHub Actionsの設定ファイルを作成します。以下の例を参考にします。

今回は使用しないNPM_TOKENをコメントアウトした以外は例の通りです。

.github/workflows/release.yml

name: Release
on:
  push:
    branches:
      - master
jobs:
  release:
    name: Release
    runs-on: ubuntu-18.04
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Setup Node.js
        uses: actions/setup-node@v1
        with:
          node-version: 12
      - name: Install dependencies
        run: npm ci
      - name: Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          # NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: npx semantic-release

semantic-releaseを試す

コミットメッセージのルール

これまで設定したファイルをfirst commitというメッセージでコミットすると、以下のようにhuskyによりエラーになります。

$ git commit -m 'first commit'
husky > commit-msg (node v12.18.3)
⧗   input: first commit
✖   subject may not be empty [subject-empty]
✖   type may not be empty [type-empty]

✖   found 2 problems, 0 warnings
ⓘ   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint

husky > commit-msg hook failed (add --no-verify to bypass)

コミットメッセージはsemantic-releaseによって解析され、メッセージの内容に応じてバージョンが更新される・リリースノートに記載されます。

下記はsemantic-releaseのREADME.mdを参考に作成したメッセージ内容とリリースタイプの対応表です。

コミットメッセージ リリースタイプ バージョン更新例
fix(books): 書籍取得関数の取得件数の誤り修正 パッチリリース v1.0.0 → v1.0.1
feat(books): 書籍削除関数の追加 マイナーリリース v1.0.0 → v1.1.0
perf(books): 取得件数オプションを削除

BREAKING CHANGE: これは破壊的変更です

メジャーリリース v1.0.0 → v2.0.0

type(scope): messageという形式でコミットメッセージを書きます。

typefixfeat以外にも様々な種類があります。fixfeat以外はセマンティックバージョンの更新に影響しません。いずれのtypeでも、BREAKING CHANGE:をコミットメッセージの末行に記載するとメジャーアップデートとして処理されます。

  • feat: A new feature
  • fix: A bug fix
  • docs: Documentation only changes
  • style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
  • refactor: A code change that neither fixes a bug nor adds a feature
  • perf: A code change that improves performance
  • test: Adding missing or correcting existing tests
  • chore: Changes to the build process or auxiliary tools and libraries such as documentation generation

引用元

scopeは変更対象のモジュール名や機能等を記載します。scopeはGitHubのリリースノートにも記載されるため、適当な文字列ではなくライブラリとして意味のある文字列の方が良いかと思います。

そして、commitlintは上記のようなメッセージ形式になっているかをチェックしてくれます。そのため、コミットメッセージの不足・誤りで重要な機能追加や修正がセマンティックバージョンの更新に反映されない、といった事故を防ぐことができます。

なお、commitlintはConventional Commitsという規約に基づいてチェックしています。commitlintは他にも様々な設定ができて奥が深いです。気になる方はGitHubリポジトリのREADME.mdを参照ください。

では、実際にコミットメッセージをルールにしたがって書いてpushします。今回は初回コミットにつきtypechoreにしています。

$ git commit -m 'chore: first commit'

なお、GitHubは新規作成したリポジトリのデフォルトブランチがmainになっているため、新たにmasterブランチを作成してデフォルトブランチをmasterに変更します。mainブランチを利用しても良いのですがmasterの方が馴染みがあるため…。

GitHub Actionsでsemantic-releaseを実行する

GitHub Actionsによるリリースを確認します。まずはsrc/index.tsを適当に修正します。

- console.log('hello world');
+ console.log('hello world!!!!!!!!!');

こちらの修正はfix扱いとしてコミットし、masterブランチにpushします。

$ git add .
$ git commit -m 'fix(hello world): add many exclamation mark'
$ git push

GitHub Actionsを確認すると、masterブランチへのpushに対応してワークフローが実行されます。

ワークフロー内では下図のようなジョブが実行されています。

重要なのは下から三番目のReleaseです。下図の通り、semantic-releaseによりコミットメッセージの解析、タグの作成、リリースノートの作成等が自動で実行されています。ちなみに、今回は以前のタグ・リリースが存在しなかったためv1.0.0として初回リリースが実行されています。

GitHubのリリースを確認すると、タグとリリースノートが作成されていることがわかります。リリースノートにはコミットメッセージ(fix(hello world): add many exclamation mark)に応じた内容が記載されています。

複数コミットで試してみる

通常の開発シーンではdevelopブランチからfeatureブランチを切ってプルリクエストを作成して…といった手順を踏むと思いますが、今回は簡略化します。

ローカルでmasterブランチからfeatureブランチを切って複数コミットを作成し、masterブランチにマージしてGitHubにpushしてみます。

$ git checkout -b feature/test

以下のようにtypefixfeatのコミットを複数作成しました。これら一連のコミットメッセージがどのように分析され、結果的にどのようにバージョン管理されるのか確認します。

$ git log --pretty=format:"%s" -4
feat(hello world): feat2
fix(hello world): fix2
feat(hello world): feat1
fix(hello world): fix1

このfeatureブランチの変更をmasterブランチにマージし、GitHubにpushします。

$ git checkout master
$ git merge feature/test
$ git push

Github Actionsが実行された結果、マイナーリリースとしてバージョンがv1.0.0からv1.1.0にあがりました。

このように、複数コミットが存在する状態でもsemantic-releaseが適切な次のバージョンを判定してくれます。2回パッチリリース、2回マイナーリリースしてv1.2.2、といった更新にはなりません。

メジャーリリースしてみる

以下のように、コミットメッセージの末行にBREAKING CHANGEを含むコミットを作成してGitHubにpushします。

feat(hello world): feat3

BREAKING CHANGE: this is a breaking change

GitHub Actionsが実行された結果、メジャーリリースとしてバージョンがv1.1.0からv2.0.0にあがりました。

まとめ

以上のように、semantic-releaseとGitHub Actionsを組み合わせると簡単にnpmパッケージのバージョン管理・リリースノート作成が行えます。本記事のように、npm resistoryへpublishしないプライベートなパッケージでも利用可能です。

今回はnpmパッケージでしたが、semantic-release自体はnpmパッケージ以外(package.jsonが存在しないプロジェクト)でも使用できます。また、各種設定やプラグインを利用することで目的に合わせてリリースフローをカスタマイズすることができます(例えばchange logの作成 等)。

もし個人や業務でプライベートなnpmパッケージを開発・運用する場合は、ぜひGitHub Actionsとsemantic-releaseを使ってみてください。