1
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?

Dependabot 自動マージで本番デプロイまで回したら、GITHUB_TOKEN の罠で CI もデプロイも起動しなかった話

1
Last updated at Posted at 2026-09-27

はじめに

依存パッケージの更新を放置しないために、Dependabot の PR を自動マージするようにしました。
自動マージそのものは数行で書けます。ただ、「マージ → main で CI → 本番デプロイ」までつなげると、エラーも出さずに静かに壊れる箇所がいくつもありました。

この記事は、GitHub Actions と Dependabot を使っている個人開発者・小規模チーム向けです。
そのまま持ち込めるコードに加えて、「なぜそう書くのか」を1つずつ説明します。

TL;DR

  • GITHUB_TOKEN でマージすると、main への push で CI が起動しない。E2E もデプロイも走らず、エラーも出ない。マージには fine-grained PAT を使う
  • トリガーは pull_request ではなく workflow_run(CI 完了)にする。Dependabot が起動したワークフローには読み取り専用トークンしか渡らず、Secrets も渡らない
  • PAT を持つワークフローでは、PR のコードを checkout も実行もしない。依存のインストールスクリプトに書き込み権限付きのトークンを晒さないため
  • Dependabot の PR では Actions Secrets が渡らず、E2E が必ず落ちる。PR 段階では E2E をスキップし、main への push では必ず回してデプロイのゲートにする
  • PAT の期限切れ・空のシークレット・放置 PR は「静かに」壊れる。疎通確認ジョブと放置 PR 通知で検知する

前提

  • GitHub Actions で CI(lint / 型チェック / テスト / ビルド / E2E)を回している
  • main への push で CI が全部通ったときだけ、CI の deploy ジョブが本番デプロイする
    • ホスティング側の Git 連携による自動デプロイは止めておく(CI を通らずにデプロイされてしまうため)
  • 例は npm ですが、言語やフレームワークは問いません

全体像

目的は「常に最新であること」ではありません。
まとめて上げる羽目にならないよう小さく継続的に追従し、1回あたりの移行コストとリスクを下げることです。

登場するファイルは3つ + CI です。

ファイル 役割
.github/dependabot.yml 更新のグループ化と ignore
.github/workflows/dependabot-auto-merge.yml CI 完了後の自動マージ + PAT の疎通確認
.github/workflows/dependabot-review-notify.yml 人間の対応が必要な PR の通知
.github/workflows/ci.yml 既存の CI(E2E の条件とデプロイのゲートだけ触る)

1. Dependabot の設定:グループ化と ignore の基準

.github/dependabot.yml
version: 2
updates:
  - package-ecosystem: npm
    directory: "/"
    schedule:
      interval: weekly
      day: monday
      time: "09:00"
      timezone: Asia/Tokyo
    open-pull-requests-limit: 5
    groups:
      # minor / patch は1本のPRにまとめる。メジャーだけ個別PR。
      # グループ名はブランチ名 dependabot/npm_and_yarn/minor-and-patch-<hash> に現れ、
      # 自動マージ判定に使われる。名前を変えるときはワークフロー側も直すこと。
      minor-and-patch:
        patterns: ["*"]
        update-types: [minor, patch]
    ignore:
      # 型定義は実行環境の Node メジャーと揃える。Node 本体を上げるときに手動で。
      - dependency-name: "@types/node"
        update-types: ["version-update:semver-major"]
      # 例: 上流が未対応で CI が落ちるもの。対応版が出たら外す(外す条件を必ず書く)。
      # - dependency-name: "eslint"
      #   update-types: ["version-update:semver-major"]

  - package-ecosystem: github-actions
    directory: "/"
    schedule:
      interval: weekly
      day: monday
      time: "09:00"
      timezone: Asia/Tokyo
    open-pull-requests-limit: 3
    groups:
      # npm 側と同じグループ名にする(ワークフローがブランチ名で判定するため)
      minor-and-patch:
        patterns: ["*"]
        update-types: [minor, patch]

ブランチ名を「自動マージしてよい」ことの証明にする

グループの update-types を minor / patch に限定すると、グループ名がブランチ名に入ります。

dependabot/npm_and_yarn/minor-and-patch-<hash>

つまり、ブランチ名がそのまま「メジャー更新を含まない」ことの証明になるので、ワークフロー側はブランチ名だけで判定できます。

注意点は2つです。

  • エコシステムが複数あるときはグループ名を揃える
  • グループ名を変えるときはワークフローの判定条件も直す。忘れないよう、両方のファイルにコメントで相互参照を書いておく

なお、ブランチ名は手で真似できるので、ワークフロー側では「作成者が Dependabot であること」も確認します(後述)。

メジャー更新を ignore する基準

ignore するのは次の2種類に絞っています。

① 「新しいほど良い」とは限らない依存

代表例が @types/node のような実行環境の型定義です。本番の Node より新しいメジャーを入れると、実行環境に存在しない API が型の上では OK になります。
結果、型チェックは緑なのに本番で落ちるという最悪のパターンになります。実行環境を上げるときに手動で揃えます。

② 上流が未対応で、上げると CI が必ず落ちるもの

リンターや TypeScript のメジャー更新で、プラグインや周辺ツールが追いついていないケースです。

ここで学んだのは、peer 宣言や npm install --dry-run が通っても互換性の証明にはならないということです。実際に上げて CI で確かめるしかありません。

そして ignore には**「いつ外すか」の条件を必ずコメントで書きます**。書かないと永久に放置されます。

2. 自動マージのワークフロー

.github/workflows/dependabot-auto-merge.yml
name: Dependabot auto-merge

# pull_request ではなく workflow_run: Dependabot 起動の実行は読み取り専用トークンのため。
# PR のコードは checkout しない: PAT を依存のインストールスクリプトに晒さないため。
on:
  workflow_run:
    workflows: ["CI"]
    types: [completed]
    branches:
      - "dependabot/*/minor-and-patch-*"
  workflow_dispatch:   # token-check 用

permissions:
  contents: read   # マージは PAT で行うので既定トークンは読み取りのみ

jobs:
  auto-merge:
    runs-on: ubuntu-latest
    if: >-
      ${{ github.event_name == 'workflow_run'
      && github.event.workflow_run.event == 'pull_request'
      && github.event.workflow_run.conclusion == 'success'
      && startsWith(github.event.workflow_run.head_branch, 'dependabot/')
      && contains(github.event.workflow_run.head_branch, '/minor-and-patch-') }}
    steps:
      - name: CI が通った Dependabot PR をマージ
        env:
          # GITHUB_TOKEN でマージすると main への push で CI が起動しない → PAT を使う
          GH_TOKEN: ${{ secrets.AUTOMERGE_TOKEN }}
          REPO: ${{ github.repository }}
          BRANCH: ${{ github.event.workflow_run.head_branch }}
        run: |
          set -euo pipefail
          if [ -z "${GH_TOKEN:-}" ]; then
            echo "::error::AUTOMERGE_TOKEN が未設定です。"
            exit 1
          fi
          # 作成者が Dependabot であることも確認(ブランチ名の偽装対策)
          num="$(gh pr list --repo "$REPO" --head "$BRANCH" --state open \
                   --json number,author \
                   --jq '[.[] | select(.author.login == "app/dependabot" or .author.login == "dependabot[bot]") | .number] | first // empty')"
          if [ -z "$num" ]; then
            echo "対象の open な Dependabot PR がありません。"
            exit 0
          fi
          gh pr merge "$num" --repo "$REPO" --squash --delete-branch

  token-check:
    if: ${{ github.event_name == 'workflow_dispatch' }}
    runs-on: ubuntu-latest
    steps:
      - name: AUTOMERGE_TOKEN の疎通確認(マージはしない)
        env:
          GH_TOKEN: ${{ secrets.AUTOMERGE_TOKEN }}
          REPO: ${{ github.repository }}
        run: |
          set -euo pipefail
          [ -n "${GH_TOKEN:-}" ] || { echo "::error::AUTOMERGE_TOKEN が空です(--body \"\$(...)\" の失敗など)"; exit 1; }
          gh api "repos/$REPO" --jq '.full_name' > /dev/null 2>&1 \
            || { echo "::error::リポジトリを読めません(期限切れ・Repository access 外)"; exit 1; }
          gh pr list --repo "$REPO" --state open --limit 1 > /dev/null 2>&1 \
            || { echo "::error::Pull requests を読めません"; exit 1; }
          push_perm="$(gh api "repos/$REPO" --jq '.permissions.push // "unknown"')"
          [ "$push_perm" = "true" ] && echo "OK: 書き込み権限あり" \
            || echo "::warning::書き込み権限を確認できません (permissions.push=$push_perm)"

このファイルには、ハマりどころが3つ詰まっています。

罠①:GITHUB_TOKEN でマージすると、その後の CI もデプロイも起動しない

これが一番の罠です。

GitHub には、GITHUB_TOKEN で起こしたイベントからは新しいワークフローを起動しないという仕様があります(workflow_dispatch と repository_dispatch を除く)。ワークフローが無限に連鎖するのを防ぐためです。

そのため GITHUB_TOKEN でマージすると、こうなります。

  1. 自動マージは成功する
  2. main に push される
  3. でも CI が起動しない → E2E もデプロイも走らない

「マージされたのに、検証もデプロイもされていない」状態になり、エラーも出ません。 Actions の画面を見に行くまで気づけません。

対策は、fine-grained PAT(Personal Access Token)を作って Secrets に登録し、マージに使うことです。PAT でのマージは通常の push として扱われるので、CI がちゃんと起動します。

PAT の権限は最小限にします。

項目 設定
Repository access 対象リポジトリのみ
Contents Read and write
Pull requests Read and write

罠②:トリガーを pull_request にすると、マージ API を呼べない

素直に書くと on: pull_request で Dependabot の PR を拾いたくなります。しかし、Dependabot が起動したワークフローには読み取り専用の GITHUB_TOKEN しか渡らず、Actions の Secrets も渡りません。これではマージできません。

一方、CI の完了イベント(workflow_run)から起動したワークフローは、元の実行が Dependabot 由来でも通常の権限と Secrets で動きます。なので「CI が完了した」タイミングでマージ判定をします。

workflow_run.branches の絞り込みも入れておくのがおすすめです。

    branches:
      - "dependabot/*/minor-and-patch-*"
  • * は / に一致しないので、dependabot/npm_and_yarn/minor-and-patch-xxx の形だけにマッチします
  • 絞らないと、main への push を含むすべての CI 完了で起動し、if で弾かれた skipped の実行が Actions 一覧に毎回並んでうるさくなります
  • if 条件は多層防御としてそのまま残しておきます

罠③:PAT を持つワークフローで PR のコードを動かしてはいけない

このワークフローは actions/checkout をしていません。意図的です。

更新後の依存をインストールすると、postinstall などのスクリプトが動きます。そこに書き込み権限付きの PAT が見える状態だと、サプライチェーン攻撃の格好の的になります。

そこで役割をきっちり分けます。

役割 Secrets やること
CI 持たない 依存を実際にインストールして検証する
auto-merge PAT を持つ GitHub API を叩くだけ(コードは触らない)

「CI の最後にマージのジョブを足せば1ファイルで済むのでは?」とも考えましたが、やめました。

  • Dependabot 起動の CI に PAT を渡すには、PAT を Dependabot secrets にも二重登録する必要がある → 期限更新の管理箇所が増える
  • PAT と依存コードの実行が同じワークフローに来て、隔離の境界が曖昧になる

得られるのは見た目の整理だけなので、割に合いません。

GitHub 標準の auto-merge(gh pr merge --auto)は、ブランチ保護で必須チェックを設定していることが前提です。無料プランのプライベートリポジトリなど、ブランチ保護を使えない環境では、このように自作する意味があります。

3. Dependabot の PR と Secrets・E2E の扱い

罠④:Dependabot の PR では E2E が必ず落ちる

罠②と同じ理由で、Dependabot が起動した実行には Actions の Secrets が渡りません(渡るのは別枠の「Dependabot secrets」だけです)。
E2E が外部サービスの接続情報を Secrets から読んでいる場合、値が空になって必ず失敗します。

ここでは、Dependabot の PR では E2E をスキップすると判断しました。
毎週必ず赤い CI が出ると、CI そのものが信用されなくなるからです。

ただし、デプロイ前のゲートは緩めません。

.github/workflows/ci.yml(関係部分のみ)
name: CI

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

jobs:
  lint-build:   # lint → 型チェック → テスト → ビルド(Secrets 不要)
    ...
  e2e:
    # Dependabot 発の PR とフォークの PR は除外(Secrets が渡らず必ず落ちるため)。
    # push イベントでは実行者に関わらず常に実行=デプロイ前のゲートは維持。
    if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]') }}
    ...
  deploy:
    needs: [lint-build, e2e]
    if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
    ...

ポイントは次のとおりです。

  • push イベントでは実行者に関係なく E2E を必ず回す(PAT でのマージ後の push はここに来る)
  • deploy は needs で E2E の通過を条件にする
  • E2E が落ちればデプロイはスキップされ、本番は前回の正常版のまま残る。main を revert するだけで復旧できる

つまり「PR 段階の E2E」は省略しても、「本番に出る前の E2E」は省略していません。

代替案として、Dependabot secrets に同じ値を登録して除外条件を外す方法もあります。二重管理のコストと引き換えに、PR の段階で E2E まで回せます。

罠⑤:0.x 系パッケージの minor 更新は破壊的変更を含みうる

semver 上、0.12 → 0.13 は minor 扱いです。しかし 0.x は安定性の保証がないため、実質的な破壊的変更が入ることがあります。そして自動マージの対象に入ってしまいます。

防波堤になるのは、CI(型チェック・テスト・ビルド)とマージ後の E2E です。上の構成なら落ちてもデプロイされないので、失敗は安全側に倒れます。

心配なパッケージがあれば、そのパッケージだけグループから外して個別 PR にする手もあります。

4. 静かに壊れる箇所への対策

自動化は「動いているうちは見なくなる」ので、止まったことに気づけないのが一番怖いところです。ここでは3つの「静かな故障」に備えます。

PAT の期限切れ → token-check で疎通確認

fine-grained PAT には有効期限があります。切れても、PR が open のまま残るだけで表面上は何も起きません。

そこで、auto-merge ワークフローに手動実行(workflow_dispatch)用の token-check ジョブを入れています(前掲のコード参照)。確認する順番は次のとおりで、マージは一切しません。

  1. トークンが空でないか
  2. リポジトリを読めるか(期限切れ・Repository access 外ならここで落ちる)
  3. PR を読めるか
  4. 書き込み権限があるか

PAT を更新したら、Actions 画面からこのジョブを手動実行して緑になることを確認します。

シークレットが空文字で登録される → コマンド置換を使わない

gh secret set でこう書くと危険です。

gh secret set AUTOMERGE_TOKEN --body "$(some-command)"

コマンド置換が失敗しても、空文字のまま登録に成功してしまいます。エラーにならないので気づけません。

対策は次のいずれかです。

  • ブラウザの Settings → Secrets and variables → Actions から登録する
  • クリップボード経由にする(macOS なら --body "$(pbpaste)")

いずれにしても、トークン文字列をシェルのコマンドラインに直接置かないようにします(シェルの履歴に残るため)。

放置 PR → スキャン前日に GitHub 自身の通知で知らせる

次の2種類の PR を、Dependabot のスキャン前日に洗い出します。

  • メジャー更新の PR(そもそも自動マージしない)
  • 72時間以上 open のままの minor-and-patch PR(自動マージが機能していない疑い)
.github/workflows/dependabot-review-notify.yml
name: Dependabot 要レビューPR通知

on:
  schedule:
    - cron: "0 1 * * 0"   # 毎週日曜 10:00 JST(Dependabot のスキャン前日)
  workflow_dispatch:

permissions:
  contents: read
  pull-requests: write

jobs:
  notify:
    runs-on: ubuntu-latest
    steps:
      - env:
          GH_TOKEN: ${{ github.token }}   # マージしないので GITHUB_TOKEN で足りる
          REPO: ${{ github.repository }}
          OWNER: your-github-id
        run: |
          set -euo pipefail
          prs="$(gh pr list --repo "$REPO" --state open \
                   --json number,title,headRefName,createdAt,author \
                   --jq '[.[] | select(.author.login == "app/dependabot" or .author.login == "dependabot[bot]")]')"
          count="$(echo "$prs" | jq 'length')"
          [ "$count" -eq 0 ] && exit 0
          now_epoch="$(date -u +%s)"
          for i in $(seq 0 $((count - 1))); do
            number="$(echo "$prs" | jq -r ".[$i].number")"
            branch="$(echo "$prs" | jq -r ".[$i].headRefName")"
            created_epoch="$(date -u -d "$(echo "$prs" | jq -r ".[$i].createdAt")" +%s)"
            age_hours=$(( (now_epoch - created_epoch) / 3600 ))
            reason=""
            if [[ "$branch" != dependabot/*/minor-and-patch-* ]]; then
              reason="自動マージ対象外(メジャー更新など)"
            elif [ "$age_hours" -ge 72 ]; then
              reason="自動マージ対象のはずが ${age_hours} 時間 open のまま"
            fi
            if [ -n "$reason" ]; then
              gh pr edit "$number" --repo "$REPO" --add-assignee "$OWNER" || true
              gh pr edit "$number" --repo "$REPO" --add-reviewer "$OWNER" || true
              gh pr comment "$number" --repo "$REPO" \
                --body "🔔 @$OWNER このPRは自動マージされません(理由: ${reason})。"
            fi
          done

設計上のこだわりは3つです。

  • 外部サービス(Slack など)は使わない。assign・レビュー依頼・メンションで、GitHub 自身の通知に載せる
  • 該当が無ければ何もしない。空振りの通知を送るとノイズになり、そのうち見なくなる
  • こちらはマージも push もしないので GITHUB_TOKEN で足りる。schedule 起動は Dependabot 由来ではないので、書き込み権限も普通に使える

5. 導入手順

  1. .github/dependabot.yml を置く
  2. fine-grained PAT を発行する(対象リポジトリのみ・Contents RW / Pull requests RW)
  3. ブラウザの Settings → Secrets and variables → Actions に AUTOMERGE_TOKEN として登録する
  4. 2つのワークフローを置き、CI の E2E / deploy の条件を調整する
  5. Actions 画面から「Dependabot auto-merge」を手動実行し、token-check が緑になることを確認する
  6. 急ぐときは Insights → Dependency graph → Dependabot の「Check for updates」で即時スキャンする(設定の不備もここに表示される)

プレースホルダは次の3つです。

プレースホルダ 中身
your-github-id 通知先の GitHub ID
AUTOMERGE_TOKEN PAT を登録する Secret 名
CI CI ワークフローの name

まとめ

  • 自動マージ自体は簡単。難しいのは「マージ後に CI とデプロイがちゃんと動くこと」
  • GITHUB_TOKEN でマージすると後続のワークフローが起動しない → PAT でマージする
  • Dependabot 起動の実行は読み取り専用・Secrets なし → workflow_run でマージ判定する
  • PAT を持つワークフローではコードを checkout も実行もしない。検証は Secrets を持たない CI に任せる
  • Dependabot の PR では E2E をスキップしても、main への push では必ず E2E を回してデプロイのゲートにする
  • PAT の期限切れ・空のシークレット・放置 PR は静かに壊れる → 疎通確認ジョブと放置 PR 通知で検知する

「自動化したから安心」ではなく、「自動化が止まったときに気づける」ところまで作って、ようやく放置しない仕組みになると感じました。

参考

1
1
0

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
1
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?