0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CI全緑でも、マージしてはいけないPRがあった。GitHub Actionsは「どこで止めるか」で設計する

0
Last updated at Posted at 2026-10-08

はじめに

🤔「CI はどこまで作り込めばいいんだろう…」
😥「E2E を毎回回すと遅いし、回さないと不安…」
🫠「本番のデプロイ、うっかり変なブランチから出してしまわないか心配…」

どこまで作り込むかに、決まった答えはありません。
ただ、CI が全部緑でも、安心できるとは限りません。
CI で確かめていないことは、そもそも失敗しようがないからです。

個人開発している口コミサービス あじぴた では、
GitHub Actions の 8 本のワークフローで、3 つの環境を回しています⚙️
作る人も、使う人も、壊す人も自分なので、どこで止めるかは自分で決めるしかありませんでした。

この記事では、

  • 速いチェックは全部の PR に、重いテストは develop / main の手前だけに掛ける線の引き方
  • 手動実行で本番に未レビューのコードが出るのを、構造で防ぐ方法
  • 復旧用のオプションを、普段の自動実行では使わせない工夫

を、実際のワークフローと一緒に書きます🚀

この記事は、個人開発サービス「あじぴた」の設計を書くシリーズの 1 本です。
単体で読めるように書いていますが、サービスの全体像はハブ記事にまとめています。

全体像:3 つの環境と、2 つの流れ

あじぴたのアプリ本体は、Next.js(Vercel)と Supabase でできています。
環境は 3 つです。

環境 用途 何が動いているか
ローカル 日常の開発 Docker 上の Supabase(DB・認証・ストレージ・Edge Functions)
クラウドの開発環境 プレビューの接続先・実機での確認 Supabase の無料プロジェクト
本番 本番 Supabase の有料プロジェクト

ブランチは feature/* → develop → main の順に流れます。
大きな機能を作るときは、間に topic/* を挟むこともあります。
topic/* は、STORY(複数の Issue に分かれる大きな機能のまとまり)ごとに切るブランチで、複数の PR を束ねてから develop に入れるためのものです。
develop に入ると開発環境へ、main に入ると本番へ、自動で反映される形です。

feature/* ─PR→(topic/*)─PR→ develop ─────PR────→ main
                               │                    │
                               ├ DB の変更を開発環境へ  ├ DB の変更を本番へ
                               └ 関数を開発環境へ      └ 関数を本番へ

ワークフローは、大きく 「確かめる」ものと**「反映する」もの**に分かれます。

分類 ワークフロー いつ走るか
確かめる 静的チェック(lint・型・単体テスト) 全 PR
確かめる E2E(Playwright) develop / main 向けの PR
確かめる DB のテスト(pgTAP) develop / main 向けの PR
反映する DB の変更の反映(開発環境 / 本番) develop / main への push
反映する Edge Functions の反映(開発環境 / 本番) develop / main への push
その他 開発環境の自動復帰 全 PR

ローカルを主軸にした理由

日常の開発は、ほぼローカルの Docker だけで完結させています🐳

  • データベースのリセットが一瞬で済むので、マイグレーションの試行錯誤が速い
  • 開発でクラウドの無料枠を使わずに済む
  • CI も同じ構成で動くので、ローカルで通れば CI でも通る

マイグレーションが 185 本まで増えても回せているのは、リセットが速いおかげだと思っています。

速いチェックは、全部の PR に掛ける

lint・型チェック・単体テストは、ブランチを絞らずに全 PR で走らせています。
3 つ合わせて 1 分前後で終わるので、絞る理由がありません。

「手元で回すから大丈夫」が通じない PR があった

もともとは、この 3 つは手元で回してから PR を出す運用でした。
CI に載せなかったのは、手元で十分だと思っていたからです。

ところが、手元で回す機会が無い PRがありました。Dependabot です🤖

ある日、Dependabot が ESLint のメジャーバージョンを上げる PR を出してきました。
CI は全部緑で、マージできる状態でした。
念のため手元で lint を回してみると、lint そのものが起動直後に異常終了しました😱

依存しているプラグインが、新しい ESLint で削除された古い API を呼んでいたのが原因です。
CI に lint が無いので、誰もそれを確かめていなかったわけです。

人が出す PR だけを想定したルールは、人以外が出す PR を素通りさせる。

これをきっかけに、lint・型チェック・単体テストを CI に移しました。

警告も 0 件に固定する

あわせて、lint は警告が 1 件でもあれば失敗にしています。

{
  "scripts": {
    "lint": "eslint --max-warnings=0"
  }
}

ESLint は、警告だけなら終了コード 0(成功)を返します。
Next.js の推奨設定では、未使用の変数や <img> の直書きなどが警告
扱いなので、
何も設定しないと CI は緑のまま素通りします⚠️

導入した時点で警告が 0 件だったので、溜まる前に 0 で固定しました。
後から溜まった警告を 0 にするのは大変ですが、今 0 のものを 0 に保つのはタダです✨

重いテストは、develop / main の手前だけ

一方、E2E と DB のテストは、develop と main に向けた PR でだけ走らせています。
どちらも Supabase を起動してデータベースをリセットするところから始まるので、時間がかかるからです。

on:
  pull_request:
    branches:
      - main
      - develop
    paths-ignore:
      - '**.md'
      - 'docs/**'

feature ブランチ同士の PR では、ローカルで確認する運用にしています。
デグレを見つけたいのは、共有のブランチに入る手前なので、そこで必ず通す形です。

ドキュメントだけの PR はスキップする

paths-ignore で、Markdown と docs/ だけの PR はスキップしています。

ここで大事なのは、paths-ignore の挙動です。
変更されたファイルが「すべて」除外パターンに当てはまるときだけスキップされます。

PR の中身 E2E / DB テスト
ドキュメントだけ スキップ
ドキュメント + SQL 走る
ドキュメント + 実装 走る

「docs のついでに SQL も直した」という PR でテストが飛ばされる心配はありません👍
逆に言えば、supabase/**.sql のような挙動に効くパスは、除外に入れてはいけません。

本番への反映は、「気をつける」ではなく構造で防ぐ

DB の変更と Edge Functions を本番に反映するワークフローには、手動実行も付けています。
反映が途中で失敗したときに、やり直せるようにするためです。

手動実行は、どのブランチでも選べてしまう

ここに落とし穴があります。

GitHub Actions の手動実行(workflow_dispatch)は、実行するときにブランチやタグを自由に選べます。
うっかり develop や作業中のブランチを選ぶと、未レビューの変更が本番に出ます😇

特に DB の変更は、本番に一度適用すると簡単には戻せません。

そこで、main 以外では、コードを取得する前に失敗させるようにしました。

jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
      # main 以外の ref では、checkout の前に落とす
      - name: Guard - main 以外の ref では実行しない
        if: github.ref != 'refs/heads/main'
        env:
          REF: ${{ github.ref }}
        run: |
          echo "::error::main でのみ実行できます(指定された ref: $REF)"
          exit 1
      - uses: actions/checkout@v7
      # ...

「本番は main から出す」をルールとして覚えておくのではなく、
main 以外からは出せない形にしておく方が確実です🛡️

${{ github.ref }} を run: の中に直接書かず、env: 経由で渡しているのは、
スクリプトインジェクションを避けるためです。
ブランチ名には任意の文字列を使えるので、シェルに直接埋め込むと、名前に仕込んだコマンドが実行される余地が生まれます。

同時に走らせない

反映のワークフローには、すべて concurrency を付けています。

concurrency:
  group: migrate-prod
  cancel-in-progress: false

main に連続で push しても、反映は 1 つずつ順番に処理されます。
cancel-in-progress: false なので、実行中の反映が途中で止められることもありません。
DB の変更を途中で止めると、中途半端な状態が残るからです。

「順番の食い違い」は、止まって気づくべきもの

Supabase CLI の supabase db push には、--include-all というオプションがあります。
データベースに適用済みの最新より古い日付のマイグレーションも、まとめて適用するものです。

実際、開発環境への反映が 6 回連続で失敗したことがありました😵

レビューの対応でマイグレーションのファイル名(先頭の日時)を変えたところ、
開発環境にはそれより新しい日付のマイグレーションがすでに適用されていました。
変更後のファイルが「過去への割り込み」と見なされて、拒否されたわけです。

--include-all を付ければ、この失敗はすぐに消えます。
でも、いつも付けておくと、適用順序の食い違いに気づけなくなります。
本来、順序の食い違いは止まって気づくべきものです。

そこで、--include-all は手動実行のときだけ選べるオプションにしました。

workflow_dispatch:
  inputs:
    include_all:
      description: 詰まったときの復旧用。自動実行では使わない
      type: boolean
      default: false
- name: Push migrations
  run: supabase db push ${{ inputs.include_all && '--include-all' || '' }}

普段は止まって知らせてもらい、事情を理解したうえで、人が選んで通す形です🔑

開発環境を、PR を出したら自動で起こす

Supabase の無料プロジェクトは、1 週間アクセスが無いと一時停止します。

あじぴたでは、クラウドの開発環境を Vercel のプレビューの接続先にしています。
しばらく開発が空いたあとに PR を出すと、プレビューがデータベースに繋がらず、エラーになります💤

そこで、PR を出したタイミングで、開発環境を自動で起こすワークフローを入れました。

  1. Supabase の管理 API で、開発環境の状態を確認する
  2. 停止中のときだけ、復帰の API を呼ぶ(稼働中に呼ぶとエラーが返るため)
  3. 失敗しても、PR は止めずに警告だけ出す

最後の点は、意識して決めました。
復帰に失敗しても、プレビューの不具合はほかのチェックで気づけます。
補助の仕組みが、本来の作業を止めてはいけないと考えています。

まとめ

  • 速いチェックは全部の PR に掛ける。手元で回す前提は、Dependabot のような人以外の PRを素通りさせる🤖
  • 重いテストは、共有ブランチに入る手前で必ず通す。paths-ignore は「すべて」が当てはまるときだけスキップ
  • 本番への反映は、main 以外からは出せない構造にする。同時に走らせない🛡️
  • 復旧用のオプションは手動のときだけ選べるようにして、普段は止まって気づける状態を保つ🔑

個人開発の CI/CD は、作り込むほど自分の時間を使います。
全部を毎回確かめるのではなく、**「どこで止めれば一番効くか」**で線を引くと、少ない手数で安心が増えると思います💡

シリーズの他の記事も、よろしければ📚

このシリーズでは、個人開発サービス「あじぴた」の設計をテーマごとに書いています。
サービスの全体像や、ほかの記事の一覧はハブ記事にまとめています👇

🔗 「低評価を公開しない」口コミサービスを個人開発した話 — 4,300コミット・6リポジトリの全体像

これまでに公開した記事です。

🔗 Google Places APIで月$1,440の請求が来る前に — 個人開発で従量課金を「呼ばない」8層の防壁
🔗 Next.js 16 で Web Vitals を測って直した実録!loading.tsx で LCP が 2 倍になった罠と関数リージョン
🔗 「全部見せるためのRLS」は書かない。Supabaseで管理画面だけRLSをバイパスした理由
🔗 ディレクトリ構成は「AIへの指示書」になる。Next.js App Routerで自分の設計論を答え合わせした話
🔗 漏洩してもエラーは出ない。非公開データをアプリではなくDBで守る、SupabaseのRLSを選んだ理由
🔗 RLSが壊れてもテストは緑のまま。漏れても気づけない非公開データを、pgTAPで「落ちるテスト」にする

次の記事では、AI に文脈をどう渡しているかと、そこから気づいた「AI だけの話ではない」ことを書きました🤖

🔗 新人が初日に迷うことは、AIも毎回迷う。だから暗黙のルールを、すべて言葉にした

設計の中身を通して読みたい方には、解剖ドキュメントも公開しています🔬
🔗 あじぴたの内側(ajipita-inside)

参考になれば幸いです🙏

0
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?