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 를 돌려 보고서를 남기세요.

참고

단계 8개

  1. 규칙이 없으니 아무나 트렁크에 바로 밀 수 있었다
  2. 규칙을 받는 쪽에 두자 직접 push 가 막혔다
  3. 잠그기만 하면 아무도 못 들어간다 — 초록불인 커밋만 받는다
  4. 병합 커밋을 돌려보내고 rebase 를 시킨다
  5. 며칠짜리 브랜치가 문제의 대부분을 만들고 있었다
  6. 각각 초록불이던 둘을 합치자 빨간불이 되었다
  7. 대기열이 지금의 트렁크 끝 위에 얹어 다시 검사한다
  8. 규칙을 어긴 push 가 어디서 어떤 말로 막히는지 한 장으로