BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

GitHub Actionsとrelease-it npmでリリース作業を自動化する

BASE BANK 株式会社 Dev Division でSoftware Developer をしている清水( @budougumi0617 )です。

みなさんの開発現場でも社内ライブラリ・モジュールとして開発しているコード・GitHubリポジトリがあると思います。
そのようなリポジトリはパッケージ管理システムを経由して利用することがほとんどですが、そのためにはリリース作業を行う必要があるかと思います。

私のチームでは先日GitHubリポジトリのリリース作業をGitHub Actionsで自動化したので、本記事ではその内容を共有したいと思います。

TL;DR

今回はGitHub Actionsとrelease-it npmを使っています。

github.com

www.npmjs.com

上記の技術を組み合わせることで次のような自動リリースのワークフローを構築しました。

  • (Pull Requestがマージされるなどで)mainブランチにコミットがプッシュされたらタグを打ち、GitHubリリースを作成する。
  • 前回リリースとの差分でコミットメッセージのリリースノートを作成する
  • 特定のファイルまたはディレクトリが更新されていたときだけリリースする
  • コミットメッセージに応じてセマンティックバージョンのパッチ/マイナー/メジャーアップデートを切り替える
  • Actions上でしか使わないnpmパッケージなので、リポジトリにpackage.jsonを置かない

GitHub Actionsを使って自動化を行なうと、コミット内容に応じた操作が簡単に実現できます。
ただし、次の制約もありました。

  • プロテクトブランチを利用している場合はGitHub Actions上からコミットをプッシュすることはできない
    • リリース用のPRをつくるといった迂回策が必要

そのため、今回の自動リリースでは「リポジトリ内のversion変数の値を更新してコミットしておく」のような操作は含んでいません。

なお、今すぐ試してみたい方は以下の2つのファイルを用意するだけで実現できます。

  • .release-it.json
  • .github/workflows/release.yml

.release-it.jsonの内容は次のとおりです。GitHubリポジトリのルートディレクトリに配置します。

{
  "requireUpstream": false,
  "requireCleanWorkingDir": false,
  "github": {
    "release": true
  },
  "git": {
    "commit": false,
    "push": false,
    "requireUpstream": false,
    "requireCleanWorkingDir": false
  },
  "npm": {
    "publish": false,
    "ignoreVersion": true
  }
}

.github/workflows/release.ymlの内容は次のとおりです。

name: auto release demo
on:
  push:
    # mainブランチにコミットがpushされたときに限定
    branches:
      - main
    # 上記条件に加えてgenディレクトリ配下が変更されたときのみという条件を追加
    paths:
      - gen/**
jobs:
  auto-release:
    runs-on: ubuntu-latest
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      RELEASE_IT_VERSION: 14.2.1
    steps:
      - name: Check out codes
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: '12'
      - name: Set releaser settings
        run: |
          git config --global user.name release-machine
          git config --global user.email email@example.com
      - name: Major release
        id: major
        if: contains(toJSON(github.event.commits.*.message), 'bump up version major')
        run:  npx release-it@${RELEASE_IT_VERSION} -- major --ci
      - name: Minor release
        id: minor
        # メジャーバージョンアップをしていないときマイナーバージョンアップを行なうか
        if: steps.major.conclusion == 'skipped'  && contains(toJSON(github.event.commits.*.message), 'bump up version minor')
        run:  npx release-it@${RELEASE_IT_VERSION} -- minor --ci
      - name: Patch release
        # コミットメッセージに特に指定がない場合はマイナーバージョンを更新する
        if: "!(steps.major.conclusion == 'success' || steps.minor.conclusion == 'success')"
        run:  npx release-it@${RELEASE_IT_VERSION} -- patch --ci

今回のサンプルYAMLの場合はmain ブランチにgenディレクトリ内への変更を含んだPRをマージすると自動でリリースが行なわれます。
また、bump up version majorといったメッセージが含まれていた場合はメジャーバージョンアップが行なわれます。

f:id:budougumi0617:20201126022318p:plain
サンプルリリースページ

なお、本記事で利用している各ツールのバージョンは以下のとおりです。

ツール名 バージョン
GitHub Actions v2
release-it npm 14.2.1
Node.js 12.X系

以下のURLは実際にGitHub Actionsで何回か自動リリースをしてみたサンプルリポジトリです。

リリース作業を自動化したい

どんな言語を使っていても、業務で開発を行なっていると社内ライブラリを作成することがあると思います。
作成したライブラリはnpmやComposer、Go Modulesなどのパッケージ管理システムを経由して使うことになるのが大半だと思います。
そうなると一定の更新ごとにタグを設定し、バージョン管理する必要が出てきます。
とはいえ 「PRをマージしたらgit tagコマンドを打って…」と各開発者が行なうのは億劫です。
そのため、mainブランチにPRがマージされたら(コミットがプッシュされたら)自動でタグ打ち、リリースするという自動化を試みました。
もちろんタグはリリースのたびにセマンティックバージョンがインクリメントされるようにします。

GitHub Actions上でrelease-it npmを実行してリリースをする

release-it npmはよしなにセマンティックバージョンをインクリメントしながらリリースノートも作ってGitHubリリースを作成してくれるコマンドです。
たとえば、現時点のバージョンが0.1.2だったとき、次のように実行するとマイナーバージョンをインクリメントした0.2.0バージョンのリリースを作成してくれます。

$ npm run release -- minor --ci

次のリンクはrelease-it npmで作成されたリリースノートです。

f:id:budougumi0617:20201126022351p:plain
リリースノートのサンプル

GitHub Actions上でNode.js環境を用意して実行するだけで終わりかと思いきや、いろいろ設定する必要があったので、ポイントを解説していきます。

GitHub Actions実行時にタグも取得しておく

Set fetch-depth: 0 to fetch all history for all branches and tags.

今回構築する自動リリースのワークフローでは既存のタグからリリースするセマンティックバージョンを決定します。 そのため、GitHub Actions実行時にタグも一緒にチェックアウトしておく必要があります。タグはGitHub Actionsを利用時にほぼ100%useされているであろうactions/checkoutに対してfetch-depth: 0オプションを渡すだけで取得可能です。

      - name: Checkout codes
        uses: actions/checkout@v2
        with:
          fetch-depth: 0

特定のパス配下が更新されたときのみリリースする

今回自動リリースしたいリポジトリはコードの自動生成を行なっていました。そのため、次のような事情がありました。

  • PRがマージされるたびのリリースは (どんどんバージョンが上がってしまうので)してほしくない
  • 自動生成したコードが配置されている gen/ディレクトリの内容が変更されたときだけリリースしたい

OpenAPIやgRPCなどを利用して同リポジトリ内でクライアントコードを自動生成したりしていると、同様のニーズが生まれると思います。

最初はCircleCIを利用して自動リリースを実現しようと思ったのですが、パスを使ってCIを制御するのはGitHub Actionsのほうが簡単だったので、GitHub Actionsで自動リリース作業を行なうことにしました。

GitHub Actionsのワークフローでは次のような制御をすることができます。

コミット内容を確認して特定ディレクトリに更新があったか確認する

GitHub Actionsでは、on.<push|pull_request>.pathsを使うことで、特定ディレクトリに更新があったときだけにジョブの実行を制限できます 1

次のサンプルコードは以下の2つの条件を満たしたときのみ実行される設定です。

  • mainブランチにコミットがプッシュされた
  • プッシュされたコミットの中にgen/ディレクトリ内の更新が含まれていた
on:
  push:
    branches:
      - main
    paths:
      - gen/**

ワークフローの制御にコミットメッセージを利用する

このデータ構造はおそらく公式ドキュメントに明示的に載っていないのですが、GitHub Actionsでブランチにpushされた一連のコミットの情報をジョブ実行中に利用可能です。
ワークフロー実行時の情報はgithub contextとして参照できるのですが、この中のgithub.eventでpushされたコミットの情報を持っています2
この情報をパースするとコミットの内容をワークフロー中に使うことができます。
次のコードは「コミットのメッセージに'bump up version major'があったらtrueになる」式です。

contains(toJSON(github.event.commits.*.message), 'bump up version major')

これと、jobs.<job_id>.steps.if、を使うことで、「コミットメッセージによって実行されるstep」をワークフローに用意することができます。

jobs:
  sample:
    runs-on: ubuntu-latest
    steps:
      - name: teststep
        if: contains(toJSON(github.event.commits.*.message), 'コミットメッセージを確認')
        run: echo 'executed!!'

また、contextのsteps.<step id>.conclusionを用いることで前ステップの実行結果を利用して else if のような制御を行なうことも可能です。

jobs:
  ifelse-pattern:
    steps:
      - name: foo
        id: foo
        if: contains(toJSON(github.event.commits.*.message), 'foo')
        run:  echo 'if step!'
      - name: bar
        id: bar
        # fooステップがスキップされた && コミットメッセージにbarを含む場合に実行する
        if: steps.foo.conclusion == 'skipped'  && contains(toJSON(github.event.commits.*.message), 'bar')
        run:  echo 'elseif step!'

次のリンクは実際にステップをいくつかスキップしているActionsの実行結果です。

ここまではGitHub Actionsの設定方法でしたが、次はrelease-it npmをGitHub Actions上で使うコツです。

タグの設定とリリースはするが、コミットはしない

release-it npmはGitHub Actions上でnpxコマンドで実行しているのでpackage.jsonは不要です。 が、release-it npm用の設定を用意する必要があります。 今回利用している設定は次のとおりです。

{
  "requireUpstream": false,
  "requireCleanWorkingDir": false,
  "github": {
    "release": true
  },
  "git": {
    "commit": false,
    "push": false,
    "requireUpstream": false,
    "requireCleanWorkingDir": false
  },
  "npm": {
    "publish": false,
    "ignoreVersion": true
  }
}

ざっくり説明すると、次のような設定です。

  • GitHubリリースを作成する
  • gitのコミットは作成しない
  • gitのpushはしない
  • npmの公開はしない
  • バージョンを決定するためにpackage.json内のバージョンを参照しない

鋭い方は「”pushしない”ってことはタグも公開されないんじゃないの?」と思うかもしれませんが、いいのか悪いのか、リリースを行なうときにタグはプッシュされるようです。

Actionsからプロテクトブランチにはコミットをpushできない

ここまで便利なGitHub Actionsでしたが、ひとつ制約があります。それはプロテクトブランチにコミットをプッシュすることができない点です。抜け道がないか探していたのですがなさそうなので諦めました。
なので、先ほどのrelease-it npm用の設定ファイルは「タグの設定とリリースはするけどコミットはプッシュしない」という内容になります。
「自動リリースするときはpackage.jsonの中にあるversionも更新したい(コミットプッシュしたい)んだけど!」というようなニーズももちろんあると思います。
しかし、今回のユースケースではリリースタグでバージョンが管理されていればファイルとしてバージョンが参照できる必要はなかったので、こちらも妥協しました。

あとは開発するだけ!

以上の設定を行うと、特定条件のコミットを作るだけで自動でリリースされるようになります。
それぞれの設定は独立しているので、お好みでカスタマイズしていただけばと思います。

  • 特定のディレクトリに限定する必要はないのでon.<push|pull_request>.pathsの設定は削除する
  • マッチするコミットメッセージを変更する etc...
name: auto release demo
on:
  push:
    # mainブランチにコミットがpushされたときに限定
    branches:
      - main
    # 上記条件に加えてgenディレクトリ配下が変更されたときのみという条件を追加
    paths:
      - gen/**
jobs:
  auto-release:
    runs-on: ubuntu-latest
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      RELEASE_IT_VERSION: 14.2.1
    steps:
      - name: Check out codes
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: '12'
      - name: Set releaser settings
        run: |
          git config --global user.name release-machine
          git config --global user.email email@example.com
      - name: Major release
        id: major
        if: contains(toJSON(github.event.commits.*.message), 'bump up version major')
        run:  npx release-it@${RELEASE_IT_VERSION} -- major --ci
      - name: Minor release
        id: minor
        # メジャーバージョンアップをしていないときマイナーバージョンアップを行なうか
        if: steps.major.conclusion == 'skipped'  && contains(toJSON(github.event.commits.*.message), 'bump up version minor')
        run:  npx release-it@${RELEASE_IT_VERSION} -- minor --ci
      - name: Patch release
        # コミットメッセージに特に指定がない場合はマイナーバージョンを更新する
        if: "!(steps.major.conclusion == 'success' || steps.minor.conclusion == 'success')"
        run:  npx release-it@${RELEASE_IT_VERSION} -- patch --ci

終わりに

「PRマージしたぞー!」と思っても、そのあとにポチポチリリース作業をするのは億劫でした。
これで少しでも生産性があがるといいなと思っています。

なお、GitHub Actionsからプロテクトブランチへの直接コミットプッシュはできないのですが、renovateはPRを作成、Appで自動承認、自動マージという迂回をしてプロテクトブランチへのプッシュを実現しているようです。

GitHub Apps - renovate-approve · GitHub

もっと突き詰めたくなったら同様の操作を実装してファイル更新も含めた自動リリースを実現したいなと思います。

最後に、BASE BANKでは新しくデザイナーとカスタマーサクセスの募集を開始したので、ぜひご応募お待ちしています。

www.wantedly.com

www.wantedly.com

参考リンク


  1. 「特定のディレクトリに更新があったときは無視する」という逆制御もできます。

  2. contextをechoして無理やり確認しました。