Github Action을 활용한 CI&CD - 발생할 수 있는 문제들

박지예·2026년 8월 3일

공부2026

목록 보기
13/20

ci cd에 대해 학습하였다.

정의 (가볍게)

CI

코드를 합치기 전에 자동으로 검증하는 것. 테스트, 빌드 가능여부, 코드 리뷰 같은 "이 코드 합쳐도 되나?" 의 검사

CD

CI로 검증한 코드를 자동으로 배포 가능한 상태까지 만든다.
더 나아가 상태를 통과하면 프로덕션까지 자동화 할 수 있다.

google play 내부 테스트 트랙에 자동 업로드 한다거나
ios testflight 내부 테스트 트랙에 자동으로 업로드 한다거나 하는 식으로 셋팅이 가능하다.

<< 관련 step 은 아래에 좀 더 자세히 정리하겠음

어떻게 자동으로 관리하는가 (with Github Action)

Gihub Action

.github/workflows/xxx.yml

위 프로젝트 폴더 위치에 .yml 또는 (.ymal) 파일을 구성해두면 Github 가 자동으로 인식한다.

저 파일에 셋팅하는 걸 워크 플로우, 그 워크 플로우를 읽고 실행해주는 github의 기능이 "Github Action" 이다.

.yml 파일의 구성에 따라 이벤트 (push 나 pr)이 발생하면
VM(Virtual Machine, 가상 머신)에서 그 안의 step 을 위에서 아래로 실행한다.

위 글에선 이 github Action 으로 CI/CD 를 어떻게 구성하는지 개념을 학습할 것이다.

PR을 올렸을때 발생하는 CI

PR을 올리면 자동으로 CI를 실행하게 하고 싶다.

여기서 먼저 알면 좋은 정보는 github는 저장소에 push 할때마다 push될 때 마다 push 이벤트를 발생시키고, .github/workflows/ 안에 있는 yaml 파일 중 조건이 맞는 워크플로우를 실행한다.

pr-test.yml

on:
  pull_request:
    branches:
      - main
      - dev

main이랑 dev를 대상으로 하는 pr 일때 실행한다.

  • Unity EditMode 테스트 → PlayMode 테스트 → Addressable 빌드 검증 → 결과 파싱 → Slack 알림

job 아래 있는 step 의 name 으로 PR 제목의 [skip test] 를 넣으면 건너 뛴다거나 [clean cache] library 캐시를 지우고 시작하는 트릭도 있다.

실제 내가 프로젝트에서 썼던 yml 파일을 작성할 수 있는 프롬포트를 작성해보았다. ( 각 기능 정의 위주)

main/dev로 향하는 PR이 열리거나 업데이트되면 도는 GitHub Actions 워크플로우를 만들어줘.
러너는 self-hosted 맥(custom-macos-light)이고, Unity 6000.3.16f1이 /Applications/Unity/Hub/Editor/...에 설치돼 있어. 액션 쓰지 말고 Unity CLI를 batchmode로 직접 호출해.
EditMode 테스트 → PlayMode 테스트 순서로 돌리고, Unity 배치모드가 가끔 이유 없이 죽으니까 각각 최대 3회 재시도해줘.
테스트가 실패해도 워크플로우를 중간에 끊지 말고 끝까지 진행한 다음, NUnit XML 결과를 파싱해서 통과/실패 개수와 실패한 테스트 이름을 뽑아줘.
결과를 Slack 웹훅(시크릿 SLACKWEBHOOK_URL...)으로 보내줘. 전부 통과면 초록, 테스트 실패면 빨강에 실패 목록 포함, XML 파일 자체가 없으면 "실행 실패"로 구분해서 주황으로.
테스트 결과와 Unity 로그는 아티팩트로 7일 보관.
마지막 스텝에서 최종 성공/실패를 판정해서 PR에 체크 표시가 제대로 뜨게 해줘.
PR 제목에 [skip test]가 있으면 전부 건너뛰고 성공 처리.

pr-claude-review.yml

pr 를 claude 가 코드리뷰해주는 것도 yml 파일로 셋팅할 수 있다.

  • pr이 열리거나 갱신되면 claude 가 자동 코드리뷰를 하게 셋팅 할 수 있다 (.github/claude-review-instructions.md 등의 파일을 가지고 지침을 설정할 수 있다)

관련 체크 내용에서는

  • 변경사항에 대한 간단한 요약이나,
  • 프로젝트 내부 규칙 (네이밍이나 asset 관리 등)
  • 코드 리뷰
    • SSOT (Single Source of Truth) 위반
    • 리소스 관리
    • 구조/가독성
    • 불필요한 diff
  • 그 외 각 실행 task등을 에이전트&서브에이전트에 위임해서 테스트 시킬 수 있다.
    관련 구조는 다음에 추가로 학습하겠다.

PR을 올렸을때 발생하는 CD

relase/ 브랜치에 실행하면 발생하게 설정할 수 있다. (실 배포 브랜치)

파이프라인은 Init(버전,빌드 번호 결정, 커밋 메시지의 플래그 파싱) -> 안드로이드와 iOS 빌드 병렬 실행 -> finalize

각 플랫폼 별 빌드 flow

Android

Unity 빌드 -> ABB 생성 -> PlayStore 내부 트랙 업로드 -> Crashlytics 심볼 업로드 (파이어베이스의 크래시 수집 서비스)

iOS

Unity 빌드 -> IPA 생성 -> TestFlight 업로드

Finalize

버전 파일 커밋, 빌드 크기 리포트, Slack 알림, 가챠 확률 검증 워크 플로우 트리거 등을 각 프로젝트에 맞게 셋팅 할 수 있다.

release-build.yml (ex)

Unity 모바일 게임의 릴리즈 CD 파이프라인을 GitHub Actions로 만들어줘.

[트리거]
- release/v*.*.* 패턴 브랜치에 push되면 자동 실행
- workflow_dispatch로 수동 실행도 가능 (버전 입력받기)
- 동시에 두 개 돌지 않게 concurrency 설정

[환경]
- Unity 6 계열, self-hosted macOS 러너
- Git LFS 사용 프로젝트

[잡 구성]
1. init: 브랜치명에서 버전 추출, 커밋 메시지에서 [skip ios],
   [skip android], [debug] 플래그 파싱해서 출력값으로 넘기기
2. android_build (init 의존): Unity로 AAB 빌드 → keystore 서명
   → Play Store 내부 테스트 트랙 업로드
3. ios_build (init 의존, android와 병렬): Unity로 Xcode 프로젝트
   빌드 → 인증서/프로파일 설치 → IPA 생성 → TestFlight 업로드
4. finalize (둘 다 완료 후): 성공/실패 요약 Slack 알림

[시크릿]
서명 키(keystore, iOS 인증서), 스토어 업로드용 API 키,
알림용 웹훅은 GitHub Secrets에 있다고 가정하고 참조만 해줘.
적절한 시크릿 이름은 네가 정해줘.

각 스텝이 왜 필요한지 주석으로 설명해줘.

프로젝트 CD에서 발생할 수 있는 사고와 해결방법

1. Sed가 iPhone 빌드 번호 대신 다른 설정까지 덮어쓴 사고

iOS 빌드 번호를 올리려고 ProjectSettings.asset 에서 iPhone: 줄을 sed로 치환했는데 (버전 번호 변경용)
ProjectSettings.asset 에선 iPhone: 이라는 키가 다른 부모 블록 안에서 여러번 등장한다.

applicationIdentifier:
    iPhone: com.basakansoft.jobmaker     # ← 번들 ID
buildNumber:
    iPhone: 2                            # ← 이걸 바꾸고 싶었던 것
scriptingDefineSymbols:
    iPhone: DOTWEEN;...;JOBMAKER_NATIVE_SHARE   # ← 컴파일 심볼

어떻게 해결했나?
sed 의 주소 범위(range) 문법을 사용한다

sed -i '' "/^  buildNumber:/,/^  [^ ]/ s/^    iPhone: .*/    iPhone: $BUILD_NUMBER/"

/시작패턴/,/끝패턴/ 형태로 "buildNumber: 줄부터, 다음 2칸 들여쓰기 키(= 다음 블록 시작)가 나올 때까지"로 치환 범위를 제한한다. 그 범위 밖의 iPhone: 줄은 건드리지 않는다.

2. Watchdog이 SIGKILL을 성공적으로 처리해 빌드가 통과할 뻔한 사고

run-unity-safe.sh

Unity를 배치모드로 돌리면 Android 모듈이 초기화되는 작업(EDM4U Resolve 등)에서, 실제 작업은 끝났는데 종료 정리 단계의 adb kill-server 에서 영원히 멈추는 알려진 버그가 있다.

해결 1차
watchdog 패턴으로 스크립트가 Unity를 백그라운드로 띄우고, 5초마다 살아있는지 확인하다가 제한시간을 넘기면 SIGKILL로 강제 종료.

# 137 = SIGKILL (watchdog), 143 = SIGTERM. Treat as success.
if [ "$RC" -eq 137 ] || [ "$RC" -eq 143 ]; then
  exit 0
fi

타임아웃이 될 정도면 실제 작업은 끝났고, 종료만 멈춘 것이다. 라고 가정하였다.

그러나 Unity 가 진짜 느려서 빌드 도중에 죽었다면?
종료 코드는 똑같이 137이고 (누군가 SIGKILL로 죽였다는 뜻) 스크립트는 success를 반환한다.

SIGKILL : 운영체제가 프로세스에게 보내는 즉시 사망 명령

해결 2차
release-build.yml:548 에 명시적 검증을 추가하였다.

if [ ! -f "$BUILD_PATH" ]; then
  echo "✗ ERROR: expected build artifact $BUILD_PATH not found"
  exit 1
fi

3. Play Store 업로드 HTTP 408 - 배포 실패가 빌드 전체를 죽이던 사고

빌드는 멀쩡히 끝났는데 playstore 업로드 단계에서 HTTP 408(Request Timeout)으로 실패하면서 파이프 라인 전체가 실패처리 되었다. 30분 넘게 걸리는 unity 빌드가 통채로 버려지는 상황이였다.

ABB가 너무 거대해서 업로드 자체가 오래 걸리고, CI 러너 자체도 온갖 작업이 한 머신 안에 돌기 때문에 당연히 문제가 생길 수 있다.

어떻게 해결했나?
1.업로드 스탭에 continue-on-error: true 를 추가하여 업로드가 실패해도 빌드는 성공처리

2.업로드 전에 ABB를 러너 로컬에 백업. - 자동 업로드가 죽어도 SHH로 꺼내 수동 배포 가능

3.GitHub Artifact 업로드는 제거. 로컬에도 백업하니까 굳이 필요 없다.

4.네트워크는 호스트 재부팅으로 정상화한 뒤 업로드를 재활성화

4.외장 exFAT 디스크의 AppleDouble(._) 파일이 Xcode 컴파일을 깨뜨린 사고

이걸 알아보기 전에!!!!! 미리 알아야할 지식들을 가볍게 정리하겠다.

Swift Package Manager(SPM)
iOS/맥 개발용 패키지 관리자
내가 예시로 공부하고 있는 프로젝트에서 iOS 의존 라이브러리 소스(ex.firebaseiOS,appLovin 를 자동으로 받아와 컴파일해주는 역할을 한다.

Unity iOS 빌드의 step 이해

① Build iOS with Unity   → Unity가 C# 코드를 변환해서 "Xcode 프로젝트"를 생성 (build/iOS)
② pod install            → CocoaPods 의존성 설치
③ Build IPA (xcodebuild) → Xcode가 전체를 컴파일해서 실제 앱(IPA) 생성  ← 사고는 여기서

자 다시 돌아오면, iOS 빌드가 SMP 의존성 컴파일 단계에서 에러가 발생하여 깨졌다.
원인은 CI 러너의 하드웨어적 구성이다.
셀프호스티드 맥 러너(빌드컴)의 내장 디스크 용량을 아끼려고 Xcode의 DerivedData(빌드 캐시)를 외장 T7 SSD에 뒀는데, 이 디스크가 exFAT 포맷이다. 여기서 macOS의 특성이 발동한다.

macOS는 파일마다 확장 속성(xattr — Finder 태그, 격리 플래그 등 메타데이터)을 저장하는데, exFAT는 확장 속성을 지원하지 않는다. 그래서 macOS는 exFAT 디스크에 파일을 쓸 때 메타데이터를 ._파일명 형태의 별도 파일(AppleDouble)로 만들어서 옆에 저장한다.

ex) Foo.swift를 쓰면 ._Foo.swift가 같이 생긴다.

파일 시스템 : 디스크의 파일을 어떤 규칙으로 기록할지에 대한 약속 ( ex. exFAT)

문제는 SPM 체크아웃(의존성 소스 코드)이 DerivedData 안에 있었다는 것. 소스 디렉토리에 ._*.swift 파일들이 생기니 Xcode가 이걸 진짜 소스 파일로 취급하려다 컴파일이 깨졌다.

어떻게 해결했나

xcodebuild clean archive \
  ...
  -clonedSourcePackagesDirPath "$RUNNER_TEMP/spm" \

-clonedSourcePackagesDirPath로 SPM 체크아웃 위치만 내장 APFS 디스크(RUNNERTEMP)로 분리했어요. APFS는 확장 속성을 네이티브로 지원하니 . 파일이 안 생긴다.

수십 GB짜리 무거운 빌드 캐시는 여전히 T7에 두고, 문제에 민감한 소스 체크아웃만 분리했다. 그리고 바로 아래에 du -sh로 SPM 디렉토리 크기를 매번 측정하는 스텝을 둬서, 내장 디스크가 감당 가능한지 계속 관찰하고 있다.

사실 iOS 관련 개념은 이번에 처음 접해보는거라 계속 보면서 익숙해져야할 듯 싶다

profile
게임 클라이언트 개발자

0개의 댓글