TECH NOTE · AUTOMATION OPS
표시가 있는지만 확인하면 3주 전 숫자가 오늘의 배포 근거가 됩니다. 커밋 해시와 시각을 강제해 낡은 증거를 막는 방법입니다.
지난주 테스트 결과로 오늘 배포를 승인하고 있었다
릴리스 체크리스트에 검증 결과를 적어 두는 방식을 쓰고 있었다. 테스트 몇 개 통과, 번들 크기 얼마, 이런 것들이다.
사전검증 스크립트는 그 문서에 표시가 있는지만 봤다.
// 처음 버전 — 마커가 있으면 통과
if (!text.includes("<!-- release-evidence:final-tree:start -->")) {
throw new Error("release evidence section missing");
}
그래서 이런 일이 생겼다. 코드를 고치고, 테스트를 추가하고, 번들 구성을 바꿨다. 체크리스트는 안 고쳤다. 사전검증은 통과했다.
문서에는 3주 전 숫자가 그대로 있었고, 그게 오늘의 근거로 쓰였다.
증거에는 두 가지가 필요하다
무엇이 문제였는지 정리하면 이렇다.
증거가 증거이려면 무엇을 확인했는지와 어느 시점의 것인지가 같이 있어야 한다. 앞만 있으면 오래된 사실이고, 뒤만 있으면 아무 내용이 없다.
체크리스트에는 앞만 있었다. “테스트 112개 통과”는 사실이지만, 어느 커밋의 112개인지가 없다.
그래서 형식을 정하고 그 형식을 강제했다.
if (!/^sourceCommit: [0-9a-f]{7,40}$/m.test(section)) {
throw new Error("final-tree evidence requires a hexadecimal sourceCommit");
}
if (!/^verifiedAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+09:00$/m.test(section)) {
throw new Error("final-tree evidence requires a +09:00 verifiedAt timestamp");
}
커밋 해시와 시각을 안 적으면 사전검증이 실패한다. 이제 문서를 안 고치면 배포를 못 한다.
시간대까지 고정한 건 의도다. +09:00을 강제하지 않으면 어떤 날은 UTC로, 어떤 날은 로컬로 적혀서 나중에 순서를 비교할 수 없다.
항목마다 통과를 요구한다
시각만으로는 부족했다. 항목 자체가 빠져 있어도 통과했기 때문이다.
필수 항목을 나열하고 전부 pass인지 본다.
for (const key of [
"rust",
"frontend",
"windowsTarget",
"macBundleSmoke",
"ollamaDirect",
"ollamaSummary",
"codeSecurityReview",
]) {
if (!new RegExp(`^${key}: pass$`, "m").test(section)) {
throw new Error(`final-tree evidence requires ${key}: pass`);
}
}
여기까지는 자동으로 돌릴 수 있는 것들이다. 안 통과하면 배포하면 안 되는 게 명확하다.
문제는 자동으로 못 하는 항목이다.
못 한 것을 통과로 적지 않게 한다
서명, 실기기 확인, 외부 감사 같은 것들은 정당하게 막힐 수 있다. 개발자 계정이 없거나, 그 기계가 없거나, 네트워크가 막혀 있다.
이걸 pass로 적으면 거짓이다. 빈칸으로 두면 사전검증이 못 잡는다.
그래서 두 값 중 하나를 요구한다.
for (const key of ["npmAudit", "sttLive", "macManual", "windowsManual", "signing"]) {
if (!new RegExp(`^${key}: (?:pass|blocked)$`, "m").test(section)) {
throw new Error(`final-tree evidence requires ${key}: pass|blocked`);
}
}
blocked라고 적으면 통과한다. 다만 그 글자가 문서에 남는다.
막힌 것을 숨기지 않고 드러내는 게 목적이다. 배포 결정을 내리는 사람이 “서명은 안 됐다”를 알고 결정하는 것과, 모르고 결정하는 것은 완전히 다르다.
커밋 메시지에도 그 판단을 남겼다.
Constraint: Real-host, signing, STT, and online-audit gates may be
legitimately blocked and must remain explicit instead of being guessed
as passes.
옛 숫자를 이름으로 막는다
마지막 장치가 좀 거칠다. 이미 낡은 것으로 확인된 값을 직접 나열해서 금지했다.
for (const stale of [
"hermes_pet: 112 passed",
"pet-core: 215 passed",
"bundleSizeBytes: 19824640",
]) {
if (text.includes(stale)) {
throw new Error(`desktop release checklist still contains superseded evidence: ${stale}`);
}
}
우아하지 않다. 값이 바뀔 때마다 이 목록도 고쳐야 한다.
그래도 넣은 이유가 있다. 형식 검사는 형식만 본다. 커밋 해시를 새로 적고 시각을 갱신하면서, 정작 테스트 개수는 복사해 온 옛 숫자를 그대로 둘 수 있다. 형식 검사로는 못 잡는다.
한 번 실제로 그렇게 통과한 값들이라 이름으로 막아 뒀다. “이 값이 다시 보이면 누군가 복사한 것” 이라는 신호다.
일반적인 해법은 아니다. 테스트 개수를 사람이 적는 대신 스크립트가 실행해서 채우게 하면 이 문제 자체가 없어진다. 다만 그러면 아무도 그 숫자를 안 본다는 다른 문제가 생긴다. 여기서는 사람이 적되 옛 값 재사용만 막는 쪽을 골랐다.
그래서 무엇이 달라졌나
이 장치들의 효과는 하나로 요약된다. 코드를 고치면 문서를 고쳐야 배포가 된다.
이전에는 문서 갱신이 성의의 문제였다. 이제는 절차의 문제다. 잊으면 사전검증이 막는다.
커밋 메시지의 지시문이 그걸 말한다.
Directive: Update the final-tree evidence block after every source
change that invalidates release evidence.
어디에 쓸 만한가
같은 구조가 필요한 곳이 있다.
| 상황 | 낡을 수 있는 증거 |
|---|---|
| 배포 승인 | 테스트 결과, 성능 수치, 보안 검토 |
| 아키텍처 결정 기록 | 그때의 제약, 벤치마크 |
| 규제 대응 문서 | 감사 일자, 대상 버전 |
| README의 사용법 | 예제 출력, 버전 |
공통점은 문서가 코드보다 느리게 바뀐다는 것이다. 사람이 성실하기를 기대하는 대신, 안 고치면 다음 단계가 막히게 만든다.
가장 단순한 형태는 문서에 커밋 해시를 적고, 그 해시가 현재 HEAD와 다르면 경고하는 것이다. 그것만으로도 “언제 확인한 건지 모르는 문서”는 사라진다.
정리
- 증거에는 무엇을 확인했는지와 어느 시점인지가 같이 있어야 한다
- 표시가 있는지만 보면 3주 전 숫자가 오늘의 근거로 쓰인다
- 커밋 해시와 시각을 형식까지 정해 강제한다. 시간대를 고정해야 순서 비교가 된다
- 자동 항목은
pass를 요구하고, 외부 요인으로 막히는 항목은blocked를 명시하게 한다 - 못 한 것을 통과로 위장하지 않는 게 목적이다
- 형식 검사는 형식만 본다. 복사된 옛 값은 따로 막아야 한다
- 핵심은 코드를 고치면 문서를 고쳐야 배포가 되게 만드는 것
문서가 낡는 걸 성실함으로 막으려 하면 실패한다. 절차로 막아야 한다.