GitHub Actions의 pull_request 이벤트는 머지 커밋을 기준으로 빌드한다

Gio·2026년 3월 1일

배경

CI 과정에서 이상한 컴파일 에러가 발생했다. 분명히 로컬에서는 잘 동작하고, 변경한 파일도 모두 push했는데 CI만 계속 실패하는 상황이었다.

에러 메시지는 다음과 같았다.

e: CoursesViewModel.kt:74:39 No parameter with name 'originalCourses' found.

작업 내용은 CoursesUiState(UI 상태를 담는 data class)에서 originalCourses 필드를 제거하는 것이었고, 관련 코드도 모두 수정해서 push까지 완료한 상태였다. 그런데 CI는 originalCourses를 찾을 수 없다는 에러를 계속 뱉고 있었다.

원인

결론부터 말하면, GitHub Actions의 pull_request 이벤트는 PR 브랜치 자체가 아니라 base 브랜치(main)와 머지한 결과를 기준으로 빌드한다.

상황을 정리하면 이렇다.

  • PR 브랜치에서는 CoursesUiState data class의 originalCourses 필드를 제거
  • PR을 올려둔 사이, main 브랜치에 새로운 커밋이 추가됨
  • 그 커밋은 CoursesViewModel에서 CoursesUiState를 생성할 때 originalCourses = ... 형태의 named argument를 사용 — 내가 수정한 코드가 아닌, main에 새로 올라온 코드임
  • 두 변경사항이 파일의 서로 다른 위치를 건드렸기 때문에 텍스트 레벨의 충돌(conflict)은 발생하지 않음
  • 그러나 CI는 두 브랜치를 합친 결과로 빌드하기 때문에, 이미 제거된 필드명을 named argument로 사용하는 코드가 남아 컴파일 에러 발생

이처럼 텍스트 레벨에서는 충돌이 없지만 두 변경사항을 합쳤을 때 코드가 깨지는 경우를 의미적 충돌(semantic conflict) 이라 한다. Git은 이를 자동으로 잡아주지 못하고, CI가 잡아준 것이다.

공식 문서 근거

1. GitHub Actions 공식 문서

Events that trigger workflows - GitHub Docs

pull_request 이벤트 섹션에 다음과 같이 명시되어 있다.

Note that GITHUB_SHA for this event is the last merge commit of the pull request merge branch.

GITHUB_SHA는 GitHub Actions에서 현재 워크플로우가 빌드하는 기준 커밋의 SHA를 담고 있는 환경변수다. 즉, pull_request 이벤트에서는 PR 브랜치의 최신 커밋이 아니라 base 브랜치와 머지된 결과 커밋이 기준이 된다는 것을 의미한다.

또한 pull_request_target 이벤트를 설명하는 부분에서 pull_request와 대비하여 다음과 같이 설명한다.

This event runs in the context of the default branch of the base repository, rather than in the context of the merge commit, as the pull_request event does.

pull_request_target은 base 브랜치 컨텍스트에서 실행되는 반면, pull_request머지 커밋 컨텍스트에서 실행된다는 것을 명확히 설명하고 있다.

2. actions/checkout 관련 문서

실제로 코드를 체크아웃하는 것은 actions/checkout 액션인데, GITHUB_REF 환경변수의 기본값이 refs/pull/<PR번호>/merge로 설정되기 때문에 별도 설정 없이 actions/checkout을 사용하면 머지된 코드를 체크아웃하게 된다.

GitHub Community Discussion에서도 이를 확인할 수 있다.

The GITHUB_REF of pull request event is the PR merge branch. So if you don't specify the ref part, it checks out the merged source code.

만약 PR 브랜치의 실제 최신 커밋을 체크아웃하고 싶다면 다음과 같이 명시적으로 지정해야 한다.

- uses: actions/checkout@v4
  with:
    ref: ${{ github.event.pull_request.head.sha }}

왜 이렇게 동작하는가?

이 동작은 의도된 설계다. CI의 목적은 단순히 "이 브랜치의 코드가 동작하는가?"가 아니라 "이 PR을 머지했을 때 main이 깨지지 않는가?" 를 검증하는 것이기 때문이다.

PR 브랜치만 빌드하면 main과의 의미적 충돌이 있어도 통과되어버릴 수 있다. 머지 커밋 기준으로 빌드함으로써 이런 경우를 사전에 잡아낼 수 있다.

해결 방법

이번 경우의 해결 방법은 간단했다. 최신 main을 PR 브랜치에 rebase하여 두 변경사항이 함께 반영되도록 한 뒤, CoursesViewModel에서 더 이상 존재하지 않는 named argument를 제거하면 된다.

정리

  • GitHub Actions의 pull_request 이벤트는 PR 브랜치가 아닌 base 브랜치와 머지한 결과 커밋을 기준으로 빌드한다.
  • 로컬에서는 성공해도 CI에서 실패하는 경우, base 브랜치의 최신 변경사항을 PR 브랜치에 반영해보자(rebase 또는 merge).
profile
틀린 부분을 지적받기 위해 업로드합니다.

0개의 댓글