Skip to content

릴리스 ​

변경이 커밋에서 CHANGELOG를 거쳐 릴리스가 되기까지의 과정이에요. 무엇이 깨는 변경인지는 버전 정책, 커밋 형식은 기여 안내에 있어요.

한눈에 보기 ​

커밋 (Conventional Commits) ──▶ PR: scripts/check.sh가 커밋 형식과 CHANGELOG [Unreleased]를 확인
                                     │
                                     ▼  main에 병합
릴리스 PR: node scripts/release.mjs bump <단계>  ──▶ 버전을 모든 곳에서 한 번에 바꾸고
                                                    [Unreleased]를 ## [X.Y.Z] - 날짜로 옮김
                                     │
                                     ▼  병합 후, 관리자가 태그 vX.Y.Z (release.mjs check --tag가 태그와 버전을 대조)
태그 push: .github/workflows/release.yml ──▶ macOS·Linux 묶음을 빌드·스모크 테스트하고, 그것들과 SHA256SUMS를
                                           GitHub Release에 붙임 (릴리스는 CHANGELOG로 만듦)
  • 변경 기록의 원천은 커밋이에요. 모든 커밋은 Conventional Commits 형식이고(scripts/check.sh와 .githooks/commit-msg가 확인), 그 타입으로 릴리스 단계가 정해져요.
  • CHANGELOG.md는 사람이 읽는 글이에요. Keep a Changelog 형식으로, 사용자에게 보이는 변경(feat, fix, perf, revert, 깨는 변경)은 같은 PR에서 ## [Unreleased] 아래에 한 줄씩 적어요. node scripts/release.mjs notes가 커밋에서 초안을 만들어 줘요(git-cliff, 설정은 cliff.toml).
  • CHANGELOG 항목은 문서처럼 영어로 쓰고, 같은 항목을 한국어 사본 Changelog에도 적어요. git-cliff 초안은 커밋 메시지에서 나오니, 그걸 읽고 사용자에게 맞는 문장으로 다시 써요. 섹션 이름(Added, Changed ...)과 코드·식별자는 두 파일 모두 그대로예요.
  • 문장은 사람이, 구조는 기계가 맞춰요. PR마다 scripts/check.sh(check-commits.mjs --changelog)가 [Unreleased]에 새로 생긴 항목의 섹션을 git-cliff 초안의 섹션과 대조해요(아래 반자동인 이유).
  • 공개 인터페이스는 기계로도 확인해요. api/c-abi.txt(libzeropty_c.dylib의 zeropty_* 심볼과 zeropty.h 선언)와 api/node.txt(Node 모듈 내보내기)가 기준선이에요. scripts/check.sh가 빌드 결과와 비교하고(node scripts/check-api.mjs), PR에서 기준선의 줄이 사라지면 깨는 변경 표시(!·BREAKING CHANGE:)와 **BREAKING** 항목을 요구해요(버전 정책).
  • 이 저장소의 도구는 커밋, 태그, push, 배포를 하지 않아요. 파일만 바꾸고, 나머지는 사람이 확인하고 해요.

반자동인 이유 ​

CHANGELOG를 git-cliff 출력 그대로 쓰지 않고, 초안을 사람이 사용자에게 맞게 다시 써요. 일부러 고른 방식이에요.

  • 독자가 달라요. 커밋 메시지는 코드를 고치는 사람에게 쓰는 한 줄이고, CHANGELOG는 zeropty를 쓰는 사람에게 쓰는 문서예요. 커밋 제목 나열로는 "무엇을 바꿔야 하는지"가 빠져요.
  • 여러 커밋이 한 항목이 돼요. 기능 하나가 feat 커밋 여러 개로 나뉘거나, 아직 릴리스하지 않은 기능을 같은 주기에 fix로 고치면, 초안에는 여러 줄이지만 사용자에게는 한 줄이에요.
  • 대신 빠뜨리거나 엉뚱한 곳에 적는 실수는 scripts/check.sh가 막아요. check-commits.mjs --changelog는 PR의 커밋을 cliff.toml과 같은 규칙(scripts/lib/conventional.mjs의 sectionsFor)으로 분류하고, [Unreleased]에서 새로 생기거나 바뀐 항목이 그 섹션에 있는지 봐요.
커밋git-cliff 초안의 섹션검사가 요구하는 새 항목
featAdded### Added 아래
fixFixed### Fixed 아래
fix(security)Security### Security 아래
perf, revertChanged### Changed 아래
깨는 변경(!, BREAKING CHANGE:)Changed### Changed나 ### Removed 아래 - **BREAKING** ...
refactor, docs, test, build, ci, chore, style(빠짐)없음
아무 type + Changelog: skip 꼬리말 (깨는 변경 제외)(빠짐)없음
  • [Unreleased]의 제목은 Added, Changed, Deprecated, Removed, Fixed, Security만, 이 순서로, 한 번씩 쓸 수 있어요. 항목은 모두 ### 제목 아래에 있어야 해요. ZEROPTY_SKIP_CHANGELOG=1이어도 항상 확인해요.
  • 문장은 비교하지 않아요. 항목은 초안을 옮기지 않고 다시 쓰니, 섹션만 맞으면 통과예요.
  • 두 분류 규칙(cliff.toml과 sectionsFor)이 어긋나면 scripts/test/cliff.test.mjs가 실패해요. 실제 git-cliff로 커밋을 렌더링해서 각 줄의 섹션을 sectionsFor와 비교해요.
  • 아직 릴리스하지 않은 기능([Unreleased]에 있는 기능)을 고친 fix처럼 사용자에게 새 소식이 아닌 커밋은 커밋 메시지 끝에 Changelog: skip 꼬리말을 달아요. 그 커밋만 검사와 git-cliff 초안에서 빠지고, 같은 PR의 다른 커밋은 그대로 검사돼요. 필요하면 기존 항목의 문장을 고쳐요. 깨는 변경에는 쓸 수 없어요(커밋 검사가 거부해요).
  • PR 전체가 사용자에게 보이지 않는데 type이 feat/fix라면 PR 설명에 그렇게 적고, 관리자가 ZEROPTY_SKIP_CHANGELOG=1 scripts/check.sh로 항목 검사를 건너뛰어요. 이때도 커밋 형식 검사는 해요.

버전이 적힌 곳 ​

release.mjs bump가 아래를 한 번에 바꾸고, tests/version.rs·bindings/c 단위 테스트·release.mjs check가 서로 같은지 확인해요.

파일내용
Cargo.toml[workspace.package] version. 세 크레이트가 version.workspace = true로 물려받아요
Cargo.lockzeropty, zeropty-c, zeropty-node 항목
bindings/c/include/zeropty.hZEROPTY_VERSION_MAJOR·MINOR·PATCH·PRERELEASE·IS_PRERELEASE·STRING (NUMBER는 계산돼요)
client/zeropty.mjs, client/embedded.mjsexport const VERSION
CHANGELOG.md## [X.Y.Z] - YYYY-MM-DD와 맨 아래 비교 링크

zeropty --version, 서버의 #{version}, zeropty_version(), Node의 version()은 빌드할 때 Cargo 버전에서 나와서 따로 고칠 곳이 없어요.

bump는 한국어 사본 Changelog를 건드리지 않아요. 릴리스 섹션을 다듬을 때(아래 4단계) 같은 제목과 항목을 그곳에도 손으로 옮겨요.

CHANGELOG 맨 아래 링크는 있는 태그만 가리켜요. 이전 버전의 태그가 있으면 compare/vA...vB, 이전 버전에 태그가 없으면 [Unreleased] 링크가 시작하는 커밋에서 시작해요. 없는 태그를 가리키는 링크가 생기면 node --test scripts/test가 실패해요. 단, 지금 릴리스하는 버전의 태그는 예외예요(릴리스 PR이 그 링크를 넣고, 태그는 머지 뒤에 달아요).

명령 ​

sh
node scripts/release.mjs check              # 모든 곳의 버전이 같은지 (scripts/check.sh도 실행)
node scripts/release.mjs check --tag v0.2.0 # 그리고 태그가 그 버전인지 (태그가 달린 HEAD에서 scripts/check.sh도 실행)
node scripts/release.mjs next               # 마지막 태그 이후 커밋으로 계산한 다음 버전
node scripts/release.mjs notes              # [Unreleased] 초안 (git-cliff, 없으면 npx로 받아요)
node scripts/release.mjs release-notes 0.2.0 # GitHub Release 본문 (CHANGELOG.md의 ## [0.2.0]으로 만들어요)
node scripts/release.mjs bump minor --dry-run
node scripts/release.mjs bump <major|minor|patch|auto|X.Y.Z[-pre]> [--date YYYY-MM-DD] [--generate]
node scripts/check-commits.mjs --range origin/main..HEAD --changelog   # scripts/check.sh의 커밋·변경 기록 검사만 (섹션 대조 포함)
scripts/smoke-version.sh                    # dist/의 CLI·서버·C·Node 버전이 같은지 (scripts/check.sh의 version smoke tests 단계)
scripts/package.sh                          # dist/release/에 릴리스 묶음과 SHA256SUMS를 만들고 푼 상태로 스모크 테스트 (rustup target add aarch64-apple-darwin x86_64-apple-darwin 필요)
rustup run 1.88.0 cargo check --workspace --all-targets --locked   # MSRV (scripts/check.sh의 MSRV 단계)
node scripts/check-api.mjs                  # dist/(./build.sh)의 C ABI·Node 내보내기가 api/*.txt와 같은지
node scripts/check-api.mjs --base origin/main   # 기준선에서 사라진 줄이 깨는 변경으로 표시됐는지 (PR 검사)
node scripts/check-api.mjs --update         # 인터페이스를 일부러 바꿨을 때 기준선 갱신 (같은 커밋에)
node --test scripts/test/*.test.mjs         # 도구 자체의 테스트 (git-cliff 렌더링 포함)
  • bump auto는 next가 계산한 단계를 써요. 1.0 이전에는 깨는 변경이 있으면 MINOR, 아니면 PATCH예요.
  • bump는 먼저 check가 통과하는지, 작업 트리가 깨끗한지 보고, 하나라도 실패하면 아무 파일도 바꾸지 않아요.
  • [Unreleased]가 비어 있으면 멈춰요. --generate를 주면 git-cliff 초안으로 채워요(그래도 읽고 고쳐 써요).
  • 버전을 바꾼 뒤 cargo metadata --locked로 Cargo.lock이 맞는지 확인해요.

릴리스 절차 ​

  1. main에서 scripts/check.sh가 통과하는지 확인해요. --quick 없이 모든 단계(MSRV, 예제, rustdoc 포함)가 통과해야 해요.

  2. 단계를 정해요.

    sh
    git switch main && git pull
    node scripts/release.mjs next

    계산 결과를 버전 정책의 규칙과 맞춰 봐요. MSRV를 올렸거나 build·refactor 커밋이 사용자에게 보이는 변화를 냈다면 계산에 안 잡히니 직접 단계를 골라요.

  3. 릴리스 브랜치에서 버전을 올려요.

    sh
    git switch -c release/0.2.0
    node scripts/release.mjs bump minor      # 또는 auto, 0.2.0, 0.2.0-rc.1
  4. CHANGELOG를 다듬어요. ## [0.2.0] 섹션을 사용자 입장에서 읽어요.

    • 같은 일을 한 줄로 묶고, 내부 사정은 빼요.
    • 깨는 변경은 **BREAKING**과 함께 무엇을 바꿔야 하는지 적어요.
    • 성능 수치를 적는다면 벤치마크에 측정된 값만 써요.
    • 같은 변경을 한국어 사본 Changelog에도 해요.
  5. 확인해요.

    sh
    cargo build --release --workspace && cargo test --release --workspace && ./build.sh
    dist/zeropty --version                   # zeropty 0.2.0
    node scripts/release.mjs check
  6. 커밋하고 PR을 올려요. 커밋 메시지는 chore(release): 0.2.0이에요(git-cliff가 이 커밋은 CHANGELOG에서 빼요). PR 제목도 같게 해요.

  7. 병합한 뒤 관리자가 태그를 만들어요.

    sh
    git switch main && git pull
    git tag -a v0.2.0 -m "zeropty 0.2.0"
    git push origin v0.2.0

    push하기 전에 node scripts/release.mjs check --tag v0.2.0으로 태그와 버전이 같은지 확인해요(태그가 달린 HEAD에서는 scripts/check.sh도 이 검사를 해요).

  8. 태그가 릴리스를 올려요. .github/workflows/release.yml(macOS runner, Linux 묶음은 Linux runner)이 태그와 버전을 대조하고 scripts/package.sh와 scripts/package-linux.sh를 돌려, 그 파일들을 릴리스에 붙여요.

    • zeropty-0.2.0-darwin-universal.tar.gz: 폴더 zeropty-0.2.0-darwin-universal/ 하나에 zeropty(arm64 + x86_64, lipo), shim.dylib(arm64, arm64e, x86_64), zeropty-tty, claude-zeropty, LICENSE-APACHE, LICENSE-MIT가 실행 권한 그대로 들어 있어요. zeropty와 shim.dylib는 ad-hoc 서명(codesign -s -)이에요. Developer ID 서명과 공증은 나중에 더할 수 있어요.
    • zeropty-0.2.0-linux-x86_64.tar.gz: 같은 이름의 폴더 하나에 zeropty, shim.so(LD_PRELOAD용), zeropty-tty, claude-zeropty, 라이선스가 있어요. package-linux job이 Debian 11(rust:1.88-bullseye)에서 scripts/package-linux.sh로 만들어서 glibc 2.29 이상이 필요해요. 스크립트가 파일마다 가장 새 glibc 심볼을 출력하고, 푼 묶음을 직접 돌려 확인해요.
    • SHA256SUMS: 모든 묶음의 SHA-256. 두 묶음이 다 만들어지면 publish job이 만들어요. 버전을 고정하는 쪽은 이 해시를 고정해요.

    태그에 릴리스가 아직 없으면 워크플로가 node scripts/release.mjs release-notes 0.2.0을 본문으로 만들어요(CHANGELOG 섹션을 항목당 한 줄로, 상대 링크는 태그 기준 절대 링크로). 프리릴리스 부분이 있는 태그(v0.2.0-rc.1)는 프리릴리스가 되고 "Latest"가 되지 않아요. 워크플로가 닿기 전(push 후 1분쯤)에 손으로 만든 릴리스는 본문을 그대로 둬요. Actions에서 실행 결과를 보고, 파일들을 받아 shasum -a 256 -c SHA256SUMS를 돌려 확인해요. 두 바이너리는 macOS 11 이상에서 로드돼요(package.sh가 슬라이스마다 최소 버전을 확인해요).

    • 워크플로는 릴리스에 이미 있는 파일을 바꾸지 않아요. 다시 빌드하면 바이트가 같지 않아서, 해시가 바뀌면 이전 해시를 고정한 쪽이 깨지기 때문이에요. 일부러 바꾸려면 릴리스에서 파일을 지운 뒤 다시 돌려요.
    • 다시 돌리면 태그가 가리키는 커밋을 그대로 다시 빌드해요. 그래서 저장소 밖의 실패(runner, 네트워크)에만 도움이 되고, 묶음 쪽을 고친 내용은 새 커밋과 다음 PATCH 릴리스로 내보내야 해요.
    • 임베딩 라이브러리와 bash builtin은 묶음에 없고 ./build.sh로 만들어요.

0.1.0은 첫 공개 커밋(c612089)이고 v0.1.0 태그가 달려 있어요. 0.1.1이 이 도구로 만든 첫 릴리스예요. next·notes는 마지막 태그부터 계산해요.

포맷 ​

워크스페이스 전체가 rustfmt 형식이에요(설정은 rustfmt.toml). 진행 중인 브랜치가 main에 다 들어간 뒤 style: cargo fmt --all 커밋(e12f84f) 하나로 한 번에 포맷했어요.

  • scripts/check.sh의 rustfmt 단계가 cargo fmt --all --check로 트리 전체를 확인해요. 커밋 전에 cargo fmt --all을 돌려요.
  • 포맷 커밋 앞에서 갈라진 오래된 브랜치는 충돌한 파일에서 자기 쪽을 고르고 cargo fmt --all을 다시 돌리면 돼요. 포맷은 기계적이라 결과가 같아요.
  • git blame에서 포맷 커밋을 건너뛰려면 git blame --ignore-rev e12f84f <파일>로 봐요.

프리릴리스와 패치 릴리스 ​

  • 프리릴리스: node scripts/release.mjs bump 0.2.0-rc.1. 태그는 v0.2.0-rc.1. 정식 릴리스는 bump patch(0.2.0-rc.1 → 0.2.0)나 bump 0.2.0이에요. 헤더의 ZEROPTY_VERSION_PATCH와 ZEROPTY_VERSION_NUMBER는 숫자만 담아서 0.2.0-rc.1과 0.2.0이 같아요. 둘은 ZEROPTY_VERSION_PRERELEASE("rc.1", 정식은 "")와 ZEROPTY_VERSION_IS_PRERELEASE(#if로 쓸 수 있는 1/0), ZEROPTY_VERSION_STRING("0.2.0-rc.1")으로 구별해요.
  • 이전 버전 수정(backport): 태그에서 브랜치를 만들어요(git switch -c release/0.1.x v0.1.0). 수정 커밋을 cherry-pick하고, [Unreleased]에 적고, bump patch로 같은 절차를 따라요. main의 CHANGELOG에도 그 버전 섹션을 옮겨 적어요.

문제가 생기면 ​

증상해결
tests/version.rs 실패버전을 손으로 고쳤어요. node scripts/release.mjs check로 어긋난 곳을 보고, bump로 다시 맞춰요
scripts/check.sh의 commit messages 단계: 커밋 형식git rebase -i로 메시지를 고쳐요(reword). fixup 커밋은 --autosquash로 합쳐요
scripts/check.sh의 commit messages 단계: changelog## [Unreleased]에 줄을 더해요. 사용자에게 안 보이는 변경이면 ZEROPTY_SKIP_CHANGELOG=1 scripts/check.sh로 항목 검사를 건너뛰어요
needs an entry under ### Fixed for: ...항목을 다른 섹션에 적었어요. 커밋 type에 맞는 섹션으로 옮겨요(반자동인 이유의 표). node scripts/release.mjs notes가 git-cliff의 분류를 보여줘요
[Unreleased]: "### Fixes" is not one of ...제목은 Keep a Changelog의 여섯 개만, Added → Security 순서로 써요
깨는 변경인데 **BREAKING** 줄이 없다고 실패[Unreleased]에 - **BREAKING** ... 줄을 더해요
check-api.mjs: api/*.txt does not match the build인터페이스를 일부러 바꿨다면 node scripts/check-api.mjs --update 후 api/를 같이 커밋해요. 아니라면 바뀐 선언·내보내기를 되돌려요
check-api.mjs --base: public API removed or changed기준선에서 줄이 사라졌어요(함수 제거, 시그니처 변경). 의도했다면 커밋에 !나 BREAKING CHANGE:, [Unreleased]에 - **BREAKING** ...을 더하고, 아니라면 새 이름으로 더하는 방식으로 바꿔요
scripts/check.sh의 rustfmt 단계 실패cargo fmt --all로 포맷하고 커밋해요(포맷)
scripts/check.sh의 MSRV 단계: Rust 1.88.0 is not installedrustup toolchain install 1.88.0
bump가 nothing under ## [Unreleased]로 멈춤항목을 쓰거나 --generate를 줘요
태그를 잘못 만들었어요push 전이면 git tag -d vX.Y.Z. push한 뒤라면 지우지 말고 다음 PATCH를 내요

MIT 또는 Apache-2.0 라이선스로 배포해요.