🤝

semantic-release と GitHub Protected Branch の共存を実現する

2024/09/30に公開

これはなに

semantic-release は GitHub Protected Branch との相性が悪いと随所で言われていますが、その理由と解決策についてまとめたものです。Protected Branch に対して semantic-release が差分を直接コミットするための手法を紹介します。

前提知識

@semantic-release/git

@semantic-release/git は、semantic-release によるリリースフローで発生した差分を Git リポジトリにコミットするプラグインです。例えば Node パッケージのリリースプロセスでは package.jsonversion フィールドや CHANGELOG.md を生成ないし更新しますが、これらの差分を Git リポジトリーにコミットするために @semantic-release/git が利用されます。

GitHub Protected Branch

https://docs.github.com/ja/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches

GitHub にはブランチに対して保護を設定する機能があります。これにより、ブランチへの直接プッシュやマージが制限されます。例えば main ブランチに対して保護を設定することで main ブランチへの直接プッシュを禁止し、代わりに Pull Request を通じてマージするように促せるため、開発プロセスの整備に役立ちます。

問題点

@semantic-release/git はリリースするプロセスが実施されるブランチに対して差分をコミットしますが、そのブランチが保護設定されている場合、コミットが拒否され失敗してしまいます。当然ですね。そのための保護設定なのだから。Protected Branch の設定で "Allow force pushes" を有効にしても同様に失敗します。

Allow force pushes

失敗する原因

@semantic-release/git が Protected Branch へのコミットに失敗するのは、semantic-release が参照する GITHUB_TOKEN に十分な権限がないためです。

GitHub Actions (GHA) でリポジトリーに対し何かしら操作する場合は、 secrets.GITHUB_TOKEN という GitHub が自動生成するトークンを利用するのが一般的です。このトークンは GHA ワークフローを実行する度に自動生成され、有効期間が短いことから非常に手軽かつセキュアに利用できるのが強みですが、その分権限範囲が非常に狭く、Protected Branch へのコミット権限を持っていません。これが原因で @semantic-release/git が失敗するというわけです。

https://docs.github.com/ja/actions/security-for-github-actions/security-guides/automatic-token-authentication

一般的に知られている回避策

Personal Access Token を利用する

Personal Access Token (PAT) は、個々の GitHub ユーザーアカウントが発行するトークンであり、secrets.GITHUB_TOKEN よりも多くの権限を持たせられます。よって、これを semantic-release が参照する GITHUB_TOKEN として利用することで Protected Branch へのコミットが可能となります。

https://docs.github.com/ja/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens

手軽に利用できるため、semantic-release 公式ドキュメントや多くのブログ記事でこの方法が紹介されています。しかし PAT は特定個人の GitHub アカウントに直接紐づいたものであり、いうなればそのアカウントになりすましてリポジトリーを操作するのと同じです。つまり、アカウントのパスワードのようなものであることから、セキュリティリスクが高いことが指摘されています。

Pull Request を通じてコミットする

一般的に Protected Branch は Pull Request を通じて差分をマージすることを前提としているため、リリースプロセスで生じる差分を Pull Request に変換すればこの問題を回避できます。 changesets など他のリリースツールはこの手法を採用していますが、semantic-release の強みである「徹底した自動化」が大幅に削がれてしまいます。

解決策: GitHub App を利用する

リリースワークフローのための GitHub App を作成し、この App が発行するトークンを semantic-release に参照させて Protected Branch への直接コミットを可能にします。この方法であれば PAT のように特定個人のアカウントに依存しないトークンを semantic-release に参照させられます。

GitHub App とは

GitHub App とは、GitHub に対し Bot のような振る舞いをする処理を実現するものであり、リポジトリーや Organization にインストールして使用します。Slack + GitHubCodecov などが有名な GitHub App ですが、今回のようにリリースワークフローで参照するトークンを発行するだけのシンプルな App も簡単に作成できます。

https://docs.github.com/ja/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps

PAT は有効期限を無制限に設定できないため定期的に再発行する手間が発生しますが、GitHub App によるトークンは secrets.GITHUB_TOKEN と同様に GHA ワークフロー実行の度に発行されるため有効期限が短く、セキュリティリスクが低いのが利点です。

また、GitHub Organization に紐づいた App も作成できるため、これならば Organization 全体で利用可能となると同時に属人性を完全に排除できます。

手順

1. GitHub App を作成する

Register new GitHub App ページにて GitHub App を新規作成します。以下の設定を行います。

  • GitHub App name: Marketplace 全体でユニークな名前にすること
  • Homepage URL: http://localhost など適当な URL で OK
  • Webhook:
    • Active: 使用しないのでチェックを外す
  • Permissions:
    • Repository permissions:
      • Administration: Read and Write
      • Contents: Read and Write
      • Issues: Read and Write
      • Metadata: Read-only
      • Pull Requests: Read and Write
  • Where can this GitHub App be installed?: Only on this account を選択

2. GitHub App をインストールする

作成した GitHub App を対象リポジトリにインストールします。https://github.com/settings/apps を開き、作成した GitHub App の Edit ボタンを押します。

Install GitHub App

次に左メニューにある Install App を選択し、表示されている GitHub アカウントを選択してその配下にあるリポジトリーを選択します。

Select repositories

All を選択すればその GitHub アカウントが持つ全てのリポジトリーへ一括インストールされますが、個別に選択するのが望ましいでしょう。

3. App ID と秘密鍵を secrets としてリポジトリーに登録する

GHA ワークフローから GitHub App にアクセスするためには、App ID と秘密鍵が必要です。これらをリポジトリーの secrets として登録します。

https://github.com/settings/apps/[app-name] にアクセスし、App ID をメモしておきます。

次に Private keys セクションにある Generate a private key ボタンを押し、秘密鍵( pem ファイル)をダウンロードします。

Generate a private key

App ID と秘密鍵を入手したら、これらをリポジトリーの secrets として登録します。リポジトリーの Settings > Secrets and Variables > Actions と進み、以下の secrets を登録します。名称は任意ですが、以下の例では BOT_APP_IDBOT_PRIVATE_KEY にしています。

  • BOT_APP_ID: App ID
  • BOT_PRIVATE_KEY: 秘密鍵の内容

これで GHA ワークフロー から GitHub App を認可できるようになりました。

4. Branch Protection の設定を変更する

Require a pull request before merging オプションにチェックが付いていることを確認します。

5. GitHub Actions ワークフローを設定する

secrets.GITHUB_TOKEN を用いたワークフローを GitHub App が発行するトークンを用いるように変更します。GitHub App の参照には create-github-app-token という GitHub 公式の GHA を使います。

.github/workflows/release.yml
name: Release

on:
  push:
    branches:
      - main

permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
+     - name: Generate Token
+       id: app-token
+       uses: actions/create-github-app-token@v1
+       with:
+         app-id: ${{ secrets.BOT_APP_ID }}
+         private-key: ${{ secrets.BOT_PRIVATE_KEY }}

      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
+         persist-credentials: false

      - name: Setup node.js
        uses: actions/setup-node@v4

      - name: Install dependencies
        run: npm install

      - name: Release
        run: npm run release
        env:
-         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+         GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

actions/checkout を呼びだす際に persist-credentials オプションを必ず false にします。この設定を行わないと secrets.GITHUB_TOKEN が参照され続けてしまい、GitHub App が発行するトークンが利用されず CD が失敗します。

手順は以上です。これで Protected Branch に対して semantic-release が差分を直接コミットできるようになります。

締め

GitHub App を活用することで、リポジトリーのセキュリティーを維持しつつ semantic-release の強みである「徹底した自動化」の両立が可能となります。リリースワークフローの厳格性を保ちつつソフトウェア開発に取り組むことで、品質の向上や開発効率の向上に貢献できることでしょう。

参考文献

https://gonzalohirsch.com/blog/semantic-release-and-branch-protection-rules/

https://stackoverflow.com/questions/74744498/github-pushing-to-protected-branches-with-fine-grained-token/76550826#76550826

https://www.codingfeline.com/2020/04/18/actions-checkout-v2-push/

Discussion