Zenn
🫥

うん。これで事故るならしょうがない。GitHub ルールセットでGitの操作ミスと無縁になる

2024/10/17に公開
2

『あっぶね。mainブランチにpushするとこだった。。』こんなヒヤリハットとは無縁になり、心理的安全性の高いGit環境を実現するGitHubのルールセット(ブランチ保護ルールの進化系)を作り込んだので、機能説明と併せてご紹介します。
”ジュニアなメンバーは絶対に事故らないガードレールを設置し、コアメンバーが責任を持ってハンコを押す。”そんなGit環境の構築を目指します。

GitHubルールセットとは?

公式ドキュメントを要約すると、Gitのブランチ、またはタグへのPushアクションを実施できるアクターを限定するなどのガードレールをGitリポジトリーに設置できる機能です。GitHubユーザーであればブランチ保護ルール(branch protection)には馴染みがあるかと思いますが、ブランチ保護ルールに追加のルールが加わり、設定の柔軟性が増したものと考えてもらえれば良いです。(ルールセットGAのブログ

ルールセットの仕様には以下の特徴があります。

  • 複数のルールを設定可能。複数のルールで同じ設定項目が定義されている場合、最も制限の厳しいルールが適用される。(優先順位の様な概念は存在しない)
  • リポジトリ個別で設定するだけでなく、Organizationレベルでルールを設定できる(Teamプラン以上の契約が必要)
  • Enterpriseプランのみが対象のルールも複数存在する(コミットメッセージを検証してくれる等)

ルールセットは高機能なため、この記事では以下の前提を置かせていただきます。

  • GitHubのTemaプラン契約で利用できるルールに限定
  • Organization配下のprivateリポジトリが対象
  • リポジトリ毎にルールセットを設定する(Organizationレベルでの設定は対象外)
  • CI(CD)にはGitHubアクションを利用

この記事で紹介するルールセットの要件

まず実現したい要件です。ブランチのみ保護するパターンと、Botと連携しつつタグも保護する2パターンを定義します。

ルールセットでは特定のルール適用を除外(パイパス)できるアクターをバイネームではなくOrganizationのリポジトリロールで指定します。本記事でも利用する、デフォルトのOrganizationのリポジトリロールを転載しておきます。

ロール 説明
Read プロジェクトの表示またはディスカッションを行う、コードを書かないコントリビューターにお勧めします
Triage issues、ディスカッション、pull request を予防的に管理する必要があるが Write アクセス権は必要ない共同作成者にお勧めします
Write プロジェクトに積極的にプッシュするコントリビューターにお勧めします
Maintain リポジトリを管理する必要があるが、機密または破壊的なアクションへのアクセス権は不要なプロジェクト マネージャーにお勧めします
Admin セキュリティの管理やリポジトリの削除などの機密および破壊的なアクションを含む、プロジェクトへのフル アクセス権が必要な方にお勧めします

本流ブランチを保護する - 基本パターン

ジュニアなメンバー、外部のパートーナーさんの事故リスクをゼロにしつつ、コアメンバーにコードレビューを義務化されることで、コードの品質を担保させる要件です。

  • ブランチ戦略にはGitHub Flowを採用し、mainブランチを保護対象とする
  • mainブランチに更新を反映させるには、以下の条件が必要
    • Pull Requestを経由する必要がある。(直接のPushは禁止)
      • Pull Requestには2人以上のレビューApproveが必要。ただしhead(feature)ブランチにpushしたアクターを除く。
        • レビューApproveにカウントされるのは、CODEOWNERSに定義されたアクターに限られる
        • レビューApprove後、head(feature)ブランチが更新された場合、更新前のレビューApproveはリセットされる(再レビューが必要)
      • Pull Request上で実施されたディスカッションは全て解決する必要がある
    • 規定のステータスチェックをパスする必要がある(GitHubアクションのCIアクション)
    • headブランチ(featureブランチ)は、mainの最新を取り込んでいなくても良い(コンフリクトが無い場合)
  • mainブランチの削除、force pushは禁止
  • Pull Requestのマージは、Linear history(Squash、Rebase)を強制
  • Pull Requestのマージは、Admin、またはMaintainロールに所属するアクターのみ可能

Botと連携しつつ、タグも保護する - アドバンスドなパターン

別の記事で紹介しましたが、release-pleaseと統合し、リリースノート、リリースタグの設定をBotに実施させるケースでの追加要件を定義します。

https://zenn.dev/kuritify/articles/conventional-commits-definitive-guide

  • Release PRにも、基本要件と同じルールを適用
  • Gitタグの操作はBotのみ可能(Admin、 Maintainロールも不可)

ルールセットの設定内容と機能検証

前章で定義した要件を実現するルールセットの設定内容を、各種ルールの機能検証をしつつ説明していきます。

次章で詳細を説明しますが、画面でポチポチするのも辛いので、対話形式でルールセットを設定するツールを作ってみました。コードベースの方が理解しやすい方はこちらを参照ください。

https://github.com/kuritify/no-more-git-oops

本流ブランチを保護する - 基本パターン

先に最終的な設定内容を共有します。以下2つのルールセットを設定することで実現します。

  • branch-all-users-rules: 全ユーザーに適用される基本ルール
  • branch-exclude-core-contributors-rule: コアメンバー以外に適用されるルール

これらのルールセットを適用すると、Gitアクティビティはそれぞれ以下の挙動になります。

Pull Requestマージの条件が揃っていない状態。

Pull Requestマージの条件が揃った状態。コアメンバーのみマージが可能で、Liner History(Squash、Rebase)でのマージのみ可能です。

Adminロールを付与されたユーザーであっても、Pull Requestを経由しないPushはエラーになり、ルールセットで禁止される旨が通達されます。

$ git push origin main
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 297 bytes | 297.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
remote: error: GH013: Repository rule violations found for refs/heads/main.
remote: Review all repository rules at https://github.com/kuritify-org/rulesets-poc/rules?ref=refs%2Fheads%2Fmain
remote:
remote: - Changes must be made through a pull request.
remote:
remote: - Required status check "code-check" is expected.
remote:
remote: - Cannot update this protected ref.
remote:
To github-k.com:kuritify-org/rulesets-poc.git
 ! [remote rejected] main -> main (push declined due to repository rule violations)
error: failed to push some refs to 'github-k.com:kuritify-org/rulesets-poc.git'

# 当然、force pushも禁止されます
$ ❯ git push --force origin main
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 297 bytes | 297.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
remote: error: GH013: Repository rule violations found for refs/heads/main.
remote: Review all repository rules at https://github.com/kuritify-org/rulesets-poc/rules?ref=refs%2Fheads%2Fmain
remote:
remote: - Changes must be made through a pull request.
remote:
remote: - Required status check "code-check" is expected.
remote:
remote: - Cannot update this protected ref.
remote:
To github-k.com:kuritify-org/rulesets-poc.git
 ! [remote rejected] main -> main (push declined due to repository rule violations)
error: failed to push some refs to 'github-k.com:kuritify-org/rulesets-poc.git'

万が一直接のpush操作が必要になった場合、branch-all-users-rulesのパイパスリストに、Adminロールを一時的に設定することで、他のアクターのガードレールはそのままに、直接のpush操作が可能になります。

それでは早速各種ルールの詳細を説明していきます。

まずは保護するブランチを指定します。Add targetで、include by patternを選択し、mainを設定します。(release/*のようにワイルドカードも指定可能です)

続いて、以下の要件を実現するルールを設定します。

  • Pull Requestを経由する必要がある。(直接のPushは禁止)
    • Pull Requestには2人以上のレビューApproveが必要。ただしhead(feature)ブランチにpushしたアクターを除く。
      • レビューApproveにカウントされるのは、CODEOWNERSに定義されたアクターに限られる
      • レビューApprove後、head(feature)ブランチが更新された場合、更新前のレビューApproveはリセットされる(再レビューが必要)
    • Pull Request上で実施されたディスカッションは全て解決する必要がある

ほとんど直感的に理解できるルールかと思います。Require review from Code Ownersを有効にしますので、CODEOWNERSファイルをリポジトリに配置しておく必要があります。以下の様に、コアメンバーTeamの指定が保守性が高いでしょう。また、CODEOWNERSに設定するユーザー(必須レビュワー)は、Admin、Maintainロールのユーザーでなくても問題ありません。

$ cat .github/CODEOWNERS
* @yourr-org-name/your-team-name

Monorepoを管理しており、同じリポジトリ内でも必須レビュワーが異なる場合は、ディレクトリ単位で設定も可能です。

$ cat .github/CODEOWNERS

# IaCはクラウンドエンジニアを設定
iac/** @your-cloud-developer-a @your-cloud-developer-b

# DevOps担当者をレビュワーに
.github/** @your-devops-a @your-devops-b

# 上記以外は、your-team-nameにフォールバックされます
* @yourr-org-name/your-team-name

Require approval of the most recent reviewable pushを有効にすることで、headブランチ(featブランチ)にpushしたユーザーのレビューは、必須Approver数にカウントされません。この設定により、必ずコードを修正したユーザー以外のレビューを必須にできます。

次に以下の要件を実現するルールを設定します。

  • 規定のステータスチェックをパスする必要がある(GitHubアクションのCIアクション)
  • headブランチ(featureブランチ)は、mainの最新を取り込んでいなくても良い(コンフリクトが無い場合)

前者の要件はRequire status checks to passにチェックをし、Add Checksにパスを必須とするステータスチェックを入力します。これはheadブランチ(featureブランチ)の最新コミットがcommit status APIでパスにマークされたかどうかを判定します。

GitHubアクションを利用している場合、GitHubアクションJobの成否が、commit statusの成否になるため、以下の様なGitHubアクションを配置し、Add Checksのフォームにcode-checkを入力し設定します。

name: Continuous Integration

on:
  pull_request:
    branches:
      - main
    types: [opened, reopened, synchronize]

jobs:
  # このジョブ名を設定する
  code-check:
    runs-on: ubuntu-latest
    steps:
      - name: run code check
        run: |
          echo 'format, lint, UT and more'

後者のheadブランチ(featureブランチ)は、mainの最新を取り込んでいなくても良い(コンフリクトが無い場合)要件実現のため、Require branches to be up to date before mergingを有効にしない意思決定をしました。

別で有効にしているDismiss stale pull request approvals when new commits are pushedと、このRequire branches to be up to date before mergingが組み合わさると、baseブランチの最新版を取り込む度にそれまでのレビューのApproveカウントがリセットされてしまいます。mainブランチには、並行かつ高頻度で変更がなされるため、その変更を取り込む度に再度Approveが必要になるのは開発体験が低いであろうという理由です。コンフリクトが起きていれば、解決のタイミングで再レビューが必要になるため、品質への影響は最小限で済むはずです。

続いてPull Requestのマージ方法をLiner historyに限定します。

  • Pull Requestのマージは、Liner history(Squash、Rebase)を強制

続いて危険な操作を禁止にしていきます。

  • mainリポジトリの削除、force pushは禁止

ここまでの設定をbranch-all-users-rulesとして保存します。

最後に、以下の要件を実現するため、branch-exclude-core-contributors-ruleを作成し、キャプチャーの様に、ルールはRestrict updatesのみチェックし、Bypass listRepository adminMaintainロールをAlwaysで設定します。

  • Pull Requestのマージは、Maintain、またはAdminロールの人間のみ可能

Restrict updatesルールの設定を切り出すことで、Admin、Maintainロールのアクターであっても、mainブランチの直接Pushは禁止しつつ、Pull Requestのマージを該当ロールに所属するアクターに限定できます。Pull Requestマージの条件はパイパスされないため、必須レビュー、status checkパスの条件は同様に適用されます。

バイパスの設定時に、For pull requests onlyも選択できますが、Alwaysにする理由があります。万が一トラブルシュートで直接Pushが必要になった場合、branch-all-users-rulesのパイパスリストに担当する人間のロールを追加することで、一時的に直接Pushを許可させることができますが、For pull requests onlyにしていると、引き続き直接Pushが禁止されたままになるためです。

以上で本流ブランチを保護する設定は完了です。保護対象のブランチやstatus check名を御社の要件にあわせてカスタマイズし、ご利用いただければ幸いです。

Botと連携しつつ、タグも保護するアドバンスなパターン

アドバンスドなパターンとして、release-pleaseと統合したフローでの追加ルールセットと、追加で必要な対応について説明します。

release-pleaseと統合している前提になりますが、Botと連携する開発フローをお持ちの方にも参考になるかと思います。

再掲になりますが、別記事でrelease-pleaseと統合した開発フローについて紹介しました。
https://zenn.dev/kuritify/articles/conventional-commits-definitive-guide

release-pleaseはmainブランチにpushされたConventional Commitsを検知して、自動生成したCHANGELOGとVersion Bumpを含むRelease PRをBot経由で作成、更新(追加コミットの追従)をしてくれます。リリースのタイミングが来たら、人間がRelease PRをマージします。Release PRマージをトリガーに、BotがGitタグとGitHubのリリースを発行してくれます。

このリリースフローをより堅守にするため、以下の要件を定義しています。

  • Release PRにも、基本要件と同じルールを適用
  • Gitタグの操作はBotのみ可能(Admin、Maintainロールも不可)

まず前者の要件の実現方法です。Release PRの作成、かつRelease PRのheadブランチのPushは、Botユーザーが実行するため、GitHubアクションデフォルトの自動生成されるアクセストークンを利用する場合、別のGitHub Actionsがトリガーされず、その結果、パスが必須になっているstatus checkが動かないため、マージ可能な条件を満たせない。という状況になります。

release-please公式でもこの点が留意事項として挙げられています。

By default, Release Please uses the built-in GITHUB_TOKEN secret. However, all resources created by release-please (release tag or release pull request) will not trigger future GitHub actions workflows, and workflows normally triggered by release.created events will also not run. From GitHub's docs:

When you use the repository's GITHUB_TOKEN to perform tasks, events triggered by the GITHUB_TOKEN will not create a new workflow run. This prevents you from accidentally creating recursive workflow runs.

-- https://github.com/google-github-actions/release-please-action?tab=readme-ov-file#github-credentials

この問題に対応するため自動で生成されるアクセストークンをGitHub Appのインストール アクセス トークンに置き換えます。

またまた手前味噌になりますが、GitHub Appのインストール アクセス トークンの使い方、GitHub Appの設定方法は別の記事で紹介しています。
https://zenn.dev/kuritify/articles/gitsubmodule-and-action#github-actionsとの統合

上記の記事のGitHub Appは別リポジトリをcloneするのが目的であるため、release-pleaseに必要な権限を追加する必要があります。

このGitHub Appを作成した状態で、release-please用のGitHub Actionsを以下の様に設定することで、Release PRにおいてもGitHubアクションがトリガーされ、必須status checkをパスできます。

name: Release

on:
  push:
    branches:
      - main

permissions:
  contents: write
  pull-requests: write

jobs:
  release-please:
    runs-on: ubuntu-latest
    steps:
      - name: Generate a Git App installe access token
        id: generate-token
        uses: actions/create-github-app-token@v1.11.0
        with:
          app-id: ${{ vars.GH_APP_ID }}
          private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
          owner: "<your-org>"

      - name: release-please
        uses: googleapis/release-please-action@v4
        id: release
        with:
          token: ${{ steps.generate-token.outputs.token }}

続いて、後者のGitタグの操作はBotのみ可能(Maintain、Adminロールも不可)要件の実現方法です。release-pleaseの実行を独自のGitHub APPに切替えたことで、Gitタグの発行も同じGitHub App経由で実行されます。

以下のタグルールセットを作成し、パイパスリストに独自GitHub Appを設定してあげることで対応できます。

この状態で、GitタグをPushしようとすると、Adminロールであってもエラーになります(エラーメッセージがわかりづらいですが)。

$ git tag -a created-admin-role -m 'created by admin role'
$ git push origin created-admin-role
Enumerating objects: 10, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 8 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 716 bytes | 716.00 KiB/s, done.
Total 6 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
remote: error: GH013: Repository rule violations found for refs/tags/created-admin-role.
remote: Review all repository rules at https://github.com/kuritify-org/rulesets-poc/rules?ref=refs%2Ftags%2Fcreated-admin-role
remote:
remote: - Cannot create ref due to create name restrictions.
remote:
To github-k.com:kuritify-org/rulesets-poc.git

release-please経由(独自GitHub App経由)でのタグ発行は想定通り成功します。

ブランチ保護と同様に、Admin、Maintainロールをバイパスリストに設定することで、コアメンバーのみタグを扱えるといった要件を実現できます。

ルールセットの設定を自動化する

最後にルールセットの設定の自動化について紹介します。

前章にて、現状ベストだと考える設定を紹介しましたが、保護するブランチ、必須にするstatus check、必須レビューの数など、管理するコードベースで異なるのが常です。

そのため、最近ヘビーユースしているPLOPを拡張し、柔軟にルールセット設定を自動化するツールを開発しました。

https://github.com/kuritify/no-more-git-oops

対話形式のプロンプトを介し、本記事でご紹介したルールセットを設定できます。

御社の要件に基づいてカスタマイズ可能なAPIも提供しています。

PLOPは、主目的であるコードGeneratorの機能も非常に強力なので、PLOPにご興味ある方がいらっしゃればコメントください。別途記事を執筆します。

認証に使うトークンについては、Classic PAT(Personal Access Token)がシンプルで良いかと思います。Fine-grainedの場合、Organizationのポリシーによっては、管理者にアクセスできるリポジトリを設定してもらう必要があるためです。

Classic PATの権限はrepoにチェックをすればOKです。

もちろんREST API経由で自動化も可能です。

curl -L \
  -X POST \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer <YOUR-TOKEN>" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  https://api.github.com/repos/OWNER/REPO/rulesets \
  -d '{"name":"super cool ruleset","target":"branch","enforcement":"active","bypass_actors":[{"actor_id":234,"actor_type":"Team","bypass_mode":"always"}],"conditions":{"ref_name":{"include":["refs/heads/main","refs/heads/master"],"exclude":["refs/heads/dev*"]}},"rules":[{"type":"commit_author_email_pattern","parameters":{"operator":"contains","pattern":"github"}}]}'

BodyになるJSONは既存のルールをExportして再利用できるので、Web UIで設定し、Exportして利用するのが良いでしょう。

まとめ

以上、GitHub ルールセットでGit操作のオペレーションミスを防ぐ方法について、私なりのベスト設定も併せてご紹介しました。Git操作に限らず、適切なガードレールを設置することで心理的に安全な開発環境を提供できるよう努めていきましょう。

2

Discussion

ログインするとコメントできます