왜 CI 실패 분류가 필요한가
파이프라인이 커질수록 실패의 원인은 다양해진다. 코드 버그, 플래키 테스트, 인프라 타임아웃, 의존성 설치 실패, 시크릿 만료 등이 한데 섞여 "빨간 X"로만 표시된다. 개발자는 매번 로그 수천 줄을 스크롤하며 원인을 찾아야 하고, 이 반복 작업이 리드타임을 갉아먹는다. 특히 플래키 실패를 진짜 버그로 오인해 재실행만 반복하거나, 반대로 진짜 회귀를 "또 플래키겠지"라며 방치하는 문제가 생긴다. 실패를 자동으로 분류하면 알림에 원인 카테고리와 담당 후보를 붙일 수 있어, 대응 속도가 크게 달라진다.
실패 유형 분류 체계
먼저 조직에 맞는 카테고리를 정의한다. 과하게 세분화하면 규칙 유지 비용이 커지므로 5~7개 정도가 실용적이다.
| 카테고리 | 대표 신호 | 기본 조치 |
|---|---|---|
| test_failure | assertion, expected/got | 커밋 작성자에게 알림 |
| flaky | 재시도 성공 이력, timeout 산발 | 재실행 후 이슈 트래킹 |
| infra | runner offline, 5xx, OOMKilled | 플랫폼팀 에스컬레이션 |
| dependency | npm ERR, could not resolve | 락파일/캐시 점검 |
| config | secret not found, invalid yaml | 파이프라인 소유자 알림 |
규칙 기반 1차 분류
대부분의 실패는 로그 패턴 매칭만으로 충분히 걸러진다. 우선순위를 두어 가장 구체적인 규칙부터 검사한다.
import re
RULES = [
("infra", r"(runner.*offline|OOMKilled|no space left|5\d\d Server Error)"),
("dependency", r"(npm ERR!|could not resolve|Could not find a version|ECONNRESET)"),
("config", r"(secret .* not found|invalid workflow|yaml: line \d+)"),
("test_failure", r"(AssertionError|Expected .* but got|FAILED .*::)"),
]
def classify(log: str) -> str:
for label, pattern in RULES:
if re.search(pattern, log, re.IGNORECASE):
return label
return "unknown"
핵심은 순서다. 인프라·설정 오류를 test_failure보다 먼저 검사해야, 테스트 로그에 섞인 스택트레이스가 오분류되지 않는다.
플래키 판정은 이력으로
단일 실행 로그만으로 플래키를 판단하면 오탐이 많다. 같은 테스트가 최근 N회 중 실패와 성공을 오갔는지를 이력에서 확인하는 편이 정확하다.
-- 최근 20회 실행에서 성공/실패가 공존하면 flaky 후보
SELECT test_id
FROM test_runs
WHERE started_at > now() - interval '7 days'
GROUP BY test_id
HAVING count(*) FILTER (WHERE status = 'fail') > 0
AND count(*) FILTER (WHERE status = 'pass') > 0
AND count(*) >= 5
ORDER BY count(*) FILTER (WHERE status = 'fail') DESC;
이렇게 뽑은 목록을 분류기와 결합해, 규칙이 test_failure로 판정하더라도 flaky 후보에 속하면 라벨을 재조정한다.
알림 봇 연동
분류 결과를 CI 워크플로 마지막 단계에서 호출하고, 카테고리별로 채널과 멘션을 다르게 라우팅한다.
on:
workflow_run:
workflows: ["ci"]
types: [completed]
jobs:
triage:
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Classify and notify
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
RUN_ID: ${{ github.event.workflow_run.id }}
run: python scripts/triage.py --run-id "$RUN_ID"
알림에는 카테고리, 실패한 잡, 실패 로그 요약 5~10줄, 실행 링크를 넣는다. 로그 전체를 붙이면 오히려 읽지 않으므로, 매칭된 라인 주변만 잘라 보낸다.
운영상 주의점
규칙 기반 분류는 처음엔 잘 맞지만 시간이 지나면 도구 버전 변경으로 로그 형식이 바뀌어 조용히 오분류된다. 그래서 unknown 비율을 지표로 추적하고, 주기적으로 unknown 샘플을 리뷰해 규칙을 보강해야 한다. 또한 알림 라우팅은 반드시 사람이 조정할 수 있어야 한다. 자동 멘션이 틀리면 신뢰가 빠르게 무너지므로, 확신이 낮은 경우엔 개인 멘션 대신 팀 채널로만 보낸다.
LLM 분류로 확장할 때
규칙으로 잡히지 않는 unknown이 누적되면 LLM 보조 분류를 붙일 수 있다. 다만 전체를 LLM에 맡기지 말고, 규칙이 unknown을 반환한 경우에만 호출해 비용과 지연을 억제하는 계단식 구조가 현실적이다. LLM 출력은 정의된 카테고리 중 하나로 강제하고, 근거 로그 라인을 함께 반환하도록 해 사람이 검증할 수 있게 한다. 분류 정확도는 결국 라벨링된 실패 데이터의 품질에 달려 있으므로, 봇이 보낸 알림에 "이 분류가 맞았나" 피드백 버튼을 두어 학습 데이터를 쌓는 것이 장기적으로 가장 효과적이다.