ssk@tech:~$ cat bump-version.md

bump-version 스크립트 + VERSION 파일 설계

다중 플랫폼 (macOS / Android / iOS / 그 외) 으로 같은 앱을 배포할 때 semver 만 한 곳에서 관리하기 위한 패턴. 빌드 카운터는 플랫폼마다 독립.

연관 문서: macos-brew-deploy.md (이 패턴 위에 올라가는 macOS Homebrew 배포)


VERSION 단일 소스 (semver)

레포 루트에 한 줄짜리 VERSION 파일.

1.2.3
  • 형식: <semver> = X.Y.Z (SemVer)
  • 모든 플랫폼이 이 파일에서 semver 를 읽어 자기 manifest 에 반영
  • 직접 편집하지 말고 bump-version 스크립트만 사용

빌드 카운터는 VERSION 에 두지 않는다

빌드 카운터는 플랫폼별 매니페스트가 권위 소스:

플랫폼 빌드 카운터 위치
macOS Info.plistCFBundleVersion
iOS Info.plistCFBundleVersion
Android build.gradle(.kts)versionCode

이유:

  • 플랫폼 독립 release 가능. macOS 만 핫픽스 재배포해도 Android build 카운터를 건드릴 필요 없음.
  • 각 스토어 (App Store / Play Store / Homebrew) 가 요구하는 단조 증가 규약과 자연 부합. 한쪽 플랫폼만 build 가 앞서 가도 무관.
  • 공유 카운터로 묶어 두면 lockstep 강제 → 한 플랫폼 빌드 실패 / 잠시 보류 시에도 다른 쪽 카운터가 빈다.
  • 빌드 카운터가 매니페스트와 같은 파일에 있으니 stale state 가 구조적으로 없음.

Semver 만 단일 소스인 이유

빌드는 독립이어도 semver 는 “이 코드가 표현하는 사용자 가시 버전” 이라 모든 플랫폼이 같아야 한다. semver 가 어긋나면 같은 코드가 플랫폼별로 다른 “버전” 으로 출시되어 사용자 혼란.


핵심 원칙: 책임 분리

책임 담당
Semver 결정 (VERSION 파일 갱신) bump-version
Semver → 플랫폼 manifest 로 propagate release-<platform> (빌드 직전)
플랫폼 build 카운터 +1 release-<platform> (빌드 직전)
VERSION 변경 commit bump-version 자체
Manifest 변경 commit (semver + build 동시) release-<platform>
오케스트레이션 (mac+android 순차 실행) release

이 분리가 핵심인 이유:

  • bump-version 이 plist / gradle 까지 손대면 빌드 안 하는 플랫폼의 manifest 도 매번 흔들림
  • propagate 가 빌드 직전에 일어나므로 stale manifest 가 VERSION 과 어긋날 수 없음
  • 오케스트레이터가 git add -A 로 통째로 커밋하지 않으므로 의도하지 않은 파일이 묻어 들어가지 않음
  • build 카운터가 플랫폼 매니페스트 안에 있으므로 release-<platform> 이 자기 manifest 만 보면 됨 (cross-platform coupling 0)

모드

명령 동작
./bump-version 현재 semver 출력만 (no-op)
./bump-version 1.2.0 semver 명시적 지정
./bump-version --bump 마지막 semver 세그먼트 +1 (patch bump)
./bump-version -h / --help 헤더 주석 출력

이전 디자인의 --build-only 는 더 이상 필요 없음. 빌드 카운터 +1 은 각 release-<platform> 의 책임. 슬러그 / 배포 스크립트만 바꿔서 동일 semver 로 재배포할 때도 그냥 ./release-<platform> 만 다시 돌리면 됨.


Self-commit

VERSION 이 실제로 변했을 때만 Bump version to X.Y.Z 메시지로 자체 커밋. 변경 없으면 무 commit. 오케스트레이터가 git add -A 같은 광범위 커밋을 할 필요가 없어 안전.


SemVer 검증

^[0-9]+\.[0-9]+\.[0-9]+$ regex 로 검사. 비-SemVer 입력은 거부.


동작 의사코드

# 입력: VERSION 한 줄 "<semver>"
SEMVER="$(tr -d '[:space:]' < VERSION)"

case "$1" in
    "")             # no-op (print only)
    --bump)         # semver last segment ++
    -h|--help)      # 헤더 주석 출력
    *)              # explicit semver
esac

# regex 검증
echo "$SEMVER" > VERSION

# git diff 가 변경 감지하면 자체 commit "Bump version to $SEMVER"

release-<platform> 와의 계약

bump-version 이 VERSION 한 곳에 semver 만 보장하는 대신, 각 release-<platform> 스크립트는 빌드 직전에 다음을 책임진다:

  1. VERSION 에서 semver 를 읽어 자기 매니페스트의 사용자 가시 필드에 propagate (예: CFBundleShortVersionString, gradle versionName)
  2. 자기 매니페스트의 빌드 카운터 필드를 +1 (예: CFBundleVersion, gradle versionCode)
  3. 매니페스트가 실제로 변했을 때만 commit (Bump <platform> to v<semver> (build <N>))

구현 코드는 각 플랫폼 배포 문서 참고 (macOS → macos-brew-deploy.md).


추후 개선 후보

스크립트 안에서 끝나는 개선만 (release-* 나 git tag 같은 외부 책임 제외).

1. atomic write 🔴

현재 echo "$X" > VERSION_FILE 직접 redirect. 디스크 가득 차거나 SIGINT 들어오면 VERSION 이 빈/잘린 상태로 남을 수 있음.

TMP="$(mktemp "${VERSION_FILE}.XXXXXX")"
echo "$SEMVER" > "$TMP"
mv "$TMP" "$VERSION_FILE"

2. semver 단위 별 bump 🟡

현재 --bump 는 patch 만 올림. minor / major 올릴 때 사용자가 직접 ./bump-version 2.0.0 입력 → 타이핑 실수 위험.

--bump | --bump-patch)  PARTS[2]=$((PARTS[2]+1)) ;;
--bump-minor)           PARTS[2]=0; PARTS[1]=$((PARTS[1]+1)) ;;
--bump-major)           PARTS[2]=0; PARTS[1]=0; PARTS[0]=$((PARTS[0]+1)) ;;

3. no-op 명시 감지 🟡

./bump-version 1.2.3 시 현재 VERSION 도 1.2.3 이면 변경 없음 → 그냥 print only. 메시지로 명시.

if [[ "$1" == "$SEMVER" ]]; then
    echo ">> Already at $SEMVER (no change)." >&2
    exit 0
fi

4. --print / --json 🟢

CI / 다른 스크립트가 현재 semver 파싱.

--print)  echo "$SEMVER"; exit 0 ;;
--json)   printf '{"semver":"%s"}\n' "$SEMVER"; exit 0 ;;

5. --dry-run 🟢

VERSION 에 쓰지도, commit 도 안 하고 결과만 표시.

우선순위

등급 항목 이유
🔴 1 atomic write 한 번 corrupt 되면 손 복구 필요, 비용 거의 없음
🟡 2 semver 단위 bump, 3 no-op 감지 자주 쓰는 흐름, 의도 vs 결과 불일치 줄임
🟢 4 print/json, 5 dry-run 편의 / 자동화

함정과 교훈

  • VERSION 은 semver 만. build 도 한 곳에 모으려 하지 말 것 (플랫폼 lockstep 강제 → 핫픽스 못 함).
  • build 카운터는 매니페스트 안에서 단조 증가. 각 release-<platform> 이 +1 만 책임. 절대 reset / 수동 편집 금지 (App Store / Play Store 가 거부).
  • bump-version 은 VERSION 만 만진다. plist / gradle 까지 손대게 만들면 빌드 안 한 플랫폼의 manifest 가 매번 흔들림. 매니페스트 propagate 는 각 release-* 의 책임.
  • Semver 동기화. 한 코드가 표현하는 버전은 플랫폼 공통이어야 함 — VERSION 한 곳에서 읽어 모든 플랫폼이 같은 값을 쓰는 이유.

참고 위치

  • 패턴 적용 레포: ~/work/airplay_touch, ~/work/audiocast, ~/work/audiocast-driver
  • 주요 스크립트: {bump-version, release, release-mac, release-android, VERSION}