안전한 코드를 배포하라: 실제로 중요한 GitHub Actions 패턴
Source: Dev.to

대부분의 CI 가이드가 가진 문제점
“GitHub Actions unit tests”를 검색하고 가이드를 찾아 YAML을 복사하면 동작합니다 — 하지만 다음과 같은 상황이 발생합니다:
- 문서 전용 PR이 20분짜리 빌드를 트리거합니다.
- CI에서는 테스트가 통과하지만 프로덕션에서는 충돌합니다.
- 한 번씩만 실패하는 플레이키 테스트가 무작위로 실패해 팀 전체를 차단합니다.
이것은 실제 문제입니다. 해결 방법은 다음과 같습니다.
1. 경로 필터링 — 변경 사항이 없을 때 빌드 건너뛰기

- name: Detect changes
id: changes
uses: dorny/paths-filter@v3
with:
filters: |
code:
- '**/*.cpp'
- '**/*.h'
- '**/*.hpp'
- '**/CMakeLists.txt'
- name: Run Tests
if: steps.changes.outputs.code == 'true'
run: make run-tests실제 영향: README.md의 오타 수정만으로도 이전에는 러너 시간을 20분이나 소모했습니다. 이제는 5초 이내에 완료됩니다.
2. Docker 안에서 빌드 및 테스트 실행

러너 환경 드리프트는 재현성을 해치는 조용한 살인자입니다. Ubuntu 버전이 바뀌고, 시스템 라이브러리가 업데이트되면 이유를 설명할 수 없는 빨간 CI가 갑자기 나타납니다.
- name: Build Docker image with tests enabled
run: |
docker build . -f Dockerfile -t myapp-ci \
--build-arg WITH_TESTS=true
- name: Run tests inside container
run: |
mkdir -p ./test-output
docker run --rm \
-v ./test-output:/app/build/Testing/Temporary \
myapp-ci \
ctest --test-dir /app/build --output-on-failure왜 ./test-output을 마운트하나요? 테스트가 실패하면 로그가 필요합니다. 마운트하지 않으면 컨테이너가 종료될 때 로그가 사라집니다.
3. ASAN 및 TSAN 실행 — 코드가 생각보다 안전하지 않음

프로덕션에 도달하는 대부분의 C++ 버그는 다음 단계에서 차단됩니다:
- AddressSanitizer (ASAN) – 버퍼 오버플로, use‑after‑free, 메모리 누수
- ThreadSanitizer (TSAN) – 데이터 레이스, 데드락
jobs:
release:
uses: ./.github/workflows/test.yml
with:
preset: release
asan:
uses: ./.github/workflows/test.yml
with:
preset: asan
tsan:
needs: asan # 순차 실행 — 두 작업 모두 CPU 집약적
uses: ./.github/workflows/test.yml
with:
preset: tsan⚠️ ASAN과 TSAN을 동시에 실행하지 마세요. 두 도구가 같은 러너 자원을 경쟁하면 실제 버그가 아닌 자원 부족으로 인한 거짓 실패가 발생합니다. needs:를 사용해 순서를 지정하세요.
4. 전체 스위트가 아닌 실패한 테스트만 재시도

플레이키 테스트는 존재합니다. 이를 부정하면 팀에 매주 몇 시간씩 손해가 됩니다. 잘못된 해결책은 테스트에 DISABLED_를 붙이거나 무시하는 것이고, 올바른 해결책은 다음과 같습니다:
- name: Run Tests
run: ctest --test-dir build --output-on-failure --parallel $(nproc)
- name: Retry Failed Tests
if: failure()
run: |
ctest --test-dir build \
--rerun-failed \
--output-on-failure \
--repeat until-pass:3--rerun-failed는 CTest에 내장된 옵션으로, 마지막 실행 결과를 읽어 실패한 테스트만 다시 실행합니다. 20분짜리 전체 스위트 재시도가 90초짜리 타깃 재시도로 바뀝니다.
5. 디버그용 상세 출력 — 일반 실행을 오염시키지 않기
-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftonlb35bmo190yxk0q4w.png)
- name: Run Tests (normal)
run: ctest --test-dir build --output-on-failure
- name: Run Tests (verbose)
if: github.event.inputs.verbose == 'true'
run: ctest --test-dir build --output-on-failure --verbose깊은 진단이 필요할 때는 verbose 입력을 토글하고, 그렇지 않을 경우 일상적인 실행을 위해 출력이 깔끔하게 유지되도록 하세요.
자세한 테스트 출력
매 실행마다 자세한 테스트 출력은 수천 줄의 잡음과 같습니다. 하지만 새벽 2시에 디버깅을 할 때는 모든 바이트가 필요합니다.
- name: Run Tests
if: runner.debug != '1'
run: ctest --test-dir build --output-on-failure
- name: Run Tests (verbose)
if: runner.debug == '1'
run: ctest --test-dir build --verbose디버그 모드를 활성화하려면: GitHub Actions → Re‑run jobs → Enable debug logging. YAML을 변경할 필요도 없고, 새로운 커밋도 필요 없습니다. 스위치를 전환하기만 하면 됩니다.
6. 하나의 설정 블록으로 중복 실행 취소

수정을 푸시하고, 뭔가를 놓쳤다는 걸 깨달아 다시 푸시합니다. 이제 같은 브랜치에 두 개의 CI 실행이 생깁니다. 첫 번째 실행은 낭비입니다.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true모든 워크플로우의 상단에 두 줄만 추가하면 됩니다. 새로운 푸시가 이전 실행을 즉시 종료시킵니다. 비용도 없고, 고민도 없으며, 즉각적인 승리입니다.
요약
CI는 코드다. 같은 원칙을 적용하라: 낭비 없이, 유용한 실패, 유지보수가 쉬운.
