# D3RO Voice 업데이트 시스템 평가와 2026 방법론 기준일: 2026-09-13. 이 문서는 현재 자동 업데이트·릴리스 체계를 평가하고, 2026년 9월 기준 업계 방법론에 맞춘 목표 아키텍처와 결정 사항을 기록한다. 운영 절차는 [`release-guide.md`](./release-guide.md), 정책 정본은 [`release/update-policy.json`](../../release/update-policy.json)이다. --- ## 1. 결론 **기반은 좋다. 그러나 "업데이트 시스템"이라 부르기엔 미완성이다.** 현재 데스크톱은 `electron-updater` + GitLab Generic Package Registry feed로 동작하며, 버전 SSOT·CHANGELOG gate·메타데이터 자가검증·서명 fail-closed까지 갖춘 릴리스 파이프라인을 이미 보유한다. 이 부분은 업계 표준보다 앞서 있다. 반면 다음은 없다. - **업데이트 채널**: stable/beta/alpha 구분이 없고 `latest.yml` 하나만 존재한다. - **메이저/증분 정책**: major 승격, 최소 지원 버전, 강제 업데이트, 버전 갭에 따른 full 재설치 판단이 전혀 없다. 모든 업데이트가 동일하게 취급된다. - **단계적 롤아웃(staged rollout)과 킬 스위치**: 없다. - **배포 호스트 일관성**: 제품의 공개 배포 허브(admin·site 다운로드)는 Forgejo `git.chanpaca.net/yunchan/d3ro-voice`인데, 정작 updater feed는 GitLab (project 1172)을 가리킨다. 게다가 `verify-release-metadata.mjs`는 Forgejo 릴리스 배포를 **금지**하고 있어 Forgejo로 릴리스할 수 없다. - **업데이트 서명**: `latest.yml` 메타데이터 서명(Ed25519)이 "future work"로만 남아 있다. 코드 서명은 요구하지만 feed 자체 무결성은 TLS + SHA-512에만 의존한다. **판정: `[~]` (부분).** 데스크톱 단일 채널·단일 호스트 업데이터로는 동작하지만, 다중 채널·강제 업데이트·Forgejo 배포·릴리스 관리 방법론을 갖춘 "업그레이드 체계"는 아직 아니다. 이 문서와 함께 추가된 코드가 그 격차를 메운다. --- ## 2. 현재 구현 인벤토리 | 구성 | 위치 | 상태 | |---|---|---| | 업데이터 서비스 | `apps/desktop/src/main/services/UpdateService.ts` | 단일 feed, 4h 주기, 동의 다이얼로그, skip version | | feed SSOT | `apps/desktop/src/main/update-feed.ts` | GitLab project 1172 `latest` (버전 비고정) | | 빌더 publish | `apps/desktop/electron-builder.yml` | `provider: generic`, `detectUpdateChannel: false` | | 버전 SSOT | `release/product-version.json` | `1.1.0` / `1010001` | | 버전 동기화 | `scripts/ci/sync-version.mjs` | 태그·패키지·lockfile·Android/iOS/.NET 일치 fail-closed | | 메타데이터 검증 | `scripts/ci/verify-release-metadata.mjs` | feed·publisher·workflow 계약 self-test | | GitLab publisher | `scripts/ci/publish-gitlab-release.mjs` | 버전별 + `latest`, 설치자산 우선, public 재검증 | | Windows 산출물 gate | `scripts/ci/verify-windows-release-artifact.ps1` | Authenticode·PE version·SHA-512 | | CI | `.gitlab-ci.yml` `package-*` → `publish-release` | 태그 전용 | | Forgejo | `.forgejo/workflows/*` | **사이트 배포만**. 릴리스 배포 금지됨 | | 릴리스 노트 | `CHANGELOG.md` (Keep a Changelog) | publisher가 섹션 누락 시 실패 | --- ## 3. 갭 상세 ### 3.1 채널 부재 `detectUpdateChannel: false`이고 `updater.channel` 설정이 없어 항상 `latest.yml`을 읽는다. 베타/알파 사용자를 분리할 수 없고, prerelease를 안정 채널에 노출하지 않으려면 매번 수동으로 feed를 조작해야 한다. ### 3.2 메이저/증분 판단 부재 `_promptUserConsent`는 버전과 무관하게 동일한 3버튼 다이얼로그를 띄운다. `update-available` 이벤트에 `isMandatory` 필드가 선언돼 있지만 실제로 계산하지 않는다. major 승격(예: 1.x → 2.x)에서 사용자가 "나중에"를 눌러 구버전에 남을 수 있고, 버전 갭이 큰 클라이언트가 differential patch를 시도하다 실패할 수 있다. ### 3.3 Forgejo 배포 금지 `verify-release-metadata.mjs:147-148`은 Forgejo workflow에 `sync-and-publish-forgejo-release`가 포함되면 실패시킨다. 이 스크립트가 버전을 `1.0.0`으로 하드코딩한 legacy이기 때문이다. 결과적으로 Forgejo에는 릴리스가 존재하지만(v1.0.0, 2026-08-20) 최신 체계에서는 갱신되지 않는다. 제품의 공개 다운로드 페이지(`site/`, `apps/web/download`, `apps/admin/releases`)는 모두 Forgejo를 정본으로 본다. ### 3.4 staged rollout / 킬 스위치 부재 `stagingPercentage`를 쓰지 않아 새 버전이 전 사용자에게 즉시 노출된다. 잘못된 릴리스 회수는 "더 높은 버전으로 forward-fix"만 가능하다. ### 3.5 메타데이터 서명 부재 SHA-512는 TLS 종단 간 무결성만 보장한다. Feed 호스트가 침해되면 악성 설치자를 서명 검증 이전에 내려줄 수 있다. 2026년 표준(Doyensec SafeUpdater, Sparkle 2.9 signed feed, Tauri ed25519 강제)은 **매니페스트 서명**을 요구한다. ### 3.6 릴리스 gate 신뢰성 저하 (발견) `apps/desktop/package.json`의 `typecheck`는 `tsc --noEmit`인데 `tsconfig.json`이 `files: []` + `references` 구조라 **아무 파일도 검사하지 않는다**. 실제로 `tsc -p tsconfig.node.json --noEmit`을 돌리면 다수의 기존 오류가 나온다 (`bootstrap.ts`, `support-handlers.ts` 등). 즉 "typecheck GREEN"은 데스크톱 main/preload에 대해 아무 의미가 없고, 릴리스 게이트가 이 착시에 의존한다. (`npm run lint`와 vitest는 정상 동작.) ### 3.7 버전 번호 이력 불일치 git 태그는 `v1.0.0`에서 멈춰 있고, live `latest.yml`은 `0.2.1-alpha`다. 제품 버전 정본은 `1.1.0`이다. 태그→릴리스 파이프라인은 "같은 커밋에서 검증"을 요구하므로 `1.1.0` 릴리스가 아직 시작되지 않았음을 보여준다. --- ## 4. 2026 방법론 (조사 요약) ### 4.1 프레임워크 선택 | 옵션 | 특징 | 판단 | |---|---|---| | **electron-updater + electron-builder v26/27+** | blockmap 차분, 채널, `stagingPercentage`, Windows 서명 검증, generic provider | **유지**. 이미 통합돼 있고 가장 성숙 | | Velopack | Squirrel 대체, JS/Electron 지원, delta | 빌드 프레임워크 교체 비용 큼 | | Tauri updater | ed25519 서명 강제, `latest.json` | Tauri 전용 | | Sparkle | macOS 전용, 서명 feed, critical update | 교차 플랫폼 아님 | | update-electron-app | 공식 Squirrel 경로 | 채널·staged rollout 없음 | 결론: **electron-updater를 유지하고 v27+ 보안 기본값(파일 단위 `files[]` 메타데이터, fail-closed 서명 검증, 명시적 publish) 방향으로 설정을 정렬**한다. ### 4.2 차분(delta) vs 전체(full) - electron-updater는 캐시된 이전 설치자를 기준으로 `.blockmap`의 변경 블록만 HTTP Range로 내려받는다. 첫 업데이트는 항상 full이다. - **full로 강제해야 하는 경우**: (a) 현재 버전을 신뢰할 수 없음/손상, (b) major 승격(스키마·ABI 변경), (c) minor 갭이 임계 이상, (d) 서버가 Range 미지원. - 구현: `autoUpdater.disableDifferentialDownload`를 위 조건에서 켠다. ### 4.3 채널과 staged rollout - 채널: `latest`(stable) / `beta` / `alpha`. 클라이언트 `updater.channel` + `allowPrerelease`로 선택. 빌드 측은 `generateUpdatesFilesForAllChannels`로 채널별 메타데이터를 생성할 수 있다. - staged rollout: `latest.yml`의 `stagingPercentage` + 클라이언트의 영구 사용자 해시. 회수는 **더 높은 버전**으로만 가능(같은 버전 재배포 불가). ### 4.4 릴리스 관리 방법론 - **SemVer**: breaking=MAJOR, 하위호환 기능=MINOR, 버그픽스=PATCH. 릴리스된 버전은 불변(immutable). 잘못된 릴리스는 새 PATCH로 forward-fix. - **Conventional Commits** → `feat`=MINOR, `fix`=PATCH, `BREAKING CHANGE`/`!`=MAJOR. 자동화 도구는 release-please(릴리스 PR 게이트) / semantic-release(완전 자동) / changesets(모노레포). 이 저장소는 SSOT+수동 승인 게이트를 이미 쓰므로 **release-please식 게이트를 유지**하고 커밋 컨벤션만 도입한다. - **Trunk-based development**가 2026 기본. 릴리스 브랜치는 필요 시 JIT로 자르고 태그 후 제거. 이 저장소는 `main` + 태그 릴리스로 이미 TBD에 가깝다. - **Git 태그**: annotated + 서명(`git tag -s`) + `v` 접두 semver. 보호 태그 규칙으로 삭제·강제 이동을 막는다. 릴리스 태그는 절대 이동하지 않는다. - **CHANGELOG**: Keep a Changelog. `[Unreleased]`를 PR에서 유지하고 릴리스 시 날짜 섹션으로 이동. YANKED 표기. ### 4.5 CI/CD - 코드 품질 CI는 모든 push에서, 릴리스/배포 CI는 **태그에서만**. - 태그가 버전 SSOT·CHANGELOG·테스트·서명을 모두 통과해야 publish. - 자산을 먼저 업로드하고 **메타데이터(latest.yml)를 마지막에** 게시해 배포 중 404를 막는다(현 publisher가 이미 구현). - 공개 URL에서 재검증(fail-closed). 출처: semver.org, conventionalcommits.org, keepachangelog.com, electron.build auto-update/code-signing/publish, forgejo.org packages/actions, trunkbaseddevelopment.com, sre.google release-engineering, Doyensec SafeUpdater. --- ## 5. 목표 아키텍처 ```text ┌───────────────────────────── release/update-policy.json (SSOT) │ author commit → version:check → test → build (signed) │ └→ tag vX.Y.Z (annotated, protected) │ ┌───────────────┼───────────────────────────┐ │ │ │ GitLab CI GitHub Actions Forgejo Actions (primary) (mirror) (release hub + feed) │ │ │ └───────────────┴──────────────┬────────────┘ ▼ publish-forgejo-release.mjs (canonical) publish-gitlab-release.mjs (legacy mirror) │ ┌────────────────────────┴────────────────────────┐ ▼ ▼ Forgejo Generic Registry (canonical feed) GitLab Generic Registry (legacy) .../generic/d3ro-voice// .../generic/d3ro-voice// │ ▼ electron-updater → 채널(latest/beta/alpha) → 정책(mandatory/full-vs-delta) ``` - **Canonical runtime feed**: Forgejo `https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/latest`. - **Legacy mirror**: GitLab project 1172 `latest`. 기존 `0.2.1-alpha` 설치본이 GitLab을 계속 폴링하므로, 새 설치자가 Forgejo feed를 갖는 버전을 받을 때까지 유지한다. 마이그레이션 완료 후 제거 가능. - **정책 SSOT**: `release/update-policy.json`. 빌드에 번들되는 기본값 + feed에서 원격 오버라이드(선택). 서버에서 정책을 내려 강제 업데이트·킬 스위치를 즉시 적용할 수 있다. --- ## 6. 결정 기록 (ADR 요약) 1. **ADR-UPD-01**: electron-updater를 유지한다. 빌드 프레임워크 교체는 비용 대비 이득이 없다. 2. **ADR-UPD-02**: Forgejo를 canonical 배포·feed 호스트로 한다. GitLab은 legacy mirror로 남긴다. 3. **ADR-UPD-03**: 채널(latest/beta/alpha)과 최소 지원 버전·강제 업데이트·full 재설치를 `release/update-policy.json`으로 정본화한다. 4. **ADR-UPD-04**: 태그는 annotated `vX.Y.Z`이며 이동하지 않는다. 릴리스는 forward-fix만 허용한다. 5. **ADR-UPD-05**: Forgejo 배포 금지 조항을 제거하고, 버전 하드코딩 없는 `publish-forgejo-release.mjs`로 대체한다. 6. **ADR-UPD-06** (권고): 데스크톱 `typecheck`가 실제로 검사하도록 `tsc -b` 또는 `tsc -p tsconfig.node.json`로 교체하고 기존 오류를 별도 워크스트림에서 정리한다. 이 문서 범위에서는 릴리스 게이트가 착시에 의존하지 않도록 `verify-release-metadata.mjs`에 최소한의 경고를 남긴다.