LabHub
배우기 러닝패스 코스

CI/CDパイプライン

ブランチでは緑だったのに、マージした途端に赤になった

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

로컬 베어 저장소를 서버로 삼아 받는 쪽 훅으로 보호 브랜치를 강제하고, 커밋별 검사 기록으로 필수 검사를 세우고, 선형 이력을 지키고, 브랜치의 나이를 재고, 의미 충돌을 재현한 뒤, 병합 대기열로 초록불을 병합 뒤까지 끌고 갑니다.

왜 중요한가

브랜치의 초록불은 '내 변경 더하기 그때의 트렁크' 를 본 결과입니다. 다른 브랜치가 먼저 들어가면 그 결과는 더 이상 지금의 트렁크에 대한 보증이 아닙니다. 그래서 초록불이던 것이 병합 뒤 빨간불이 되는 일이 생기고, 그때 깨지는 것은 한 사람의 브랜치가 아니라 모두가 딛고 선 트렁크입니다. 막는 자리는 두 곳뿐입니다. 하나는 받는 쪽의 규칙 — 밀어 넣는 쪽에 둔 규칙은 복제로 따라오지 않고 누구나 끌 수 있어서 강제가 되지 못합니다. 다른 하나는 병합 직전의 재검사 — 줄을 세워 지금의 트렁크 끝 위에 얹어 다시 검사하고 통과한 것만 넣는 것입니다. 이 실습은 그 둘을 인터넷도 호스팅 서비스도 없는 파드 안에서 git 만으로 직접 만들어 봅니다. 호스팅 서비스의 브랜치 보호와 병합 대기열은 이 얼개에 이름을 붙인 것이고, 그 기능들이 무엇을 보증하고 무엇을 보증하지 못하는지는 직접 한 번 만들어 본 사람만 압니다.

단계

  1. /root/trunk 에서 시작합니다. 먼저 '서버' 역할을 할 베어 저장소 /root/trunk/server.git 를 기본 브랜치 main 으로 만들고, 검사 결과를 커밋별로 적어 둘 빈 디렉터리 /root/trunk/server.git/status 를 만드세요. 그다음 그 서버를 /root/trunk/app 으로 복제하고 신원을 dev / dev@example.com 으로 세운 뒤, 파이썬 파일 세 개와 시험 실행기를 만들어 커밋하고 main 에 바로 push 하세요 — calc.py(discounted(price, rate)price - price * rate 를 돌려준다), checkout.py(cart_total(prices, rate) 가 각 값의 할인가를 더한다), tests.py(discounted(100, 0.2) == 80cart_total([100, 200], 0.2) == 240 을 단언하고 ok 를 찍는다), run-tests.sh(자기 디렉터리로 옮겨 python3 tests.py 를 돌린다, 실행 권한 필요). 마지막으로 같은 서버를 /root/trunk/app2 로 한 번 더 복제해 신원을 other / other@example.com 으로 세우고, README.md 를 더해 역시 main 에 바로 push 하세요. 그러고 나서 git --git-dir=/root/trunk/server.git log --format='%h %ae %s' main 의 출력을 그대로 /root/trunk/evidence/01-direct-push.txt 에 저장하세요.
  2. 서버 훅 /root/trunk/server.git/hooks/update 를 만들고 실행 권한을 주세요. 인자는 차례로 갱신할 레퍼런스 이름·옛 값·새 값입니다. 레퍼런스가 refs/heads/main 이면 표준오류에 무엇을 하라는 안내를 한 줄 내고 1 로 끝내고, 그 밖의 레퍼런스는 아무 말 없이 0 으로 끝냅니다. 그다음 로컬 훅 /root/trunk/app/.git/hooks/pre-commit 을 만들어 실행 권한을 주세요 — 스테이징된 변경에 DO-NOT-COMMIT 이 들어 있으면 거절하고, 그렇지 않으면 통과시킵니다. 이제 세 가지를 확인해 /root/trunk/evidence/02-hooks.txt<이름> <값> 세 줄로 적으세요. main-push/root/trunk/app 에서 커밋 하나를 만들어 main 에 바로 push 했을 때의 결과(accepted 또는 rejected), same-push-both한 번의 push 명령으로 main 과 기능 브랜치를 함께 밀었을 때 서버에 올라간 것(all·feature-only·none 중 하나), cloned-hook 은 서버를 /root/trunk/app3 으로 새로 복제했을 때 그 사본에 pre-commit 훅이 있었는지(present 또는 absent)입니다.
  3. 먼저 검사 러너 /root/trunk/ci-check.sh <server.git> <커밋SHA> 를 만들고 실행 권한을 주세요. 서버에서 그 커밋의 트리를 임시 디렉터리로 꺼내(git --git-dir=<server> archive <커밋>) run-tests.sh 를 돌리고, 결과를 <server.git>/status/<커밋 40자리 SHA> 파일의 첫 줄pass 또는 fail 로 적습니다. 화면에는 <40자리 SHA> <pass|fail> 한 줄을 내고, 종료 코드는 통과 0 · 실패 1 · 인자가 모자라거나 서버에 없는 커밋이면 2 입니다. 임시 디렉터리는 남기지 않습니다. 그다음 /root/trunk/server.git/hooks/update 를 고쳐 refs/heads/main 에 이 규칙들을 걸으세요 — 새 값이 0 으로만 된 갱신(삭제)은 거절, 옛 값이 새 값의 조상이 아닌 갱신(강제 갱신)은 거절, 그리고 git rev-list <옛값>..<새값> 으로 얻은 새 커밋 하나하나에 대해 기록이 없으면 거절하고 첫 줄이 pass 가 아니어도 거절합니다. 거절 메시지에는 문제가 된 커밋의 40자리 SHA 를 넣으세요. 마지막으로 /root/trunk/app 에서 기능 브랜치 두 개를 서버에 올리고 — 하나는 시험이 통과하는 변경, 하나는 tests.py 에 일부러 깨지는 단언을 더한 변경 — 각각에 ci-check.sh 를 돌린 뒤, /root/trunk/evidence/03-status.txt 에 두 줄을 green <40자리 SHA> passred <40자리 SHA> fail 로 적으세요.
  4. /root/trunk/server.git/hooks/update 에 규칙을 하나 더 더하세요 — refs/heads/main 으로 들어오는 새 커밋 가운데 부모가 둘 이상인 커밋(병합 커밋)이 있으면, 그 커밋의 40자리 SHA 와 rebase 라는 낱말이 함께 든 안내를 표준오류에 내고 거절합니다. 검사 기록이 전부 초록불이어도 거절해야 합니다. 그다음 규칙이 실제로 도는지 확인하세요 — /root/trunk/app 에서 origin/main 에서 갈라진 브랜치 feature/merge-demo 를 만들어 커밋 하나를 올리고, 거기에 origin/feature/green--no-ff 로 병합한 뒤, 새 커밋 전부에 pass 기록을 손으로 넣고 그 브랜치 끝을 main 으로 push 하세요. 거절 메시지 첫 줄을 remote: 접두사를 떼고 /root/trunk/evidence/04-linear.txt 에 그대로 저장하세요.
  5. /root/trunk/branch-age.sh <server.git> [최대일수] 를 만들고 실행 권한을 주세요. 최대일수의 기본값은 3 입니다. 서버의 refs/heads/feature/*이름 오름차순으로 훑어 브랜치마다 한 줄씩 <브랜치이름> <앞선커밋수> <나이> <OK|STALE> 을 냅니다. 브랜치이름은 refs/heads/ 를 뗀 것, 앞선커밋수는 main..<브랜치> 의 커밋 수, 나이는 그 범위에서 가장 오래된 커밋의 커미터 시각과 지금 사이의 날수(내림)입니다. 나이가 최대일수를 넘으면 STALE, 아니면 OK 입니다. STALE 이 하나라도 있으면 종료 코드 1, 없으면 0, 인자가 모자라거나 서버가 없으면 2 입니다. 그다음 /root/trunk/app 에서 기능 브랜치 두 개를 더 올리세요 — 하나는 첫 커밋의 작성·커미터 시각이 40일 전feature/old-report, 하나는 지금 만든 feature/quick-fix 입니다. 마지막으로 /root/trunk/branch-age.sh /root/trunk/server.git 3 의 출력을 /root/trunk/evidence/05-branch-age.txt 에 저장하세요.
  6. /root/trunk/app 에서 origin/main 에서 갈라진 기능 브랜치 두 개를 만들어 서버에 올리세요. 두 브랜치가 고치는 파일이 겹치면 안 됩니다 — 그래야 깨끗이 합쳐지고도 깨진다는 것이 드러납니다. feature/sem-acalc.py 만 고쳐 discounted 가 결과를 round(...) 로 반올림해 돌려주게 합니다. feature/sem-bcheckout.pycoupon_total(price)(= discounted(price, 0.15))를 더하고 tests.pycoupon_total(105) == 89.25 단언을 더합니다. 두 브랜치 각각에 /root/trunk/ci-check.sh 를 돌려 초록불 기록을 남기세요. 그다음 서버를 임시 디렉터리로 복제해 feature/sem-bfeature/sem-a 위로 rebase 해 보고(파일 충돌이 나면 안 됩니다) 그 결과에서 run-tests.sh 를 돌리세요. 확인한 것을 /root/trunk/evidence/06-semantic.txt 에 네 줄로 적으세요 — sem-a PASS, sem-b PASS, merged <PASS|FAIL>, textual <clean|conflict>.
  7. /root/trunk/merge-queue.sh <server.git> <대기열파일> 을 만들고 실행 권한을 주세요. 대기열 파일은 한 줄에 브랜치 이름 하나이고 빈 줄과 # 로 시작하는 줄은 건너뜁니다. 항목마다 차례대로 이렇게 합니다 — 지금의 main 끝을 받아 와 그 위로 브랜치를 rebase 하고(얹을 수 없으면 REJECTED <브랜치> conflict), 얹은 커밋들을 refs/queue/<브랜치> 로 먼저 올린 뒤 그 커밋 하나하나에 같은 디렉터리의 ci-check.sh 를 돌리고(하나라도 실패하면 REJECTED <브랜치> tests), 전부 초록불이면 그 끝을 main 으로 push 하고(거절당하면 REJECTED <브랜치> push) MERGED <브랜치> <40자리 SHA> 를 냅니다. 서버에 없는 브랜치는 REJECTED <브랜치> missing 입니다. 실패한 항목은 빼고 다음 항목으로 계속 갑니다. 끝나면 refs/queue/* 는 남기지 않습니다. 종료 코드는 전부 병합되면 0, 거절된 것이 있으면 3, 인자나 대기열 파일이 잘못되면 2 입니다. 그다음 /root/trunk/queue.txtfeature/sem-a, feature/sem-b, feature/quick-fix 를 이 순서로 적고 대기열을 돌려, 출력을 /root/trunk/evidence/07-queue.txt 에 저장하세요.
  8. /root/trunk/protection-report.sh <server.git> <보고서파일> 을 만들고 실행 권한을 주세요. 탐침마다 서버 사본을 새로 떠서 원본 서버는 한 글자도 바꾸지 않습니다. 다섯 가지를 이 순서로 실제로 밀어 봅니다 — force-main(끝 커밋을 고쳐 써서 강제 갱신), no-status(기록 없는 새 커밋), failed-status(기록을 fail 로 남긴 새 커밋), merge-commit(새 커밋 전부에 pass 를 기록한 병합 커밋), queue-merge(새 커밋 전부에 pass 를 기록한 선형 커밋). 탐침마다 한 줄씩 <탐침이름> <BLOCKED|ALLOWED> <거절 메시지 첫 줄 또는 -> 를 적고, 마지막 줄에 SUMMARY blocked=<수> allowed=<수> 를 적습니다. 거절 메시지는 push 의 표준오류에서 remote: 로 시작하는 첫 줄에서 접두사를 뗀 것입니다. 보고서는 파일로 남기고 화면에도 냅니다. 상위 디렉터리가 없으면 만듭니다. 종료 코드는 앞의 네 가지가 모두 막히고 queue-merge 만 통과했으면 0, 그렇지 않으면 3, 인자가 잘못되면 2 입니다. 마지막으로 /root/trunk/protection-report.sh /root/trunk/server.git /root/trunk/report/protection.txt 를 돌려 보고서를 남기세요.

참고

규칙이 없으니 아무나 트렁크에 바로 밀 수 있었다

/root/trunk 에서 시작합니다. 먼저 '서버' 역할을 할 베어 저장소 /root/trunk/server.git 를 기본 브랜치 main 으로 만들고, 검사 결과를 커밋별로 적어 둘 빈 디렉터리 /root/trunk/server.git/status 를 만드세요. 그다음 그 서버를 /root/trunk/app 으로 복제하고 신원을 dev / dev@example.com 으로 세운 뒤, 파이썬 파일 세 개와 시험 실행기를 만들어 커밋하고 main 에 바로 push 하세요 — calc.py(discounted(price, rate)price - price * rate 를 돌려준다), checkout.py(cart_total(prices, rate) 가 각 값의 할인가를 더한다), tests.py(discounted(100, 0.2) == 80cart_total([100, 200], 0.2) == 240 을 단언하고 ok 를 찍는다), run-tests.sh(자기 디렉터리로 옮겨 python3 tests.py 를 돌린다, 실행 권한 필요). 마지막으로 같은 서버를 /root/trunk/app2 로 한 번 더 복제해 신원을 other / other@example.com 으로 세우고, README.md 를 더해 역시 main 에 바로 push 하세요. 그러고 나서 git --git-dir=/root/trunk/server.git log --format='%h %ae %s' main 의 출력을 그대로 /root/trunk/evidence/01-direct-push.txt 에 저장하세요.

베어 저장소는 작업 트리가 없는 저장소라 push 를 받는 쪽으로 쓰기 좋습니다. git init --bare -b main 으로 기본 브랜치 이름까지 정할 수 있습니다. 파드에 git 신원이 없어서 저장소마다 git config user.nameuser.email 을 먼저 세워야 커밋이 됩니다. 지금은 아무 규칙도 없다는 것, 그래서 두 사람이 각각 트렁크를 직접 고칠 수 있다는 것이 이 단계에서 확인할 사실입니다.

규칙을 받는 쪽에 두자 직접 push 가 막혔다

서버 훅 /root/trunk/server.git/hooks/update 를 만들고 실행 권한을 주세요. 인자는 차례로 갱신할 레퍼런스 이름·옛 값·새 값입니다. 레퍼런스가 refs/heads/main 이면 표준오류에 무엇을 하라는 안내를 한 줄 내고 1 로 끝내고, 그 밖의 레퍼런스는 아무 말 없이 0 으로 끝냅니다. 그다음 로컬 훅 /root/trunk/app/.git/hooks/pre-commit 을 만들어 실행 권한을 주세요 — 스테이징된 변경에 DO-NOT-COMMIT 이 들어 있으면 거절하고, 그렇지 않으면 통과시킵니다. 이제 세 가지를 확인해 /root/trunk/evidence/02-hooks.txt<이름> <값> 세 줄로 적으세요. main-push/root/trunk/app 에서 커밋 하나를 만들어 main 에 바로 push 했을 때의 결과(accepted 또는 rejected), same-push-both한 번의 push 명령으로 main 과 기능 브랜치를 함께 밀었을 때 서버에 올라간 것(all·feature-only·none 중 하나), cloned-hook 은 서버를 /root/trunk/app3 으로 새로 복제했을 때 그 사본에 pre-commit 훅이 있었는지(present 또는 absent)입니다.

훅은 실행 권한이 없으면 git 이 오류도 없이 그냥 건너뜁니다 — 규칙이 켜져 있다고 믿는 사이 아무것도 막히지 않는 상태가 여기서 생깁니다. update 는 갱신할 레퍼런스마다 한 번씩 돌고 0 이 아니면 그 레퍼런스만 막습니다. pre-receive 였다면 받는 작업 전체가 한 번에 거부됩니다 — 두 레퍼런스를 함께 밀어 보면 둘의 차이가 그대로 드러납니다. 로컬 훅은 $GIT_DIR/hooks 에 있고 복제로 전파되지 않습니다.

잠그기만 하면 아무도 못 들어간다 — 초록불인 커밋만 받는다

먼저 검사 러너 /root/trunk/ci-check.sh <server.git> <커밋SHA> 를 만들고 실행 권한을 주세요. 서버에서 그 커밋의 트리를 임시 디렉터리로 꺼내(git --git-dir=<server> archive <커밋>) run-tests.sh 를 돌리고, 결과를 <server.git>/status/<커밋 40자리 SHA> 파일의 첫 줄pass 또는 fail 로 적습니다. 화면에는 <40자리 SHA> <pass|fail> 한 줄을 내고, 종료 코드는 통과 0 · 실패 1 · 인자가 모자라거나 서버에 없는 커밋이면 2 입니다. 임시 디렉터리는 남기지 않습니다. 그다음 /root/trunk/server.git/hooks/update 를 고쳐 refs/heads/main 에 이 규칙들을 걸으세요 — 새 값이 0 으로만 된 갱신(삭제)은 거절, 옛 값이 새 값의 조상이 아닌 갱신(강제 갱신)은 거절, 그리고 git rev-list <옛값>..<새값> 으로 얻은 새 커밋 하나하나에 대해 기록이 없으면 거절하고 첫 줄이 pass 가 아니어도 거절합니다. 거절 메시지에는 문제가 된 커밋의 40자리 SHA 를 넣으세요. 마지막으로 /root/trunk/app 에서 기능 브랜치 두 개를 서버에 올리고 — 하나는 시험이 통과하는 변경, 하나는 tests.py 에 일부러 깨지는 단언을 더한 변경 — 각각에 ci-check.sh 를 돌린 뒤, /root/trunk/evidence/03-status.txt 에 두 줄을 green <40자리 SHA> passred <40자리 SHA> fail 로 적으세요.

베어 저장소 안에서 훅이 돌 때 git rev-parse --git-dir 은 그 저장소를 가리킵니다 — 기록 자리를 절대 경로로 못박지 마세요. 사본에서도 같은 규칙이 돌아야 합니다. git merge-base --is-ancestor A B 는 A 가 B 의 조상이면 0 으로 끝납니다. 기록을 남기는 자리는 CI 만 쓸 수 있는 곳이라고 가정합니다 — 실제 서비스라면 그 자리가 곧 '필수 상태 검사' 입니다. 검사 러너는 작업 사본이 아니라 서버에 들어온 커밋을 봐야 합니다. 아직 서버에 없는 커밋은 꺼낼 수 없습니다.

병합 커밋을 돌려보내고 rebase 를 시킨다

/root/trunk/server.git/hooks/update 에 규칙을 하나 더 더하세요 — refs/heads/main 으로 들어오는 새 커밋 가운데 부모가 둘 이상인 커밋(병합 커밋)이 있으면, 그 커밋의 40자리 SHA 와 rebase 라는 낱말이 함께 든 안내를 표준오류에 내고 거절합니다. 검사 기록이 전부 초록불이어도 거절해야 합니다. 그다음 규칙이 실제로 도는지 확인하세요 — /root/trunk/app 에서 origin/main 에서 갈라진 브랜치 feature/merge-demo 를 만들어 커밋 하나를 올리고, 거기에 origin/feature/green--no-ff 로 병합한 뒤, 새 커밋 전부에 pass 기록을 손으로 넣고 그 브랜치 끝을 main 으로 push 하세요. 거절 메시지 첫 줄을 remote: 접두사를 떼고 /root/trunk/evidence/04-linear.txt 에 그대로 저장하세요.

git rev-list --parents -n 1 <커밋> 은 그 커밋과 부모들을 한 줄로 냅니다 — 낱말이 셋 이상이면 병합 커밋입니다. 선형 이력을 요구하는 이유는 취향이 아닙니다. 되돌리기와 범인 좁히기가 눈에 띄게 쉬워지고, '이 커밋이 트렁크에서 통과했는가' 라는 질문에 커밋 하나로 답할 수 있게 됩니다. 거절은 막는 일의 절반입니다 — 나머지 절반은 지금 무엇을 해야 하는지 알려 주는 것입니다.

며칠짜리 브랜치가 문제의 대부분을 만들고 있었다

/root/trunk/branch-age.sh <server.git> [최대일수] 를 만들고 실행 권한을 주세요. 최대일수의 기본값은 3 입니다. 서버의 refs/heads/feature/*이름 오름차순으로 훑어 브랜치마다 한 줄씩 <브랜치이름> <앞선커밋수> <나이> <OK|STALE> 을 냅니다. 브랜치이름은 refs/heads/ 를 뗀 것, 앞선커밋수는 main..<브랜치> 의 커밋 수, 나이는 그 범위에서 가장 오래된 커밋의 커미터 시각과 지금 사이의 날수(내림)입니다. 나이가 최대일수를 넘으면 STALE, 아니면 OK 입니다. STALE 이 하나라도 있으면 종료 코드 1, 없으면 0, 인자가 모자라거나 서버가 없으면 2 입니다. 그다음 /root/trunk/app 에서 기능 브랜치 두 개를 더 올리세요 — 하나는 첫 커밋의 작성·커미터 시각이 40일 전feature/old-report, 하나는 지금 만든 feature/quick-fix 입니다. 마지막으로 /root/trunk/branch-age.sh /root/trunk/server.git 3 의 출력을 /root/trunk/evidence/05-branch-age.txt 에 저장하세요.

커밋 시각은 GIT_AUTHOR_DATEGIT_COMMITTER_DATE 로 정할 수 있고 @<에포크초> 형식을 받습니다. git log -1 --format=%ct 가 커미터 시각을 에포크초로 냅니다. git for-each-refrefs/heads/feature 를 주면 그 아래만 나옵니다. 정렬은 로케일을 타니 LC_ALL=C 를 붙이는 편이 안전합니다. 왜 나이를 재는가 — 재기 시작하면 대개 며칠짜리 브랜치 몇 개가 병합 사고의 대부분을 만들고 있다는 것이 드러납니다.

각각 초록불이던 둘을 합치자 빨간불이 되었다

/root/trunk/app 에서 origin/main 에서 갈라진 기능 브랜치 두 개를 만들어 서버에 올리세요. 두 브랜치가 고치는 파일이 겹치면 안 됩니다 — 그래야 깨끗이 합쳐지고도 깨진다는 것이 드러납니다. feature/sem-acalc.py 만 고쳐 discounted 가 결과를 round(...) 로 반올림해 돌려주게 합니다. feature/sem-bcheckout.pycoupon_total(price)(= discounted(price, 0.15))를 더하고 tests.pycoupon_total(105) == 89.25 단언을 더합니다. 두 브랜치 각각에 /root/trunk/ci-check.sh 를 돌려 초록불 기록을 남기세요. 그다음 서버를 임시 디렉터리로 복제해 feature/sem-bfeature/sem-a 위로 rebase 해 보고(파일 충돌이 나면 안 됩니다) 그 결과에서 run-tests.sh 를 돌리세요. 확인한 것을 /root/trunk/evidence/06-semantic.txt 에 네 줄로 적으세요 — sem-a PASS, sem-b PASS, merged <PASS|FAIL>, textual <clean|conflict>.

텍스트 충돌은 도구가 알려 주므로 시간이 들 뿐 놓치지 않습니다. 놓치는 것은 서로 다른 줄을 고쳐 깨끗이 합쳐지는데 결과만 틀린 경우입니다. 파이썬의 round 는 소수 첫째 자리가 5 인 값을 가까운 짝수로 보냅니다 — 반올림이 들어가면 .25 로 끝나던 값이 더는 그 값이 아닙니다. 브랜치에서 한 검사는 '내 변경 더하기 그때의 트렁크' 를 본 것이지, '합쳐진 뒤의 트렁크' 를 본 것이 아닙니다. 작업 사본을 어지럽히지 않으려면 합쳐 보는 일은 임시 복제본에서 하세요.

대기열이 지금의 트렁크 끝 위에 얹어 다시 검사한다

/root/trunk/merge-queue.sh <server.git> <대기열파일> 을 만들고 실행 권한을 주세요. 대기열 파일은 한 줄에 브랜치 이름 하나이고 빈 줄과 # 로 시작하는 줄은 건너뜁니다. 항목마다 차례대로 이렇게 합니다 — 지금의 main 끝을 받아 와 그 위로 브랜치를 rebase 하고(얹을 수 없으면 REJECTED <브랜치> conflict), 얹은 커밋들을 refs/queue/<브랜치> 로 먼저 올린 뒤 그 커밋 하나하나에 같은 디렉터리의 ci-check.sh 를 돌리고(하나라도 실패하면 REJECTED <브랜치> tests), 전부 초록불이면 그 끝을 main 으로 push 하고(거절당하면 REJECTED <브랜치> push) MERGED <브랜치> <40자리 SHA> 를 냅니다. 서버에 없는 브랜치는 REJECTED <브랜치> missing 입니다. 실패한 항목은 빼고 다음 항목으로 계속 갑니다. 끝나면 refs/queue/* 는 남기지 않습니다. 종료 코드는 전부 병합되면 0, 거절된 것이 있으면 3, 인자나 대기열 파일이 잘못되면 2 입니다. 그다음 /root/trunk/queue.txtfeature/sem-a, feature/sem-b, feature/quick-fix 를 이 순서로 적고 대기열을 돌려, 출력을 /root/trunk/evidence/07-queue.txt 에 저장하세요.

refs/queue/ 로 먼저 올리는가 — 검사 러너는 서버에 들어온 커밋만 꺼낼 수 있는데, 얹어서 새로 만든 커밋은 아직 서버에 없습니다. 보호된 것은 main 하나뿐이니 다른 레퍼런스로는 자유롭게 올릴 수 있습니다. 실제 서비스의 대기열도 이렇게 임시 브랜치를 만들어 검사합니다. 앞 항목이 들어가면 다음 항목의 기준이 달라집니다 — 그래서 대기열의 값어치는 '다시 검사한다' 에 있습니다. 앞 항목 하나가 깨졌다고 줄 전체를 세우면 대기열을 쓰는 이유가 없어집니다.

규칙을 어긴 push 가 어디서 어떤 말로 막히는지 한 장으로

/root/trunk/protection-report.sh <server.git> <보고서파일> 을 만들고 실행 권한을 주세요. 탐침마다 서버 사본을 새로 떠서 원본 서버는 한 글자도 바꾸지 않습니다. 다섯 가지를 이 순서로 실제로 밀어 봅니다 — force-main(끝 커밋을 고쳐 써서 강제 갱신), no-status(기록 없는 새 커밋), failed-status(기록을 fail 로 남긴 새 커밋), merge-commit(새 커밋 전부에 pass 를 기록한 병합 커밋), queue-merge(새 커밋 전부에 pass 를 기록한 선형 커밋). 탐침마다 한 줄씩 <탐침이름> <BLOCKED|ALLOWED> <거절 메시지 첫 줄 또는 -> 를 적고, 마지막 줄에 SUMMARY blocked=<수> allowed=<수> 를 적습니다. 거절 메시지는 push 의 표준오류에서 remote: 로 시작하는 첫 줄에서 접두사를 뗀 것입니다. 보고서는 파일로 남기고 화면에도 냅니다. 상위 디렉터리가 없으면 만듭니다. 종료 코드는 앞의 네 가지가 모두 막히고 queue-merge 만 통과했으면 0, 그렇지 않으면 3, 인자가 잘못되면 2 입니다. 마지막으로 /root/trunk/protection-report.sh /root/trunk/server.git /root/trunk/report/protection.txt 를 돌려 보고서를 남기세요.

보고서는 '적어 넣는 것' 이 아니라 '찔러 본 결과' 여야 합니다. 훅을 빼 놓은 서버에 대고 돌리면 다섯 줄이 모두 통과로 뒤집히는 것이 그 증거입니다 — 채점기가 실제로 그렇게 해 봅니다. 베어 저장소는 디렉터리 복사만으로 사본이 됩니다. 사본에도 훅과 기록이 함께 따라온다는 점을 이용하세요. 강제 갱신을 만드는 가장 짧은 길은 끝 커밋을 고쳐 쓰는 것입니다. 마지막으로 돌려 본 다섯 줄을 앞 단계들에서 만든 규칙과 하나씩 맞춰 보면, 어떤 규칙이 어떤 사고를 막고 있는지가 한눈에 보입니다.