Mac CI 스크립트 권한과 줄바꿈 관리

CI/CD ·약 8분 읽기

Mac CI 스크립트 권한과 줄바꿈 관리

빌드 스크립트가 개발자 컴퓨터에서는 정상적으로 실행되지만 클라우드 Mac에 푸시하면 Permission denied, bad interpreter 오류가 발생하거나 같은 줄 끝에 원인을 알기 어려운 $'\r'이 나타날 수 있습니다. 이런 문제는 대개 비즈니스 로직과 무관하며 Git 파일 모드, 줄바꿈 형식, 실제 인터프리터가 하나의 명시적인 규칙으로 통일되지 않아 발생합니다. 먼저 실패한 명령, 커밋 번호, 스크립트 경로를 저장한 다음 아래 순서대로 확인하면 캐시를 반복해서 삭제하는 것보다 빠르게 해결할 수 있습니다.

먼저 어느 계층에서 실패했는지 확인하기

“스크립트 실패”라는 메시지만 보고 곧바로 chmod +x를 추가해서는 안 됩니다. 셸 스크립트에는 최소 네 가지 상태가 관련됩니다. 작업 트리의 권한, Git 인덱스에 기록된 모드, 파일의 실제 바이트, 그리고 runner가 스크립트를 호출하는 방식입니다.

script="ci/build.sh"

ls -l "$script"
git ls-files -s -- "$script"
file "$script"
head -n 1 "$script" | cat -vet

git ls-files -s 출력의 첫 번째 열이 100755이면 Git에 실행 비트가 기록되어 있다는 뜻이고, 100644이면 일반 파일이라는 뜻입니다. cat -vet 출력에서 줄 끝에 ^M이 보이면 파일에 CRLF가 섞여 있는 것입니다. 첫 줄에는 #!/bin/zsh 또는 #!/usr/bin/env bash처럼 유효한 shebang이 있는지도 확인해야 합니다.

호출 방식도 반드시 기록해야 합니다. bash ci/build.sh는 파일에 실행 권한이 없어도 되지만, ./ci/build.sh는 권한을 확인하고 shebang을 읽습니다. Xcode 빌드 단계, 작업 스케줄러 스크립트, 로컬 터미널이 서로 다른 호출 방식을 사용하면 “CI에서만 실패한다”는 착각이 생길 수 있습니다.

수정 목표는 특정 작업을 우연히 통과시키는 것이 아니라, 스크립트를 어떤 인터프리터로 실행할지, 직접 실행을 허용할지, 어떤 줄바꿈 형식을 사용할지를 저장소에 명확히 기록하는 것입니다.

Git 인덱스에서 실행 비트 수정하기

직접 실행하는 진입점 스크립트에는 실행 비트를 기록해야 합니다. 반면 source로 불러오거나 인터프리터에 명시적으로 전달하는 스크립트 조각은 일반적으로 100644를 유지합니다. 모든 .sh 파일을 실행 파일로 바꾸지 않도록 먼저 진입점 목록을 작성합니다.

git update-index --chmod=+x ci/build.sh
git update-index --chmod=+x ci/test.sh
git diff --summary
git ls-files -s -- ci/build.sh ci/test.sh

git diff --summary에 모드 변경이 표시되지 않으면 로컬 환경에서 권한 추적이 비활성화되어 있는지 확인합니다.

git config --get core.fileMode
git config core.fileMode true

core.fileMode는 현재 작업 트리에서 변경 사항을 감지하는 방식에만 영향을 줍니다. 커밋을 통해 실제로 전달되는 것은 Git 인덱스입니다. runner 시작 시 chmod -R +x .를 실행하는 방식을 장기적인 해결책으로 사용해서는 안 됩니다. 저장소의 잘못된 상태를 숨길 뿐 아니라 설정 파일, 비밀 정보가 포함된 자료, 일반 데이터 파일에 불필요한 권한을 부여할 수 있습니다.

직접 실행을 허용할 스크립트를 ci/entrypoints.txt에 한 줄씩 등록할 수 있습니다. 검증 시에는 이 목록만 확인하고, 새로운 진입점을 추가할 때 등록 파일도 함께 갱신하도록 하면 코드 리뷰에서도 권한 경계가 변경된 이유를 파악할 수 있습니다.

LF를 고정하고 기존 파일을 신중하게 정규화하기

저장소 루트의 .gitattributes를 줄바꿈 규칙의 유일한 기준으로 사용해야 합니다. 일반적인 빌드 파일에는 다음과 같은 기준을 적용할 수 있습니다.

*.sh       text eol=lf
*.command  text eol=lf
*.zsh      text eol=lf
*.xcconfig text eol=lf
*.pbxproj  text eol=lf
*.png      binary
*.zip      binary

규칙을 추가해도 이미 커밋된 파일이 자동으로 다시 작성되지는 않습니다. 깨끗한 브랜치에서 다음 명령을 실행합니다.

git add --renormalize .
git status --short
git diff --check
git diff --cached --stat

차이를 먼저 검토한 뒤 정규화 결과만 별도 커밋하고, 기능 변경과 섞지 마십시오. 프로젝트 파일이 한꺼번에 대량 변경되었다면 실제 변경 내용이 줄바꿈뿐인지 표본을 확인해야 합니다. 압축 파일, 코드 생성기, 외부 스크립트가 CRLF를 다시 유입할 수도 있으므로 .gitattributes가 검사 게이트를 대체할 수는 없습니다.

검사 결과 일반적인 원인 올바른 조치
100644인데 직접 실행함 인덱스에 권한이 기록되지 않음 git update-index --chmod=+x로 수정
줄 끝에 ^M이 표시됨 파일이 CRLF를 사용함 LF로 정규화한 뒤 커밋
bad interpreter shebang에 CR이 포함되었거나 경로가 유효하지 않음 첫 줄을 수정하고 인터프리터가 존재하는지 확인
로컬에서는 성공하지만 CI에서는 실패함 호출 방식 또는 셸이 다름 호출 명령을 출력하고 셸을 명시적으로 선택

인터프리터 차이를 명시적인 규칙으로 만들기

macOS의 기본 대화형 셸과 CI가 실제로 사용하는 셸은 서로 다를 수 있습니다. 스크립트가 Bash 배열, [[ ]], 프로세스 치환을 사용한다면 sh가 암묵적으로 해석하게 해서는 안 됩니다. zsh 문법에 의존하는 경우에도 직접 선언해야 합니다.

#!/usr/bin/env bash
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root"

대화형 설정 파일이 PATH, 별칭, 환경 변수를 제공할 것이라고 가정해서는 안 됩니다. CI 진입점은 최소한의 PATH를 직접 설정하고 command -v xcodebuild, command -v ruby 등의 명령으로 도구가 존재하는지 확인해야 합니다. 필요한 환경 변수는 작업 설정에서 주입하고, 스크립트는 변수 이름만 확인하며 민감한 값은 출력하지 않아야 합니다.

Bash shebang이 있는 파일을 sh script.sh로 호출하는 것도 피해야 합니다. 명시적으로 지정된 sh가 shebang을 무시하기 때문입니다. DebugMac 노드에서 실행되는 작업 역시 특정 세션에서 우연히 사용할 수 있었던 셸을 기준으로 실행 환경을 추정하지 말고 저장소에 선언된 규칙을 따라야 합니다.

커밋 전 검사와 CI 이중 게이트 구축하기

로컬 훅은 피드백 시간을 줄여 주지만 유일한 방어 수단으로 삼을 수는 없습니다. 일반적인 clone 과정에서는 훅이 자동으로 활성화되지 않기 때문입니다. CI에서는 종속성 설치와 Xcode 빌드 전에 가벼운 검사를 실행해야 합니다.

#!/usr/bin/env bash
set -euo pipefail

while IFS= read -r script; do
  [[ -z "$script" ]] && continue
  mode="$(git ls-files -s -- "$script" | awk '{print $1}')"
  [[ "$mode" == "100755" ]] || {
    printf 'entrypoint is not executable: %s\n' "$script" >&2
    exit 1
  }
done < ci/entrypoints.txt

if git grep -Il $'\r' -- '*.sh' '*.zsh' '*.command' > /tmp/crlf-files; then
  printf 'CRLF detected:\n' >&2
  cat /tmp/crlf-files >&2
  exit 1
fi

게이트에서는 등록 파일에 기재된 경로가 실제로 존재하는지도 확인하고, 커밋되지 않은 정규화 차이가 있으면 실패 처리해야 합니다. 최종 검증은 네 단계로 진행할 수 있습니다. 새로 체크아웃하고, 진입점 스크립트를 직접 실행하고, git status --porcelain이 비어 있는지 확인한 다음, 비대화형 세션에서 최소 빌드를 한 번 완료합니다. 이렇게 하면 비용이 큰 종속성 다운로드와 컴파일이 시작되기 전에 권한 및 줄바꿈 문제가 드러나며, runner 현장을 수정하는 방식에 의존하지 않아도 됩니다.

자주 묻는 질문

bash로 실행하는 스크립트에도 실행 권한이 필요한가요?

bash script.sh 방식에는 필수가 아니지만 직접 실행, 빌드 단계, 일부 runner는 권한을 확인합니다. 실제 진입점은 Git에서 100755 모드로 관리해야 합니다.

.gitattributes를 추가하면 기존 CRLF가 바로 수정되나요?

아닙니다. git add --renormalize .을 실행하고 변경 내용을 검토한 뒤 커밋해야 합니다. 생성되거나 외부에서 들어온 파일은 CI에서 계속 검사해야 합니다.

Debug M4

다음 빌드를 전용 Apple Silicon 물리 노드에서 실행

대여 기간과 노드 리전을 선택하고 Mac Mini M4, 16GB RAM, 256GB SSD 및 결제할 USD 금액을 확인한 뒤 주문을 제출하세요.

클라우드 Mac 지금 대여