# D3RO Voice 릴리스 가이드 기준일: 2026-09-18. 이 문서는 desktop GitLab 패키지·자동 업데이트와 mobile store release의 경계를 분리한다. 태그 생성이나 HTTP 200 하나만으로 배포 완료를 선언하지 않는다. ## 현재 release identity | 항목 | 정본 | 현재 판정 | |---|---|---| | 제품 버전 | `release/product-version.json`: `1.3.0` | source SSOT 확정 | | Android | versionCode `1030001` | production AAB 미생성 | | iOS | build `1030001` | production archive 미검증 | | Android upload key | alias `d3ro-upload-20260821`, cert SHA-256 `4F:AC:69:24:...:15:2B:54` | external PKCS12·user-only ACL·Credential Manager·private-key readback GREEN; CI secret·복구 백업·AAB signer 대조 대기 | | release evidence | Ed25519 public `release/mobile-release-evidence-public.pem`, keyId `2797d3e6...4a890b7f` | external private key ACL·roundtrip GREEN; CI private-key secret·복구 백업 대기 | | desktop offline license | Ed25519 public `apps/desktop/resources/license/production-public.pem`, keyId `5c52b765...81a887f` | 새 전용 keypair·external private ACL·roundtrip·desktop production build GREEN; admin `ADMIN_LICENSE_PRIVATE_KEY` secret 주입 대기 | | Windows Authenticode | external public-trust code-signing certificate | production PFX 없음. Forgejo 저장소 시크릿 4종(`WIN_CSC_LINK`/`WIN_CSC_KEY_PASSWORD`/`WIN_CSC_EXPECTED_SIGNER_SUBJECT`/`FORGEJO_TOKEN`)이 0건이라 릴리스 파이프라인이 fail-closed. `scripts/ci/set-forgejo-secrets.mjs --check`로 확인한다. GREEN 전까지 게시 금지 | | Firebase | Console `u/0`, `u/1` 모두 D3RO project 없음 | 사용자 승인 후 project·Android app 생성 필요 | | AdMob | app `ca-app-pub-1039714767792854~6427959892`; banner `/9840591290`; rewarded `/2255790918` | SSOT 확정. `검토 필요`·`광고 게재 제한`·store 미연결·결제 프로필 미완료 | | updater feed (canonical) | `https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/latest` | Forgejo Generic Registry. GitLab project 1172은 legacy mirror | | release notes | `CHANGELOG.md` `## [1.3.0]` + Play changelog `1030001.txt` (ko/en) | 태그 전 확정·검증 필수 | | 직전 게시본 | Forgejo Release `v1.1.0` (2026-09-15 게시, unsigned installer) | `v1.2.0`·`v1.3.0` 파이프라인은 Forgejo 저장소 시크릿이 없어 실패 = 게시본 없음. 설치본이 있는 마지막 버전은 `1.1.0` | live canonical feed(`git.chanpaca.net/.../d3ro-voice/latest`)의 `latest.yml`은 현재 `1.1.0`을 보고한다. 시크릿을 채우고 `v1.3.0` 파이프라인이 GREEN이 되면 그 값이 `1.3.0`으로 올라가고, 그때부터 기존 설치본이 자동 업데이트를 받는다. ## desktop 릴리스 파이프라인 ```text authoritative release commit → version/check/test/build GREEN → annotated tag v1.3.0 → package-windows (build-win-x64) → package-macos (build-mac-arm64) → publish-release (build-linux-x64) ├─ publish-forgejo-release.mjs ← canonical │ ├─ Forgejo Generic Registry /d3ro-voice/1.3.0/ (버전별 보존) │ ├─ Forgejo Generic Registry /d3ro-voice/latest/ (updater feed + update-policy.json) │ └─ Forgejo Release + CHANGELOG notes + 자산 첨부 └─ publish-gitlab-release.mjs ← legacy mirror (pre-Forgejo 설치본) ├─ GitLab Generic Registry /d3ro-voice/1.3.0/ ├─ GitLab Generic Registry /d3ro-voice/latest/ └─ GitLab Release ``` - Forgejo·GitHub Actions도 동일한 `publish-forgejo-release.mjs`로 수렴한다. 어느 빌더가 성공해도 canonical feed는 하나다. - `publish-forgejo-release.mjs`는 설치 자산을 먼저, `latest.yml`을 마지막에 게시하고, 공개 URL에서 재검증한 뒤 Release 자산을 첨부한다. - 설치 바이너리는 저장소에 커밋하지 않는다. 배포 정본은 Forgejo feed이며, `site/public/releases/*/`, `apps/web/public/releases/*/`, `apps/api-server/wwwroot/releases/*/`는 build graph 밖이라 배포 산출물에 포함되지 않는다. 사이트·웹 다운로드 센터는 로컬 경로가 아니라 feed URL을 링크한다. (역사적 `1.0.0` 자산만 추적 상태로 남아 있다.) - `scripts/ci/sync-version.mjs --check --tag v1.3.0`는 태그, `release/product-version.json`, package/lockfile, Android/iOS 버전 면의 일치를 fail-closed로 검증한다. - `scripts/ci/verify-release-metadata.mjs`는 배포 메타데이터와 CI/publisher 계약을 검증한다. - 같은 gate는 desktop license public key가 Ed25519이고 `release/product-version.json`의 `desktopLicensePublicKeyId`와 일치하는지 검증한다. `electron.vite.config.ts`는 이 파일을 직접 읽으므로 누락·손상된 키로는 build가 시작되지 않는다. - `scripts/ci/publish-forgejo-release.mjs`는 canonical이다. 버전별 패키지를 먼저 올리고, `latest`에서 설치 자산 참조를 검증한 뒤 `latest.yml`과 `update-policy.json`을 마지막에 게시하고 공개 URL에서 재검증한다. `scripts/ci/publish-gitlab-release.mjs`는 legacy mirror로 동일 자산을 GitLab에도 올린다. - Windows installer와 `latest.yml`은 필수다. macOS 산출물이 없는 Windows-only release를 의도했다면 그 판단을 release record에 남긴다. - `package-windows`는 external public-trust code-signing PFX를 protected file variable `WIN_CSC_PFX_FILE`로, 암호와 exact certificate subject를 protected `WIN_CSC_KEY_PASSWORD`, `WIN_CSC_EXPECTED_SIGNER_SUBJECT`로 받아야 한다. GitHub Release도 `WIN_CSC_LINK`, `WIN_CSC_KEY_PASSWORD`, `WIN_CSC_EXPECTED_SIGNER_SUBJECT`가 모두 없으면 실패한다. - `scripts/ci/verify-windows-release-artifact.ps1`는 installer와 unpacked app의 Authenticode `Valid`, exact signer subject, non-self-signed code-signing EKU, PE version, `latest.yml` path/size/SHA-512를 검증한다. 로컬 self-signed `Everything2EverythingDev`는 production 신뢰 인증서가 아니며 gate에서 명시적으로 거부한다. - 현재 GitLab Linux runner는 Shell executor이므로 job의 `image:`를 툴체인 근거로 간주하지 않는다. `.gitlab-ci.yml`은 `scripts/ci/bootstrap-linux-toolchain.sh`를 source해 .NET 10.0.302, Deno 2.8.1, Microsoft OpenJDK 17.0.20.1, Android command-line tools 15859902를 고정 해시로 검증·캐시한 뒤 실행한다. ## 자동 업데이트 계약 `apps/desktop/src/main/update-feed.ts`와 `apps/desktop/electron-builder.yml`은 버전 없는 같은 canonical Forgejo URL을 가리켜야 한다. ```text https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/latest ``` legacy mirror(제거 예정, 런타임 참조 금지): ```text https://gitlab.twentyoz.kr:8443/api/v4/projects/1172/packages/generic/d3ro-voice/latest ``` 아래 면이 하나라도 깨지면 release를 중단한다. 1. Forgejo Generic Registry의 무인증 public pull이 허용됐다 (`FORGEJO_TOKEN`으로 게시, 익명으로 읽기). 2. `latest.yml`의 `version`이 태그와 일치한다. 3. `latest.yml` URL/path가 같은 `latest` 경로의 실제 installer를 참조한다. 4. installer 파일명에 공백이 없다: `D3RO-Voice-Setup--x64.exe`. 5. metadata SHA-512와 다운로드한 installer가 일치한다. 6. installer와 `win-unpacked/D3RO Voice.exe` 모두 external public-trust certificate의 Authenticode `Valid`이고, signer subject가 protected CI identity와 정확히 일치한다. 7. 이전 실제 설치본이 feed를 탐지하고, 다운로드·재시작·버전 상승을 끝까지 완료한다. 2026-09-16 live canonical `latest.yml`은 `1.1.0`을 보고한다(2026-09-15 hand-publish). 그 installer는 Authenticode 서명이 없으므로 정책상 정상 게시본이 아니다. `1.2.0`은 서명 gate를 통과한 CI build로 이 값을 대체하는 forward-fix다. legacy GitLab feed는 여전히 `0.2.1-alpha`다. ## 업데이트 채널과 메이저/증분 정책 정책 정본은 `release/update-policy.json`이고, 런타임 로직은 `apps/desktop/src/main/update-policy.ts`다. publisher가 이 파일을 feed 루트에 게시하면 실행 중 앱이 내려받아 적용한다. | 필드 | 의미 | |---|---| | `defaultChannel` / `channels` | `latest`(stable) · `beta` · `alpha`. `allowPrerelease`가 false면 stable 고객은 prerelease를 받지 않는다 | | `minimumSupportedVersion` | 이 미만 설치본은 업데이트가 **필수**(연기·건너뛰기 불가) | | `forceInstallBelow` | 이 미만은 다이얼로그 없이 즉시 설치. null이면 비활성 | | `fullInstallOnMajorChange` | major 승격 시 blockmap 차분 대신 전체 설치자 | | `fullInstallVersionGap` | 같은 major에서 minor 갭이 이 값 이상이면 전체 설치자 | | `stagingPercentage` | stable 업데이트를 노출할 사용자 비율(0~100). 강제 업데이트는 무시 | | `killSwitch` | true면 업데이트 확인 자체를 중단 | - **증분(delta)**: 기본. `.blockmap`으로 변경 블록만 받는다. 첫 업데이트는 항상 full이다. - **전체(full)**: 현재 버전을 신뢰할 수 없거나, major 승격이거나, minor 갭이 임계 이상일 때 `disableDifferentialDownload`를 켠다. - **회수(rollback)**: `stagingPercentage`를 낮추거나 `killSwitch`를 켠다. 이미 배포된 버전은 되돌리지 않고 더 높은 patch로 forward-fix한다. ## `1.3.0` 릴리스 절차 (canonical = Forgejo Actions) **선행 조건 — Forgejo 저장소 시크릿.** `.forgejo/workflows/release.yml`은 아래 4개가 없으면 fail-closed로 중단한다. 실측(2026-09-18): 저장소 시크릿이 0건이라 `v1.2.0`과 `v1.3.0` 태그 파이프라인이 서명 가드에서 실패했고, 그래서 설치본·업데이트가 게시되지 않았다. 현재 상태 점검과 등록은 다음 한 줄로 한다. ```bash node scripts/ci/set-forgejo-secrets.mjs --check node --env-file-if-exists=.env scripts/ci/set-forgejo-secrets.mjs --write ``` | 시크릿 | 의미 | |---|---| | `WIN_CSC_LINK` | public-trust Authenticode PFX (base64). 개발용 `Everything2EverythingDev`는 거부된다 | | `WIN_CSC_KEY_PASSWORD` | PFX password | | `WIN_CSC_EXPECTED_SIGNER_SUBJECT` | 인증서의 정확한 subject. `verify-windows-release-artifact.ps1`이 이 identity를 요구한다 | | `FORGEJO_TOKEN` | `write:package` + `write:repository` (릴리스 게시 단계) | 1. `release/product-version.json`의 version/build 값과 모든 버전 면을 `npm run version:check`로 대조한다. 2. `CHANGELOG.md` `## [1.3.0] - 2026-09-18` 섹션을 사용자 변경점 중심으로 확정한다. publisher는 이 섹션과 그에 대응하는 Play changelog(`apps/mobile-rn/metadata/android/*/changelogs/.txt`)가 없으면 실패한다. 3. dirty/untracked 작업을 임의로 reset·clean하지 말고, release 범위만 검토 가능한 authoritative commit으로 보존한다. 4. 같은 commit에서 lint, typecheck, test, build, release metadata·security·artifact gate를 전부 GREEN으로 만든다. 5. **로컬 전사 엔진 검증**: 파이프라인은 `sidecar:setup` → `sidecar:build` → `verify-sidecar-bundle.mjs`를 패키징 전에 실행한다. 이 게이트가 없으면 설치본에 엔진이 빠진 채 게시된다(과거 실제 사고). 로컬에서 `electron-builder --dir`만 볼 때는 서명 실패로 extraResources 복사가 중간에 끊기므로 `-c.win.forceCodeSigning=false`로 확인한다. 6. desktop offline license를 제공한다면 external private key를 admin의 `ADMIN_LICENSE_PRIVATE_KEY` secret로 주입하고, 저장소 public key와 sign/verify roundtrip 및 발급 감사 로그를 확인한다. 7. 이미 게시된 버전보다 높은 annotated 태그 `v1.3.0`을 생성해 push한다. `npm run release:tag -- --dry-run`으로 검증한 뒤 `npm run release:tag`(GPG 사용 시 `-- --sign`)와 `git push chanpaca v1.3.0`를 실행한다. 태그는 불변이며 게이트를 시작하는 후속 단계지 검증을 대체하지 않는다. 이미 게시된 버전을 재게시하지 않는다: canonical publisher는 버전별 자산이 다른 바이트를 가지면 fail-closed로 중단한다. 8. 시크릿을 나중에 채웠다면 태그를 새로 만들 필요가 없다 — `release.yml`은 `workflow_dispatch`를 지원하므로 Forgejo UI에서 해당 태그 ref로 수동 실행한다. 9. Forgejo Actions run(`/actions/tasks` API 또는 UI)에서 단계별 결과를 확인한다. pending/stuck/skipped를 GREEN으로 기록하지 않는다. 10. Forgejo Release note/asset, `latest.yml`, `update-policy.json`, installer hash를 외부 public URL에서 다시 검증한다. 11. 이전 설치본에서 자동 업데이트 E2E를 실행하고 실행 중 버전·프로세스·사용자 데이터 보존, 그리고 **업데이트 후 로컬 받아쓰기 1회 성공**을 확인한다. ## 서명 없이 내놓기 (portable 채널) 인증서가 없어도 사용자가 설치할 수 있어야 할 때 사용한다. 자세한 조사·비교·제약은 [`unsigned-distribution.md`](./unsigned-distribution.md)에 있다. ```bash npm run release:portable:build # 7z 분할 볼륨 + Scoop 매니페스트 생성 npm run release:portable:check # 게시 예정 목록 확인(실제 업로드 없음) npm run release:portable # Forgejo portable 채널 게시 ``` - 자동 업데이트 피드(`latest.yml`)는 **건드리지 않는다** — 서명된 릴리스 전용이다. - 볼륨은 불변이다: 같은 버전 경로에 다른 바이트가 있으면 게시가 중단된다. - 태그/수동 실행 워크플로: `.forgejo/workflows/portable.yml` (필요 시크릿: `FORGEJO_TOKEN`). - 사용자 설치: Scoop 버킷(`bucket/`) 또는 `install-d3ro-voice.ps1`. ## `1.2.0` 릴리스 절차 (기록) 1. `release/product-version.json`의 version/build 값과 모든 버전 면을 `npm run version:check`로 대조한다. 2. `CHANGELOG.md` `## [1.2.0] - 2026-09-16` 섹션을 사용자 변경점 중심으로 확정한다. publisher는 이 섹션이 없으면 실패해야 한다. 3. dirty/untracked 작업을 임의로 reset·clean하지 말고, release 범위만 검토 가능한 authoritative commit으로 보존한다. 4. 같은 commit에서 lint, typecheck, test, build, release metadata·security·artifact gate를 전부 GREEN으로 만든다. Windows는 production Authenticode PFX를 주입한 CI build와 `verify-windows-release-artifact.ps1` GREEN이 필수다. 5. desktop offline license를 제공한다면 external private key를 admin의 `ADMIN_LICENSE_PRIVATE_KEY` secret로 주입하고, 저장소 public key와 sign/verify roundtrip 및 발급 감사 로그를 확인한다. 6. 이미 게시된 버전보다 높은 annotated 태그 `v1.2.0`을 생성해 push한다. `npm run release:tag -- --dry-run`으로 검증한 뒤 `npm run release:tag`(GPG 사용 시 `-- --sign`)와 `git push origin v1.2.0`를 실행한다. 태그는 불변이며 게이트를 시작하는 후속 단계지 검증을 대체하지 않는다. 이미 게시된 버전을 재게시하지 않는다: canonical publisher는 버전별 자산이 다른 바이트를 가지면 fail-closed로 중단한다. 7. GitLab에서 package-windows, package-macos, publish-release와 의도한 mobile job 상태를 모두 확인한다. publish-release가 Forgejo와 GitLab 양쪽에 게시했는지 로그로 확인한다. pending/stuck/skipped를 GREEN으로 기록하지 않는다. 8. Forgejo Release note/asset, `latest.yml`, `update-policy.json`, installer hash를 외부 public URL에서 다시 검증한다. 9. 이전 설치본에서 자동 업데이트 E2E를 실행하고 실행 중 버전·프로세스·사용자 데이터 보존을 확인한다. ## mobile release와의 경계 Desktop release를 게시해도 Android production 출시가 자동으로 완료되지 않는다. Android는 다음을 별도로 증명한다. - 준비된 local upload/evidence key와 AdMob identity를 protected CI secret에 주입하고, 사용자 승인 후 생성한 production Firebase config와 함께 version `1.2.0`, versionCode `1020001` AAB 생성 - package/config/upload signer/ABI/16 KB page size/signed provenance GREEN - public APK 게시 없이 Play internal track에 제한 업로드 - Play-signed 실기기 E2E, 12명·연속 14일 closed test, production access 승인 - Data safety/App content/production 선언은 사용자의 action-time 검토·승인 후에만 제출 현재 local upload/evidence key는 존재하지만 CI secret 주입·복구 백업·production Firebase·AAB가 없고 Play production access도 disabled이므로 Android production 출시는 RED다. ## rollback - Desktop: 게시 전이면 `latest` metadata를 바꾸지 않는다. 이미 업데이트된 클라이언트는 downgrade하지 말고 더 높은 patch 버전으로 forward-fix한다. - Play: staged rollout을 중단하고 더 높은 versionCode의 수정 AAB를 새로 검증·배포한다. - 복구 작업도 release record에 artifact hash, 시각, 판단자, 영향 범위를 남긴다. ## 참고 파일 - `release/product-version.json` — 제품 version/build SSOT - `release/android-release-identity.json` — Play app/package/certificate identity - `release/mobile-release-evidence-public.pem` — release evidence public key - `apps/desktop/resources/license/production-public.pem` — desktop offline license public key SSOT - `scripts/ci/sync-version.mjs` — 버전 면 동기화·검증 - `scripts/ci/build-portable.mjs` — 서명 없는 portable 산출물(7z 분할 볼륨 + Scoop 매니페스트) - `scripts/ci/publish-portable-release.mjs` — portable 채널 게시(불변 가드) - `scripts/install/install-d3ro-voice.ps1` — 수동 설치 스크립트(해시 검증 + 결합 + 해제) - `scripts/ci/create-release-tag.mjs` — 릴리스 태그 게이트 (annotated/서명, 불변) - `scripts/ci/verify-release-metadata.mjs` — release metadata 자가 검증 - `scripts/ci/verify-windows-release-artifact.ps1` — Windows version·updater metadata·Authenticode gate - `scripts/ci/publish-gitlab-release.mjs` — legacy GitLab registry·Release mirror publisher - `apps/desktop/src/main/update-feed.ts` — runtime updater URL SSOT (canonical Forgejo + legacy mirror 상수) - `release/update-policy.json` — 채널·최소 지원 버전·강제 업데이트·full/delta·staged rollout·킬 스위치 SSOT - `apps/desktop/src/main/update-policy.ts` — 정책 파싱·결정 순수 로직 - `scripts/ci/publish-forgejo-release.mjs` — canonical Forgejo registry·Release·feed publisher - `apps/desktop/electron-builder.yml` — builder publish URL·artifact contract - `docs/deployment/update-system-assessment.md` — 평가·2026 방법론·ADR - `docs/v3/play/04-release-checklist.md` — Play Console·AAB·closed test·production gate