Operations
Git + 릴리즈 원칙
Git, commit, push gate, and release rules for Codaro changes.
Git 및 릴리즈 원칙
CLAUDE.md의 게이트가 SSOT다. 이 문서는 그 게이트를 풀어 쓴 운영 절차다.
AI 흔적 금지
- 커밋, 주석, 문서 어디에도 AI 생성 흔적 문구를 남기지 않는다.
- 커밋 메시지의 제목, 본문, trailer 어디에도
AI,GPT,OpenAI,ChatGPT,Codex,Claude,Anthropic,Generated by,Co-Authored-By,Assisted-by같은 도구명/생성 흔적/기여자 흔적을 남기지 않는다. Co-Authored-By,Assisted-by,Generated by류의 contributor trailer는 사람/도구 여부와 무관하게 이 저장소 커밋 메시지에서 사용하지 않는다..githooks/commit-msg가 커밋 메시지의 AI 도구명, generated-by 문구, contributor trailer를 차단한다.
브랜치
- 로컬 브랜치 생성 금지. 모든 작업은 로컬
main에서 직접 수행하고, 새 로컬 브랜치 생성/전환/비-mainpush를 하지 않는다. - 저장소 Git hook 경로는
.githooks이며,reference-transaction과pre-push가refs/heads/main외 로컬 브랜치 ref 생성/갱신/푸시를 차단한다. (태그 ref는 허용한다.) git switch -c,git checkout -b,git branch <name>,git push origin HEAD:<branch>,gh pr checkout모두 금지.- 외부 contributor PR은 검토/코멘트/머지할 수 있다. 단 로컬 브랜치를 만들지 않는 GitHub 도구,
gh pr view/diff, detached/FETCH_HEAD 방식만 사용한다. - 직접 원격 반영은
main -> origin/main만. 기본 원격은https://github.com/eddmpython/codaro.git. GitHub 원격 작업은gh사용을 허용한다. isolation: "worktree"는 사용하지 않는다.
스테이징 규율 — 자기 변경만
작업 도중 떠 있던 다른 변경이 본 커밋에 끼면 이력이 한 의도로 안 읽히고 무관 회귀가 숨는다. 그래서 매 커밋:
- 세션 시작 스냅샷 — 작업 직전
git status --short. 이미 떠 있던 staged/unstaged/untracked는 본 세션 작업 아님으로 분류한다. - 본인 변경만 명시 add —
git add <만진 파일>로 경로를 직접 적는다.git add -A·git add .금지. 디렉터리 단위 add는 본 세션이 그 디렉터리 전체를 작성/이동한 경우만. - staging 격리 점검 —
git commit직전git diff --cached --name-only가 본인 add 목록과 정확히 일치하는지 확인. 다르면git restore --staged <파일>로 분리. - commit 후 사후 점검 —
git show --stat HEAD로 실제 들어간 파일 확인. pre-commit hook이 무관 파일을 자동 re-stage했으면 push 전git reset --soft HEAD~1+git restore --staged로 되돌리고 재커밋.
- 한 커밋 = 한 의도. 기능 추가 + 무관 리팩토링 + 자동 생성물을 섞지 않는다. 자동 생성물(landing
generated/등)은 별도 "정리: 생성물 동기화" 커밋으로 분리한다. - 새 기능·버그픽스에는 그 동작을 잠그는 테스트가 같은 커밋 묶음에 동행한다. 의도적 미테스트면 메시지에 사유를 남긴다.
커밋 메시지
- 반드시 한글. 변경 성격 + 실제로 무엇을 바꿨는지 함께 담는다.
- 주체 중립 또는 사용자 주어. 1인칭 자기참조 금지.
Push 게이트 (위에서 아래로 첫 매치)
- force-push / tag-force / rewrite-push — 사용자가 "force 해" · "tag 옮겨" · "rewrite push" 같은 단어로 직접 명시한 경우만. 자체 판단 force 금지. main 직접 force push 요청 시 위험을 먼저 경고한 뒤
--force-with-lease로 진행. - push 거부/보류 트리거 — 발화에 "푸시 하지마" · "push 보류" · "올리지마" · "로컬에만 두자"가 있으면 사이클 끝에도 push 안 함. "커밋 N개 정리 완료, push 보류 중" 한 줄 보고.
- 그 외 일반 작업 (bugfix · feature · refactor · loop · 위임 task) — 사이클 끝마다 자동 push. 사이클 =
tests/run.py preflight(또는 목표 지정 gate) 통과 + 본 세션 변경 commit 완료 시점. "사이클 N push 완료" 한 줄 보고. 커밋마다 자동 push 금지.
과거 "push는 사용자 명시 지시 때만" default는 폐기되었다.
릴리즈 발동 게이트 — 두 관문 (AND)
릴리즈(버전 bump + 태그 + GitHub Release 게시)는 push와 다른 무거운 작업이다. push는 Push 게이트대로 사이클마다 자동이지만, 릴리즈는 두 관문을 모두 통과할 때만 발동한다. 마구잡이 릴리즈 금지 — 마구잡이 릴리즈란 (a) 사용자 요청 없이 내는 릴리즈, (b) 더러운 상태에서 내는 릴리즈 둘 다를 말한다.
- 관문 1 — 사용자 요청. 사용자가 "어느 정도 안정화됐다"고 판단하고 릴리즈를 명시적으로 요청해야 한다. 이 요청 없이는 어떤 경우에도 릴리즈하지 않는다. preflight가 green이고 커밋이 깔끔해도 자동으로 릴리즈하지 않는다 — 그건 push 대상이지 릴리즈 대상이 아니다. (릴리즈를 먼저 제안·재촉하지 않는다.)
- 관문 2 — 청결 판정(Claude). 사용자 요청이 들어오면, Claude가 아래 깨끗함/더러움 기준을 전수 점검한다. 전부 깨끗하면 추가 질문 없이 자동으로 릴리즈 절차를 끝까지 실행한다(아래 릴리즈 절차). 하나라도 더러우면 릴리즈를 멈추고, 무엇이 어떻게 더러운지 항목별로 보고한 뒤 정공법으로 청소하고(우회·임시 hack 금지), 청소 후 다시 청결 판정을 한다. 청소할 수 없는 더러움이 남으면 릴리즈하지 않고 사용자에게 그 사실을 보고한다.
즉 릴리즈 = 사용자 요청 AND 청결. 사용자 요청이 청결 판정을 면제하지 않는다 — 요청이 있어도 더러우면 청소가 먼저다.
깨끗함/더러움 기준
아래 9개를 모두 충족해야 "깨끗함"이다. 하나라도 위반하면 "더러움"이고, 그 자체로 릴리즈 중단 사유다.
- 게이트 green —
uv run python -X utf8 tests/run.py preflight의 전 게이트 통과 + 릴리즈 표면 게이트(launcher-test,install-launcher-smoke등). 빨간 게이트·스킵된 게이트는 곧 더러움이다. - 빌드 산출물 동기화 — 소스를 바꿨으면 에디터
src/codaro/webBuild/**와 landinggenerated/**가 현재 소스로 재빌드된 상태여야 한다. 소스는 바뀌었는데 산출물은 옛것인 stale 빌드는 더러움이다. - 작업 트리·이력 정합 —
git status클린(미커밋 변경 0),origin/main과 동기화,root-clean게이트 통과. 루트/작업 트리에 임시·스크래치·로그·preview 산출물 없음. 켜져 있지만 미완성인(half-done) 코드 경로가 릴리즈 범위에 없음. - 임시·디버그 잔재 0 — 이번 릴리즈에 들어가는 변경에
TODO/FIXME/XXX, 디버그print/console.log, 주석 처리된 죽은 코드, 임시 hack·우회·fallback 잔재가 없다. - 버전·CHANGELOG·락 정합 — 제품 릴리즈 버전은 **무조건
0.0.x**다.pyproject.tomlversion == 태그(v0.0.x) == (런처를 건드렸으면)launcher/codaro-launcher/Cargo.toml, 그리고uv.lock·launcher/Cargo.lock이 그 버전으로 동기화됨. CHANGELOG에 그 버전 섹션이 실제 파일 단위 변경을 담아 작성됨(문구 나열·placeholder 금지). 다운로드 자산명 계약(다운로드 링크 계약) 유지.0.1.0,0.2.0,1.0.0처럼 minor/major를 올린 버전은 그 자체로 더러움이다. - AI 흔적 0 — 커밋·주석·문서·코드 본문 어디에도 금지 문구(
AI/GPT/Claude/Generated by/Co-Authored-By등) 없음. - 핵심 경로 무회귀 — 직전 릴리즈 대비 사용자 핵심 경로(런처 더블클릭 → 네이티브 창 → 백엔드 프로비저닝 → 에디터/커리큘럼 로드)가 깨지지 않음. 알려진 깨진 기능을 릴리즈에 싣지 않는다.
- 이번 변경 도입 버그 0 — 이번 릴리즈 범위의 변경이 새로 도입한 알려진 크래시·버그가 미해결로 남아있지 않다.
- 발행 아티팩트 cross-env 수신 검증 — 게시(또는 draft →
--draft=false승격) 직후, 릴리즈의release-manifest.json이 가리키는 모든 자산을 빌드 환경이 아닌 별도 환경에서 실제로 끝까지 받아 (a) HTTP 200, (b) 실측 바이트 == Content-Length, (c) 실측 sha256 == manifest sha256, (d) ranged GET 206 지원을 확인했다. 검증은uv run python -X utf8 docs/skills/ops/tools/verifyPublishedRelease.py <manifest-url> --strict(또는published-release-smoke게이트 /release-smoke워크플로)로 수행한다. 200만 확인하고 본문 수신·무결성·타임아웃 내성을 안 본 검증은 더러움이다 — "내 로컬은 되는데 사용자 환경은 터진다"는 정확히 이 빈틈에서 난다.
판정 결과는 한 줄로 보고한다 — 깨끗하면 "청결 판정 9/9 통과 → 릴리즈 진행", 더러우면 "릴리즈 중단: {위반 항목} 더러움 → 청소 후 재판정".
릴리즈 절차
릴리즈는 항상 규칙을 점검하고, 릴리즈마다 릴리즈 메시지(CHANGELOG 섹션)를 작성한다. GitHub Releases가 제품 launcher의 control plane이고, PyPI는 같은 버전의 Python developer install 채널이다. 둘 다 릴리즈 시점의 같은 pyproject.toml version에서 나온다.
- 버전 결정 — 제품 릴리즈 버전은 무조건
0.0.x라인이다. 릴리즈 때는 끝자리x만 1씩 올린다.0.1.0,0.2.0,1.0.0처럼 minor/major를 올리는 버전은 금지한다.pyproject.toml의version이 릴리즈 버전이며(uv.lock동기화), 런처는launcher/codaro-launcher/Cargo.toml의version이다. 릴리즈 워크플로우는 태그가v{pyproject.version}과 일치하는지 검증하므로 태그도 반드시v0.0.x여야 한다. - CHANGELOG.md 섹션 작성 — Keep a Changelog 형식.
## {version} - {date}헤딩 + 한 줄 요약 +### Added/Changed/Fixed/Removed+### Verification(실행한 게이트/검증). 파일 단위로 무엇을 바꿨는지 구체적으로 적는다. 문구 나열이 아니라 실제 변경을 적는다. - 게이트 통과 —
tests/run.py preflight+ 릴리즈 표면 게이트(install-launcher-smoke,launcher-test등) 통과. - 태그 + 푸시 —
git tag vX.Y.Z && git push origin vX.Y.Z. 이 태그가.github/workflows/product-release.yml을 트리거하고, GitHub Release가 published 되면.github/workflows/publish.yaml이 PyPI Trusted Publisher 업로드를 수행한다. - 릴리즈 본문 = CHANGELOG 섹션 — 워크플로우가
docs/skills/ops/tools/extractChangelogSection.py --version {pyproject.version}로 해당 섹션을 뽑아 GitHub Release 본문으로 쓴다. CHANGELOG에 그 버전 섹션이 없으면 추출이 비-0으로 실패해 릴리즈가 막힌다 — "릴리즈 메시지 항상 작성"을 기계적으로 강제한다. GitHub 자동 생성 릴리즈 노트(generate_release_notes)는 켜지 않는다. 자동 "Full Changelog" 비교 링크가 잘못된 태그(v0.2.x등)를 이전 릴리즈로 잡을 수 있으므로, 비교 링크를 넣어야 한다면 직전0.0.x릴리즈에서 현재0.0.x릴리즈로 직접 지정한다. - 다운로드 검증 — 릴리즈 완료 후 문서의 모든 다운로드 URL이 200으로 떨어지는지 확인하고(아래 다운로드 링크 계약), 실제 런처 exe를 내려받아 사용자 경로(프로비저닝 → 네이티브 창 → 백엔드)가 동작하는지 확인한다.
- PyPI 배포는 GitHub Actions Trusted Publisher 기준을 유지한다. long-lived PyPI API token을 저장하지 않고, PyPI publisher 설정은 project
codaro, ownereddmpython, repositorycodaro, workflowpublish.yaml(또는 PyPI에publish.yml로 등록된 경우.github/workflows/publish.ymlalias)와 workflow 파일의 실제 값이 한 글자까지 일치해야 한다. PyPI publisher의 environment를 비워두면 workflow publish job에도environment:를 선언하지 않는다. PyPI 쪽 environment를 채우는 경우에만 workflow의environment:도 같은 값으로 맞춘다. - 릴리즈 자산: 런처 exe(고정 파일명) + sha256 + SPDX SBOM,
python-runtime-win-x64.zip(백엔드 의존성 사전 설치) + sha256, exactcodarowheel + sha256,release-manifest.json(wheel/runtime를 핀). 자산 파일명 규칙은 아래 다운로드 링크 계약을 따른다. - PyPI는 launcher 설치 경로가 아니다. launcher는 PyPI latest를 resolve하지 않고
release-manifest.json의 exact wheel URL과 sha256만 신뢰한다.
다운로드 링크 계약 — 항상 최신 릴리즈
README·랜딩·문서의 모든 "다운로드" 버튼/링크는 이 형태만 쓴다:
https://github.com/eddmpython/codaro/releases/latest/download/<자산명>
releases/latest는 GitHub가 draft/prerelease가 아닌 가장 최근 게시 릴리즈로 자동 해석한다. 따라서 이 URL은 언제나 최신 릴리즈의 자산을 받는다 — 새 버전을 낼 때마다 버튼을 손댈 필요가 없다.- 단, 끝의
<자산명>이 그 최신 릴리즈에 실제로 존재하는 정확한 파일명이어야 한다. 없으면 404 → GitHub가 릴리즈 페이지/오류로 리다이렉트해 "엉뚱한 데로 간다".
자산 파일명은 고정한다
"항상 최신 다운로드"가 깨지는 유일한 경로는 자산 파일명 변경이다. 그러므로:
- 런처 자산명은 한 번 정하면 릴리즈 전반에서 불변으로 유지한다. 게시된 자산명은
gh release view --json assets로 확인한다. - 파일명을 바꿔야 하면 순서를 지킨다: (1) 워크플로우가 새 이름으로 자산을 빌드/업로드 → (2) 그 릴리즈를 게시 → (3) 게시 확인 후에야 README·랜딩·문서 링크를 새 이름으로 갱신. 미게시 이름을 먼저 가리키면 즉시 404다. 순서를 뒤집지 않는다.
게시 후 다운로드 검증 (필수)
릴리즈 게시 직후, 두 층으로 확인한다.
(1) 링크 계약 — 자산명/200. 문서에 적힌 모든 다운로드 URL이 실제로 200으로 떨어지는지:
curl -sI -L -o /dev/null -w "%{http_code} %{url_effective}\n" \
"https://github.com/eddmpython/codaro/releases/latest/download/<자산명>"
200 + content-disposition: attachment; filename=<자산명>이면 통과. 404면 자산명이 릴리즈와 어긋난 것이니 링크를 게시 자산명으로 맞춘다.
(2) cross-env 수신 — 본문/무결성 (필수, 위 헤더 확인만으론 부족). curl -sI는 헤더만 본다 — 본문이 느려서 끊기거나 sha가 어긋나도 200은 떨어진다. 그래서 release-manifest.json이 가리키는 모든 자산을 끝까지 받아 무결성까지 본다:
uv run python -X utf8 docs/skills/ops/tools/verifyPublishedRelease.py \
"https://github.com/eddmpython/codaro/releases/latest/download/release-manifest.json" --strict
각 자산에 대해 200 + 실측 바이트 == Content-Length + 실측 sha256 == manifest sha256 + ranged 206을 확인한다. 게시 시 release-smoke 워크플로(release: published)가 이를 자동 실행하지만, 수동 릴리즈에서도 위 명령으로 직접 점검한다. 이 검증이 깨끗함/더러움 기준의 9번 항목이다.
사례 (2026-06-05)
런처에서 Failed to read response bytes for ... : error decoding response body: operation timed out — 사용자 환경에서 provision이 큰 zip 본문을 받다 멈췄다. 원인은 read_source_bytes http 경로의 timeout 미설정 + 메모리 통적재 + 재시도 0회였고, 자기 업데이트(self_update.rs)는 이미 스트리밍/timeout을 했는데 첫 설치 경로만 취약한 비대칭이었다. 모든 launcher 테스트가 file://만 써서 http 경로 커버리지가 0이라 어느 게이트도 못 잡았다. 근본 해결: download.rs(분절 timeout + resume + 재시도) + 다운로드 mock 테스트의 3-OS 매트릭스 + 본 cross-env 수신 게이트(verifyPublishedRelease.py)를 도입하고, 청결 기준에 9번을 추가했다.
사례 (2026-05-31)
README가 Codaro.exe(런처 자산을 그 이름으로 바꾸려던 다음 릴리즈용)를 가리켰으나, 게시된 최신 릴리즈는 v0.0.5이고 런처 자산명은 CodaroLauncher.exe였다. 미게시 이름이라 모든 다운로드 링크가 404 → 엉뚱한 페이지로 빠졌다. 링크를 게시 자산 CodaroLauncher.exe로 되돌려 즉시 복구했다. 근본 해결은 런처를 새 이름으로 빌드/업로드하는 릴리즈를 먼저 게시한 뒤 링크를 그 이름으로 옮기는 것이다.