From 1f71cf6faeb209c85b49a0260312897a25cc41d9 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Sun, 30 Aug 2026 09:20:49 +0900 Subject: [PATCH] ci: allow protected mobile candidate --- .gitlab-ci.yml | 32 +- docs/v3/MOBILE_RELEASE_HANDOFF_2026-08-29.md | 733 ++++++++++++++++++ scripts/ci/verify-mobile-release-boundary.mjs | 41 + 3 files changed, 797 insertions(+), 9 deletions(-) create mode 100644 docs/v3/MOBILE_RELEASE_HANDOFF_2026-08-29.md diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index efafcb9..9bd9eae 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -284,10 +284,11 @@ package-macos: rules: - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+.*$/' -# Protected, manually approved Play Console handoff. Configure the three -# *_FILE variables below as protected GitLab file variables scoped to the -# mobile-production-release environment. All other credentials must be -# protected, masked, and hidden project/group variables. +# Protected, manually approved Play Console candidate/stable handoff. The main +# branch path only creates restricted artifacts; publish-release remains tag-only. +# Configure the three *_FILE variables below as protected GitLab file variables +# scoped to the mobile-production-release environment. All other credentials +# must be protected, masked, and hidden project/group variables. mobile-production-release: stage: package image: reactnativecommunity/react-native-android@sha256:24ca7ab5a70ec0b78a81bdc5eeea5924c2531531d53971b6f2321aff08446c36 @@ -320,10 +321,11 @@ mobile-production-release: tar -xJf /tmp/node.tar.xz -C /tmp/node24 --strip-components=1 export PATH="/tmp/node24/bin:$PATH" node --version | grep -Fx "v${NODE_VERSION}" - git fetch --no-tags origin main + git fetch --no-tags origin main:refs/remotes/origin/main git merge-base --is-ancestor "$CI_COMMIT_SHA" origin/main test "$CI_COMMIT_SHA" = "$(git rev-parse HEAD)" test -z "$(git status --porcelain --untracked-files=all)" + test "${CI_COMMIT_REF_PROTECTED:-}" = "true" SOURCE_TREE_SHA="$(git rev-parse "${CI_COMMIT_SHA}^{tree}")" VERSION_NAME="$(node -p "require('./release/product-version.json').version")" VERSION_CODE="$(node -p "require('./release/product-version.json').androidVersionCode")" @@ -331,7 +333,16 @@ mobile-production-release: ADMOB_APP_ID="$(node -p "require('./release/android-release-identity.json').adMobAppId")" ADMOB_BANNER_UNIT_ID="$(node -p "require('./release/android-release-identity.json').adMobBannerUnitId")" ADMOB_REWARDED_UNIT_ID="$(node -p "require('./release/android-release-identity.json').adMobRewardedUnitId")" - test "$CI_COMMIT_TAG" = "v${VERSION_NAME}" + if [ -n "${CI_COMMIT_TAG:-}" ]; then + test -z "${CI_COMMIT_BRANCH:-}" + test "$CI_COMMIT_TAG" = "v${VERSION_NAME}" + test "$(git rev-parse "refs/tags/${CI_COMMIT_TAG}^{commit}")" = "$CI_COMMIT_SHA" + RELEASE_GIT_REF="refs/tags/${CI_COMMIT_TAG}" + else + test "${CI_COMMIT_BRANCH:-}" = "main" + test "$CI_COMMIT_SHA" = "$(git rev-parse origin/main)" + RELEASE_GIT_REF="refs/heads/main" + fi echo "$VERSION_NAME" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' echo "$VERSION_CODE" | grep -Eq '^[1-9][0-9]{0,9}$' test "$VERSION_CODE" -le 2100000000 @@ -387,7 +398,7 @@ mobile-production-release: --repository "$CI_PROJECT_PATH" \ --commit-sha "$CI_COMMIT_SHA" \ --tree-sha "$SOURCE_TREE_SHA" \ - --git-ref "refs/tags/$CI_COMMIT_TAG" \ + --git-ref "$RELEASE_GIT_REF" \ --workflow-identity "gitlab-ci/mobile-production-release" \ --run-id "$CI_JOB_ID" \ --run-attempt "1" \ @@ -414,7 +425,7 @@ mobile-production-release: --expected-repository "$CI_PROJECT_PATH" \ --expected-commit-sha "$CI_COMMIT_SHA" \ --expected-tree-sha "$SOURCE_TREE_SHA" \ - --expected-git-ref "refs/tags/$CI_COMMIT_TAG" \ + --expected-git-ref "$RELEASE_GIT_REF" \ --expected-workflow-identity "gitlab-ci/mobile-production-release" \ --expected-run-id "$CI_JOB_ID" \ --expected-run-attempt "1" \ @@ -449,7 +460,10 @@ mobile-production-release: - apps/mobile-rn/android/app/build/outputs/release-app-links-evidence.json manual_confirmation: '검증된 production AAB를 생성해 Maintainer 전용 Play Console handoff로 보낼까요?' rules: - - if: '$D3RO_MOBILE_PRODUCTION_RELEASE_ENABLED == "true" && $D3RO_ANDROID_EMULATOR_RUNNER == "true" && $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/' + - if: '$D3RO_MOBILE_PRODUCTION_RELEASE_ENABLED == "true" && $D3RO_ANDROID_EMULATOR_RUNNER == "true" && $CI_COMMIT_REF_PROTECTED == "true" && $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/' + when: manual + allow_failure: false + - if: '$D3RO_MOBILE_PRODUCTION_RELEASE_ENABLED == "true" && $D3RO_ANDROID_EMULATOR_RUNNER == "true" && $CI_COMMIT_REF_PROTECTED == "true" && $CI_COMMIT_BRANCH == "main"' when: manual allow_failure: false - when: never diff --git a/docs/v3/MOBILE_RELEASE_HANDOFF_2026-08-29.md b/docs/v3/MOBILE_RELEASE_HANDOFF_2026-08-29.md new file mode 100644 index 0000000..3cbd43a --- /dev/null +++ b/docs/v3/MOBILE_RELEASE_HANDOFF_2026-08-29.md @@ -0,0 +1,733 @@ +# D3RO Voice 모바일 출시 핸드오프 — 2026-08-29 + +## 0. 결론 + +**모바일 앱은 아직 출시 가능 상태가 아니다.** Google Play 등록정보의 텍스트, +아이콘, feature graphic 초안은 준비됐지만 production AAB, Firebase Android 설정, +upload signing, Play-signed 설치본, 실제 핵심 기능 E2E, 스토어용 앱 스크린샷이 없다. + +2026-08-29 실제 `1.1.0` debug 앱을 새 Pixel 7 에뮬레이터에서 열어 보니 다음을 +확인했다. + +- Metro가 없으면 `Unable to load script` 오류 화면이 표시된다. +- Metro 연결 후 첫 부팅 중 빈 흰 화면이 잠시 표시됐다. +- 이후 실제 onboarding과 로그인 화면은 열렸다. +- 인증 세션이 없어 녹음·전사·AI·팀 화면에는 진입하지 못했다. +- production/Play-signed 설치본이 아니므로 이번 캡처는 Google Play 제출물이 아니다. + +사용자 요청에 따라 작업은 여기서 중단했다. `/goal`은 완료로 처리하지 않는다. + +## 1. 중단 상태 + +종료 확인일: 2026-08-29 KST + +| 항목 | 종료 상태 | +| ------------------- | --------------------------------------------------------------------------- | +| 전용 캡처 AVD | `D3RO_PlayCapture_20260829` 생성됨, `emulator-5556` 종료됨 | +| 기존 AVD | `emulator-5554`만 연결 상태로 보존. 기존 `0.0.0-e2e` 데이터는 건드리지 않음 | +| Metro | `localhost:8081` listener 없음 | +| 로컬 Supabase | `supabase stop --workdir server` 완료, local data는 Docker volume에 백업됨 | +| Android 앱 프로세스 | 전용 `emulator-5556` 종료로 함께 중단 | +| Play Console | 더 이상 변경하지 않음. 기본 등록정보 페이지는 초안 상태 | +| Firebase | 프로젝트 생성 버튼을 누르지 않음. 신규 Firebase 프로젝트 없음 | +| Git | 이 핸드오프 문서 외 추가 stage/commit/push 없음 | +| Forgejo 배포 | run #46 실패 상태. 반복 push/retry하지 않음 | + +## 2. 이번 세션에서 이룬 것 + +### 2.1 버전과 릴리즈 메타데이터 + +- 제품 버전: `1.1.0` +- Android versionCode: `1010001` +- iOS build number: `1010001` +- release date: `2026-08-29` +- SSOT: `release/product-version.json` +- root, desktop, mobile package version과 Android/iOS 버전 동기화 검증 GREEN +- `CHANGELOG.md`의 `1.1.0` 항목 갱신 +- Android `ko-KR`/`en-US` release notes 준비: + - `apps/mobile-rn/metadata/android/ko-KR/changelogs/1010001.txt` + - `apps/mobile-rn/metadata/android/en-US/changelogs/1010001.txt` +- `npm run release:metadata` GREEN +- `npm run release:metadata:test` GREEN, fail-closed negative 6건 + +### 2.2 Play 등록정보 초안 + +Play 앱: + +- 개발자 계정: 개인 Play Console 계정 +- Play app ID: `4976102237698469110` +- package: `com.d3ro.voice` +- 기본 언어: `ko-KR` +- 상태: draft + +저장한 초안: + +- 앱 이름: `D3RO Voice` +- 짧은 설명: 42자 +- 자세한 설명: 1,213자 +- 유명 음성 기록·전사 앱의 정보 구조를 비교하되 문구를 복제하지 않고, + 첫 효용 → 결과 중심 기능 → 요금·광고 → 개인정보 → 책임 고지 순서로 작성 +- 파일: `docs/v3/play/01-store-listing.md` + +중요 경계: + +- 초안만 저장했다. +- 리뷰, App content, Data safety, internal/closed/production 제출은 하지 않았다. +- 휴대전화 스크린샷 영역에는 아무 이미지도 업로드하지 않았다. + +### 2.3 아이콘과 feature graphic + +Play icon: + +- 파일: `docs/v3/play/assets/d3ro-voice-play-icon-512.png` +- 규격: 512×512 PNG +- SHA-256: `0FAE53FB40CCE4B9FB0213CBEDB4E087CA00A8F8C438D6731C5C47C4AFD096B1` +- Play Console 업로드와 preview 확인 완료 + +Feature graphic canonical E: + +- 제출본: `docs/v3/play/assets/feature-graphic/candidate-e-generated-signal-stage.png` +- 규격: 1024×500, PNG24, sRGB, alpha 없음 +- SHA-256: `A5D82B65B95027ACE184FA06A3BBDA3433995C70F064932B5733C3D3A99644D9` +- 생성 원본, prompt, 제출본, hash를 별도로 보존 +- Play Console preview에서 crop·thumbnail contrast·규격 경고 없음을 확인 +- 이것은 **feature graphic**이며 앱 스크린샷이 아니다. + +검증: + +- `npm run release:play:assets` GREEN +- `npm run release:play:assets:test` GREEN +- canonical E 변조 포함 fail-closed negative 7건 GREEN + +### 2.4 Git과 CI + +현재 권위 있는 SHA: + +```text +4872d6f6c50a15104792fb31a97176527c3cd9a2 +``` + +다음 ref가 모두 위 SHA와 일치한다. + +- local `main` +- `origin/main` +- `chanpaca/main` + +주요 릴리즈 커밋: + +```text +4872d6f docs(release): update 1.1.0 launch metadata +0a8cf43 feat(play): finalize store graphics and copy +563d4cb fix(ci): make mobile Gradle wrapper executable +6a9608c ci: bootstrap Linux release toolchains +30d5127 build(release): forbid unsigned Windows artifacts +ec5aec7 fix(release): retire legacy capture publisher +547d4ad security: remove credential automation from releases +f342ea8 fix(release): sync legacy mobile version +3e9e04f ci(release): require trusted Windows signing +5205dcd feat(release): prepare 1.1.0 candidate +``` + +GitLab CI: + +- 최종 pipeline: `72066` +- URL: `https://gitlab.twentyoz.kr:8443/d3r0/voice/-/pipelines/72066` +- commit: `4872d6f6` +- 결과: SUCCESS, 7/7 +- GREEN jobs: lint/typecheck, unit, API, Edge, mobile-quality, + build-workspaces, mobile-android +- 이전 graphics/copy pipeline `72065`도 SUCCESS, 7/7 + +주의: CI GREEN은 production AAB, Play 설치, 실제 provider, 스크린샷 검증을 +대체하지 않는다. + +### 2.5 실제 앱 실행 시도 + +새 전용 AVD: + +- 이름: `D3RO_PlayCapture_20260829` +- device profile: Pixel 7 +- system image: Android 34 Google APIs Play Store x86_64 +- serial: 실행 시 `emulator-5556` +- 원래 해상도: 1080×2400 +- 캡처 시 임시 override: 1080×1920 +- 현재 상태: 종료됨 + +설치한 실제 debug APK: + +- 파일: `apps/mobile-rn/android/app/build/outputs/apk/debug/app-debug.apk` +- package: `com.d3ro.voice` +- label: `D3RO Voice` +- versionName: `1.1.0` +- versionCode: `1010001` +- targetSdk: 36 +- SHA-256: `7AD0FD0716D8F900053B31D72C53ADC035889D8B507D95602FEC49F5A5C78B87` +- signer: Android Debug +- debug signer SHA-256: + `FAC61745DC0903786FB9EDE62A962B399F7348F0BB6F899B8332667591033B9C` + +정상 확인: + +- 새 AVD 설치 성공 +- cold start가 `com.d3ro.voice/.MainActivity`로 실행됨 +- Metro 연결 후 React Native `D3ROVoice` root 실행 확인 +- 실제 onboarding과 login UI 확인 +- Keychain이 빈 새 설치에서 인증 세션 없음 확인 + +## 3. 하지 못한 것 + +### 3.1 실제 핵심 기능 검증 + +다음 화면은 실제 앱에서 열지 못했다. + +- 녹음 ready/recording/paused 상태 +- 실제 WAV 생성과 foreground notification +- 오디오/MP4 가져오기 +- 실제 전사 결과와 history detail +- Talk AI 응답과 신고 sheet +- 생성 문서/회의록 +- 팀/초대/역할 화면 +- 개인정보, export/restore, account deletion의 authenticated 경로 +- 광고 동의·배너·rewarded SSV +- Play Billing purchase/restore + +원인: + +- 새 debug 설치에는 로그인 세션이 없다. +- debug 앱은 production Supabase를 사용한다. +- 안전한 인증 우회나 기능 화면 deep link가 없다. +- auth session은 Android Keychain에 암호화되므로 AsyncStorage/adb seed로 우회할 수 없다. +- 사용자의 실제 계정을 캡처에 사용하지 않았다. +- 외부 disposable 계정도 새로 만들지 않았다. + +### 3.2 production Android release + +아직 없는 것: + +- Firebase project와 Android app +- `google-services.json` +- production upload keystore path/alias/password CI 설정 +- release evidence private key CI 설정 +- Firebase project ID/number/mobile SDK app ID CI 설정 +- `D3RO_MOBILE_PRODUCTION_RELEASE_ENABLED=true` +- Android emulator runner production gate +- production signed AAB +- `v1.1.0` tag +- Play internal track upload +- Play-signed install +- App Bundle Explorer/signing evidence + +production AAB는 생성되지 않았다. self-signed, debug, E2E 또는 출처 불명 +keystore를 사용하지 않았다. + +### 3.3 Google Play 스크린샷 + +현재 store-ready 휴대전화 스크린샷 수: **0장** + +- Play Console screenshot slot은 비어 있다. +- 최소 2장, 권장 4장 이상이 필요하다. +- 실제 핵심 기능 화면을 아직 열지 못했으므로 업로드하지 않았다. + +### 3.4 Firebase + +생성 예정으로 준비했던 값: + +- project name: `D3RO Voice Production` +- project ID: `d3ro-voice-production` +- Android package: `com.d3ro.voice` +- Developer Program: off +- Gemini: off +- Analytics: off + +하지만 최종 `프로젝트 만들기` 버튼은 누르지 않았다. Firebase project/API +identity는 생성되지 않았다. 내일 action-time 승인 후 다시 시작해야 한다. + +### 3.5 공개 사이트 배포 + +Forgejo push 자체는 완료됐다. + +- `chanpaca/main`: `4872d6f6c50a15104792fb31a97176527c3cd9a2` +- sparse pack 문제를 비활성화해 136MiB 전송을 3.66MiB로 줄인 뒤 push 성공 + +하지만 배포는 실패했다. + +- Forgejo deploy-site run: `#46` +- 결과: FAILURE +- 실패 위치: `Set up job` +- 경과: 10분 53초 +- checkout/build/Cloudflare/verify: 모두 0초, 시작되지 않음 +- 원인: `linux-builder` runner가 job을 claim/initialize하지 못해 timeout + +현재 public 상태는 최신 배포가 아니다. + +- `/release-identity.json`: 404 +- root `/`: 404 +- live `assetlinks.json`: 과거 인증서 SHA 유지 +- live download page: 과거 `v1.0.0 STABLE` / `Signed & Verified` 문구 유지 +- privacy/terms/delete-account: 기존 공개 페이지는 접근 가능 + +새 commit이나 반복 push로 해결할 문제가 아니다. runner 복구 후 run #46만 +재실행하고 public exact verification을 해야 한다. + +### 3.6 Windows 공개 릴리즈 + +- 로컬 installer는 Authenticode `NotSigned` +- public-trust code-signing certificate/PFX가 없음 +- 기존 self-signed `CN=Everything2EverythingDev`는 public release에 사용 금지 +- stable `v1.1.0` tag/release와 updater publish는 계속 차단 + +## 4. 새로 발견된 것 + +### 4.1 설치돼 있던 앱은 production 앱이 아니었다 + +기존 `emulator-5554`의 `com.d3ro.voice`: + +- versionName: `0.0.0-e2e.20260829.1` +- versionCode: `202608291` +- installer: `null` +- microphone/notification permission: 미허용 + +기존 상태를 캡처해 Play 증거로 쓰면 안 된다. 기존 에뮬레이터는 보존했다. + +### 4.2 debug APK는 Metro 없이 standalone 실행되지 않는다 + +`app-debug.apk` 첫 cold start에서 Metro가 없으면 다음 오류가 실제로 표시됐다. + +```text +Unable to load script. +Make sure you're running Metro or that your bundle 'index.android.bundle' +is packaged correctly for release. +``` + +이는 debug variant의 예상 개발 경계이지만, source/build GREEN만으로 앱을 +열었다고 판단하면 안 된다는 실증이다. 최종 Play 캡처는 embedded bundle을 가진 +production/Play-signed 설치본이어야 한다. + +### 4.3 Metro 연결 직후 빈 흰 화면이 관찰됐다 + +Metro bundle 로딩 뒤 첫 캡처는 status/navigation bar 외 전부 흰 화면이었다. +수 초 뒤 onboarding이 표시됐다. production build에서 다음을 별도로 검증해야 한다. + +- cold start splash/boot feedback +- blank frame 지속 시간 +- provider/auth 초기화 실패 시 fail-closed UI +- offline/slow network 시작 경로 +- process death 후 recovery + +### 4.4 실제 onboarding은 열렸지만 핵심 기능은 인증 뒤에 있다 + +실제 onboarding과 signup/login 화면은 정상 렌더됐다. 그러나 debug 앱에는 +안전한 local/demo 기능 route가 없고 인증 우회도 없다. local disposable +Supabase + E2E APK를 사용하면 기능 UI를 열 수 있지만 그 결과는 TEST-DEMO이며 +Play-signed production 증거가 아니다. + +### 4.5 캡처용 해상도를 별도로 고정해야 한다 + +Pixel 7 AVD 기본 캡처는 1080×2400이었다. Play draft 요구와 일관된 세로 캡처를 +위해 dedicated AVD에 `wm size 1080x1920`을 적용했다. 내일 재개 시 실제 값을 +다시 확인하고, Play-signed 설치본에서도 동일 viewport가 제품 UI를 자르거나 +왜곡하지 않는지 검증해야 한다. + +### 4.6 작업 트리가 매우 dirty하다 + +중단 시 `git status --short` 집계: + +- total: 197 +- modified: 55 +- deleted: 17 +- untracked: 125 + +대부분 기존 build/test/generated/user 작업이다. 전체 stage, 전체 commit, +reset, restore, clean을 금지한다. 이번에 새로 생긴 캡처 디렉터리만 별도로 +식별됐다. + +## 5. 캡처 증거 원장 + +디렉터리: + +`docs/v3/play/assets/screenshots/1.1.0-debug-capture-20260829/` + +| 파일 | 규격 | SHA-256 | 내용 | 판정 | +| ------------------------ | --------- | ------------------------------------------------------------------ | ----------------------------------------- | ------------------------------------------------- | +| `00-launch.png` | 1080×2400 | `4818E5266471D07A63B0162D60B673354E17DF8BD0F557FE0ADDF9E04EE94711` | Metro 미연결 `Unable to load script` 오류 | **폐기**, 오류 증거만 보존 | +| `01-runtime.png` | 1080×1920 | `89947F52FE4CA38A2729D1C706CAB5C0AD87A45405843F4769D0ABDFD4F440CC` | Metro 연결 직후 빈 흰 화면 | **폐기**, boot 문제 증거만 보존 | +| `02-after-bootstrap.png` | 1080×1920 | `95AC6B0B2792086DE2D38AE719FBED222A3595158A2C9F37403E31F756FD6FC8` | 실제 `1.1.0` onboarding 1/5 | 실제 앱 화면이지만 **debug이며 store-ready 아님** | + +세 파일 모두 Play Console에 업로드하지 않는다. `02-after-bootstrap.png`도 +production/Play-signed 재촬영 전에는 참고 증거일 뿐이다. + +## 6. 내일 진행해야 할 것 + +### P0 — 앱을 먼저 검증한다 + +1. 아래 `재개 전 사실 확인` 명령을 실행한다. +2. Firebase project/app 생성에 대한 action-time 승인을 다시 받는다. +3. Firebase Android app `com.d3ro.voice`를 등록한다. +4. Play app-signing SHA-1/SHA-256과 upload key SHA를 Firebase에 정확히 등록한다. +5. `google-services.json`을 내려받아 package/project identity를 검증한다. +6. public-trust와 별개인 Android upload key를 기대 SHA와 대조한다. +7. GitLab protected/masked/hidden CI variables를 구성한다. +8. `mobile-production-release` job으로 production AAB를 만든다. +9. AAB version/package/SDK/merged manifest/dependency/signature/provenance를 검증한다. +10. 사용자 확인 후 Play internal track에 AAB를 업로드한다. +11. Play가 서명한 설치본을 clean install한다. +12. 로그인, 녹음, 전사, AI, 팀, 신고, 광고, Billing, 삭제를 실제로 검증한다. + +### P0 — 실제 스토어 스크린샷을 만든다 + +최종 캡처는 Play-signed 설치본과 dedicated reviewer/capture 계정을 사용한다. +사용자의 실제 이메일, 회의, 음성, 팀, 토큰을 사용하지 않는다. + +권장 6장 흐름: + +1. **녹음** — 녹음 준비 또는 실제 recording 상태, 명확한 controls와 상태 표시 +2. **전사 결과** — 실제 WAV에서 생성된 transcript/history detail +3. **AI 문서화** — 실제 transcript 기반 요약/문서와 책임 있는 사용 경계 +4. **Talk + 신고** — 실제 AI 응답과 사용자가 접근 가능한 report affordance +5. **작업/팀** — Works hub 또는 팀 협업의 실제 접근 가능한 구조 +6. **데이터 관리** — privacy, export/restore, device/account deletion 경로 + +각 이미지 acceptance criteria: + +- 1080×1920 PNG 또는 Console이 수락하는 검증된 실제 device ratio +- package `com.d3ro.voice` +- versionName `1.1.0`, versionCode `1010001` +- installer `com.android.vending` +- Play app-signing certificate SHA-256이 기대값과 일치 +- DEBUGGABLE 아님 +- test AdMob label/fixture/debug menu/error/red screen/blank screen 없음 +- 생성형 mock UI 없음 +- 실사용자 PII, reviewer credential, token, 실제 회의 내용 없음 +- 화면의 모든 기능이 같은 Play 설치본에서 실제 접근 가능 +- crop/자막을 쓰더라도 실제 UI를 가리거나 기능을 과장하지 않음 + +### P1 — Forgejo 공개 배포 복구 + +1. `linux-builder` runner service/registration/labels/capacity를 복구한다. +2. 새 commit이나 push를 만들지 않는다. +3. run #46만 재실행한다. +4. public `release-identity.json`의 commit이 + `4872d6f6c50a15104792fb31a97176527c3cd9a2`인지 확인한다. +5. version이 `1.1.0`인지 확인한다. +6. root/privacy/terms/delete-account가 HTTPS 200인지 확인한다. +7. download page에서 stale `v1.0.0 STABLE`과 unsigned `Signed & Verified` + 문구가 사라졌는지 확인한다. +8. `assetlinks.json`이 Play app-signing SHA만 포함하고 upload SHA를 포함하지 + 않는지 확인한다. + +### P1 — Windows 공개 릴리즈 + +1. public-trust Windows code-signing certificate를 준비한다. +2. expected signer subject를 CI secret와 gate에 설정한다. +3. installer와 unpacked exe 모두 Authenticode `Valid`를 검증한다. +4. 설치된 앱, registry, updater feed, visible version까지 E2E한다. +5. 그 이후에만 `v1.1.0` tag/release/updater publish를 진행한다. + +## 7. 재개 전 사실 확인 + +저장소 루트 PowerShell: + +```powershell +git status --short +git rev-parse HEAD +git rev-parse origin/main +git rev-parse chanpaca/main +git ls-remote origin refs/heads/main +git ls-remote chanpaca refs/heads/main +adb devices -l +Get-NetTCPConnection -LocalPort 8081 -State Listen -ErrorAction SilentlyContinue +docker ps --format '{{.Names}}' | Select-String 'supabase_.*d3ro-voice' +npm run release:metadata +npm run release:play:assets +npm run release:mobile:boundary +``` + +기대 기준: + +- 세 Git SHA가 모두 `4872d6f6c50a15104792fb31a97176527c3cd9a2` +- 기존 `emulator-5554` 외 새 캡처 emulator는 자동으로 실행돼 있지 않음 +- Metro 8081 listener 없음 +- local Supabase containers 없음 +- release metadata/assets/boundary GREEN + +## 8. 캡처 AVD 재개 명령 + +이 명령은 **debug 진단용**이다. 최종 스토어 캡처는 Play-signed 설치본으로 한다. + +```powershell +$emulatorExe = "$env:ANDROID_SDK_ROOT\emulator\emulator.exe" +Start-Process -FilePath $emulatorExe -ArgumentList @( + '-avd', 'D3RO_PlayCapture_20260829', + '-port', '5556', + '-no-snapshot-save', + '-no-boot-anim', + '-gpu', 'auto' +) + +adb -s emulator-5556 wait-for-device +adb -s emulator-5556 shell getprop sys.boot_completed +adb -s emulator-5556 shell wm size 1080x1920 +``` + +Debug + Metro가 필요할 때만: + +```powershell +Set-Location apps/mobile-rn +npm run start -- --port 8081 +adb -s emulator-5556 reverse tcp:8081 tcp:8081 +adb -s emulator-5556 install -r android/app/build/outputs/apk/debug/app-debug.apk +adb -s emulator-5556 shell am start -W -n com.d3ro.voice/.MainActivity +``` + +실제 화면 캡처 기본형: + +```powershell +adb -s emulator-5556 shell screencap -p /sdcard/d3ro-capture.png +adb -s emulator-5556 pull /sdcard/d3ro-capture.png +``` + +중단: + +```powershell +adb -s emulator-5556 emu kill +supabase stop --workdir D:\workspace\D3ROVoice\server +``` + +## 9. 인증서와 identity 기준 + +Play app-signing SHA-256: + +```text +01:00:19:21:DB:4F:33:40:85:CE:21:E4:B8:DE:CC:BD: +71:DA:87:67:C5:6E:3B:59:83:2A:A1:C8:29:EA:0D:AB +``` + +Upload key SHA-256: + +```text +4F:AC:69:24:82:1C:50:DA:AB:ED:76:49:32:A5:3C:48: +6F:8C:6C:5F:34:B9:F1:8D:B9:20:AA:40:99:15:2B:54 +``` + +두 값은 서로 달라야 한다. Firebase에는 Google/FCM/auth 요구에 맞춰 필요한 +Play app-signing 및 upload certificate fingerprint를 명확히 구분해 등록한다. +`assetlinks.json`에는 Play가 최종 사용자 APK에 적용하는 app-signing SHA만 둔다. + +## 10. 외부 변경과 확인 경계 + +다음은 action-time 사용자 확인 후에만 한다. + +- Firebase project 최종 생성 +- Firebase Android app 등록과 persistent API identity 생성 +- GitLab secret/protected variable 생성·변경 +- Play internal/closed/production track AAB 업로드와 저장 +- App content, Data safety, 등급, 금융 기능, FGS, AI/UGC 선언 저장 +- reviewer credential 입력 +- merchant/payment/tax profile 변경 +- production review 제출·staged rollout +- public tag/release/updater publish +- cloud account/project/API key 삭제 + +## 11. 금지 사항 + +- `00-launch.png`, `01-runtime.png`, `02-after-bootstrap.png`를 Play 최종 + 스크린샷으로 업로드하지 않는다. +- feature graphic을 앱 스크린샷이라고 부르지 않는다. +- 생성 이미지, mock UI, 로그인/초대 failure evidence를 제품 스크린샷으로 쓰지 않는다. +- debug/E2E APK, sideload installer, self-signed certificate를 production 증거로 쓰지 않는다. +- 실제 사용자의 이메일, 토큰, 음성, 전사, 회의, 팀을 캡처에 노출하지 않는다. +- unknown/nontracked keystore를 비밀번호 추측으로 사용하지 않는다. +- public-trust Windows certificate 없이 stable tag/release를 만들지 않는다. +- Forgejo runner failure에 새 commit/push를 반복하지 않는다. +- dirty worktree 전체를 stage/commit/reset/restore/clean하지 않는다. +- legal/Data safety/production 제출을 자동 진행하지 않는다. + +## 12. 소스 오브 트루스 + +- 모바일 완료 상태: `docs/v3/MOBILE_APP_COMPLETION_SSOT.md` +- 이전 제품화 핸드오프: `docs/v3/MOBILE_PRODUCTIZATION_HANDOFF_2026-08-21.md` +- Play 패키지 안내: `docs/v3/play/README.md` +- Play 등록정보: `docs/v3/play/01-store-listing.md` +- Data safety 초안: `docs/v3/play/02-data-safety-draft.md` +- App content 초안: `docs/v3/play/03-app-content-draft.md` +- 출시 체크리스트: `docs/v3/play/04-release-checklist.md` +- 신고 운영: `docs/v3/play/05-content-reporting-operations.md` +- version SSOT: `release/product-version.json` +- Android release identity: `release/android-release-identity.json` +- CI production job: `.gitlab-ci.yml`의 `mobile-production-release` +- Play asset verifier: `scripts/ci/verify-play-store-assets.mjs` +- release metadata verifier: `scripts/ci/verify-release-metadata.mjs` +- mobile boundary verifier: `scripts/ci/verify-mobile-release-boundary.mjs` + +## 13. 내일 첫 체크포인트 + +내일은 Play Console부터 만지지 않는다. 다음 순서로 시작한다. + +1. Git/프로세스/AVD/local stack 상태 재확인 +2. 이 문서의 `새로 발견된 것`을 재현 가능한 issue로 분리 +3. production-like cold start에서 red screen/blank screen이 없는지 먼저 검증 +4. Firebase/signing/AAB gate를 닫음 +5. Play-signed install에서 핵심 기능 E2E +6. 그 뒤 실제 스토어 스크린샷 촬영과 Play draft 업로드 + +이 순서를 바꾸지 않는다. + +## 14. 2026-08-30 재개 감사 + +이 절은 2026-08-29의 중단 스냅샷을 지우지 않고, 다음 날 다시 확인한 현재 +상태와 새 작업을 추가한다. 이 절의 사실이 앞 절의 역사적 상태와 충돌하면 이 +절을 우선한다. + +### 14.1 현재 상태 재검증 + +- local `main`, `HEAD`, local `origin/main`, local `chanpaca/main`, 실제 + `origin/main`은 계속 + `4872d6f6c50a15104792fb31a97176527c3cd9a2`다. +- 실제 `chanpaca/main` 조회와 Forgejo public/LAN API, run #46 API는 timeout이라 + 현재 원격 상태를 확정하지 못했다. remote-tracking ref만으로 최신 원격 상태를 + 주장하지 않는다. +- 작업 트리는 198개 변경이며 staged는 0개다. 2026-08-29의 197개에 이 핸드오프 + 문서 자체가 untracked로 추가된 수치다. 전체 stage/reset/restore/clean 금지는 + 그대로다. +- Android emulator와 emulator/qemu 프로세스는 0개다. 전용 + `D3RO_PlayCapture_20260829` AVD와 userdata는 디스크에 남아 있다. + `emulator-5554`는 현재 연결돼 있지 않으므로 앞 절의 "연결 상태로 보존"은 + 2026-08-29 종료 시점의 역사적 사실로만 본다. +- Fold6 무선 ADB endpoint가 두 줄로 보이지만 같은 물리기기다. 이를 두 대의 + 기기 증거로 세지 않는다. +- Metro 8081 listener와 D3ROVoice Supabase container는 없다. DB/storage/ + edge-runtime Docker volume은 남아 있지만 이번 감사에서 내용 무결성까지 다시 + 검증한 것은 아니다. +- Firebase CLI와 GCP에서 exact `d3ro-voice-production` project는 없거나 현재 + 계정에서 접근 불가임을 확인했다. Firebase project 생성은 여전히 action-time + 승인 대상이다. +- 현재 인앱 Play Console 세션은 `TWENTYOZ` 조직 계정이며 6개 앱 목록에 + `com.d3ro.voice`와 Play app ID `4976102237698469110`이 없다. 이 계정에서는 + 변경하지 않았다. Chrome 세션도 올바른 개인 개발자 계정으로 연결돼 있지 않다. + 다음 단계 전에 해당 앱을 보유한 개인 Play 계정 로그인이 필요하다. +- GitLab browser 세션은 로그인돼 있지 않고 저장된 Git credential로 variables API + 조회도 401이었다. 따라서 production CI variable 존재 여부를 추측하지 않는다. + +### 14.2 production-like cold start 진단 + +기존 `app-e2e.apk`를 전용 `emulator-5556`에 clean install해 Metro와 local +Supabase 없이 cold start했다. + +- artifact: `apps/mobile-rn/android/app/build/outputs/apk/e2e/app-e2e.apk` +- version: `0.0.0-e2e.20260829.1` / `202608291` +- SHA-256: `0D20ABB8C87B088C308E2379A91448F051C13B3778C1D79962C5D004B0F5BABE` +- `DEBUGGABLE=false`, `assets/index.android.bundle` 포함 +- signer: Android Debug +- 판정: embedded-bundle 시작 진단용 TEST-DEMO이며 production/Play 증거가 아님 + +진단 원본은 `scratch/mobile-release-coldstart-20260830/`에 둔다. + +| 프레임 | SHA-256 | 관찰 | +| ------------------- | ------------------------------------------------------------------ | ------------------------------------------ | +| `frame-00330ms.png` | `0D718DB0947DD7CDD598C13DA36EF06440A12ED418B9ACABE28806832678C2AC` | Android launch splash와 실제 launcher icon | +| `frame-01828ms.png` | `163BC50C7AA0EB8227E7803719454256B5255359A5CB2FD6E2326878B26D23CA` | 앱 bootstrap spinner | +| `frame-02526ms.png` | `3A9C6F58B2CD0FBA33D39074D659FEB6A4B2807FF8E6CD9BD8EE7DD0CBE2B624` | onboarding 1/5 표시 | +| `frame-08003ms.png` | `DE53F4EF82F6D1714E9D13CA2B02104FE3F4C71015C3F689ED4DE3CDFF924BAA` | 같은 onboarding이 안정적으로 유지됨 | + +Android log의 `Displayed com.d3ro.voice/.MainActivity`는 `+1s606ms`였다. fatal +exception, red screen, `Unable to load script`, 흰 화면 고착은 재현되지 않았다. +Firebase 미구성 경고와 새 설치의 Keychain entry 없음 로그는 남았으며 production +Firebase 설치본에서 다시 확인해야 한다. 진단 뒤 `emulator-5556`을 다시 종료했고 +Metro/Supabase는 시작하지 않았다. + +따라서 2026-08-29의 빈 흰 화면은 현재까지 debug+Metro bootstrap에서 본 +증거다. embedded bundle 경로의 영구 결함으로 일반화하지 않는다. production +Play-signed 설치본의 cold start 검증은 여전히 RED다. + +### 14.3 새로 확인한 release artifact 경계 + +- 저장소 어디에도 production AAB가 없다. +- 저장소/CI 작업 디렉터리에 materialize된 `google-services.json`, + `android/app/release.keystore`, release evidence private key, release + snapshot/publication은 없다. +- 저장소 밖 upload PKCS12와 Ed25519 evidence private key는 실제로 남아 있다. + `verify-windows-android-upload-key.ps1`은 alias + `d3ro-upload-20260821`, certificate SHA-256 + `4F:AC:69:24:82:1C:50:DA:AB:ED:76:49:32:A5:3C:48:6F:8C:6C:5F:34:B9:F1:8D:B9:20:AA:40:99:15:2B:54`, + private-key readback과 Credential Manager target을 다시 GREEN으로 확인했다. + evidence keypair도 keyId + `2797d3e63affd2348941bc2871b57e520c958f7f5da1a4bbe8aa46904a890b7f`와 + sign/verify roundtrip이 GREEN이다. 없는 것은 CI secret materialization, + offline 복구 백업, 실제 AAB signer/evidence다. +- `npm run release:mobile:config`는 실제 production config가 없으므로 + `google_services_file_unreadable`로 fail-closed한다. +- 기존 `android/app/build/outputs/apk/release/app-release.apk`는 2026-08-20의 + 오래된 `1.0/1`이다. SHA-256은 + `8530D5ABEE72DB00E332B1362D5B3568CFBD080405D15DC27125F66E18A354B3`, signer + SHA-256은 `06EEC757...2E481`이며 현재 verifier가 차단한다. 필수 Whisper + model도 없어 `missing_entry_assets/models/ggml-tiny.bin`으로 실패한다. + 절대 재사용하지 않는다. +- local Node는 `.nvmrc`의 `24.19.0`이 아니라 `23.4.0`이다. production artifact는 + 이 dirty local checkout에서 만들지 않고 고정된 CI image/toolchain에서 만든다. + +### 14.4 AAB/tag 교착의 소스 수정 + +기존 GitLab job은 exact stable tag가 있어야만 production AAB를 만들 수 있었지만, +이 문서는 Windows public-trust signing 전 stable tag를 금지했다. 이 순서는 실행 +불가능했다. + +다음 두 파일을 좁게 수정했다. + +- `.gitlab-ci.yml` +- `scripts/ci/verify-mobile-release-boundary.mjs` + +새 계약: + +- protected exact `main`에서는 `mobile-production-release`를 수동 실행해 + Maintainer 전용 candidate AAB/evidence만 만든다. +- branch candidate evidence ref는 `refs/heads/main`이다. +- exact stable semver tag 경로는 계속 지원하며 tag evidence ref를 쓴다. +- `publish-release`는 계속 tag-only다. main candidate가 public release를 + 트리거하지 않는다. + +mobile boundary self-test, release metadata self-test, YAML parser 2종, job shell +syntax, `git diff --check`는 GREEN이다. 두 코드 파일은 staged/committed/pushed되지 +않았고 이 핸드오프 문서도 untracked다. 사용자 확인 후 이 세 파일만 좁게 +commit/push해야 실제 GitLab candidate job을 사용할 수 있다. + +### 14.5 Play-signed 캡처 기기 교착 + +현재 production release 계약은 `arm64-v8a`만 허용하지만 전용 Pixel 7 AVD는 +`x86_64`다. 따라서 현 AAB의 Play-signed APK를 이 AVD에 설치할 수 있다고 +가정하면 안 된다. + +Google Play의 현재 공식 요구는 휴대전화 screenshot의 최소 변 320px, 최대 변 +3840px, 긴 변이 짧은 변의 2배 이하이며, 추천 노출은 1080×1920 이상의 9:16 +세로 이미지 4장 이상이다. 실제 앱 경험을 보여야 한다. + +- 규격 근거: + `https://support.google.com/googleplay/android-developer/answer/9866151` +- Play-signed universal/device APK 근거: + `https://support.google.com/googleplay/android-developer/answer/9844279` + +최종 선택지는 다음 중 하나를 production AAB 전에 확정한다. + +1. arm64 Fold6에 Play internal 설치 후 실제 UI를 왜곡하지 않는 9:16 store + canvas로 구성하고 Console 수락을 검증한다. +2. release AAB에 검증된 `x86_64` 지원을 추가하고 verifier/CI/실기기 matrix를 + 함께 확장한 뒤 전용 AVD에서 Play 설치를 검증한다. + +단순 crop/stretch, debug/E2E 재촬영, installer/certificate 검증 없는 sideload는 +대안이 아니다. + +### 14.6 현재 재개 순서와 승인 요청 + +1. 위 두 CI 파일과 이 핸드오프 문서의 좁은 commit/push를 승인받는다. +2. Firebase project `d3ro-voice-production` 최종 생성과 Android app + `com.d3ro.voice` 등록을 action-time 승인받는다. +3. 올바른 개인 Play Console 계정과 GitLab Maintainer 세션에 로그인한다. +4. Firebase fingerprints/config를 검증하고 GitLab protected/masked/hidden + variables를 구성한다. +5. protected exact `main` candidate job과 emulator prerequisite를 실행한다. +6. AAB/evidence를 독립 검증한 뒤 별도 승인으로 Play internal track에 올린다. +7. Play-signed arm64 설치본으로 핵심 E2E를 통과한다. +8. 실제 UI 기반 6장만 제작·검증·업로드한다. +9. `linux-builder`와 Forgejo 접근 경로를 복구한 뒤 run #46만 재실행한다. + +Firebase 생성, GitLab variable 변경, Play 업로드, legal declaration, tag/public +release는 이번 감사에서 실행하지 않았다. diff --git a/scripts/ci/verify-mobile-release-boundary.mjs b/scripts/ci/verify-mobile-release-boundary.mjs index 268acbe..457949a 100644 --- a/scripts/ci/verify-mobile-release-boundary.mjs +++ b/scripts/ci/verify-mobile-release-boundary.mjs @@ -190,6 +190,47 @@ function verifySourceContracts() { assert(releaseWorkflow.includes('test "$GITHUB_SHA" = "$(git rev-parse origin/main)"'), 'dispatch_main_sha_guard_missing') assert(releaseWorkflow.includes('test "$RELEASE_TAG" = "v$VERSION_NAME"'), 'release_tag_identity_guard_missing') + const gitlabWorkflow = readWorkspaceFile('.gitlab-ci.yml') + const mobileProductionJobStart = gitlabWorkflow.indexOf('mobile-production-release:\n') + const publishReleaseJobStart = gitlabWorkflow.indexOf('\npublish-release:\n') + assert(mobileProductionJobStart >= 0, 'gitlab_mobile_production_job_missing') + assert(publishReleaseJobStart > mobileProductionJobStart, 'gitlab_publish_release_job_missing') + const mobileProductionJob = gitlabWorkflow.slice(mobileProductionJobStart, publishReleaseJobStart) + const publishReleaseJob = gitlabWorkflow.slice(publishReleaseJobStart) + for (const required of [ + 'git fetch --no-tags origin main:refs/remotes/origin/main', + 'test "${CI_COMMIT_REF_PROTECTED:-}" = "true"', + 'test -z "${CI_COMMIT_BRANCH:-}"', + 'test "$CI_COMMIT_TAG" = "v${VERSION_NAME}"', + 'test "$(git rev-parse "refs/tags/${CI_COMMIT_TAG}^{commit}")" = "$CI_COMMIT_SHA"', + 'RELEASE_GIT_REF="refs/tags/${CI_COMMIT_TAG}"', + 'test "${CI_COMMIT_BRANCH:-}" = "main"', + 'test "$CI_COMMIT_SHA" = "$(git rev-parse origin/main)"', + 'RELEASE_GIT_REF="refs/heads/main"', + '--git-ref "$RELEASE_GIT_REF"', + '--expected-git-ref "$RELEASE_GIT_REF"', + '$CI_COMMIT_REF_PROTECTED == "true" && $CI_COMMIT_TAG =~ /^v\\d+\\.\\d+\\.\\d+$/', + '$CI_COMMIT_REF_PROTECTED == "true" && $CI_COMMIT_BRANCH == "main"', + ]) { + assert( + mobileProductionJob.includes(required), + `gitlab_mobile_candidate_contract_missing_${required.replace(/[^a-z0-9]+/gi, '_')}`, + ) + } + assert( + (mobileProductionJob.match(/\n\s+when: manual/g) ?? []).length === 2, + 'gitlab_mobile_candidate_manual_rules_invalid', + ) + assert( + !mobileProductionJob.includes('--git-ref "refs/tags/$CI_COMMIT_TAG"'), + 'gitlab_mobile_evidence_ref_tag_only', + ) + assert( + publishReleaseJob.includes('$CI_COMMIT_TAG =~ /^v\\d+\\.\\d+\\.\\d+.*$/'), + 'gitlab_public_release_stable_tag_rule_missing', + ) + assert(!publishReleaseJob.includes('CI_COMMIT_BRANCH'), 'gitlab_public_release_branch_trigger_enabled') + const packageJson = JSON.parse(readWorkspaceFile('package.json')) assert( packageJson.scripts?.['release:mobile:boundary'] === 'node scripts/ci/verify-mobile-release-boundary.mjs',