Dependabot PR処理のための自動化

2025-02-04 hit count image

Monorepo環境でDependabot設定とGitHub Actionsを使用してPR処理を自動化する方法について学びます。

github_actions

概要

前回の投稿では、Dependabot PRを効率的に処理するためのリスク分類基準について学びました。

この投稿では、前回のブログで定義した分類基準に基づいて、Dependabot設定とGitHub Actionsを使用してPR処理を自動化する方法について学びます。

Dependabot設定

Dependabotを使用して依存関係の更新PRを自動的に生成するには、.github/dependabot.ymlファイルを作成し、以下のように設定します。

version: 2
enable-beta-ecosystems: true

updates:
  # GitHub Actions
  - package-ecosystem: github-actions
    directory: '/'
    open-pull-requests-limit: 5
    schedule:
      interval: weekly
      day: monday
      time: '09:00'
      timezone: Asia/Tokyo
    commit-message:
      prefix: 'deps(common)'
    groups:
      github-actions:
        patterns:
          - '*'

  # Root npm dependencies (common dependency management)
  - package-ecosystem: npm
    directory: '/'
    open-pull-requests-limit: 10
    schedule:
      interval: weekly
      day: monday
      time: '09:00'
      timezone: Asia/Tokyo
    commit-message:
      prefix: 'deps(common)'
    groups:
      # TypeScript types (low risk)
      typescript-types:
        patterns:
          - '@types/*'
        update-types:
          - minor
          - patch

      # ESLint/Prettier related (low risk)
      linting:
        patterns:
          - 'eslint*'
          - '@eslint/*'
          - 'prettier*'
          - 'stylelint*'
        update-types:
          - minor
          - patch

      # Testing tools (low risk)
      testing:
        patterns:
          - '@testing-library/*'
          - 'vitest*'
          - 'jest*'
        update-types:
          - minor
          - patch

      # Patch updates batch processing
      patch-updates:
        update-types:
          - patch
        exclude-patterns:
          - 'react*'
          - 'typescript'
          - 'vite*'

  # Apps settings
  - package-ecosystem: npm
    directory: '/apps/app1'
    open-pull-requests-limit: 3
    schedule:
      interval: weekly
      day: monday
      time: '09:00'
      timezone: Asia/Tokyo
    commit-message:
      prefix: 'deps(app1)'
    ignore:
      - dependency-name: '@packages/*'
      - dependency-name: 'eslint-config-custom'
    groups:
      minor-and-patch:
        update-types:
          - minor
          - patch

  - package-ecosystem: npm
    directory: '/apps/app2'
    open-pull-requests-limit: 3
    schedule:
      interval: weekly
      day: monday
      time: '09:00'
      timezone: Asia/Tokyo
    commit-message:
      prefix: 'deps(app2)'
    ignore:
      - dependency-name: '@packages/*'
      - dependency-name: 'eslint-config-custom'
    groups:
      minor-and-patch:
        update-types:
          - minor
          - patch

  - package-ecosystem: npm
    directory: '/apps/app3'
    open-pull-requests-limit: 3
    schedule:
      interval: weekly
      day: monday
      time: '09:00'
      timezone: Asia/Tokyo
    commit-message:
      prefix: 'deps(app3)'
    ignore:
      - dependency-name: '@packages/*'
      - dependency-name: 'eslint-config-custom'
    groups:
      minor-and-patch:
        update-types:
          - minor
          - patch

  - package-ecosystem: npm
    directory: '/apps/app4'
    open-pull-requests-limit: 3
    schedule:
      interval: weekly
      day: monday
      time: '09:00'
      timezone: Asia/Tokyo
    commit-message:
      prefix: 'deps(app4)'
    ignore:
      - dependency-name: '@packages/*'
      - dependency-name: 'eslint-config-custom'
    groups:
      minor-and-patch:
        update-types:
          - minor
          - patch

  - package-ecosystem: npm
    directory: '/apps/app5'
    open-pull-requests-limit: 3
    schedule:
      interval: weekly
      day: monday
      time: '09:00'
      timezone: Asia/Tokyo
    commit-message:
      prefix: 'deps(app5)'
    ignore:
      - dependency-name: '@packages/*'
      - dependency-name: 'eslint-config-custom'
    groups:
      minor-and-patch:
        update-types:
          - minor
          - patch

  - package-ecosystem: npm
    directory: '/apps/template'
    open-pull-requests-limit: 3
    schedule:
      interval: weekly
      day: monday
      time: '09:00'
      timezone: Asia/Tokyo
    commit-message:
      prefix: 'deps(template)'
    ignore:
      - dependency-name: '@packages/*'
      - dependency-name: 'eslint-config-custom'
    groups:
      minor-and-patch:
        update-types:
          - minor
          - patch

  # Config packages
  - package-ecosystem: npm
    directory: '/packages/config/eslint'
    open-pull-requests-limit: 3
    schedule:
      interval: weekly
      day: monday
      time: '09:00'
      timezone: Asia/Tokyo
    commit-message:
      prefix: 'deps(common)'
    groups:
      minor-and-patch:
        update-types:
          - minor
          - patch

  - package-ecosystem: npm
    directory: '/packages/config/stylelint'
    open-pull-requests-limit: 3
    schedule:
      interval: weekly
      day: monday
      time: '09:00'
      timezone: Asia/Tokyo
    commit-message:
      prefix: 'deps(common)'
    groups:
      minor-and-patch:
        update-types:
          - minor
          - patch

  # Shared packages
  - package-ecosystem: npm
    directory: '/packages/lib/components'
    open-pull-requests-limit: 3
    schedule:
      interval: weekly
      day: monday
      time: '09:00'
      timezone: Asia/Tokyo
    commit-message:
      prefix: 'deps(components)'
    ignore:
      - dependency-name: '@packages/*'
      - dependency-name: 'eslint-config-custom'
    groups:
      storybook:
        patterns:
          - '@storybook/*'
          - 'storybook'
        update-types:
          - minor
          - patch
      minor-and-patch:
        update-types:
          - minor
          - patch

  - package-ecosystem: npm
    directory: '/packages/lib/CommonAPI'
    open-pull-requests-limit: 3
    schedule:
      interval: weekly
      day: monday
      time: '09:00'
      timezone: Asia/Tokyo
    commit-message:
      prefix: 'deps(common)'
    groups:
      minor-and-patch:
        update-types:
          - minor
          - patch

設定の主要ポイント

このDependabot設定の主な特徴は以下の通りです。

スケジュール設定

  • 毎週月曜日午前9時(東京時間)にPR生成
  • 週単位でアップデートを確認

PR数の制限

  • ルートディレクトリ:最大10個
  • GitHub Actions:最大5個
  • 個別アプリ/パッケージ:最大3個

グループ化戦略

  • typescript-types@types/*パッケージをグループ化(Minor/Patchのみ)
  • linting:ESLint、Prettier、Stylelint関連パッケージをグループ化
  • testing:テストツールをグループ化
  • patch-updates:Patchアップデートを一括処理(高リスクパッケージを除く)
  • storybook:Storybook関連パッケージをグループ化

コミットメッセージプレフィックス

  • deps(common):共通依存関係
  • deps(app1)deps(app2)など:アプリ別依存関係
  • deps(components):コンポーネントパッケージ依存関係

除外設定

  • 内部パッケージ(@packages/*)はDependabotの対象から除外
  • 共有ESLint設定(eslint-config-custom)も除外

GitHub Actionsによる自動化

GitHub Actionsを使用して、Dependabot PRに自動的にラベルを追加し、チェックリストコメントを作成できます。

.github/workflows/dependabot-labeler.ymlファイルを作成し、以下のように記述します。

name: Dependabot PR Labeler

on:
  pull_request:
    types:
      - opened
      - synchronize

jobs:
  label-and-comment:
    name: Add risk labels and checklist
    runs-on: ubuntu-latest
    timeout-minutes: 5
    if: github.event.pull_request.user.login == 'dependabot[bot]'
    permissions:
      pull-requests: write
      contents: read
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Analyze and label PR
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const fs = require('fs');
            const path = require('path');

            const { owner, repo } = context.repo;
            const prNumber = context.payload.pull_request.number;
            const prTitle = context.payload.pull_request.title;
            const prBody = context.payload.pull_request.body || '';

            // High risk packages - runtime + affects all apps + hard to detect
            const HIGH_RISK = [
              'react',
              'react-dom',
              '@reduxjs/toolkit',
              'react-redux',
              'react-router-dom'
            ];

            // Medium risk packages - categorized by risk behavior
            // Category A: Major → medium, Minor/Patch → low (CI detectable)
            const MEDIUM_RISK_CI_DETECTABLE = [
              // Build tools
              'vite',
              'typescript',
              '@vitejs/plugin-react',
              'turbo',
              // Test frameworks
              'jest',
              'vitest',
              '@playwright/test',
              // Linting & Code quality
              'eslint',
              '@typescript-eslint/',
              // Storybook
              'storybook',
              '@storybook/'
            ];

            // Category B: Major/Minor → medium, Patch → low (Runtime impact)
            const MEDIUM_RISK_RUNTIME = [
              // Runtime - all apps
              'axios',
              'i18next',
              'react-i18next',
              // App-specific runtime
              'konva',
              'react-konva',
              'recharts'
            ];

            // Low risk - Pure dev tools (always low risk, all update types)
            const LOW_RISK_DEV_ONLY = [
              '@types/',
              'prettier',
              'stylelint',
              'eslint-config-',
              'eslint-plugin-',
              '@testing-library/',
              'ts-jest',
              '@vitest/ui',
              'sass',
              'husky',
              'lint-staged',
              'remark-',
              'cspell',
              'chromatic'
            ];

            // Low risk - Runtime utilities (Major → medium, Minor/Patch → low)
            const LOW_RISK_RUNTIME = [
              'lodash',
              'js-cookie',
              'date-fns',
              'clsx',
              'react-ga4',
              'qrcode.react',
              'react-hotkeys-hook',
              'papaparse'
            ];

            // Extract package name from PR title
            // Format: "deps(scope): bump <package> from x.x.x to y.y.y"
            const packageMatch = prTitle.match(/bump\s+(@?[\w\-\/]+)\s+from/i);
            const packageName = packageMatch ? packageMatch[1] : 'unknown';

            // Determine update type from PR body or title
            let updateType = 'patch';
            const versionMatch = prBody.match(/from\s+(\d+)\.(\d+)\.(\d+)\s+to\s+(\d+)\.(\d+)\.(\d+)/);
            if (versionMatch) {
              const [, fromMajor, fromMinor, , toMajor, toMinor] = versionMatch;
              if (fromMajor !== toMajor) {
                updateType = 'major';
              } else if (fromMinor !== toMinor) {
                updateType = 'minor';
              }
            }

            // Determine base risk level (from package category)
            const isHighRiskPackage = HIGH_RISK.some(pkg => packageName === pkg || packageName.startsWith(pkg));
            const isMediumRiskCIDetectable = MEDIUM_RISK_CI_DETECTABLE.some(pkg => packageName === pkg || packageName.startsWith(pkg));
            const isMediumRiskRuntime = MEDIUM_RISK_RUNTIME.some(pkg => packageName === pkg || packageName.startsWith(pkg));
            const isLowRiskDevOnly = LOW_RISK_DEV_ONLY.some(pkg => packageName === pkg || packageName.startsWith(pkg));
            const isLowRiskRuntime = LOW_RISK_RUNTIME.some(pkg => packageName === pkg || packageName.startsWith(pkg));

            // Determine final risk level based on package category and update type
            let riskLevel = 'low';

            if (isHighRiskPackage) {
              // High risk packages: only major updates remain high risk
              riskLevel = updateType === 'major' ? 'high' : 'medium';
            } else if (isMediumRiskCIDetectable) {
              // CI detectable (build tools, test, lint, storybook): Major → medium, Minor/Patch → low
              riskLevel = updateType === 'major' ? 'medium' : 'low';
            } else if (isMediumRiskRuntime) {
              // Runtime impact (axios, i18n, app-specific): Major/Minor → medium, Patch → low
              riskLevel = updateType === 'patch' ? 'low' : 'medium';
            } else if (isLowRiskDevOnly) {
              // Pure dev tools: always low risk
              riskLevel = 'low';
            } else if (isLowRiskRuntime) {
              // Low risk runtime utilities: Major → medium, Minor/Patch → low
              riskLevel = updateType === 'major' ? 'medium' : 'low';
            } else {
              // Unknown packages: major updates become medium risk
              riskLevel = updateType === 'major' ? 'medium' : 'low';
            }

            // Define labels
            const riskLabels = {
              high: { name: 'risk: high', color: 'B60205', description: 'High risk dependency update' },
              medium: { name: 'risk: medium', color: 'FBCA04', description: 'Medium risk dependency update' },
              low: { name: 'risk: low', color: '0E8A16', description: 'Low risk dependency update' }
            };

            const updateLabels = {
              major: { name: 'update: major', color: 'B60205', description: 'Major version update' },
              minor: { name: 'update: minor', color: 'FBCA04', description: 'Minor version update' },
              patch: { name: 'update: patch', color: '0E8A16', description: 'Patch version update' }
            };

            // Create labels if they don't exist
            const labelsToCreate = [riskLabels[riskLevel], updateLabels[updateType]];

            for (const label of labelsToCreate) {
              try {
                await github.rest.issues.getLabel({
                  owner,
                  repo,
                  name: label.name
                });
              } catch (error) {
                if (error.status === 404) {
                  await github.rest.issues.createLabel({
                    owner,
                    repo,
                    name: label.name,
                    color: label.color,
                    description: label.description
                  });
                }
              }
            }

            // Add labels to PR
            await github.rest.issues.addLabels({
              owner,
              repo,
              issue_number: prNumber,
              labels: [riskLabels[riskLevel].name, updateLabels[updateType].name]
            });

            // Read checklist template from docs/dependencies
            const templatePath = path.join(process.env.GITHUB_WORKSPACE, 'docs', 'dependencies', `${riskLevel}-risk.md`);
            let commentBody = '';

            try {
              commentBody = fs.readFileSync(templatePath, 'utf8');
              // Replace placeholders
              commentBody = commentBody.replace(/\{\{packageName\}\}/g, packageName);
            } catch (error) {
              console.log(`Template file not found: ${templatePath}`);
              commentBody = `## ${riskLevel.charAt(0).toUpperCase() + riskLevel.slice(1)} Risk Dependency Update\n\nThis PR updates \`${packageName}\`.`;
            }

            // Check if bot already commented
            const comments = await github.rest.issues.listComments({
              owner,
              repo,
              issue_number: prNumber
            });

            const botComment = comments.data.find(
              comment => comment.user.login === 'github-actions[bot]' &&
                         comment.body.includes('Risk Dependency Update')
            );

            if (botComment) {
              // Update existing comment
              await github.rest.issues.updateComment({
                owner,
                repo,
                comment_id: botComment.id,
                body: commentBody
              });
            } else {
              // Create new comment
              await github.rest.issues.createComment({
                owner,
                repo,
                issue_number: prNumber,
                body: commentBody
              });
            }

            console.log(`PR #${prNumber}: ${packageName} - Risk: ${riskLevel}, Update: ${updateType}`);

このGitHub Actionsワークフローは以下の機能を実行します。

  1. Dependabot PR検出dependabot[bot]が作成したPRのみを処理
  2. パッケージ分類:PRタイトルからパッケージ名を抽出し、リスクカテゴリを判定
  3. バージョン分析:PR本文からバージョン変更タイプ(Major/Minor/Patch)を検出
  4. ラベル追加:リスクレベルとアップデートタイプに応じたラベルを自動追加
  5. チェックリストコメントdocs/dependencies/フォルダのテンプレートを使用してチェックリストコメントを作成

自動ラベリング

Dependabot PRが作成されると、自動的にリスクラベルとアップデートタイプラベルが追加されます。

リスクラベル

ラベル条件
risk: high高リスクライブラリのMajorアップデート
risk: medium高リスクライブラリのMinor/Patch、中リスクライブラリのMajor(CI検出可能)、中リスクライブラリのMajor/Minor(ランタイム)、低リスクランタイムのMajor
risk: low中リスクライブラリのMinor/Patch(CI検出可能)、中リスクライブラリのPatch(ランタイム)、低リスク開発ツール(全バージョン)、低リスクランタイムのMinor/Patch

アップデートラベル

ラベル条件
update: majorMajorバージョンアップデート
update: minorMinorバージョンアップデート
update: patchPatchバージョンアップデート

チェックリストコメントテンプレート

docs/dependencies/フォルダを作成し、GitHub Actionsワークフローで使用するチェックリストテンプレートファイルを以下のように作成します。

docs/dependencies/high-risk.md

## 高リスク依存関係の更新

このPRは**高リスク**の依存関係(`{{packageName}}`)を更新します。慎重にレビューしてください。

### 必須チェックリスト

- [ ] CI通過確認
- [ ] CHANGELOG、Breaking changes確認
- [ ] マイグレーションガイドを確認(該当する場合)
- [ ] 全てのアプリテストとチームメンバーレビュー

> **警告**: このパッケージのメジャーアップデートは、大規模なテストとコード変更が必要になる場合があります。

docs/dependencies/medium-risk.md

## 中リスク依存関係の更新

このPRは**中リスク**の依存関係(`{{packageName}}`)を更新します。

### 必須チェックリスト

- [ ] CI通過確認
- [ ] CHANGELOG確認(Breaking changesがないこと確認)
- [ ] 影響を受けるアプリテストとチームメンバーレビュー(`yarn why {{packageName}}`

### 影響を受けるアプリ

`yarn why {{packageName}}`コマンドを実行して影響を受けるアプリを確認してください。

> **注意**: マージ前にCHANGELOGを確認してください。

docs/dependencies/low-risk.md

## 低リスク依存関係の更新

このPRは**低リスク**の依存関係(`{{packageName}}`)を更新します。

### 必須チェックリスト

- [ ] CI通過確認
- [ ] 自動アサインされたメンバーがレビュー

> この更新は安全と見なされます。CI通過後にマージしてください。

これらのテンプレートファイルはPRが作成されるとGitHub Actionsによって自動的にコメントに追加されます。{{packageName}}は実際のパッケージ名に自動的に置換されます。

完了

これでDependabot設定とGitHub Actionsを使用してPR処理を自動化する方法について学びました。

この自動化を適用すると、以下のメリットが得られます。

  • ライブラリアップデートのリスクを自動分類
  • PRに自動的にラベルとチェックリストを追加
  • 一貫したレビュープロセスを維持
  • チームメンバーのレビュー負担を軽減

プロジェクトの特性に合わせてこれらの設定をカスタマイズしてご利用ください。

前回の投稿でリスク分類基準を確認できますので、再度参考にしてください。

私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!

アプリ広報

今見てるブログを作成たDekuが開発したアプリを使ってみてください。
Dekuが開発したアプリはFlutterで開発されています。

興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。



SHARE
Twitter Facebook RSS