Skip to content

Latest commit

 

History

History
184 lines (123 loc) · 15.6 KB

distribution.md

File metadata and controls

184 lines (123 loc) · 15.6 KB

비개발자 APK 배포 (Firebase App Distribution)

디자이너·PM·QA·외부 베타테스터에게 release APK 를 자동 배포하는 흐름. Firebase 프로젝트 afternote-android + 테스터 그룹 afternote 사용.

운영 자격 인계 (로컬 fallback 담당자만)

일반 배포는 GitHub Actions가 담당하므로 모든 개발자가 이 설정을 가질 필요는 없다. CI 장애 때 로컬 fallback을 수행할 배포 권한 보유자만 다음을 준비한다.

  1. 기존 release keystore와 자격 값 인계: 승인된 보안 저장소에서 기존 서명 파일과 네 값을 받는다. 새 keystore를 임의로 생성하거나 교체하지 않는다. 기존 앱과 다른 키로 서명하면 업데이트할 수 없으므로, 자격을 찾지 못하면 배포를 중단하고 복구·공식 키 교체 절차를 먼저 진행한다.

  2. local.properties 끝에 4개 키 추가 (signing config 가 읽음)

    RELEASE_STORE_FILE=/Users/<you>/afternote-release.jks
    RELEASE_STORE_PASSWORD=<keystore 비밀번호>
    RELEASE_KEY_ALIAS=afternote-release
    RELEASE_KEY_PASSWORD=<key 비밀번호>
  3. google-services.json 배치: Firebase Console → 프로젝트 설정 → 일반 → Android 앱 com.afternote.afternote_fe 카드에서 다운로드 → app/google-services.json

  4. Firebase CLI 설치 + 인증 (로컬 fallback 업로드용)

    npm install -g firebase-tools
    firebase login
  5. 기존 서명 ID 확인: ./gradlew :app:signingReport의 release SHA-1과 keytool로 구한 카카오 키 해시가 각 콘솔에 이미 등록된 값과 같은지 확인한다. 다른 값이면 새로 등록해 우회하지 말고 인계받은 keystore가 맞는지부터 확인한다.

배포 (매 회)

Firebase App Distribution 배포의 릴리스 노트에는 포함 이슈와 QA 포인트가 필요하다. 둘 중 하나라도 비어 있거나 포함 이슈에 #123 형식의 번호가 없으면 Firebase 업로드 전에 실패한다. Play 내부 테스트 트랙은 main PR 본문 전체에서 이슈 번호를 추출하며 QA 포인트를 요구하지 않는다. 생성·길이 제한은 Play 릴리스 노트 규칙을 따른다.

배포 판단 기준

배포 필요성은 develop의 미배포 앱 변경과 명시적인 묶음 정책으로 판단한다. 실제 배포 가능 여부는 릴리스 CI와 QA로 별도 확인한다.

  • 인증·온보딩·데이터 손실·API 계약·빌드/서명처럼 영향이 큰 변경은 다른 변경을 기다리지 않고 단독 배포한다.
  • 작은 변경은 하나의 QA 세션에서 회귀 원인을 구분할 수 있는 범위까지만 묶는다. 서로 다른 사용자 흐름을 한꺼번에 확인해야 하거나 함께 롤백하기 어려워지는 시점이 배포 경계다.
  • 수정 결과를 테스터가 확인해야 하는 결함이 머지되면, 묶음 크기와 관계없이 확인 가능한 빌드를 배포한다.
  • 현재 묶음의 모든 QA 포인트가 통과한 뒤 develop을 main으로 승격한다.

릴리스 PR 범위 자동 산출

아래 자동 판정은 배포 후보 준비를 권고한다. develop → main 릴리스 PR의 검증과 머지가 실제 배포 결정이다.

릴리스 PR이 열리거나 head가 갱신되면 release-scope.yml이 마지막 성공 배포 이후 develop에 머지된 PR과 Closes·Fixes·Resolves로 완료 처리할 이슈를 모아 PR 본문의 ## 포함 이슈를 채운다. head가 움직일 때마다 다시 채우므로 머지 직전에 목록을 손으로 대조할 필요가 없다.

## QA 포인트는 비어 있을 때만 구성 PR 본문에서 모은 초안으로 채우고, 사람이 쓴 문장이 있으면 건드리지 않는다. 두 섹션은 main push 시 그대로 릴리스 노트가 되므로 배포 전에 테스터가 실행할 문장으로 다듬는다.

별도 API나 유료 AI를 호출하지 않으며 기존 GitHub Actions 실행량만 사용한다. Actions의 Collect Release Scope에서 릴리스 PR 번호를 입력해 다시 산출할 수도 있다.

댓글 없는 QA 배포 판정

qa-distribution-decision.yml은 develop push, 매일 예약 실행, 성공한 Firebase 배포 뒤에 현재 미배포 묶음을 평가한다. develop에서 수동 실행할 수도 있다. 결과는 Actions의 QA Distribution Decision 실행 요약과 qa-distribution-decision artifact의 decision.json에 남긴다. PR·이슈 댓글, 이슈 생성과 자동 배포는 하지 않는다.

결과 의미
배포 필요 (deploy) 아래 운영 기준에 도달해 릴리스 후보 준비를 권고한다. 실행 자체는 성공으로 끝난다.
묶음 대기 (hold) 앱 순 변경이 없거나 아직 자동 권고 기준에 도달하지 않았다. QA 통과나 무위험을 뜻하지 않는다.
판정 불가 (unknown) 배포 기준점·전체 이력·PR 연결을 확인하지 못했다. 실패한 실행의 요약을 확인한다.

판정 기준의 정본은 POLICY다.

  • 마지막 성공 Firebase main 배포에 연결된 develop source SHA와 이번 평가 SHA를 고정한다. 실패한 배포와 릴리스 PR 생성만으로 기준선을 갱신하지 않는다.
  • 두 SHA의 Git 순 변경에서 문서·테스트·일반 CI 전용 파일을 제외한다. 앱 변경이 전부 되돌려졌으면 권고하지 않는다. 파일이 배포 당시 내용으로 돌아왔으면 그 이전 PR은 해당 파일의 집계에서 뺀다.
  • 순 변경 파일의 이력에 fix·feat·security PR이 포함되거나 실제 Firebase 릴리스 조립 경로가 바뀌면 배포를 권고한다. 이는 PR 유형에 따른 운영 정책이며 실제 사용자 영향이나 위험도를 분석한 결과가 아니다.
  • 그 밖의 앱 변경은 관련 PR 5건 또는 영향 모듈군 3개에 도달하면 권고한다. 작은 묶음도 관련 PR의 대기가 7일에 도달하면 권고한다. 새 머지가 없어도 예약 실행이 시간 경과를 확인한다.
  • API 페이지를 모두 수집하고 Git 이력을 직접 비교한다. 50개 이상의 PR도 자르지 않는다. 수집·기준선·PR 연결 실패를 빈 변경이나 묶음 대기로 바꾸지 않는다.

PR 유형과 파일 단위 이력은 사용자 흐름의 의미를 완전히 표현하지 못한다. 부분 revert의 줄 단위 기여는 확정하지 않으며, 큰 리네임·포맷 변경도 묶음 기준에 포함될 수 있다. 이 판정은 배포할 APK의 빌드·기동·QA 성공을 보증하지 않는다. 위험한 동작 변경이나 테스터 요청을 이미 알고 있다면 자동 묶음 대기와 관계없이 위 배포 판단 기준을 적용한다.

현재 PR 템플릿의 CI Test Plan은 Android 계측 테스트 범위를 지정한다. 배포 필요성이나 QA 완료의 입력으로 사용하지 않는다. QA Metadata 검증 게이트는 현재 구현에 없으며, 릴리스 PR의 QA 포인트는 사람이 사전조건·행동·기대 결과를 확인해 작성한다.

일반 배포: main → Firebase App Distribution (자동)

여기서 배포는 검증할 main 빌드를 Firebase 테스터에게 전달하는 단계이며, Play Store 프로덕션 릴리스를 뜻하지 않는다. 일반 배포의 자동 진입점은 main push 하나다.

기존 release-distribution.yml의 develop 수동 배포(workflow_dispatch)는 #1029에서 제거했다. 도착지와 산출물 버전이 main 경로와 같아 실익이 PR 생성 한 단계뿐이었던 반면, release keystore와 service account를 임의 ref에 노출하는 표면이었다.

예외적으로 firebase-wif-canary.yml은 develop 또는 main에서 보호 Environment 승인과 명시적 확인을 받은 뒤 실제 APK를 올리는 수동 인증 호환성 검증 경로다. 일반 릴리스나 임의 브랜치 배포 수단으로 사용하지 않는다. production release-distribution.yml도 #1463부터 같은 WIF 경로로 인증하고, 장기 JSON 키 경로는 #1566에서 걷었다. canary 실행 조건은 WIF canary runbook을 따른다.

develop → main 릴리스 PR 본문에 다음 섹션을 채운다.

## 포함 이슈
- #716
- #723

## QA 포인트
- 오프라인에서 오류 안내와 재시도 수단이 표시되는지 확인
- 주차를 변경한 뒤 최신 리포트가 표시되는지 확인

PR이 main에 머지되면 워크플로가 두 섹션을 릴리스 노트로 사용한다. 연결된 PR이나 필수 섹션을 찾지 못하면 배포하지 않는다. ## QA 포인트에 사전조건·행동·기대 결과가 없는 generic fallback 문구가 있으면 릴리스 노트 렌더 단계에서 실패한다.

CI가 사용하는 GitHub Secrets (Settings → Secrets and variables → Actions):

  • KAKAO_NATIVE_APP_KEY·GOOGLE_WEB_CLIENT_ID·GOOGLE_SERVICES_JSON_B64는 Environment를 사용하지 않는 dependency audit에서도 필요하므로 repository-level secret으로 둔다.
  • release keystore·Firebase 자격·WIF 설정은 보호된 release-distribution Environment의 secret으로 둔다.
키 용도
RELEASE_STORE_FILE_B64 release keystore 파일 (~/afternote-release.jks) 의 base64 인코딩
RELEASE_STORE_PASSWORD keystore 비밀번호
RELEASE_KEY_ALIAS key alias (afternote-release)
RELEASE_KEY_PASSWORD key 비밀번호
KAKAO_NATIVE_APP_KEY · GOOGLE_WEB_CLIENT_ID · GOOGLE_SERVICES_JSON_B64 release distribution·WIF canary·dependency audit가 쓰는 실서비스 앱 설정
GCP_WORKLOAD_IDENTITY_PROVIDER · GCP_FIREBASE_SERVICE_ACCOUNT release distribution과 WIF canary가 Firebase 업로드 직전에 쓰는 WIF 설정. 서비스 계정 afternote-ci 는 이 두 워크플로에만 묶여 있다(Play release runbook 사전 준비 3번)

base64 인코딩: base64 -i ~/afternote-release.jks | pbcopy (macOS)

PR 검증용 lint·unit-test·screenshot은 repository secret 대신 .github/actions/setup-ci-config가 만드는 결정적 CI 전용 placeholder를 사용한다. 이 fixture는 배포에 사용할 수 없으며, production release-distribution.yml은 승인된 환경에서 WIF로 받은 단기 자격으로만 업로드한다.

배포 provenance: 이 APK 가 어느 commit·run 에서 나왔는지 (#851)

배포 워크플로는 signing 이 끝난 그 APK 하나를 subject 로 GitHub artifact attestation을 발급하고, 업로드 전에 스스로 검증한다. 서명·저장소·signer workflow·source commit·GitHub-hosted 러너 중 하나라도 어긋나거나 attestation subject digest 가 빌드 직후 digest 와 다르면 Firebase 업로드까지 가지 않는다.

성공한 run 의 summary 에 남는 값은 넷이다: source commit SHA, sha256: artifact digest, attestation URL, run URL. APK·AAB 와 R8 mapping 자체는 여기서도 public Actions artifact 로 게시하지 않는다.

받은 APK 가 정말 그 배포 경로에서 나왔는지는 손에 든 파일로 직접 확인할 수 있다.

gh attestation verify ~/Downloads/afternote-release.apk --repo Afternote/Afternote-FE --signer-workflow Afternote/Afternote-FE/.github/workflows/release-distribution.yml --source-ref refs/heads/main --deny-self-hosted-runners

특정 릴리스 commit 으로 좁히려면 --source-digest <commit SHA> 를 더한다. 파일이 1비트라도 다르면 digest 가 달라져 검증이 실패한다.

아래 로컬 fallback 으로 올린 빌드에는 attestation 이 없다. 그 경로로 배포한 APK 는 위 명령이 실패하는 게 정상이며, 그래서 CI 장애 때만 쓴다.

로컬: 배포 자격 보유자 (fallback / 긴급 시)

먼저 원격을 갱신하고 배포할 commit이 정확히 origin/main이며 작업 트리가 깨끗한지 확인한다. 다른 브랜치나 임의 SHA에서는 이 경로를 실행하지 않는다.

git fetch origin main
git switch --detach origin/main
test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)"
test -z "$(git status --porcelain)"

위 운영 자격을 준비한 같은 checkout에서 릴리스 노트를 만들고 업로드한다.

EVENT_NAME=workflow_dispatch \
ISSUE_NUMBERS="#716, #723" \
QA_POINTS="오프라인 오류 안내 확인;주차 변경 후 재시도 확인" \
SOURCE_REF=main \
SOURCE_SHA="$(git rev-parse origin/main)" \
bash .github/scripts/render-distribution-release-notes.sh /tmp/afternote-release-notes.txt

./gradlew assembleRelease appDistributionUploadRelease \
  --releaseNotesFile=/tmp/afternote-release-notes.txt \
  --no-daemon

→ 동일하게 APK 빌드 + Firebase 업로드. CI 장애 시에만 사용한다.

Firebase App Distribution 자체는 versionCode로 빌드를 구분하지 않는다. 그래도 배포마다 값을 올린다. 이 채널의 APK가 크래시를 보내는 곳이 같은 Firebase 프로젝트의 Crashlytics이고, 거기서는 배포본이 versionName (versionCode) 한 쌍으로 묶이기 때문이다. 값이 고정이면 서로 다른 커밋의 배포본이 한 버전 아래 섞인다(#1865에서 1d0d175와 f5ad611이 둘 다 1.0 (1)로 뜬 것이 그 실측이다). release 빌드의 versionName은 사용자에게 보이는 값이라 고정이므로, 배포본을 가르는 축은 versionCode 하나뿐이다. 채널별 번호 대역과 승격 규칙은 Play release runbook의 versionCode 정책에 있다. app/build.gradle.kts를 직접 수정하지 말고 AFTERNOTE_VERSION_CODE 환경변수로 주입한다.

Play 내부 테스트 트랙과의 경계

두 채널은 목적도 산출물도 자격도 다르다. 한쪽 절차를 다른 쪽에 적용하지 않는다.

Firebase App Distribution Play 내부 테스트 트랙
목적 개발·기획·디자인·QA 내부 확인 출시 전 Play 설치 경로 검증
산출물 release APK release AAB
트리거 main push 자동 main에서 수동 실행 + environment 승인
workflow release-distribution.yml release-play-internal.yml
versionCode 배포마다 증가 (101~999,999,999 대역) 배포마다 증가 (1,000,000,000 이상 대역)
자격 release-distribution environment play-internal environment (별도 service account)
릴리스 노트 포함 이슈·QA 포인트 필수 main PR 본문의 이슈 번호를 추출한 ko-KR 문구. QA 포인트는 요구하지 않음
롤백 이전 빌드를 다시 배포 릴리스 중단 후 더 큰 versionCode로 재배포

Play 출시 이후에도 Firebase는 기존 QA 채널로 유지한다. 설치 인증서가 다르면 두 채널의 앱은 서로 위에 업데이트되지 않으므로, 테스터가 채널을 옮길 때는 삭제 후 재설치가 필요하다. 그래서 이 채널의 APK를 Play로 승격하는 경로는 만들지 않는다. 두 채널의 번호 대역을 나눈 것도 승격이 일어났을 때 Play 배포가 막히지 않게 하기 위한 방호이지 승격을 허용한다는 뜻이 아니다.

테스터 관리

  • 추가/제거: Firebase Console → App Distribution → 테스터 및 그룹 → afternote 그룹 편집
  • 신규 테스터는 첫 초대 이메일에서 App Tester 앱 설치 안내를 받음 → 이후 빌드는 자동 알림