목차
문제 상황
Yarn Workspaces 기반의 모노레포에서는 Dependabot이 package.json만 업데이트하고 yarn.lock은 갱신하지 않는 문제가 있습니다. 이를 해결하기 위해 Dependabot이 생성한 PR에서 yarn.lock 파일을 자동으로 업데이트하는 워크플로우를 운영하고 있었습니다. 이 워크플로우는 lockfile을 갱신한 뒤 커밋을 push하는데, push 이후 CI 워크플로우(lint, build, test 등)가 재실행되지 않는 문제가 발생했습니다.
모노레포에서의 Dependabot 운용에 대해서는 이전 포스트를 참고해 주세요.
문제가 된 워크플로우의 핵심 부분은 다음과 같습니다.
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.GITHUB_TOKEN }} # 이 토큰으로 push하면 다른 워크플로우가 트리거되지 않음
# ... yarn.lock 업데이트 ...
- name: Commit updated lockfile
run: |
git add yarn.lock
git diff --staged --quiet || git commit -m "chore: update yarn.lock"
git push # 이 push가 CI를 트리거하지 않음
원인
GitHub Actions에서 기본으로 제공되는 GITHUB_TOKEN으로 push하면, 다른 워크플로우가 트리거되지 않습니다. 이는 GitHub의 의도적인 설계로, 워크플로우 간 무한 재귀 실행을 방지하기 위한 것입니다.
When you use the repository’s
GITHUB_TOKENto perform tasks, events triggered by theGITHUB_TOKENwill not create a new workflow run.
즉, GITHUB_TOKEN으로 push한 커밋은 push 이벤트나 pull_request 이벤트의 synchronize 타입을 발생시키지 않으므로, CI 워크플로우가 실행되지 않습니다.
해결 방법 비교
이 문제를 해결하려면 GITHUB_TOKEN 대신 다른 인증 수단을 사용해야 합니다. 대표적으로 **PAT(Personal Access Token)**과 GitHub Apps가 있습니다.
| 항목 | PAT | GitHub Apps |
|---|---|---|
| 소유자 | 개인 | Organization |
| 권한 범위 | 사용자의 접근 권한에 종속 | 앱에 설정한 권한만 |
| 토큰 수명 | 수동 설정 (최대 1년) | 자동 발급, 1시간 후 만료 |
| 퇴사 리스크 | 있음 | 없음 |
| 감사 추적 | 개인 계정 기반 | Bot 전용 계정으로 표시 |
| Rate Limit | 개인 계정 공유 | 앱 전용 (더 높은 제한) |
방법 1: PAT (Personal Access Token)
가장 빠르게 적용할 수 있는 방법입니다.
설정 방법
PAT는 다음과 같은 절차로 생성하고, 설정할 수 있습니다.
- GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens
- Generate new token 클릭
- 설정:
- Resource owner: Organization 선택
- Repository access: 해당 repository만 선택
- Permissions:
- Contents: Read and write
- Pull requests: Read and write (필요한 경우)
- 생성된 토큰을 Repository의 Settings → Secrets → Actions에 등록
워크플로우 수정
이렇게 생성하고 설정한 PAT를 actions/checkout에 전달하면, push 시 다른 워크플로우가 정상적으로 트리거됩니다.
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.PAT_TOKEN }} # PAT로 변경
PAT의 단점
PAT는 빠르게 문제를 해결할 수 있지만, 프로덕션 환경에서 장기적으로 운영하기에는 다음과 같은 리스크가 있습니다.
| 항목 | 설명 |
|---|---|
| 개인 계정 종속 | 토큰 소유자가 퇴사하거나 계정이 비활성화되면 CI가 중단됨 |
| 넓은 권한 범위 | Fine-grained로 제한해도 개인의 접근 권한에 종속됨 |
| 수동 갱신 필요 | 만료 기간을 설정하면 주기적으로 갱신해야 함 |
| 감사 추적 | 커밋이 개인 계정 기반으로 추적됨 |
방법 2: GitHub Apps (권장)
Organization 소유의 GitHub App을 만들어 토큰을 발급받는 방법입니다. PAT의 단점을 모두 해소할 수 있으며, 프로덕션 환경에서 권장되는 방식입니다.
GitHub Apps 설정 가이드
GitHub Apps를 설정하는 과정은 다음과 같습니다.
Step 1: GitHub App 생성
Organization의 GitHub App을 생성하기 위해서는 Organization Owner 권한이 필요합니다. 권한이 있다면 다음 단계를 따라 진행합니다.
- GitHub에서 Organization 페이지로 이동
- Settings → Developer settings → GitHub Apps → New GitHub App
입력 항목:
| 항목 | 값 | 설명 |
|---|---|---|
| GitHub App name | my-ci-bot | Organization 내 유니크한 이름 |
| Homepage URL | Repository URL | 필수이지만 내부용이므로 repo URL로 충분 |
| Webhook | Active 체크 해제 | CI 토큰 발급용이므로 웹훅 불필요 |
Permissions 설정 (최소 권한 원칙)
이번 문제를 해결하기 위해서 필요한 최소 권한은 다음과 같습니다. 나머지 권한은 모두 불필요하므로, 최소 권한 원칙에 따라 필요한 권한만 부여합니다.
Repository permissions:
| Permission | Access | 용도 |
|---|---|---|
| Contents | Read and write | 코드 push |
| Metadata | Read-only | 자동 선택됨 |
| Pull requests | Read and write | PR 관련 작업이 필요한 경우 |
Organization permissions / Account permissions: 모두 불필요
설치 범위
이번 문제는 특정 repository에서 발생하는 문제이므로, GitHub App의 설치 범위를 Only on this account로 제한합니다. 이렇게 하면 App이 다른 repository에 설치되는 것을 방지할 수 있습니다.
- Where can this GitHub App be installed? →
Only on this account
Create GitHub App 클릭으로 생성을 완료합니다.
Step 2: Private Key 생성
GitHub App을 사용하려면 App ID와 Private Key가 필요합니다. App ID는 App 설정 페이지 상단에서 확인할 수 있고, Private Key는 다음 절차로 생성할 수 있습니다.
- 생성된 App 설정 페이지에서 App ID를 메모 (페이지 상단에 표시됨)
- 페이지 하단 Private keys 섹션 → Generate a private key 클릭
.pem파일이 자동 다운로드됨
이 두 값(App ID, Private Key)이 이후 워크플로우에서 토큰을 발급받는 데 사용됩니다.
Step 3: Repository에 App 설치
이렇게 생성한 GitHub App을 실제로 사용할 repository에 설치해야 합니다. 다음 단계를 통해 GitHub App을 설치합니다.
- App 설정 페이지 좌측 메뉴 → Install App
- Organization 선택 → Install
- Only select repositories → 대상 repository 선택
- Install 클릭
Step 4: Secrets 등록
이제 App ID와 Private Key를 GitHub Actions에서 사용할 수 있도록 Secrets로 등록합니다.
Repository Settings → Secrets and variables 에서 Actions와 Dependabot 각각에 동일한 Secret을 등록합니다.
- Actions: Settings → Secrets and variables → Actions → New repository secret
- Dependabot: Settings → Secrets and variables → Dependabot → New repository secret
| Secret Name | 값 |
|---|---|
MY_CI_BOT_APP_ID | Step 2에서 확인한 App ID |
MY_CI_BOT_PRIVATE_KEY | .pem 파일의 전체 내용 |
왜 두 곳 모두에 등록해야 하나요?
GitHub Actions는 워크플로우를 트리거한 주체에 따라 참조하는 secrets 저장소가 다릅니다. Dependabot이 트리거한 워크플로우는 공급망 공격 방지를 위해 제한된 보안 컨텍스트에서 실행되며, Dependabot secrets만 참조할 수 있습니다. 반면 일반적인 이벤트(수동 push, 일반 PR, workflow_dispatch 등)로 트리거된 워크플로우는 Actions secrets를 참조합니다. 따라서 동일한 워크플로우가 어떤 주체에 의해 트리거되더라도 정상 동작하려면 양쪽 모두에 등록해야 합니다.
.pem 파일 내용은 아래와 같은 형식입니다. 전체를 그대로 복사해서 등록합니다.
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
...
-----END RSA PRIVATE KEY-----
여러 repository에서 같은 App을 사용한다면, Organization Settings → Secrets에 등록하면 공유할 수 있습니다.
Step 5: 워크플로우 수정
actions/create-github-app-token 액션을 사용하여 런타임에 토큰을 생성합니다.
steps:
# 1. checkout은 GITHUB_TOKEN(read)으로 — 토큰을 .git/config에 남기지 않음
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
persist-credentials: false
# 2. 의존성 설치 (이 시점에서 .git/config에 credential 없음 → 안전)
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Update lockfile
run: yarn install --no-immutable
# 3. push 직전에만 App 토큰 생성 → 노출 시간 최소화
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.MY_CI_BOT_APP_ID }}
private-key: ${{ secrets.MY_CI_BOT_PRIVATE_KEY }}
# 4. 토큰으로 remote URL 설정 후 push
- name: Commit and push
run: |
git config user.name "my-ci-bot[bot]"
git config user.email "<APP_ID>+my-ci-bot[bot]@users.noreply.github.com"
git remote set-url origin "https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/${{ github.repository }}.git"
git add .
git diff --staged --quiet || git commit -m "chore: automated update"
git push
핵심 포인트는 다음과 같습니다.
actions/create-github-app-token@v1이 App ID와 Private Key로 **설치 토큰(installation token)**을 발급- 이 토큰은 약 1시간 동안 유효하며, 워크플로우 실행마다 새로 발급됨
- 이 토큰으로 push하면 다른 워크플로우가 정상적으로 트리거됨
- git commit의 author는
<app-name>[bot]형식을 사용
보안: 토큰 노출 범위를 최소화하는 구성
보안을 위해서는 단순히 GITHUB_TOKEN을 GitHub App 토큰으로 교체하는 것만으로는 충분하지 않습니다. 토큰이 노출되는 시간과 범위도 고려해야 합니다.
actions/checkout은 기본적으로 persist-credentials: true이므로, 전달한 토큰을 .git/config의 extraheader에 저장합니다. 만약 App 토큰으로 checkout한 뒤 yarn install을 실행하면, 의존 패키지의 post-install script가 .git/config를 읽어 write 권한을 가진 토큰을 탈취할 수 있습니다.
Dependabot PR은 외부 패키지를 업데이트하는 것이므로, 이 공급망(supply chain) 공격 시나리오에 특히 취약합니다.
[위험한 구성]
checkout (App token) → .git/config에 토큰 저장 → yarn install → post-install script가 토큰 탈취 가능
[안전한 구성]
checkout (persist-credentials: false) → .git/config에 토큰 없음 → yarn install (안전) → App 토큰 생성 → push
이를 방지하기 위해서는 다음과 같은 구성을 권장합니다.
persist-credentials: false: checkout 시.git/config에 credential을 저장하지 않음- 토큰 생성을 push 직전으로 이동:
yarn install등 외부 코드 실행 이후에 토큰을 생성 git remote set-url로 push 시에만 토큰 사용: 토큰이 필요한 순간에만 설정
permissions 설정에 대한 주의점
GitHub App 토큰으로 전환하면 잡의 permissions 블록도 재검토해야 합니다. permissions는 GITHUB_TOKEN에 대한 권한 설정이므로, 잡 내에서 GITHUB_TOKEN을 사용하지 않는다면 contents: write를 부여할 필요가 없습니다.
# Before: GITHUB_TOKEN으로 push하므로 write 필요
permissions:
contents: write
# After: push는 GitHub App 토큰이 담당하므로 read로 충분
permissions:
contents: read
최소 권한 원칙에 따라, 실제로 사용하지 않는 토큰에 불필요한 권한을 부여하지 않도록 하는 것이 보안 강화에 도움이 됩니다.
테스트 방법
GitHub App 토큰이 올바르게 작동하는지 확인하기 위해, 아래와 같은 테스트 워크플로우를 만들어 검증할 수 있습니다.
name: Test GitHub App Token
on:
push:
branches:
- test/app-token # 테스트용 브랜치에서만 실행
jobs:
test-token:
name: Test App Token
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.MY_CI_BOT_APP_ID }}
private-key: ${{ secrets.MY_CI_BOT_PRIVATE_KEY }}
- name: Verify token was generated
run: |
if [ -z "${{ steps.app-token.outputs.token }}" ]; then
echo "::error::Token generation failed"
exit 1
fi
echo "Token generated successfully"
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
- name: Test push (empty commit on temp branch)
run: |
git config user.name "my-ci-bot[bot]"
git config user.email "<APP_ID>+my-ci-bot[bot]@users.noreply.github.com"
git checkout -b test/app-token-verify
git commit --allow-empty -m "test: verify GitHub App token push"
git push origin test/app-token-verify
echo "Push succeeded"
- name: Cleanup temp branch
if: always()
run: git push origin --delete test/app-token-verify || true
테스트 순서
이렇게 작성한 테스트 워크플로우를 test/app-token 브랜치에 추가하고 push하면, 다음과 같은 과정을 통해 설정이 올바른지 검증할 수 있습니다.
- 위 워크플로우 파일을
test/app-token브랜치에 추가하고 push - Actions 탭에서 워크플로우 실행 결과 확인
- 각 스텝 성공 여부로 설정 검증:
- Generate token 성공 → Secrets 설정이 올바름
- Checkout 성공 → 토큰으로 repo 접근 가능
- Push 성공 → 토큰으로 push 가능
- 테스트 완료 후 워크플로우 파일 삭제
트러블슈팅
| 에러 메시지 | 원인 | 해결 |
|---|---|---|
could not create workflow dispatch event: HTTP 404 | App이 repository에 설치되지 않음 | Step 3의 Install App 확인 |
The permissions requested are not granted to this installation | App의 권한 부족 | App Settings → Permissions에서 권한 추가 |
Could not resolve to a valid ref | Private Key가 잘못됨 | .pem 파일 전체 내용이 Secret에 등록되었는지 확인 |
Resource not accessible by integration | 설치 범위 문제 | App이 해당 repository에 설치되었는지 확인 |
마무리
GitHub Actions에서 GITHUB_TOKEN으로 push하면 다른 워크플로우가 트리거되지 않는 것은 무한 재귀를 방지하기 위한 의도적인 설계입니다. 이를 해결하는 방법은 크게 두 가지입니다.
| 방법 | 적합한 상황 |
|---|---|
| PAT | 빠른 임시 해결이 필요한 경우 |
| GitHub Apps | 프로덕션 환경에서 권장되는 방법 |
GitHub Apps는 초기 설정에 Organization Owner 권한이 필요하다는 진입 장벽이 있지만, 한 번 설정하면 개인에 종속되지 않는 안전하고 관리하기 쉬운 토큰 관리가 가능합니다. 특히 Dependabot PR처럼 외부 패키지를 다루는 워크플로우에서는 토큰 노출 범위를 최소화하는 구성까지 고려하는 것을 추천합니다.
참고 자료
- GitHub Docs: Triggering a workflow from a workflow
- GitHub Docs: Registering a GitHub App
- actions/create-github-app-token
제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!
앱 홍보
Deku가 개발한 앱을 한번 사용해보세요.Deku가 개발한 앱은 Flutter로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.