모노레포에서 Dependabot PR이 동작하지 않는 문제 해결

2025-02-05 hit count image

Yarn Workspaces 기반 모노레포에서 Branch Protection Rule과 lockfile 갱신으로 인해 Dependabot rebase가 실패하는 문제의 원인과 해결 방법에 대해서 알아봅니다.

github_actions

이전 포스트에서는 Dependabot PR을 효율적으로 처리하기 위한 SOP와 자동화 방법에 대해서 알아보았습니다.

이번 포스트에서는 Yarn Workspaces 기반의 모노레포에서 Dependabot을 운용하면서 발생한 문제와 해결 방법에 대해서 알아보겠습니다.

배경

Yarn Workspaces 기반의 모노레포에서 Dependabot을 운용하던 중, 두 가지 문제가 동시에 발생했습니다.

  • 증상 1: @dependabot rebase 실패
Oh no! Something went wrong on our end. Please try again later.
If the problem persists, please contact GitHub support for assistance
  • 증상 2: Dependabot이 자기 브랜치를 업데이트할 수 없음
Dependabot attempted to update this pull request, but because the branch
dependabot/github_actions/github-actions-946974bce4 is protected
it was unable to do so.

원인을 조사한 결과, Branch Protection RuleGitHub Actions의 lockfile 갱신 방식 두 가지가 얽혀 있었습니다.

문제 1: Branch Protection Rule이 Dependabot 브랜치를 보호

원인

리포지토리에 **/** 패턴의 Branch Protection Rule이 설정되어 있었습니다. 이 와일드카드 패턴은 모든 브랜치에 매칭되기 때문에, dependabot/... 브랜치까지 보호 대상에 포함되어 있었습니다.

# 기존 Branch Protection Rules
**/**    → 모든 브랜치 (Dependabot 포함) ← 문제의 원인
develop  → develop 브랜치 전용
main     → main 브랜치 전용

Dependabot 브랜치가 보호되면, Dependabot은 자기 브랜치에 push할 수 없어 rebase나 업데이트가 불가능합니다.

해결: **/**를 구체적인 패턴으로 분리

Branch Protection Rules는 exclude 기능을 지원하지 않습니다. 따라서 **/**에서 dependabot/**만 제외하는 것이 불가능합니다.

해결 방법은 **/** 룰을 삭제하고, 보호가 필요한 브랜치 패턴만 개별적으로 생성하는 것입니다. 우리 프로젝트에서는 브랜치 네이밍 규칙이 {prefix}/{service}/{description} 형식으로 정해져 있으므로, 해당 prefix별로 룰을 만들었습니다.

# 변경 후 Branch Protection Rules
hotfix/**        ← 신규
release/**       ← 신규
review/**        ← 신규
feature/**       ← 신규
fix/**           ← 신규
sub-feature/**   ← 신규
merge/**         ← 신규
develop          ← 기존 유지
main             ← 기존 유지

dependabot/** 패턴은 만들지 않으므로, Dependabot 브랜치는 자연스럽게 보호 대상에서 제외됩니다.

GitHub GraphQL API로 일괄 생성

7개의 룰을 수동으로 만드는 것은 번거롭기 때문에, GraphQL API를 사용하여 일괄 생성했습니다.

# 리포지토리 ID 조회
REPO_ID=$(gh api graphql -f query='
  { repository(owner: "ORG_NAME", name: "REPO_NAME") { id } }
' --jq '.data.repository.id')

# Bypass 대상 유저의 Node ID 조회
gh api "users/USERNAME" --jq '.node_id'

# 7개 룰 일괄 생성
for pattern in "hotfix/**" "release/**" "review/**" \
               "feature/**" "fix/**" "sub-feature/**" "merge/**"; do
  gh api graphql -f query='
    mutation {
      createBranchProtectionRule(input: {
        repositoryId: "'"$REPO_ID"'"
        pattern: "'"$pattern"'"
        requiresApprovingReviews: true
        requiredApprovingReviewCount: 2
        requiresCodeOwnerReviews: true
        dismissesStaleReviews: false
        requiresConversationResolution: true
        requiresStatusChecks: true
        requiredStatusCheckContexts: []
        allowsForcePushes: true
        allowsDeletions: true
        isAdminEnforced: false
        bypassPullRequestActorIds: ["USER_NODE_ID_1", "USER_NODE_ID_2"]
      }) {
        branchProtectionRule { id pattern }
      }
    }'
done

새 룰을 모두 생성한 후, 기존 **/** 룰을 삭제합니다.

# 기존 룰 ID 조회
gh api graphql -f query='{
  repository(owner: "ORG_NAME", name: "REPO_NAME") {
    branchProtectionRules(first: 20) {
      nodes { pattern id }
    }
  }
}'

# 삭제
gh api graphql -f query='
  mutation {
    deleteBranchProtectionRule(input: {
      branchProtectionRuleId: "BPR_XXXXXXXXX"
    }) { clientMutationId }
  }'

대안: Rulesets 사용

GitHub의 새로운 Rulesets 기능을 사용하면 exclude 패턴을 지정할 수 있습니다. ALL(전체 브랜치)을 include하고 refs/heads/dependabot/**/*를 exclude하는 방식입니다.

다만, Rulesets와 Branch Protection Rules는 **독립적으로 동작(additive)**합니다. Rulesets를 추가하더라도 기존 Branch Protection Rules는 그대로 적용되므로, 반드시 기존 **/** 룰을 삭제해야 합니다.

또한 Rulesets의 Bypass list는 개별 유저를 직접 추가할 수 없고, Repository Role, Team, GitHub App 단위로만 설정할 수 있다는 점도 주의가 필요합니다.

검증

변경 후 Dependabot 브랜치의 보호 상태를 확인합니다.

gh api "repos/ORG/REPO/branches/dependabot%2F..." --jq '.protected'
# false → 정상

문제 2: GitHub Actions의 lockfile 갱신이 Dependabot rebase를 차단

Branch Protection Rule 문제를 해결한 후에도 @dependabot rebase가 여전히 실패했습니다.

Looks like this PR has been edited by someone other than Dependabot.
That means Dependabot can't rebase it - sorry!

원인

모노레포에서는 Dependabot이 각 앱의 package.json만 갱신하고 루트 yarn.lock은 갱신하지 못합니다. 이를 보완하기 위해 이전 포스트에서 소개한 GitHub Actions 워크플로우에서 yarn install을 실행하고 yarn.lock 변경분을 커밋하고 있었습니다.

# dependabot_actions.yml
- name: Commit updated lockfile
  run: |
    git config user.name "github-actions[bot]"
    git config user.email "github-actions[bot]@users.noreply.github.com"
    git add yarn.lock
    git diff --staged --quiet || git commit -m "chore: update yarn.lock"
    git push

이로 인해 Dependabot 브랜치의 커밋 히스토리는 다음과 같이 됩니다.

커밋Author
deps(pos): bump @types/node...dependabot[bot]
chore: update yarn.lockgithub-actions[bot]

Dependabot은 서버 측에서 자신이 push한 HEAD SHA를 추적합니다. 추가 커밋으로 HEAD SHA가 변경되면, 커밋 author가 누구든 상관없이 “edited by someone other than Dependabot”으로 판단하고 rebase를 거부합니다.

해결: [dependabot skip] 커밋 메시지

GitHub 공식 문서에서 이 문제에 대한 해결책을 제공하고 있습니다. 커밋 메시지에 [dependabot skip]을 포함하면, Dependabot이 해당 커밋을 “일시적인 커밋”으로 간주하여 rebase 시 force-push로 덮어씁니다.

# Before
git diff --staged --quiet || git commit -m "chore: update yarn.lock"

# After
git diff --staged --quiet || git commit -m "[dependabot skip] chore: update yarn.lock"

동작 흐름

1. Dependabot이 PR 생성         → 커밋 A
2. GitHub Action이 yarn.lock 갱신 → 커밋 B [dependabot skip]
3. @dependabot rebase 실행
4. Dependabot이 rebase 실행      → 커밋 A' (커밋 B는 force-push로 삭제)
5. synchronize 이벤트 발생
6. GitHub Action이 다시 실행      → 커밋 B' [dependabot skip]

rebase 시 [dependabot skip] 커밋이 사라지지만, 워크플로우가 synchronize 이벤트에 반응하므로 자동으로 다시 생성됩니다.

기존 PR에 대한 대응

이미 [dependabot skip] 없이 커밋이 push된 기존 PR은 @dependabot rebase가 불가합니다. 이 경우 @dependabot recreate로 PR을 재생성해야 합니다.

정리

문제원인해결
Dependabot이 브랜치를 업데이트할 수 없음**/** Branch Protection Rule이 Dependabot 브랜치까지 보호구체적인 브랜치 패턴으로 분리하여 Dependabot 브랜치를 보호 대상에서 제외
@dependabot rebase 실패GitHub Actions의 lockfile 갱신 커밋이 Dependabot의 HEAD SHA 추적을 방해커밋 메시지에 [dependabot skip] 추가

참고 자료

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

앱 홍보

책 홍보

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

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



SHARE
Twitter Facebook RSS