GitHubで保護ブランチを設定しよう
このハンズオンでは、GitHubのRulesets機能を使ってmainブランチを保護し、「CIをパスしたコードだけがPull Request経由でマージされる」という実務的な運用ルールを実際にハンズオン形式で手を動かしながら体験します。
- Rulesetsでmainブランチへの直接Push禁止とPull Request必須化
- シンプルなチェック用ワークフローを必須ステータスチェックに設定
- わざとルールを違反してみて、GitHubが変更を拒否する様子を確認
- Admin権限によるbypass(例外)の動作確認
- 保護ルールを満たす変更であれば通常通りマージできることを確認
- CODEOWNERSやPR承認数など、その他の保護ルールを机上で紹介
1. 事前準備
Gitの基礎知識を習得していることを前提とします。自信のない方は先に以下の講座を実施してください。
このハンズオンでは、以下のツールやアカウントが必要です。まだ準備できていない場合は、リンク先の手順に沿って準備をお願いします。
2. ハンズオンの概要
これまでの講座では、GitHub Actionsを使って静的解析・自動テスト・自動デプロイを自動化してきました。しかし、ここまでの構成には実は一つの穴があります。CIをすり抜けたコードが本番にデプロイされてしまうリスクです。
例えば、開発者がうっかりmainブランチに直接Pushしてしまった場合、Pull Requestの作成をトリガーにしているワークフローは一切動きません。結果として、品質チェックを通っていないコードがそのまま本番にデプロイされてしまいます。また、git push --forceによってmainの履歴をうっかり書き換えてしまうと、過去のコミットが失われる事故にもつながります。
この問題を解決するのが、保護ブランチの仕組みです。保護ブランチを設定すると、mainブランチに対する変更を「特定のルールを満たしたPull Requestのマージに限る」といった制約をつけられます。GitHubでは、この仕組みをRulesetsという機能で設定します。
| 📝 Rulesetsとは |
|---|
| Rulesetsは、GitHubがリポジトリ・Organizationのブランチ保護を柔軟に定義するために提供している仕組みです。GitHubは新規に保護設定を行う場合はRulesetsの利用を推奨しています。詳しくはAbout rulesetsに記載があります。 |
2.1 今回設定する保護ルール
今回のハンズオンでは、Rulesetsで以下のルールを設定します。
| ルール | 内容 |
|---|---|
| Restrict deletions | 指定したブランチの削除を禁止する |
| Require a pull request before merging | 指定したブランチへの変更はPull Request経由のみに限定する |
| Require status checks to pass | 指定したCIワークフローが成功しないとマージできない |
| Block force pushes | git push --forceによる指定したブランチの履歴書き換えを禁止する |
| Bypass list | Admin権限を持つユーザだけがPull Request画面からルールを例外的にバイパスできる |
平常時は全員にルールを強制し、緊急時のみ責任者がバイパスする、という実務的な運用を体験できます。
3. リポジトリの準備
保護ブランチの設定は、既存のリポジトリに適用するとこれまでの操作と混ざってしまい分かりづらくなります。このハンズオンでは、専用の新しいリポジトリを作成し、そこに最小限のファイルだけを配置した状態で進めます。
3.1 GitHubで空のリポジトリを作成
GitHubにログインし、右上の**+ボタンからNew repository**をクリックします。
以下の設定でリポジトリを作成します。
| 設定項目 | 値 | 設定の基準 |
|---|---|---|
| Owner | ご自身のGitHubアカウント | 自分のアカウントで管理するため |
| Repository name | devopscamp-branch-protection-handson | ハンズオン用のリポジトリ名 |
| Visibility | Public | GitHub Actionsの無料実行時間を気にせず使えるため |
| Initialize this repository with | 何もチェックしない | 後から自分でファイルをPushするため |
Create repositoryをクリックしてリポジトリを作成します。

作成後、「Quick setup」の画面が表示されたら、ここでは何も操作せずに次のステップに進みます。
GitHubのリポジトリ一覧にdevopscamp-branch-protection-handsonが表示されていれば、ここまでの操作は完了です。
3.2 リポジトリのClone
作成したリポジトリをローカル環境にcloneします。ローカル環境で、Windowsであればコマンドプロンプト、Macであればターミナルを開き、下記のコマンドを実行します。<your-github-account-name>の部分はご自身のGitHubアカウント名に置き換えてください。
git clone https://github.com/<your-github-account-name>/devopscamp-branch-protection-handson.git
空のリポジトリなのでYou appear to have cloned an empty repository.という警告が表示されますが、問題ありません。
3.3 サンプルファイルの作成
Visual Studio Codeでdevopscamp-branch-protection-handsonフォルダを開きます。「ファイル」メニュー >「フォルダを開く」から、先ほどcloneしたフォルダを選択してください。
このハンズオンでは、保護ルールが動作することを確認するために2つのファイルを用意します。1つ目はREADME.md、2つ目はPRの際にREADME.mdの存在をチェックするワークフローファイルです。静的解析や自動テストのような本格的な内容は不要で、「単にREADME.mdがあるかどうか」というごく単純なチェックで保護ルールの動作を確認します。
まず、リポジトリ直下にREADME.mdを作成します。
devopscamp-branch-protection-handson/
└── README.md ← このファイルを作成
作成したファイルに以下の内容を記述して保存します。
# devopscamp-branch-protection-handson
DevOps Campの保護ブランチハンズオン用リポジトリです。
次に、ワークフローファイルを作成します。.github/workflows/check-readme.ymlを作成します。
devopscamp-branch-protection-handson/
├── README.md
└── .github/
└── workflows/
└── check-readme.yml ← このファイルを作成
作成したファイルに以下の内容を記述して保存します。
name: README Check
on:
pull_request:
branches:
- main
jobs:
check-readme:
name: Check README exists
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Check README.md exists
run: |
if [ ! -f README.md ]; then
echo "Error: README.md does not exist"
exit 1
fi
echo "README.md exists"
このワークフローは、Pull Requestが作成された際に動き、リポジトリ直下にREADME.mdがあれば成功、なければ失敗するという非常にシンプルなチェックを行います。本講座ではこのワークフローを「必須ステータスチェック」として利用します。
3.4 初回コミットとPush
ローカルリポジトリに2つのファイルを配置できたので、GitHubにPushしてリモートリポジトリに反映させます。
Visual Studio Codeのターミナルを開き、以下のコマンドを1つずつ実行します。
ステージングします。
git add .
コミットします。
git commit -m "initial commit"
mainブランチにPushします。
git push origin main
GitHubのリポジトリページを更新すると、README.mdと.github/workflows/check-readme.ymlが表示されます。これが表示されていれば、ここまでの操作は完了です。

4. Rulesetの設定
mainブランチを保護するために、Rulesetを新しく作成します。Rulesetの設定画面はリポジトリのSettingsから開きます。
4.1 Rulesetの作成画面を開く
GitHubのdevopscamp-branch-protection-handsonリポジトリのページを開きます。
Settingsタブをクリックします。
左メニューからRules > Rulesetsを選択します。
New rulesetをクリックし、ドロップダウンからNew branch rulesetを選択します。

4.2 基本設定
Rulesetの基本情報を入力します。
| 設定項目 | 値 | 設定の基準 |
|---|---|---|
| Ruleset Name | main-protection | ルールセットを識別するための名前 |
| Enforcement status | Active | ルールを有効化して即座に適用するため |

Enforcement statusには、他にDisabled(ルールは存在するが無効)やEvaluate(ルール違反を記録するが強制はしない)があります。Evaluateは本番前の試運転に便利ですが、今回は動作をすぐに確認したいのでActiveを選びます。
4.3 Bypassリストの設定
Admin権限を持つユーザがPull Request画面から例外的にルールを突破できるよう、Bypassリストを設定します。
Bypass listセクションでAdd bypassをクリックし、Repository adminを選択します。
追加後、Modeを選択します。以下の設定にします。
| 設定項目 | 値 | 設定の基準 |
|---|---|---|
| Bypass mode | Pull requests | Pull Request画面からのマージ時のみバイパスを許可する |

Bypass modeには2種類あります。
| モード | 動作 |
|---|---|
| Always | 常にルールを無視できる。git pushでの直接Pushも許される |
| Pull requests | Pull Requestのマージ操作時のみバイパスが有効。CLIでの直接Pushは引き続き拒否される |
Alwaysを選ぶとadminがCLIでも自由にmainへPushできてしまいます。今回は「CLIでの直接Pushは全員禁止しつつ、Pull Request画面では緊急時にadminがバイパスできる」という実務に近い設定にしたいので、Pull requestsを選択します。
4.4 対象ブランチの指定
Rulesetは「どのブランチに対して適用するか」を指定する必要があります。今回はmainブランチに適用します。
Target branchesセクションでAdd targetをクリックし、Include default branchを選択します。これにより、リポジトリのデフォルトブランチ(main)がターゲットに追加されます。
Include default branchを使うことで、将来デフォルトブランチ名を変更した場合でも自動的に新しいデフォルトブランチに追従します。特定のブランチ名で固定したい場合はInclude by patternでブランチ名を指定します。

4.5 ルールの設定
次に、実際に強制するルールを選びます。画面下部のRulesセクションで、以下の項目にチェックを入れていきます。
Restrict deletions
この項目にチェックを入れると、mainブランチの削除が禁止されます。誤操作でmainブランチが消えることを防ぐ、基本中の基本となる設定です。

Require a pull request before merging
この項目にチェックを入れると、mainブランチへの変更はPull Request経由でのみ行えるようになります。これが「mainへの直接Push禁止」を実現する最も重要な設定です。
この項目を有効にすると、追加の設定が表示されます。今回は以下のように設定します。
| 設定項目 | 値 | 設定の基準 |
|---|---|---|
| Required approvals | 0 | 個人リポジトリでは自分自身のPRを承認できないため、今回は承認なしで進める |
| Dismiss stale pull request approvals when new commits are pushed | OFF | 承認数を0にしているため |
| Require review from Code Owners | OFF | 個人リポジトリではCODEOWNERSの動作確認ができないため、今回は無効 |
| Require approval of the most recent reviewable push | OFF | 承認数を0にしているため |
| Require conversation resolution before merging | ON | Pull Requestのコメントが未解決のままマージされないようにするため |
| 💡 ポイント |
|---|
実務ではRequired approvalsを1以上に設定し、「他の開発者の承認がないとマージできない」状態にすることが一般的です。またRequire review from Code Ownersを有効にすると、特定のファイル・ディレクトリの変更時に特定のレビュアーのApproveを強制できます。今回は個人リポジトリのため動作確認はできませんが、チーム開発では必須の設定です。 |

Require status checks to pass
この項目にチェックを入れると、指定したCIワークフローが成功していないとマージできなくなります。これが「CIをパスしたコードだけがmainに入る」という品質ゲートの中核的な設定です。
チェックを入れるとAdd checksボタンが表示されるので、クリックします。
検索窓にCheck README existsと入力し、候補に表示される**+ Add Check README exists**(Any source)を選択して追加します。

| 💡 ポイント |
|---|
Add checksでは「すでに実行されたことのあるチェック名」から選ぶか、任意の名前を手動で入力して追加できます。手動で追加する場合、ワークフローがまだ一度も実行されていなくても設定できます。入力する名前は、ワークフローのジョブ名(jobs.<job-id>.nameで指定した値)と一致させる必要があります。今回はcheck-readme.ymlでname: Check README existsと指定しているため、Check README existsと入力します。 |
Require branches to be up to date before mergingはONにします。有効にすると、mainが更新されるたびに作業用ブランチへ最新mainを取り込んでからでないとマージできなくなり、マージ後にmainで予期せぬ競合が発生することを防げます。
Do not require status checks on creationはOFFのままで進めます。このオプションは、Rulesetの対象パターンにマッチするブランチの初回作成Pushで、必須ステータスチェックの要求を免除する設定です。feature/*のようにパターンで対象を広げる運用では、ワークフローが一度も走っていない新規ブランチの初回Pushが拒否されるのを防ぐために有効化しますが、今回はRulesetの対象をmainのみに限定しているためOFFのままで挙動に影響はありません。

Block force pushes
この項目にチェックを入れると、git push --forceによるmain履歴の書き換えが禁止されます。うっかりforce pushで過去のコミットを失う事故を防ぐ、もう一つの重要な設定です。

4.6 Rulesetの作成
必要な項目をすべて設定したら、画面下部のCreateボタンをクリックします。

作成後、Rulesetの詳細画面が表示されます。main-protectionがActive状態で表示されていれば、ここまでの操作は完了です。
5. 違反してみる
設定した保護ルールが本当に機能するか、わざと違反してみて確認します。
5.1 違反1: mainブランチへの直接Push
保護ルールが設定された状態で、mainブランチに直接Pushしてみます。
Visual Studio CodeでREADME.mdを開き、末尾に次の1行を追加します。
保護ルール適用後に直接Pushするテストです。
保存したら、ターミナルで以下のコマンドを1つずつ実行します。
git add .
git commit -m "test: direct push after protection"
git push origin main
今回はPushが拒否されるはずです。以下のようなエラーメッセージが表示されます。
remote: error: GH013: Repository rule violations found for refs/heads/main.
remote:
remote: - Changes must be made through a pull request.
remote:
To https://github.com/<your-github-account-name>/devopscamp-branch-protection-handson.git
! [remote rejected] main -> main (push declined due to repository rule violations)
error: failed to push some refs to 'https://github.com/...'
Changes must be made through a pull request.というメッセージが表示されていれば、Rulesetが正しく動作しています。Bypass modeをPull requestsにしているため、admin権限を持っていてもCLIでの直接Pushは拒否されます。
5.2 違反2: 必須ステータスチェックの失敗
次に、「CIが失敗しているPull Requestはマージできない」ことを確認します。今回のワークフローは「README.mdが存在しない場合に失敗」するよう作ってあるため、わざとREADME.mdを削除したPull Requestを作成して、チェックを失敗させます。
作業用ブランチの作成とREADME.mdの削除
先ほど確認したように、mainブランチへの直接Pushは拒否されます。一方で、mainブランチ以外の作業用ブランチであれば通常通りPushでき、そこからPull Requestを作ってmainにマージする流れになります。ここでは、READMEを削除する作業用ブランチを切ってPushします。
作業用ブランチを作成します。
git switch -c test/delete-readme
Visual Studio CodeのエクスプローラーでREADME.mdを右クリックし、「削除」を選択して削除します。
変更をステージングします。
git add .
コミットします。
git commit -m "test: delete README to trigger failure"
Pushします。
git push origin test/delete-readme
Pull Requestの作成
GitHubのリポジトリページを開き、Pull requestsタブからNew pull requestをクリックします。
base: main, compare: test/delete-readmeでPull Requestを作成します。

ワークフローの実行結果とマージボタンの確認
作成したPull Requestの画面を開くと、README Check / Check README existsのワークフローが実行されます。しばらくすると実行結果が**失敗(赤バツ)**になります。
Run Check README.md existsのステップを開くと、Error: README.md does not existというメッセージが出力されてエラー終了していることが確認できます。
Pull Requestの画面を下にスクロールすると、マージボタンの周辺にRequired status check "Check README exists" is expected.(または類似の赤いメッセージ)が表示されます。Merge pull requestボタンは灰色で無効化された状態になっており、ステータスチェックが失敗している間はマージできません。
これが「CIをパスしないコードはmainに入れない」という必須ステータスチェックの効果です。

5.3 違反3: Admin bypassの動作確認
違反2の状態で、Admin権限を持つユーザがPull Request画面から例外的にマージできることを確認します。
今のPull Request画面に戻ります。マージボタンの近くに**Merge without waiting for requirements to be met (bypass rules)**というチェックボックス、または「管理者としてルールをバイパスしてマージする」といった趣旨のオプションが表示されています。
| 💡 ポイント |
|---|
Admin bypassのUI(チェックボックスやボタン)の文言・形式は、GitHubのUI更新によって変わることがあります。「bypass rules」「管理者として強制マージ」「Required checksを無視してマージ」など、類似の表現を探してください。表示されない場合は、RulesetのBypass listでRepository adminがPull requestsモードで追加されているかを確認してください。 |
このチェックボックスをオンにするとMerge pull requestボタンが有効化され、本来マージできないはずのPull Request(README.mdを削除したPull Request)を強制的にマージできます。
ただし、今回はマージせずに次のステップに進んでください。実際にマージするとmainブランチのREADME.mdが消え、このあとの操作で混乱する可能性があるためです。Admin bypassの動作は「バイパス用のUIが存在していること」の確認のみで十分です。
実務では、緊急修正(hotfix)を最速で反映したい場合や、CI環境の障害で必須チェックが動かない場合などに、責任者がbypassを使います。bypass操作はGitHubのAudit logに残るため、誰がいつbypassしたかを後から追跡可能です。
5.4 状態を元に戻す
違反確認が終わったので、状態を元に戻します。作成したPull Requestをそのままにしておくと、後続の操作で邪魔になる可能性があるためです。
GitHub画面でPull Requestを開き、Close pull requestをクリックしてクローズします(マージはしません)。
ローカルでは、mainブランチに戻します。
git switch main
不要になった作業用ブランチを削除します。
git branch -D test/delete-readme
ここまで完了していれば、違反確認は完了です。
6. 正しい変更でマージしてみる
保護ルールに違反する変更が拒否されることを確認したので、最後に保護ルールを満たす変更であれば通常通りマージできることを確認します。作業用ブランチで変更してPull Requestを作り、CIを成功させてマージするという、実務で日常的に行うフローを一度体験します。
6.1 作業用ブランチの作成とREADME.mdの編集
作業用ブランチを作成します。
git switch -c feature/update-readme
Visual Studio CodeでREADME.mdを開き、末尾に次の1行を追加します。
保護ルールを満たす変更のテストです。
変更をステージングします。
git add .
コミットします。
git commit -m "feat: add line to README"
Pushします。
git push origin feature/update-readme
6.2 Pull Requestの作成とマージ
GitHubのリポジトリページを開き、Pull requestsタブからNew pull requestをクリックします。
base: main, compare: feature/update-readmeでPull Requestを作成します。

Pull Requestの画面で、README Check / Check README existsのワークフローが実行されます。今回はREADME.mdを削除していないため、しばらくすると実行結果が成功(緑チェック)になります。Pull Requestの画面を下にスクロールすると、今度はMerge pull requestボタンが緑色で有効になっています。

Merge pull requestをクリックし、Confirm mergeでマージを確定します。マージが完了すると、Pull Requestの上部にMergedバッジが表示されます。

「保護ルールを満たすPull Requestであれば通常通りマージできる」ことが確認できました。
7. その他の保護ルールの紹介
今回のハンズオンでは個人リポジトリでも実際に試せる範囲のルールを中心に扱いましたが、実務ではさらに多くの保護ルールを組み合わせます。ここでは代表的なものを紹介します。興味のある方は、公式ドキュメントを参照しつつ実務で活用してみてください。
| ルール | 内容 | 実務での使いどころ |
|---|---|---|
| Required approvals | Pull Requestに指定した人数以上のApproveがないとマージできない | チーム開発で「最低1名のレビュー必須」を強制する |
| Require review from Code Owners | 変更ファイルに対応するCODEOWNERSに書かれたレビュアーの承認を必須化 |
「特定のディレクトリはインフラチームの承認必須」といった役割ベースのレビュー |
| Dismiss stale pull request approvals | 新しいコミットがPushされた時に過去のApproveを自動で取り消す | レビュー後の変更を見逃さないようにする |
| Require signed commits | GPGまたはSSH署名付きのコミットだけを許可 | コミットの改ざん検出・作者の保証が必要な高セキュリティ環境 |
| Require linear history | マージコミットを作らず、SquashまたはRebaseマージのみ許可 | mainの履歴を一直線に保ち、ログを読みやすくする |
| Require branches to be up to date before merging | mainの最新を取り込んでいないブランチはマージ不可 | マージ後に予期せぬ競合が発生することを防ぐ |
詳細な設定方法は、Available rules for rulesetsに記載があります。
| 💡 ポイント |
|---|
これらのルールは「厳しくすればするほど安全」というわけではありません。例えばRequire branches to be up to dateを有効にすると、大規模チームではPRがマージされるたびに他のPRの再実行が必要になり、運用負荷が上がります。組織の規模やリスク許容度に応じて、必要なルールを取捨選択することが重要です。 |
8. 不要リソースの削除
ハンズオンが完了したので、作成したRulesetとリポジトリを削除します。
8.1 Rulesetの削除
GitHubのリポジトリページからSettings > Rules > Rulesetsを開きます。
main-protectionの右側にあるメニューからDeleteをクリックします。
確認画面でDelete rulesetをクリックすると、Rulesetが削除されます。
Rulesetsの一覧からmain-protectionが消えていれば削除完了です。
8.2 リポジトリの削除(任意)
作成したdevopscamp-branch-protection-handsonリポジトリ自体が不要な場合は、併せて削除します。残しておきたい場合、この手順は省略して問題ありません。
GitHubのdevopscamp-branch-protection-handsonリポジトリのページでSettingsタブをクリックし、画面を一番下までスクロールします。
Danger ZoneのセクションにあるDelete this repositoryボタンをクリックします。
確認画面の案内に沿ってリポジトリ名を入力し、削除を実行します。
リポジトリ一覧からdevopscamp-branch-protection-handsonが消えていれば、削除完了です。
9. まとめ
このハンズオンでは、GitHubのRulesets機能を使ってmainブランチを保護する実務的な運用ルールを体験しました。
- mainブランチへの直接Pushを禁止し、変更はPull Request経由に限定できる
- 必須ステータスチェックにより、CIをパスしないコードのマージを防げる
- Force pushとブランチ削除の禁止により、mainの履歴を安全に保てる
- Admin bypassを使えば、緊急時に責任者が例外的にルールを突破できる
Required approvalsやCODEOWNERSなど、チーム開発で活用される追加ルールの存在を理解できる