- docs/REFACTOR_WAVE3_REPORT.md and the Wave 3 policy: canonical map, production changes, verification and remaining external steps. - Gap backlog: GAP-BILL-01 resolved; new GAP-BILL-02 (Payple renewal never ran, Payple client key never set), GAP-WEB-01 (tunnel host for /app), GAP-OPS-01 (NAS compose/.env drift), GAP-CI-01, GAP-I18N-02, GAP-TEAM-02. - design.md: hero loop decision (numbers taken from the app capsule), pricing mismatch closed; feature catalog SHELL-11 updated. - docs/map, release guide and mobile release docs no longer describe the deleted wwwroot, binaries, Dockerfile.admin, NAS site copy or .github CI. - docker-compose.nas.yml gains the d3ro-whisper service that only existed in the NAS copy, so the repository file is the complete definition. - refactor-wave skill: Wave 3 index and lessons P10-P12.
272 lines
25 KiB
Markdown
272 lines
25 KiB
Markdown
# D3RO Voice 릴리스 가이드
|
|
|
|
기준일: 2026-09-18. 이 문서는 desktop GitLab 패키지·자동 업데이트와 mobile store release의 경계를 분리한다. 태그 생성이나 HTTP 200 하나만으로 배포 완료를 선언하지 않는다.
|
|
|
|
## 현재 release identity
|
|
|
|
| 항목 | 정본 | 현재 판정 |
|
|
|---|---|---|
|
|
| 제품 버전 | `release/product-version.json`: `1.6.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.5.0]` + Play changelog `1050000.txt` (ko/en) | 태그 전 확정·검증 필수 |
|
|
| 직전 게시본 | Forgejo Generic Registry `1.6.0` (2026-09-24 게시, unsigned installer + runtime/portable) | `v1.2.0`·`v1.3.0` 파이프라인은 서명/사이드카 선행 조건에서 실패했고 `v1.4.0`은 게시되지 않았다. `1.3.2`·`1.3.7`·`1.5.0`·`1.6.0`은 무서명 로컬 게시 경로(GAP-REL-06)를 사용했다 |
|
|
|
|
live canonical feed(`git.chanpaca.net/.../d3ro-voice/latest`)의 `latest.yml`은 현재 `1.6.0`을 보고한다(2026-09-24 무서명 로컬 게시). `runtime-latest/runtime.json`과 `portable-latest/portable.json`도 `1.6.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/*/`는 `.gitignore`로
|
|
막혀 있어 build graph 밖이고 배포 산출물에 포함되지 않는다(추적 상태로 남아
|
|
있던 19개 설치본 ~568MiB는 2026-09-26 Wave 3, cd9d199에서 삭제됐고 그 뒤로는
|
|
추적 자산이 없다). `apps/api-server/wwwroot/`는 정적 파일 서빙 자체와 함께
|
|
통째로 삭제됐다(같은 커밋) — API는 더 이상 다운로드/릴리스 자산을 서빙하지
|
|
않는다. 사이트·웹 다운로드 센터는 로컬 경로가 아니라 feed URL을 링크한다.
|
|
|
|
- `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-<version>-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.6.0` 릴리스 기록 (2026-09-24)
|
|
|
|
1.5.0과 같은 **무서명 로컬 게시** 경로.
|
|
|
|
1. `release/product-version.json` → `1.6.0`(android/iOS `1060000`, 2026-09-24), `CHANGELOG.md` `## [1.6.0]`, Play 변경 기록 `1060000.txt`(ko-KR/en-US). `version:sync` → `version:check` GREEN.
|
|
2. 게이트 GREEN: `version:check`, `release:metadata`, `security:secrets`, `check:design`(0), `lint`, 전 워크스페이스 `typecheck`, 데스크톱 빌드, `check:desktop-renderer`(7 pages). 데스크톱 테스트(Node ABI 구성) 1,504 pass / 2 fail — 실패 2건은 환경 의존 기존 항목(RAG 임베딩, 캡션 타임아웃; `test_env_traps`). 코어 131/131.
|
|
3. 커밋 `2fe20fa` → `npm run release:tag` → annotated `v1.6.0` → `chanpaca`·`origin` main+tag push.
|
|
4. 런타임/포터블: `sidecar:build`(새 PyInstaller 빌드, `apps/desktop/sidecar-dist`) → `release:portable:build` → `release:portable`. `runtime-latest/runtime.json`·`portable-latest/portable.json` = `1.6.0`.
|
|
5. 업데이터: `release:updater -- --build --ack-unsigned`. `D3RO-Voice-Setup-1.6.0-x64.exe` 95,628,392 bytes, `latest.yml` 1.6.0, sha512 로컬 일치, 익명 206.
|
|
6. 사이트: 로컬 wrangler 배포, `release-identity.json` = commit `2fe20fa` / 1.6.0.
|
|
7. 런타임 호환: 앱은 사이드카 **최소 버전 1.5.0**(`RUNTIME_MIN_VERSION`)만 요구한다. 이미 1.5.0 이상 엔진을 가진 설치는 1.6.0 업데이트 때 엔진을 다시 받지 않고, 1.5.0 이전 엔진(마커 없음)만 한 번 다시 받는다.
|
|
8. **로컬 설치 파일 함정:** `better-sqlite3`가 Node ABI인 상태로 포장하면 설치본이 DB 없이 뜬다. 포장 전에 Electron ABI(130)인지 `new Database(':memory:')`로 확인한다(`test_env_traps`).
|
|
9. 남은 게이트: 실제 설치본 자동 업데이트 E2E(1.5.0 → 1.6.0), 스트리밍 자막(1.7.0 예정).
|
|
|
|
|
|
## `1.5.0` 릴리스 기록 (2026-09-23)
|
|
|
|
서명 인증서가 없는 상태라 CI 서명 파이프라인 대신 **무서명 로컬 게시**로 내보냈다.
|
|
|
|
1. `release/product-version.json`을 `1.5.0`(android/iOS `1050000`, 2026-09-23)으로 올리고 `npm run version:sync` → `npm run version:check` GREEN.
|
|
2. `CHANGELOG.md` `## [1.5.0] - 2026-09-23` 확정. `site/src/release.ts`와 `apps/web/src/lib/desktop-release.ts`는 `sync-version`이 자동 반영.
|
|
3. 게이트 GREEN: `version:check`, `release:metadata`, `security:secrets`, `check:design`, `lint`, `typecheck`, 데스크톱 빌드, `check:desktop-renderer`.
|
|
4. 커밋 후 `npm run release:tag` → annotated `v1.5.0` → `git push chanpaca main` + `git push chanpaca v1.5.0` (legacy mirror `origin`에도 함께).
|
|
5. 런타임을 먼저 게시: `npm run sidecar:build`(UIA 포함) → `npm run release:portable:build` → `npm run release:portable`. `runtime-latest/runtime.json`과 `portable-latest/portable.json`이 `1.5.0`을 보고하고, 로컬 해시와 원격 sha256이 일치함을 확인했다.
|
|
6. 업데이터 게시: `npm run release:updater -- --build --ack-unsigned`. 설치본 `D3RO-Voice-Setup-1.5.0-x64.exe` = 91.2MiB(95,612,108 bytes), `latest.yml` 버전 `1.5.0`, sha512가 로컬과 일치하며 canonical feed에서 익명 206을 반환한다.
|
|
7. 사이트: CI `deploy` 워크플로는 `CF_API_TOKEN` 시크릿이 없어 여전히 실패한다(GAP-REL-09b). 대신 인증된 로컬 wrangler로 `npm run build --prefix site` → release-identity 작성 → `npx wrangler pages deploy site/dist --project-name d3ro --branch main`을 실행했고, `https://d3ro.chanpaca.net/release-identity.json`이 commit `5c11ee2`, version `1.5.0`을 보고한다. 라이브 번들이 `D3RO-Voice-Setup-1.5.0-x64.exe`를 계산한다.
|
|
8. 게시 중 발견: `publish-portable-release.mjs`의 "동일 파일 건너뛰기"가 1MiB 초과 파일을 크기만으로 비교해, 크기가 우연히 같은 `runtime-latest` 볼륨 일부를 낡은 바이트로 남겼다. 파일 목록 API의 sha256으로 정확히 비교하고, 내용이 다르면 별칭 버전 전체를 지우고 다시 올리도록 수정했다(GAP-REL-11).
|
|
9. **남은 게이트**: 실제 설치본에서 자동 업데이트 E2E, Electron GUI에서 제안 오버레이·수락 삽입·비밀번호 차단 검증(GAP-INPUT-07), 모바일 production은 별개.
|
|
|
|
|
|
|
|
## `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/<versionCode>.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회 성공**을 확인한다.
|
|
|
|
## 자동 업데이트 게시 (런타임 분리 전제)
|
|
|
|
설치본은 **90MiB대**를 유지해야 한다. 로컬 AI 런타임(사이드카 엔진/ffmpeg)을 설치본에 넣으면
|
|
Cloudflare 업로드 한도(100MiB)를 넘어 `latest.yml`을 게시할 수 없다.
|
|
|
|
```bash
|
|
npm run build --workspace=@d3ro/desktop # 렌더러/메인 번들
|
|
npm run release:portable:build # 런타임 번들 생성(엔진/ffmpeg)
|
|
npm run release:portable # portable + runtime 채널 게시
|
|
npm run release:updater:check # 업데이터 게시 예정 확인
|
|
npm run release:updater -- --ack-unsigned # (인증서 없을 때만) 무서명 게시
|
|
```
|
|
|
|
- 서명 인증서가 있으면 `--ack-unsigned` 없이 게시한다(권장). 무서명 게시는 명시적 예외이며
|
|
스크립트가 플래그 없이는 즉시 실패한다(GAP-REL-06에 기록).
|
|
- **네이티브 ABI 게이트 필수**: 패키징 후 `node scripts/ci/verify-native-abi.mjs`로 확인한다.
|
|
개발 PC에서 `npm install`을 돌리면 `better-sqlite3`가 Node ABI로 재빌드되어 설치본이
|
|
시작조차 못 한다(실측: 1.3.2). 로컬에서 실행 중인 Electron이 모듈을 잠그면
|
|
`node scripts/ci/fix-native-abi.mjs --dir <win-unpacked>`로 Electron ABI를 주입한 뒤
|
|
`--prepackaged`로 설치본을 만든다(검증된 트리에서만 패키징).
|
|
- 런타임은 `runtime-latest/runtime.json`이 정본이고 앱이 처음 필요할 때 내려받는다.
|
|
런타임을 바꾸면 반드시 `release:portable`로 먼저 게시한 뒤 설치본을 게시한다.
|
|
|
|
## 서명 없이 내놓기 (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`).
|
|
- 산출물은 7z 분할 볼륨(Scoop용, 162MiB)과 zip 분할 부품(수동 설치용, 243MiB) 두 가지다.
|
|
수동 설치 스크립트는 Windows 내장 `Expand-Archive`만 쓰므로 7-Zip이 필요 없다.
|
|
- 사용자 설치: Scoop 버킷(`bucket/`) 또는 `install-d3ro-voice.ps1`.
|
|
- 한 버전의 산출물은 한 번의 빌드에서만 나온다(볼륨은 불변). CI 태그 파이프라인이 최초 게시자가 되게 하고,
|
|
이미 게시된 버전을 같은 번호로 다시 게시하지 않는다 — 필요하면 버전을 올린다.
|
|
- 로컬에서 게시할 때는 `npm run release:portable:build`가 out/와 sidecar-dist/를 먼저 요구한다
|
|
(`npm run build --workspace=@d3ro/desktop`, `npm run sidecar:build --workspace=@d3ro/desktop`).
|
|
|
|
## `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
|