콘텐츠로 이동

CI/CD 통합

Stripe의 “Minions” 시스템은 AI 에이전트 기반 CI/CD의 가장 잘 알려진 사례입니다. 이 시스템은 주 1,000개 이상의 PR을 자동으로 머지합니다.

워크플로우는 단순합니다:

1. 개발자가 태스크 설명 작성
2. 에이전트가 코드 작성
3. CI 파이프라인 자동 실행 (테스트, 린트, 타입체크)
4. CI 통과 → 에이전트가 PR 오픈
5. 인간이 코드 리뷰 후 머지

이 워크플로우의 핵심은 CI가 에이전트의 품질 게이트 역할을 한다는 점입니다. 에이전트가 아무리 자신 있게 작성한 코드라도 CI를 통과해야만 PR로 제출됩니다.

Pre-commit hooks는 에이전트가 커밋하기 전에 자동으로 실행되는 검증 단계입니다.

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-json
- id: check-yaml
- repo: local
hooks:
# TypeScript 타입 체크
- id: typescript-check
name: TypeScript Check
entry: pnpm tsc --noEmit
language: system
types: [ts, tsx]
pass_filenames: false
# 테스트 실행 (변경된 파일 관련)
- id: vitest-related
name: Run Related Tests
entry: pnpm vitest run --reporter=verbose
language: system
pass_filenames: false
# console.log 감지
- id: no-console-log
name: No console.log
entry: grep -rn "console\.log" src/
language: system
pass_filenames: false
# TODO 주석 감지
- id: no-todo-comments
name: No TODO Comments
entry: grep -rn "TODO\|FIXME\|HACK" src/
language: system
pass_filenames: false

기본 에이전트 검증 워크플로우

섹션 제목: “기본 에이전트 검증 워크플로우”
.github/workflows/agent-verify.yml
name: Agent Output Verification
on:
pull_request:
branches: [main, develop]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: TypeScript Check
run: pnpm tsc --noEmit
- name: Lint
run: pnpm eslint src/ --max-warnings 0
- name: Tests
run: pnpm vitest run --coverage
- name: Coverage Check
run: pnpm vitest run --coverage --coverage.thresholds.lines=80
- name: Build
run: pnpm build
- name: Structural Constraints Check
run: pnpm test:arch

구조적 제약 테스트 (ArchUnit 스타일)

섹션 제목: “구조적 제약 테스트 (ArchUnit 스타일)”

에이전트가 아키텍처 경계를 위반하지 않도록 코드 구조 자체를 테스트합니다.

tests/arch/architecture.test.ts
import { describe, it, expect } from 'vitest'
import { glob } from 'glob'
import * as fs from 'fs'
describe('Architecture Constraints', () => {
it('agents/ 폴더는 tools/ 만 import 할 수 있다', async () => {
const agentFiles = await glob('src/agents/**/*.ts')
for (const file of agentFiles) {
const content = fs.readFileSync(file, 'utf-8')
const imports = content.match(/from ['"]\.\.\/([^'"]+)['"]/g) ?? []
for (const imp of imports) {
const isAllowed = imp.includes('../tools') || imp.includes('../types')
expect(isAllowed, `${file}: 허용되지 않은 import: ${imp}`).toBe(true)
}
}
})
it('프로덕션 코드에 console.log가 없어야 한다', async () => {
const srcFiles = await glob('src/**/*.ts', { ignore: 'src/**/*.test.ts' })
for (const file of srcFiles) {
const content = fs.readFileSync(file, 'utf-8')
expect(
content.includes('console.log'),
`${file}: console.log 발견`
).toBe(false)
}
})
it('모든 exported 함수는 JSDoc이 있어야 한다', async () => {
const files = await glob('src/harness/**/*.ts')
for (const file of files) {
const content = fs.readFileSync(file, 'utf-8')
const exportedFunctions = content.match(/^export (async )?function \w+/gm) ?? []
const jsdocBlocks = content.match(/\/\*\*[\s\S]*?\*\//g) ?? []
expect(
jsdocBlocks.length >= exportedFunctions.length,
`${file}: export 함수 ${exportedFunctions.length}개, JSDoc ${jsdocBlocks.length}`
).toBe(true)
}
})
})

에이전트가 작성한 PR에 자동으로 레이블을 붙이고 추가 검증을 실행하는 워크플로우입니다.

.github/workflows/agent-pr-label.yml
name: Label Agent PRs
on:
pull_request:
types: [opened]
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Check if agent-authored
id: check-agent
run: |
BODY="${{ github.event.pull_request.body }}"
if echo "$BODY" | grep -q "\[agent-generated\]"; then
echo "is_agent=true" >> $GITHUB_OUTPUT
fi
- name: Add agent label
if: steps.check-agent.outputs.is_agent == 'true'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['agent-generated', 'needs-human-review']
})
- name: Require additional review for agent PRs
if: steps.check-agent.outputs.is_agent == 'true'
uses: actions/github-script@v7
with:
script: |
github.rest.pulls.requestReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
reviewers: ['senior-engineer-username']
})

에이전트가 CI 실패를 자동으로 인식하고 수정하도록 피드백 루프를 구성합니다.

CI 실패 발생
실패 로그를 에이전트 컨텍스트에 주입
에이전트가 수정 시도 (최대 3회)
성공 → PR 업데이트
실패 → 인간 에스컬레이션
harness/ci-feedback.ts
async function handleCIFailure(
failureLog: string,
agent: Agent,
maxRetries: number = 3
): Promise<'fixed' | 'escalated'> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const context = `CI 실패 (시도 ${attempt}/${maxRetries}):\n\n${failureLog}`
const result = await agent.run(context)
if (await runCI()) {
return 'fixed'
}
}
await notifyHuman('에이전트가 CI를 3회 시도 후 실패. 수동 개입 필요.')
return 'escalated'
}

CI/CD는 에이전트의 외부 검증자입니다. Pre-commit hooks로 커밋 전 검증, GitHub Actions로 PR 품질 게이트, 구조적 제약 테스트로 아키텍처 무결성을 유지하세요. Stripe Minions처럼 CI 통과를 에이전트 작업의 완료 기준으로 정의하면, 인간 리뷰어는 비즈니스 로직에만 집중할 수 있습니다.