- Published on
GitHub Actions 매트릭스 fail-fast 끄고 실패 수집하기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 다른 OS, 런타임 버전, 의존성 조합을 동시에 검증하려면 GitHub Actions의 매트릭스 전략이 가장 효율적입니다. 그런데 기본 설정(또는 팀에서 관성적으로 켜둔 설정) 때문에 한 조합이 실패하면 나머지 조합이 즉시 취소되어, 실패 원인 파악이 오히려 느려지는 경우가 많습니다. 특히 다음 상황에서 그렇습니다.
- Node
18에서만 깨지는지,20에서도 깨지는지 확인해야 할 때 ubuntu-latest만 실패하는지windows-latest도 실패하는지 비교해야 할 때- 특정 테스트가 플래키해서 한 번 실패했다고 전체를 멈추면, 재현과 원인 분리가 더 어려울 때
이 글은 매트릭스의 fail-fast를 끄는 방법과, 실패를 “끝까지 실행한 뒤” 한 번에 수집해 요약/아티팩트로 남기는 패턴을 정리합니다.
참고로 장애를 빨리 수습하려면 “실패를 더 빨리 발견”하는 것만큼 “실패 정보를 더 많이 확보”하는 것도 중요합니다. 인프라/런타임 문제를 진단할 때 로그를 끝까지 수집하는 방식은 예를 들어 EKS에서 fluent-bit 로그 누락·지연 원인 9가지 같은 운영 진단 글에서도 동일한 철학으로 반복됩니다.
매트릭스 fail-fast가 실제로 하는 일
GitHub Actions에서 매트릭스는 하나의 job을 여러 조합으로 확장해 병렬 실행합니다. 이때 strategy.fail-fast가 true이면, 어떤 조합이 실패하는 순간 진행 중인 다른 조합을 취소합니다.
- 장점: 비용 절감, “빨간불”을 빠르게 띄움
- 단점: 실패가 연쇄적으로 발생하는지, 특정 조합에서만 발생하는지 파악하기 어려움
CI에서 중요한 질문은 종종 “하나라도 실패했나”가 아니라 “어떤 범위로 실패했나”입니다. 범위를 알아야 영향도를 추정하고, 롤백/핫픽스/우회 적용을 결정할 수 있습니다.
fail-fast 비활성화: 가장 기본적인 설정
아래처럼 strategy.fail-fast: false를 지정하면, 한 조합이 실패해도 나머지 조합은 계속 실행됩니다.
name: ci
on:
push:
pull_request:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm test
여기까지가 “끝까지 실행”의 시작점입니다. 하지만 실무에서는 여기서 한 단계 더 필요합니다.
- 실패한 조합의 로그를 나중에 빠르게 찾아야 함
- 실패한 조합들을 한 눈에 요약해야 함
- 필요하면 실패한 조합의 산출물(테스트 리포트, 스크린샷, 커버리지 등)을 모아야 함
실패를 수집하는 핵심: 실패해도 업로드는 실행되게 만들기
테스트 단계가 실패하면 기본적으로 이후 step은 실행되지 않습니다. 따라서 “실패했을 때도 실행되어야 하는 step”은 if: always()를 붙여야 합니다.
예를 들어 JUnit 리포트를 업로드하고 싶다면 다음 패턴이 필요합니다.
- name: Run tests
run: npm test -- --reporter=junit --reporter-options output=reports/junit.xml
- name: Upload test report (always)
if: always()
uses: actions/upload-artifact@v4
with:
name: junit-${{ matrix.os }}-node${{ matrix.node }}
path: reports/junit.xml
if-no-files-found: warn
이렇게 하면 테스트가 실패해도 reports/junit.xml이 생성되어 있는 한 아티팩트가 남습니다. 이후에는 “어떤 조합이 실패했는지”를 자동으로 모으는 단계가 필요합니다.
패턴 1: 매트릭스 job은 계속, 최종 요약 job에서 실패 목록 만들기
가장 많이 쓰는 방식은 다음 구조입니다.
test매트릭스 job: 각 조합을 실행하고, 결과/리포트를 아티팩트로 업로드summarize단일 job: 모든 아티팩트를 다운로드하고, 실패 조합을 표로 요약
중요 포인트는 두 가지입니다.
summarize는needs: test를 걸되if: always()로 실행되어야 함test가 실패하면 기본적으로 워크플로 전체가 실패로 표시되므로, 요약 job이 실행되도록 조건을 명시해야 함
아래 예시는 “각 매트릭스 조합에서 상태 파일을 남기고”, 마지막에 이를 모아 GitHub Step Summary에 표로 출력합니다.
name: matrix-ci
on:
pull_request:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- name: Run tests
id: run_tests
run: npm test
- name: Write status marker (always)
if: always()
shell: bash
run: |
mkdir -p status
if [ "${{ steps.run_tests.outcome }}" = "success" ]; then
echo "ok" > "status/result.txt"
else
echo "fail" > "status/result.txt"
fi
echo "os=${{ matrix.os }}" > status/meta.txt
echo "node=${{ matrix.node }}" >> status/meta.txt
- name: Upload status marker (always)
if: always()
uses: actions/upload-artifact@v4
with:
name: status-${{ matrix.os }}-node${{ matrix.node }}
path: status/
summarize:
runs-on: ubuntu-latest
needs: [test]
if: always()
steps:
- name: Download all status artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Build summary
shell: bash
run: |
echo "## Matrix 결과 요약" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| OS | Node | Result |" >> "$GITHUB_STEP_SUMMARY"
echo "|---|---:|---|" >> "$GITHUB_STEP_SUMMARY"
failed=0
for d in artifacts/status-*; do
os=$(grep '^os=' "$d/meta.txt" | cut -d= -f2)
node=$(grep '^node=' "$d/meta.txt" | cut -d= -f2)
result=$(cat "$d/result.txt")
if [ "$result" = "fail" ]; then
failed=1
fi
echo "| $os | $node | $result |" >> "$GITHUB_STEP_SUMMARY"
done
if [ $failed -eq 1 ]; then
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "일부 조합이 실패했습니다. 각 조합의 로그와 아티팩트를 확인하세요." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
이 방식의 장점은 단순합니다.
- 매트릭스는 실패해도 끝까지 진행
- 마지막에 실패 조합을 표로 보여줌
- 요약 job에서
exit 1로 전체 워크플로 실패를 명확하게 유지
즉, “정보 수집은 끝까지, 최종 상태는 실패로”를 동시에 만족합니다.
패턴 2: 테스트 단계 실패를 허용하고, 마지막에만 실패 처리하기
상황에 따라서는 매트릭스 job 자체가 실패로 찍히면(빨간 X) 팀이 과민 반응하거나, 후속 job이 조건 때문에 스킵되는 문제가 생깁니다. 이때는 테스트 step에 continue-on-error: true를 적용하고, 대신 상태 마커로 실패를 기록한 뒤 마지막에 한 번만 실패 처리할 수 있습니다.
주의할 점은 “실패를 숨기지 말고” 마지막에 반드시 실패로 종료해야 한다는 것입니다.
- name: Run tests (do not stop job)
id: run_tests
continue-on-error: true
run: npm test
- name: Fail job if tests failed (after uploads)
if: ${{ steps.run_tests.outcome != 'success' }}
run: exit 1
이 패턴은 리포트 업로드, 로그 수집, 디버그 정보 출력 등을 충분히 수행한 뒤에 실패를 확정할 수 있어, “실패 수집” 관점에서 매우 유용합니다.
실패 수집을 더 쓸모 있게 만드는 실전 팁
1) 조합별 로그를 구조화해서 남기기
단순히 콘솔 로그만으로는 비교가 어렵습니다. 조합별로 파일로 남기고 아티팩트로 업로드하면, 재현 없이도 비교가 쉬워집니다.
- name: Run tests and capture log
shell: bash
run: |
mkdir -p logs
npm test 2>&1 | tee "logs/test-${{ matrix.os }}-node${{ matrix.node }}.log"
- name: Upload logs (always)
if: always()
uses: actions/upload-artifact@v4
with:
name: logs-${{ matrix.os }}-node${{ matrix.node }}
path: logs/
2) 플래키 테스트는 “재시도”와 “실패 수집”을 분리
플래키를 재시도로 덮으면 근본 원인이 묻힙니다. 재시도는 하되, 첫 실패 로그도 함께 남겨야 합니다. 예를 들어 테스트 러너가 재시도를 지원한다면 “재시도 전 로그”를 별도 파일로 저장하거나, 실패 시점의 환경 정보를 출력하는 step을 if: failure()로 추가하세요.
- name: Dump env on failure
if: failure()
shell: bash
run: |
node -v
npm -v
env | sort | sed -n '1,120p'
3) 매트릭스가 커질수록 include로 “의미 있는 조합”만 돌리기
fail-fast를 끄면 비용이 늘 수 있습니다. 따라서 조합을 무작정 늘리기보다 include로 “검증 가치가 있는 조합”을 명시하고, 나머지는 제외하는 편이 좋습니다.
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
node: 20
- os: windows-latest
node: 20
- os: ubuntu-latest
node: 18
4) 실패 요약을 PR 코멘트로 남기기
Step Summary는 Actions 화면에서만 보입니다. 리뷰어가 PR에서 바로 확인하게 하려면 코멘트 자동화를 고려할 수 있습니다. 다만 토큰 권한, 중복 코멘트 방지, 포크 PR 제한 등 운영 포인트가 있어 팀 정책에 맞춰 적용하세요.
이런 “결과를 한 번에 요약해 커뮤니케이션 비용을 줄이는” 접근은 프론트 성능 튜닝에서도 유사합니다. 예를 들어 Next.js 14 RSC 느림? TTFB 급증 7가지 해결처럼 관측 지표를 모아서 병목을 좁히는 방식이 결국 시간을 절약합니다.
언제 fail-fast를 끄는 게 좋은가
- 릴리즈 직전, “영향 범위”를 빠르게 파악해야 할 때
- 런타임/OS/아키텍처별 차이를 비교해야 할 때
- 실패가 연쇄적일 수 있어, 첫 실패만으로 원인을 특정하기 어려울 때
- 플래키가 있는 구간에서 재현 단서를 최대한 모아야 할 때
반대로 다음 상황에서는 fail-fast: true가 더 낫습니다.
- 비용이 매우 민감하고, “하나라도 실패하면 어차피 머지 불가”인 단순 게이트
- 실패 원인이 거의 항상 동일해서 추가 조합 실행이 의미 없을 때
- 병렬 실행이 많아 러너 쿼터를 쉽게 소진할 때
정리
- 매트릭스에서 한 조합 실패가 나머지를 취소하는 것이 문제라면
strategy.fail-fast: false가 출발점입니다. - “실패 수집”의 핵심은
if: always()로 아티팩트 업로드/상태 기록을 보장하는 것입니다. - 가장 실용적인 구조는 “매트릭스는 끝까지 실행 + 마지막 요약 job에서 실패 목록을 표로 출력 + 최종 실패 처리”입니다.
이 패턴을 적용하면 CI가 단순히 빨간불을 켜는 도구가 아니라, 실패의 범위와 단서를 자동으로 모아주는 진단 도구로 바뀝니다.