GitHub Preflight (사전 점검)
새 리포지토리를 푸시하면 몇 가지 지루하고 반복적인 이유로 실패하는 경우가 많지만, 오류 메시지는 실제 원인을 제대로 가리키지 않습니다. src refspec main does not match any은 브랜치 문제처럼 들리지만, 보통은 커밋이 없기 때문입니다. 작업 계정으로 조용히 성공한 푸시는 올바르게 성공한 푸시와 똑같이 보입니다. 이 스킬은 사용자가 푸시를 날리기 전에 이러한 문제를 찾아냅니다.
핵심 원칙: 진단(진단)만 하고, 변경하지 않는다. 발견된 내용을 보고하고, 복사-붙여넣기로 해결할 수 있는 명령어를 알려줍니다. 사용자가 직접 실행합니다. 여러 계정을 유지하는 이유는 정확히 어떤 계정이 작업하는지에 신경을 쓰기 때문이므로, 사용자를 대신해 git config나 git remote set-url을 조용히 실행하는 것은 정확히 잘못된 접근입니다.
실행 방법
bash <skill-dir>/scripts/preflight.sh [리포지토리-경로] [리모트-이름]
두 인수 모두 선택 사항이며, 기본값은 현재 디렉토리와 origin입니다. 스크립트는 오직 다음 명령만 읽습니다: git config --get, ls-remote, ssh -T, push --dry-run. 절대 쓰지 않습니다.
어떤 이유로 스크립트를 실행할 수 없는 경우, 아래 각 검사는 한 줄 명령어로 직접 실행하고 같은 방식으로 해석할 수 있습니다.
검사 항목의 의미
스크립트는 PASS / WARN / FAIL 라인을 출력합니다. 원시 출력보다 해석이 더 중요하므로, 그대로 붙여넣지 말고 설명해야 합니다.
리포지토리/파일/커밋. 빈 디렉토리나 커밋이 없는 리포지토리는 푸시 시 src refspec <브랜치> does not match any을 발생시킵니다. Git은 빈 상태에서 커밋을 만들 수 없습니다. 이것이 첫 푸시 실패의 가장 흔한 원인이므로, 이 문제가 발생하면 가장 먼저 언급해야 합니다.
브랜치. 새 git init은 master 브랜치를 기본으로 하는 반면, 리모트는 main을 기대할 수 있습니다. 브랜치 이름 변경은 첫 커밋 이후에 안전합니다(git branch -M main) — unborn 브랜치에는 아직 이름을 바꿀 내용이 없기 때문에 명령 순서를 잘못 맞추면 한 번 더 시도해야 합니다.
실제 리모트 URL. 정확히 아는 것이 중요합니다. 잘못 확인하기 쉽기 때문입니다: git remote get-url은 이미 url.<base>.insteadOf를 확장하며, git ls-remote --get-url도 마찬가지입니다. 따라서 둘을 비교해도 아무것도 감지되지 않습니다. 원시 값은 git config --get remote.<이름>.url에만 남아 있으므로, 스크립트는 이것을 git remote get-url --push(--push 형식은 추가로 pushurl과 pushInsteadOf를 포함하며, 이것이 실제 푸시가 사용하는 것입니다)와 비교합니다. 두 값이 다를 경우, 사용자가 구성한 URL이 Git이 실제로 접속할 URL이 아닙니다. 전형적인 함정은 전역 url."https://github.com/".insteadOf = [email protected]: 설정으로, SSH를 HTTPS로 조용히 우회시켜 자격 증명 헬퍼가 보유한 계정(종종 사용자가 의도하지 않은 계정)으로 푸시되게 합니다. 항상 실제 URL을 보고하고, 차이점을 명시적으로 표시해야 합니다.
인증 계정. SSH의 경우 스크립트는 ssh -G로 호스트를 확인(github-personal 같은 호스트 별칭을 실제 호스트명과 키로 추적)한 다음, ssh -T로 GitHub에 직접 질의합니다. GitHub는 Hi <사용자명>!로 응답하고 종료 코드 1을 반환합니다 — 여기서 종료 코드 1은 실패가 아니라 성공 케이스입니다. HTTPS의 경우 활성 gh 계정을 읽습니다. 해당 계정을 URL의 소유자 세그먼트와 비교합니다: 일치하지 않으면 푸시가 403 오류를 발생시키거나 잘못된 네임스페이스에 도달합니다.
커밋 아이덴티티. 전역 설정에서 상속된 user.email이 조용한 실패입니다. 푸시는 한 계정으로 인증되지만 모든 커밋은 다른 주소로 작성됩니다 — 나중에 수정하려면 커밋을 수정하거나 히스토리를 다시 써야 합니다. 리모트가 다른 소유자에 속한 리포지토리에서 전역 범위의 아이덴티티를 발견하면, 기술적으로 아직 문제가 발생하지 않았더라도 WARN으로 눈에 띄게 표시해야 합니다.
리모트 상태. 빈 리모트는 첫 푸시가 깔끔함을 의미합니다. 기존 ref가 있으면 일반 푸시가 비-빨리감기(non-fast-forward)로 거부되며 사용자가 먼저 pull/rebase해야 함을 의미합니다 — 거부 메시지를 보기 전에 알려주세요. 여기서 오류가 발생하면 일반적으로 리포지토리가 존재하지 않거나 계정에 접근 권한이 없음을 의미합니다. GitHub는 의도적으로 둘 다 "찾을 수 없음"으로 보고하므로, 추측하지 말고 두 가지 설명을 모두 제공하세요.
Dry-run 푸시. 커밋이 존재할 때만 가능합니다. 이것이 실제 종단 간(end-to-end) 검사입니다 — 쓰지 않고 인증과 참조 협상을 테스트합니다. 사각지대를 명심하세요: GH007(이메일 프라이버시) 같은 서버 측 거부는 실제 수신 시에만 평가되므로 잡아낼 수 없습니다.
보고 방법
검사 항목의 짧은 테이블을 제공한 다음, 실패하거나 경고한 항목에 대해서만 수정에 필요한 정확한 명령어를 실행 가능한 순서로 알려주세요. 통과한 검사를 설명으로 늘어놓지 마세요. 사용자는 무엇이 잘못되었고 무엇을 입력해야 하는지 알고 싶어 합니다.
아이덴티티를 설정해야 할 때는 전역 설정을 건드리기보다 리포지토리 로컬 설정을 선호합니다. 이 리포지토리가 기계의 기본값과 다르다는 것이 핵심이기 때문입니다:
git config user.name "<계정>"
git config user.email "<이메일>"
여러 리포지토리에 동일한 처리가 필요하다면, includeIf를 지속 가능한 해결책으로 언급하세요:
[includeIf "gitdir:~/personal/"]
path = ~/.gitconfig-personal
그리고 푸시가 GH007로 거부되면, 개인 이메일 주소를 추측하지 말고 GitHub → 설정 → 이메일에 있는 @users.noreply.github.com 주소를 가리키세요 — 키 코멘트와 로컬 설정은 레이블일 뿐, 계정이 실제로 수락하는 주소에 대한 증거가 아닙니다.