赤いビルドが十件、誰もログを読んでいなかった
한국어 원문으로 표시합니다.
목표
파이프라인의 실패를 표로 갈래를 나누고, 갈래마다 다른 대응으로 보내고, 불안정을 측정으로 증명하고, 실패한 실행을 기록만으로 다시 일으키고, 결정적인 판정 스크립트로 범인 커밋을 자동으로 찾아내는 조사 흐름을 처음부터 끝까지 만듭니다.
왜 중요한가
실패가 하루에 열 번이면 사람은 로그를 읽지 않습니다. 재시도 단추가 먼저 눌리고, 통과하면 잊힙니다. 그 습관이 붙은 파이프라인에서는 진짜 결함도 두세 번 재시도되다 어쩌다 통과하면 그대로 병합되고, 빨간불은 신호이기를 그만둡니다. 그래서 필요한 것은 더 좋은 로그가 아니라 갈래입니다. 인프라 문제는 재시도해도 되고, 불안정은 재시도로 덮으면 영원히 남고, 진짜 결함은 몇 번을 돌려도 같은 답이라 재시도가 시간만 버립니다. 갈래를 정한 다음에야 '언제 깨졌는가' 라는 질문이 성립하고, 그 질문에 답하는 이분 탐색은 판정이 결정적일 때만 믿을 수 있습니다. 한 번의 잘못된 판정이 남은 탐색 전체를 엉뚱한 구간으로 몰기 때문입니다. 이 실습은 그 사슬을 통째로 만듭니다 — 표로 가르고, 측정으로 증명하고, 기록으로 되살리고, 종료 코드로만 말하는 판정 스크립트로 범인을 찾습니다.
단계
/root/triage/rules.tsv에 분류 규칙을 표로 적으세요. 탭으로 나뉜 세 칸규칙id<TAB>갈래<TAB>확장정규식이고, 위에서부터 첫 일치가 이깁니다. 규칙 여섯 개를 이 id 와 갈래로 둡니다 —dns(infra),disk(infra),oom(infra),timeout(flaky),assert(defect),syntax(defect). 그다음/root/triage/logs/에 실패 로그 표본 여섯 편run-01.log부터run-06.log까지를 직접 쓰세요. 차례대로 이름 풀이 실패(dns)·디스크 부족(disk)·메모리 초과로 죽음(oom)·시간 초과(timeout)·단언 실패(assert)·문법 오류(syntax)가 드러나는 내용이어야 합니다. 마지막으로/root/triage/classify.sh <로그파일>을 만드세요. 규칙 표의 자리는 환경 변수RULES_FILE로 바꿀 수 있고 기본값은/root/triage/rules.tsv입니다. 첫 낱말로 갈래를, 둘째 낱말로 규칙 id 를 한 줄에 냅니다. 어느 규칙에도 걸리지 않으면unknown -을 냅니다. 로그 파일을 읽을 수 없으면 표준출력에는 아무것도 내지 말고 종료 코드 2 로 끝냅니다./root/triage/policy.tsv에 갈래별 대응을 표로 적으세요. 탭으로 나뉜 세 칸갈래<TAB>권장대응<TAB>재시도가능이고 네 줄입니다 —infra requeue yes,flaky measure no,defect bisect no,unknown read no(칸 사이는 탭). 그다음/root/triage/classify.sh를 고쳐 한 줄에 네 낱말<갈래> <규칙id> <권장대응> <재시도가능>을 내게 하세요. 정책 표의 자리는 환경 변수POLICY_FILE로 바꿀 수 있고 기본값은/root/triage/policy.tsv입니다. 그리고 갈래를 종료 코드로도 말하게 하세요 — infra 는 0, flaky 는 3, defect 는 4, unknown 은 5, 로그를 읽을 수 없으면 2 입니다. 마지막으로/root/triage/logs/의 여섯 편을 차례로 돌려 그 출력을 그대로/root/triage/triage.txt에 여섯 줄로 저장하세요(run-01 부터 run-06 순서)./root/triage/tests/에 시험 스크립트 세 개를 만드세요 —always-pass.sh(언제나 0),always-fail.sh(언제나 0 이 아님),flip.sh(홀수 번째 실행에서만 실패).flip.sh가 세는 값은 자기 파일 옆($(dirname "$0")아래)에 두어 스크립트를 복사해 옮겨도 함께 따라가게 하세요. 그다음/root/triage/flaky-probe.sh <시험스크립트> <횟수>를 만드세요. 받은 스크립트를 그 횟수만큼 돌려 한 줄로runs=<횟수> pass=<성공> fail=<실패> verdict=<판정>을 내고, 판정은stable-pass·flaky·stable-fail중 하나입니다. 종료 코드는 stable-pass 가 0, flaky 가 3, stable-fail 이 4 이고, 인자가 없거나 횟수가 1 이상의 정수가 아니면 2 입니다. 마지막으로 세 스크립트를 각각 8회로 돌린 출력을/root/triage/flaky-report.txt에 세 줄로 저장하세요(always-pass, flip, always-fail 순서)./root/triage/tests/seed-test.sh를 만드세요. 환경 변수SEED를 읽어 같은 씨앗이면 언제나 같은 결과를 내되 씨앗에 따라 성공과 실패가 갈리는 시험입니다.SEED가 비어 있으면 종료 코드 2 로 끝냅니다. 그다음/root/triage/record-failure.sh <시험스크립트> <기록파일>을 만드세요. 환경 변수SEED에 매번 다른 값을 넣어 최대 30회까지 돌려 첫 실패에서 멈추고, 기록 파일에 다섯 줄을SEED=,TZ=,LC_ALL=,SCRIPT_SHA256=(시험 스크립트의 sha256 앞 64자리),EXIT_CODE=로 적고 0 으로 끝냅니다. 30회 안에 실패가 없으면 기록을 남기지 말고 0 이 아닌 값으로 끝냅니다. 마지막으로/root/triage/replay.sh <기록파일> <시험스크립트>를 만드세요. 기록의SCRIPT_SHA256이 지금 스크립트의 해시와 다르면 시험을 돌리지 말고 종료 코드 2 로 끝내고, 같으면 기록의 SEED·TZ·LC_ALL 을 넣어 시험을 돌린 뒤 그 시험의 종료 코드로 끝냅니다. 셋을 이어/root/triage/repro/case.env를 만드세요 —record-failure.sh /root/triage/tests/seed-test.sh /root/triage/repro/case.env입니다./root/triage/repo에 연습용 git 저장소를 만드세요. 커밋은 20개이고 제목은change 1부터change 20입니다. 각 커밋은app/rate.py와app/notes.txt를 담고,python3 app/rate.py는 1번부터 11번 커밋까지100, 12번 커밋부터250을 냅니다(12번이 결함을 넣은 커밋입니다). 파드에 git 신원이 없으니 저장소 안에서git config user.name과git config user.email을 먼저 세우세요./root/triage/expected.txt에는 기대값100한 줄을 적습니다. 그다음/root/triage/oracle.sh를 만드세요. 체크아웃된 작업 트리의 뿌리에서 돌아python3 app/rate.py의 출력이 기대값과 같으면 0, 다르면 1 로 끝납니다. 기대값 파일의 자리는 환경 변수EXPECT_FILE로 바꿀 수 있고 기본값은/root/triage/expected.txt입니다. 표준출력에는 아무것도 내지 않습니다./root/triage/repo에서 첫 커밋을 good, HEAD 를 bad 로 잡고git bisect run으로 범인 커밋을 찾으세요. 판정에는/root/triage/oracle.sh를 씁니다. 찾은 뒤/root/triage/bisect/culprit.txt에 두 줄을 적으세요 —culprit=<40자리 커밋 해시>와subject=<그 커밋의 제목>. 그리고/root/triage/bisect/steps.txt에 세 줄을 적으세요 —revisions=<good 뒤부터 bad 까지의 커밋 수>,tests=<판정 스크립트가 실제로 불린 횟수>,reason=<왜 그 횟수인지 한 줄 설명>(20자 이상). 끝나면git bisect reset으로 저장소를 원래 가지로 되돌려 두세요./root/triage/repo-skip에 두 번째 저장소를 만드세요. 커밋 20개에 제목은 같은 꼴이되 이번에는 10번 커밋의app/rate.py가 문법 오류라 실행조차 되지 않고, 18번 커밋부터250을 냅니다(1번부터 17번까지는 10번을 빼고 모두100). 그다음/root/triage/oracle-skip.sh를 만드세요. oracle.sh 와 같되app/rate.py가 없거나 문법이 통과하지 않으면 종료 코드 125 로 끝냅니다. 같은 저장소에서 첫 커밋을 good, HEAD 를 bad 로 잡고 이분 탐색을 두 번 돌리세요 — 한 번은/root/triage/oracle.sh로, 한 번은/root/triage/oracle-skip.sh로. 결과를/root/triage/bisect/skip-report.txt에 네 줄로 적으세요 —naive=<oracle.sh 가 지목한 40자리 해시>,skip=<oracle-skip.sh 가 지목한 40자리 해시>,true=<진짜 범인의 40자리 해시>,limit=<건너뛰기의 한계를 적은 한 줄>(20자 이상). 끝나면git bisect reset을 잊지 마세요./root/triage/investigate.sh <로그파일> <저장소> <보고서파일>을 만드세요. 로그를/root/triage/classify.sh로 분류하고, 갈래가defect일 때만 저장소를git clone으로 복제해서 첫 커밋부터 HEAD 까지/root/triage/oracle-skip.sh로 이분 탐색해 범인을 찾습니다. 받은 저장소는 한 글자도 바뀌면 안 되고, 그 저장소에서 누군가 고치고 있던 파일도 그대로여야 합니다. 보고서는 다섯 줄입니다 —category=,action=,culprit=(defect 가 아니거나 못 찾았으면-),subject=(마찬가지),reproduce=(다시 일으키는 방법 한 줄). 보고서 파일의 상위 디렉터리가 없으면 만들고, 정상적으로 끝나면 0 으로, 인자가 모자라거나 로그·저장소를 읽을 수 없으면 2 로 끝냅니다. 만든 뒤 두 번 돌리세요 —/root/triage/logs/run-05.log와/root/triage/repo-skip으로/root/triage/report/case-defect.txt를,/root/triage/logs/run-01.log와/root/triage/repo-skip으로/root/triage/report/case-infra.txt를 만드세요.
참고
- 이 실습 파드에서는 컨테이너를 띄울 수 없습니다 — seccomp 가 새 사용자 네임스페이스 만들기를 막습니다.
podman run·podman build·buildah·unshare -U는 쓰지 않습니다. 쓰는 것은 bash·git 2.43·python3.12(표준 라이브러리)·jq·tar·sha256sum 입니다.yq·make·go·pytest는 없고 인터넷도 없습니다. - 파드에 git 신원이 설정돼 있지 않습니다. 연습용 저장소를 만들면 그 안에서
git config user.name과git config user.email을 먼저 세우세요 — 안 하면 첫git commit이Please tell me who you are로 멈춥니다. - 이 실습에서 말하는 '재현' 은 실패의 재현입니다. 같은 입력이 같은 바이트를 내는가(산출물의 재현)가 아니라, 같은 조건을 다시 만들어 같은 실패를 일으킬 수 있는가입니다. 그래서 기록하는 것도 산출물의 해시가 아니라 씨앗·환경·입력의 해시입니다.
- 흔한 실수: 판정 스크립트를 저장소 안에 두는 것. 이분 탐색이 커밋을 옮겨 다니면 그 스크립트까지 함께 바뀝니다.
- 흔한 실수: 문법 확인에
py_compile을 써서__pycache__를 작업 트리에 남기고, 그 뒤 체크아웃이 막히는 것.ast.parse는 아무것도 남기지 않습니다. - 흔한 실수:
git bisect reset을 빠뜨려 저장소를 분리된 HEAD 에 두고 나오는 것. - git bisect · git rev-list · Continuous Integration (Martin Fowler)
빨간불이 열 개인데 읽은 사람은 아무도 없다
/root/triage/rules.tsv 에 분류 규칙을 표로 적으세요. 탭으로 나뉜 세 칸 규칙id<TAB>갈래<TAB>확장정규식 이고, 위에서부터 첫 일치가 이깁니다. 규칙 여섯 개를 이 id 와 갈래로 둡니다 — dns(infra), disk(infra), oom(infra), timeout(flaky), assert(defect), syntax(defect). 그다음 /root/triage/logs/ 에 실패 로그 표본 여섯 편 run-01.log 부터 run-06.log 까지를 직접 쓰세요. 차례대로 이름 풀이 실패(dns)·디스크 부족(disk)·메모리 초과로 죽음(oom)·시간 초과(timeout)·단언 실패(assert)·문법 오류(syntax)가 드러나는 내용이어야 합니다. 마지막으로 /root/triage/classify.sh <로그파일> 을 만드세요. 규칙 표의 자리는 환경 변수 RULES_FILE 로 바꿀 수 있고 기본값은 /root/triage/rules.tsv 입니다. 첫 낱말로 갈래를, 둘째 낱말로 규칙 id 를 한 줄에 냅니다. 어느 규칙에도 걸리지 않으면 unknown - 을 냅니다. 로그 파일을 읽을 수 없으면 표준출력에는 아무것도 내지 말고 종료 코드 2 로 끝냅니다.
규칙을 코드가 아니라 표에 두는 이유는 새 실패 모양이 나타났을 때 고치는 곳이 스크립트가 아니라 데이터여야 검토와 이력이 남기 때문입니다. 탭으로 나뉜 줄은 while IFS=$'\t' read -r a b c 로 읽습니다. 마지막 줄에 개행이 없으면 read 가 그 줄을 흘리므로 || [ -n "$a" ] 를 덧붙이는 편이 안전합니다. 확장 정규식은 grep -Eq -- "$pattern" 으로 물어봅니다. 채점기는 자기가 만든 규칙 표와 자기가 만든 로그로도 이 스크립트를 돌립니다 — 로그 파일 이름을 보고 답하면 그때 걸립니다.
재시도해도 되는 실패와 절대 재시도하면 안 되는 실패
/root/triage/policy.tsv 에 갈래별 대응을 표로 적으세요. 탭으로 나뉜 세 칸 갈래<TAB>권장대응<TAB>재시도가능 이고 네 줄입니다 — infra requeue yes, flaky measure no, defect bisect no, unknown read no(칸 사이는 탭). 그다음 /root/triage/classify.sh 를 고쳐 한 줄에 네 낱말 <갈래> <규칙id> <권장대응> <재시도가능> 을 내게 하세요. 정책 표의 자리는 환경 변수 POLICY_FILE 로 바꿀 수 있고 기본값은 /root/triage/policy.tsv 입니다. 그리고 갈래를 종료 코드로도 말하게 하세요 — infra 는 0, flaky 는 3, defect 는 4, unknown 은 5, 로그를 읽을 수 없으면 2 입니다. 마지막으로 /root/triage/logs/ 의 여섯 편을 차례로 돌려 그 출력을 그대로 /root/triage/triage.txt 에 여섯 줄로 저장하세요(run-01 부터 run-06 순서).
갈래를 종료 코드로도 내는 이유는 뒤에 오는 자동화가 문자열을 다시 파싱하지 않게 하기 위해서입니다. 이 실습의 마지막 단계가 그 값을 씁니다. 재시도해도 되는 것은 인프라뿐입니다 — 불안정은 재시도로 덮으면 영원히 남고, 진짜 결함은 몇 번을 돌려도 같은 답이라 시간만 버립니다. 채점기는 자기가 만든 정책 표를 POLICY_FILE 로 물려 돌려 봅니다. 대응 낱말을 스크립트 안에 박아 두면 그때 걸립니다.
한 번 실패한 것을 불안정이라 부를 수는 없다
/root/triage/tests/ 에 시험 스크립트 세 개를 만드세요 — always-pass.sh(언제나 0), always-fail.sh(언제나 0 이 아님), flip.sh(홀수 번째 실행에서만 실패). flip.sh 가 세는 값은 자기 파일 옆($(dirname "$0") 아래)에 두어 스크립트를 복사해 옮겨도 함께 따라가게 하세요. 그다음 /root/triage/flaky-probe.sh <시험스크립트> <횟수> 를 만드세요. 받은 스크립트를 그 횟수만큼 돌려 한 줄로 runs=<횟수> pass=<성공> fail=<실패> verdict=<판정> 을 내고, 판정은 stable-pass·flaky·stable-fail 중 하나입니다. 종료 코드는 stable-pass 가 0, flaky 가 3, stable-fail 이 4 이고, 인자가 없거나 횟수가 1 이상의 정수가 아니면 2 입니다. 마지막으로 세 스크립트를 각각 8회로 돌린 출력을 /root/triage/flaky-report.txt 에 세 줄로 저장하세요(always-pass, flip, always-fail 순서).
같은 조건에서 결과가 갈리는가 — 이것이 불안정의 정의이자 판별법입니다. 한 번의 실패는 어느 갈래인지 말해 주지 않습니다. flip.sh 의 상태를 /tmp 나 못박은 경로에 두면 사본을 만들었을 때 원본과 상태를 나눠 쓰게 되어 측정이 어긋납니다. 횟수 검사에는 case "$N" in ''|*[!0-9]*) 가 짧습니다. 채점기는 학생의 tests/ 를 통째로 복사해 사본에서 돌립니다.
재현이 안 되는 실패는 조사할 수가 없다
/root/triage/tests/seed-test.sh 를 만드세요. 환경 변수 SEED 를 읽어 같은 씨앗이면 언제나 같은 결과를 내되 씨앗에 따라 성공과 실패가 갈리는 시험입니다. SEED 가 비어 있으면 종료 코드 2 로 끝냅니다. 그다음 /root/triage/record-failure.sh <시험스크립트> <기록파일> 을 만드세요. 환경 변수 SEED 에 매번 다른 값을 넣어 최대 30회까지 돌려 첫 실패에서 멈추고, 기록 파일에 다섯 줄을 SEED=, TZ=, LC_ALL=, SCRIPT_SHA256=(시험 스크립트의 sha256 앞 64자리), EXIT_CODE= 로 적고 0 으로 끝냅니다. 30회 안에 실패가 없으면 기록을 남기지 말고 0 이 아닌 값으로 끝냅니다. 마지막으로 /root/triage/replay.sh <기록파일> <시험스크립트> 를 만드세요. 기록의 SCRIPT_SHA256 이 지금 스크립트의 해시와 다르면 시험을 돌리지 말고 종료 코드 2 로 끝내고, 같으면 기록의 SEED·TZ·LC_ALL 을 넣어 시험을 돌린 뒤 그 시험의 종료 코드로 끝냅니다. 셋을 이어 /root/triage/repro/case.env 를 만드세요 — record-failure.sh /root/triage/tests/seed-test.sh /root/triage/repro/case.env 입니다.
실패를 다시 일으킬 수 없으면 불안정인지 진짜 결함인지 가를 방법이 없습니다. 그래서 기록에는 그 실행을 되살리는 데 필요한 것만 들어갑니다 — 씨앗, 시간대, 로케일, 그리고 입력이 그대로인지 확인할 해시입니다. 해시를 대조하는 이유는 입력이 바뀐 뒤의 실행은 재현이 아니라 새 실험이기 때문입니다. 조용히 통과시키면 '재현됐다' 는 잘못된 결론이 남습니다. 씨앗은 $RANDOM 을 이어 붙이거나 반복 변수를 섞어 만듭니다. 같은 씨앗으로 30번 돌리면 같은 결과가 30번 나올 뿐입니다 — 채점기가 씨앗이 몇 가지였는지 셉니다.
이분 탐색이 믿을 수 있는 판정 스크립트를 먼저 만든다
/root/triage/repo 에 연습용 git 저장소를 만드세요. 커밋은 20개이고 제목은 change 1 부터 change 20 입니다. 각 커밋은 app/rate.py 와 app/notes.txt 를 담고, python3 app/rate.py 는 1번부터 11번 커밋까지 100, 12번 커밋부터 250 을 냅니다(12번이 결함을 넣은 커밋입니다). 파드에 git 신원이 없으니 저장소 안에서 git config user.name 과 git config user.email 을 먼저 세우세요. /root/triage/expected.txt 에는 기대값 100 한 줄을 적습니다. 그다음 /root/triage/oracle.sh 를 만드세요. 체크아웃된 작업 트리의 뿌리에서 돌아 python3 app/rate.py 의 출력이 기대값과 같으면 0, 다르면 1 로 끝납니다. 기대값 파일의 자리는 환경 변수 EXPECT_FILE 로 바꿀 수 있고 기본값은 /root/triage/expected.txt 입니다. 표준출력에는 아무것도 내지 않습니다.
판정 기준을 저장소 안에 두면 커밋을 옮겨 다닐 때 기준까지 함께 바뀌어 아무것도 판정할 수 없게 됩니다. 그래서 기대값은 저장소 밖에 두고 자리를 환경 변수로 엽니다 — 그래야 같은 판정 스크립트를 다른 저장소에도 씁니다. 표준출력을 비우는 이유는 이분 탐색이 읽는 것이 종료 코드뿐이기 때문입니다. 화면에 판정을 섞어 내면 자동화한 쪽이 그 문자열을 다시 파싱하게 됩니다. 이력을 만드는 반복문은 git add -A 와 git commit -qm 으로 짧게 씁니다. 채점기는 복제본에서 커밋을 하나씩 짚어 가며 정상에서 문제 있음으로 넘어가는 자리가 정확히 한 곳인지 봅니다.
커밋 열아홉 개를 네 번 만에 좁힌다
/root/triage/repo 에서 첫 커밋을 good, HEAD 를 bad 로 잡고 git bisect run 으로 범인 커밋을 찾으세요. 판정에는 /root/triage/oracle.sh 를 씁니다. 찾은 뒤 /root/triage/bisect/culprit.txt 에 두 줄을 적으세요 — culprit=<40자리 커밋 해시> 와 subject=<그 커밋의 제목>. 그리고 /root/triage/bisect/steps.txt 에 세 줄을 적으세요 — revisions=<good 뒤부터 bad 까지의 커밋 수>, tests=<판정 스크립트가 실제로 불린 횟수>, reason=<왜 그 횟수인지 한 줄 설명>(20자 이상). 끝나면 git bisect reset 으로 저장소를 원래 가지로 되돌려 두세요.
판정 스크립트를 저장소 밖에 두는 이유가 여기서 드러납니다 — 이분 탐색은 커밋을 옮겨 다니므로, 저장소 안에 두면 그 스크립트까지 커밋마다 바뀝니다. 구간의 커밋 수는 git rev-list --count <good>..<bad> 로 셉니다. 불린 횟수는 git bisect run 이 화면에 찍는 running ... 줄을 세거나, 판정 스크립트를 한 줄씩 기록하는 껍데기로 감싸 세면 됩니다. 횟수가 커밋 수를 따라가지 않고 그 로그에 비례하는 이유를 reason= 에 적으세요. 채점기는 복제본에서 같은 이분 탐색을 스스로 돌려 답과 횟수를 맞춰 봅니다.
실행조차 안 되는 커밋 하나가 범인을 바꿔 놓았다
/root/triage/repo-skip 에 두 번째 저장소를 만드세요. 커밋 20개에 제목은 같은 꼴이되 이번에는 10번 커밋의 app/rate.py 가 문법 오류라 실행조차 되지 않고, 18번 커밋부터 250 을 냅니다(1번부터 17번까지는 10번을 빼고 모두 100). 그다음 /root/triage/oracle-skip.sh 를 만드세요. oracle.sh 와 같되 app/rate.py 가 없거나 문법이 통과하지 않으면 종료 코드 125 로 끝냅니다. 같은 저장소에서 첫 커밋을 good, HEAD 를 bad 로 잡고 이분 탐색을 두 번 돌리세요 — 한 번은 /root/triage/oracle.sh 로, 한 번은 /root/triage/oracle-skip.sh 로. 결과를 /root/triage/bisect/skip-report.txt 에 네 줄로 적으세요 — naive=<oracle.sh 가 지목한 40자리 해시>, skip=<oracle-skip.sh 가 지목한 40자리 해시>, true=<진짜 범인의 40자리 해시>, limit=<건너뛰기의 한계를 적은 한 줄>(20자 이상). 끝나면 git bisect reset 을 잊지 마세요.
125 는 '이 커밋으로는 판정할 수 없다' 입니다. 판정 불가를 1(문제 있음)로 접으면 탐색은 그 앞쪽으로 수렴해 무고한 커밋을 하나로 확신 있게 지목합니다 — 모른다고 말하지 않는 쪽이 더 나쁩니다. 문법만 확인할 때 py_compile 을 쓰면 __pycache__ 가 작업 트리에 남아 다음 체크아웃이 막힙니다. python3 -c 'import ast,sys; ast.parse(open(sys.argv[1]).read())' 는 아무것도 남기지 않습니다. 진짜 범인은 오래된 커밋부터 하나씩 짚어 처음으로 문제 있음이 되는 자리입니다 — 저장소를 복제해서 훑으면 원본을 건드리지 않습니다. limit= 에는 건너뛴 커밋이 범인과 붙어 있을 때 탐색이 어디까지만 답할 수 있는지를 적으세요.
로그 한 편을 넣으면 범인 커밋이 나오게 한다
/root/triage/investigate.sh <로그파일> <저장소> <보고서파일> 을 만드세요. 로그를 /root/triage/classify.sh 로 분류하고, 갈래가 defect 일 때만 저장소를 git clone 으로 복제해서 첫 커밋부터 HEAD 까지 /root/triage/oracle-skip.sh 로 이분 탐색해 범인을 찾습니다. 받은 저장소는 한 글자도 바뀌면 안 되고, 그 저장소에서 누군가 고치고 있던 파일도 그대로여야 합니다. 보고서는 다섯 줄입니다 — category=, action=, culprit=(defect 가 아니거나 못 찾았으면 -), subject=(마찬가지), reproduce=(다시 일으키는 방법 한 줄). 보고서 파일의 상위 디렉터리가 없으면 만들고, 정상적으로 끝나면 0 으로, 인자가 모자라거나 로그·저장소를 읽을 수 없으면 2 로 끝냅니다. 만든 뒤 두 번 돌리세요 — /root/triage/logs/run-05.log 와 /root/triage/repo-skip 으로 /root/triage/report/case-defect.txt 를, /root/triage/logs/run-01.log 와 /root/triage/repo-skip 으로 /root/triage/report/case-infra.txt 를 만드세요.
조사 스크립트가 남의 저장소에서 바로 이분 탐색을 돌리면 그 사람이 작업하던 자리를 빼앗습니다. git clone 한 임시 디렉터리에서 돌리고 trap ... EXIT 으로 치우세요. 판정 스크립트는 기대값 파일의 자리를 환경 변수에서 읽습니다 — 조사 스크립트는 그 변수를 지우지 말고 그대로 물려주면 됩니다. 채점기가 자기 기대값 파일을 물려 돌려 봅니다. 첫 커밋은 git rev-list --max-parents=0 HEAD 로 찾습니다. 인프라 갈래에서는 이분 탐색을 아예 돌리지 않아야 합니다 — 코드와 무관한 실패에 커밋을 뒤지는 것은 시간 낭비입니다.