콘텐츠로 이동

CI/CD 통합

Stripe는 Sessions 2026 개발자 키노트에서 “Minions”가 주 1,000개가 넘는 PR을 프로덕션에 전달한다고 밝혔습니다. Stripe의 설명에는 사람이 리뷰하고 승인하는 단계가 포함되므로 이를 무인 자동 머지로 해석하면 안 됩니다.

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

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

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

각 검증 단계의 책임을 분리하면 실패 원인을 더 빠르게 찾을 수 있습니다.

단계실행 시점이 장에서 맡는 역할
Pre-commit hook커밋 전형식, 타입, 관련 테스트의 빠른 확인
GitHub ActionsPR 생성·갱신 후린트, 테스트, 빌드의 일관된 재검증
구조적 제약 테스트CI 내부허용된 의존성과 코드 구조 강제
인간 리뷰CI 통과 후비즈니스 로직과 변경 의도 검토

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: bash -c 'if grep -Rn "console\.log" src/; then exit 1; else status=$?; [ "$status" -eq 1 ]; fi'
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 통과를 에이전트 작업의 완료 기준으로 정의하면, 인간 리뷰어는 비즈니스 로직에만 집중할 수 있습니다.