[GitHub Actions] Dependabot PR 처리를 위한 자동화

2025-02-04 hit count image

Monorepo 환경에서 Dependabot 설정과 GitHub Actions를 사용하여 PR 처리를 위한 SOP(Standard Operating Procedure)을 자동화하는 방법에 대해서 알아봅니다.

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: major빨강Major 버전 업데이트
update: minor노랑Minor 버전 업데이트
update: patch초록Patch 버전 업데이트

체크리스트 코멘트 템플릿

GitHub Actions 워크플로우에서 사용하는 체크리스트 템플릿 파일을 docs/dependencies/ 폴더를 만들고 다음과 같이 생성합니다.

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에 자동으로 라벨과 체크리스트 추가
  • 일관된 리뷰 프로세스 유지
  • 팀 멤버의 리뷰 부담 감소

여러분도 프로젝트의 특성에 맞게 이 설정을 커스터마이즈하여 사용해 보시기 바랍니다.

이전 포스트에서 리스크 분류 기준에 대해서 확인할 수 있으니, 다시 한 번 참고해 주세요.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

앱 홍보

책 홍보

블로그를 운영하면서 좋은 기회가 생겨 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.



SHARE
Twitter Facebook RSS