Project Factory에 GitHub Actions와 Quality Gate 적용하기

· 약 10분 읽기 · 조회 1 #GitHub Actions#CI#품질 게이트#Claude Code#자동화

Project Factory는 한 줄 프롬프트로 저장소, 코드, 테스트, 배포까지 만드는 구조다. 지금 설계에서는 검증을 에이전트의 로컬 verify와 Hook이 맡는데, 둘 다 같은 기계 안에서 돌아서 건너뛰거나 설정을 바꾸면 게이트가 사라진다. 이 글은 공장이 만드는 저장소마다 GitHub Actions 워크플로와 병합 차단 규칙을 넣어, 품질 게이트를 서버 쪽으로 옮기는 적용 설계안이다. 실제로 돌려 본 기록이 아니므로 YAML과 설정은 제안으로 읽으면 된다.

컨베이어 위의 나무 모형 집들이 계기판 달린 검사 게이트를 지나 포장 상자 쪽으로 가는 장면

한눈에 보기

공장 전체 구조는 Project Factory 설계 글에 있다. 여기서는 5단계 "검증 게이트"를 여러 겹으로 나눈다.

겹 어디서 도나 막는 것 못 막는 것
로컬 verify 에이전트 작업 디렉터리 타입 오류, 실패한 테스트, 깨진 빌드 에이전트가 실행을 건너뛰는 경우
배포 앞 Hook Claude Code 세션 verify 없이 배포 명령 실행 세션 밖에서 하는 push·배포
GitHub Actions GitHub 러너 위 항목 전부를 깨끗한 환경에서 다시 검사 검사 결과를 무시한 병합
브랜치 보호 GitHub 저장소 설정 검사가 실패한 PR의 병합 관리자 우회 허용 시

한 줄 프롬프트로 Project Factory가 워크플로 파일이 든 저장소를 만들고, Pull Request마다 GitHub Actions가 lint·타입·테스트·빌드를 돌린 뒤 품질 게이트를 통과해야 main 보호 규칙이 병합을 허용하며, 실패하면 같은 PR에 수정 커밋을 올려 다시 검사받는 흐름

왜 서버 쪽 게이트가 필요한가

로컬 verify는 빠르지만 증명이 되지 않는다. 에이전트가 "통과했다"고 보고해도 그 실행이 지금 커밋에서 일어났는지, 캐시나 로컬에만 있는 파일 덕분인지 남이 확인할 방법이 없다.

  • 깨끗한 환경. 러너는 매번 새로 받은 코드와 잠금 파일로 의존성을 설치한다. 로컬에만 있는 .env, 전역 패키지, 오래된 빌드 산출물이 결과를 바꾸지 못한다.
  • 커밋에 붙은 기록. 검사 결과가 커밋과 PR에 상태로 남는다. 감리나 리뷰에서 "이 버전이 테스트를 통과했나"에 링크 하나로 답할 수 있다.
  • 사람과 에이전트에게 같은 규칙. Hook은 Claude Code 세션 안에서만 동작한다. 사람이 터미널에서 직접 push해도 Actions와 브랜치 보호는 똑같이 적용된다.

로컬 게이트를 없애자는 뜻은 아니다. 로컬은 빠른 피드백, 서버는 최종 판정으로 역할을 나눈다.

템플릿에 워크플로를 넣는다

공장의 템플릿 카탈로그는 이미 CLAUDE.md, verify 명령, 샘플 테스트, 배포 설정을 갖고 있다. 여기에 .github/workflows/verify.yml을 하나 더한다. 원칙은 로컬과 같은 명령을 돌린다는 것이다. 워크플로가 로컬과 다른 검사를 하면 "내 쪽에선 통과"와 "CI는 실패"가 갈라지고, 에이전트는 어느 쪽을 믿을지 모른다.

웹앱 템플릿(Node)에 넣을 워크플로 제안은 다음과 같다.

name: verify
on:
  pull_request:
  push:
    branches: [main]
permissions:
  contents: read
jobs:
  verify:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run verify   # check → test(커버리지 포함) → build
  • permissions: contents: read로 토큰 권한을 읽기로 줄인다. 검사 job은 저장소에 쓸 일이 없다.
  • timeout-minutes를 둔다. 무한 대기하는 테스트가 러너 시간을 다 쓰는 것을 막는다.
  • 액션 버전(@v4)과 Node 버전은 템플릿을 만들 때 공식 문서의 최신 권장값으로 고정한다.
  • Python 템플릿이면 actions/setup-python과 make verify로 바꾸고 구조는 그대로 둔다.

게이트 항목과 기준

항목 명령 통과 기준 실패 시
lint npm run lint 오류 0 같은 PR에서 수정
타입 검사 verify의 check 단계 오류 0, 새 경고 없음 같은 PR에서 수정
테스트 verify의 test 단계 전부 통과 테스트를 약하게 고치지 않고 원인을 고친다
커버리지 test 단계의 --coverage 템플릿에 적은 기준 이상 테스트 추가
빌드 verify의 build 단계 종료 코드 0 같은 PR에서 수정

"새 경고 없음"처럼 기존 부채를 인정하는 기준은 숫자로 적어 둔다. 예를 들어 "svelte-check 경고 7개 유지"처럼 쓰면 늘어나는 순간 잡힌다. 부채를 어떻게 줄여 갈지는 기술 부채와 품질 지표 글에서 다뤘다.

커버리지 기준은 템플릿에서 숫자로

커버리지 기준을 워크플로에 따로 쓰면 로컬 verify와 갈라진다. 테스트 도구 설정에 넣어 두면 로컬과 CI가 같은 숫자로 실패한다. Vitest를 쓰는 템플릿이라면 다음과 같다(키 이름은 Vitest 버전에 따라 확인한다. v8 방식은 @vitest/coverage-v8 패키지가 필요하다).

// vite.config.ts 일부
test: {
  coverage: {
    provider: 'v8',
    thresholds: { lines: 80, branches: 70 }, // 예시 기준
  },
},

새 프로젝트는 처음부터 기준을 걸 수 있다. 기존 코드를 공장에 태울 때는 현재 값을 재서 그 값을 기준으로 두고 조금씩 올린다. 처음부터 높은 숫자를 걸면 첫 PR이 통과하지 못하고, 에이전트는 의미 없는 테스트로 숫자를 채우려 든다.

브랜치 보호로 병합을 막는다

Actions는 검사만 한다. 실패해도 병합 버튼은 눌린다. 막는 것은 저장소의 브랜치 보호(또는 ruleset) 설정이다.

설정할 것

  • main에 직접 push를 막고 PR을 거치게 한다.
  • 필수 상태 검사(required status checks)에 verify job을 넣는다.
  • 병합 전에 브랜치가 최신이어야 한다는 옵션을 켠다. 오래된 브랜치에서 통과한 결과로 병합되는 것을 막는다.
  • 관리자도 규칙을 따르게 한다. 우회가 열려 있으면 급할 때마다 열린다.

필수 검사는 job 이름으로 걸린다. 템플릿에서 job 이름을 바꾸면 보호 규칙은 옛 이름의 결과를 계속 기다리고, PR은 병합할 수 없는 상태로 멈춘다. 그래서 공장은 job 이름 verify를 템플릿 규칙으로 고정한다. 저장소를 만들 때 보호 규칙까지 같이 설정해야 하므로, 이 단계는 GitHub API 권한이 필요하고 사람 확인 뒤에 실행한다.

에이전트 루프와 연결한다

공장의 구현 루프는 Maker–Checker Loop다. 서버 게이트가 생기면 checker가 읽을 근거가 하나 늘어난다.

  1. maker가 커밋하고 PR을 연다.
  2. checker는 로컬 verify에 더해 gh pr checks --watch로 Actions 결과를 기다린다.
  3. 실패하면 gh run view --log-failed로 실패한 단계의 로그만 가져와 maker에게 넘긴다.
  4. maker는 같은 PR에 수정 커밋을 올리고, 2번으로 돌아간다.

로그 전체가 아니라 실패한 단계만 넘기는 이유는 컨텍스트다. 수천 줄 설치 로그를 넘기면 에이전트가 진짜 오류 줄을 놓친다. 반복 횟수에도 상한(예: 3회)을 두고, 넘으면 사람에게 넘긴다.

배포는 게이트 뒤에 둔다

배포 job은 검사 job에 묶는다. needs로 순서를 걸고, main에 push될 때만 돈다.

  deploy:
    needs: verify
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/deploy.sh   # 배포 토큰은 저장소 secret으로 넘긴다

배포 토큰은 저장소 secret에만 두고, PR에서 도는 job에는 넘기지 않는다. 백업과 롤백까지 포함한 배포 자동화는 공공 SI CI/CD 글에 정리했다.

정리

무엇을 누가 읽나 없으면 생기는 일
템플릿의 verify.yml 러너, 리뷰어 저장소마다 검사 내용이 다름
로컬과 같은 명령 에이전트 "로컬 통과, CI 실패"를 두고 헤맴
테스트 설정의 커버리지 기준 에이전트, 리뷰어 테스트 없는 기능이 병합됨
필수 상태 검사 GitHub 실패한 PR이 병합됨
고정된 job 이름 브랜치 보호 규칙 PR이 영원히 대기 상태로 멈춤
needs: verify 배포 배포 job 검사 전 코드가 운영에 나감

자주 하는 실수

  • CI에서만 다른 명령을 돌린다. 로컬과 판정이 갈라지면 에이전트는 CI 실패를 재현하지 못한다.
  • 검사만 켜고 브랜치 보호를 안 건다. 빨간 X가 떠도 병합은 된다.
  • job 이름을 바꾼다. 필수 검사가 옛 이름을 기다리며 모든 PR이 멈춘다.
  • 커버리지 기준을 처음부터 높게 건다. 숫자를 맞추려는 빈 테스트가 늘어난다.
  • PR job에 배포 토큰을 넘긴다. 외부 기여 PR이나 잘못된 스크립트가 토큰을 쓸 수 있다.

점검표

  • 모든 템플릿에 .github/workflows/verify.yml이 있다
  • 워크플로가 로컬 verify와 같은 명령을 실행한다
  • 검사 job의 토큰 권한이 contents: read다
  • job에 timeout-minutes가 있다
  • 커버리지 기준이 테스트 도구 설정에 숫자로 있다
  • main에 필수 상태 검사 verify가 걸려 있다
  • 관리자 우회가 꺼져 있다
  • 배포 job이 needs: verify이고 main push에서만 돈다
  • checker가 Actions 결과를 확인하고 실패 로그만 maker에게 넘긴다
  • 수정 반복에 상한이 있고, 넘으면 사람에게 넘긴다

품질 게이트를 서버로 옮긴다는 것은 "에이전트가 검사했다는 말"을 "커밋에 붙은 검사 결과"로 바꾸는 일이고, 공장은 그 워크플로와 보호 규칙을 저장소를 만들 때 함께 찍어 내면 된다.

댓글 0개

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.

댓글 쓰기

← 목록으로 돌아가기