The changelog still described unreleased work under 1.1.0, which was already published with its own notes. Those notes are restored verbatim for history, and the new work has its own 1.2.0 section that the feed publisher will turn into release notes. The release guide, infrastructure map, and mobile SSOT now carry the 1.2.0 identity, state that installer binaries are distributed through the feed and never committed, and record that the published 1.1.0 installer is unsigned and is being superseded rather than rewritten. Backlog entries cover the remaining external signing and token secrets.
215 lines
13 KiB
Markdown
215 lines
13 KiB
Markdown
# 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` | canonical Forgejo registry `latest` (버전 비고정) + legacy GitLab mirror 상수 |
|
|
| 빌더 publish | `apps/desktop/electron-builder.yml` | `provider: generic`, `detectUpdateChannel: false` |
|
|
| 버전 SSOT | `release/product-version.json` | `1.2.0` / `1020001` |
|
|
| 버전 동기화 | `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 재검증 (legacy mirror) |
|
|
| Forgejo publisher | `scripts/ci/publish-forgejo-release.mjs` | canonical feed·Release·policy 게시, 동일 버전 재게시 fail-closed |
|
|
| Windows 산출물 gate | `scripts/ci/verify-windows-release-artifact.ps1` | Authenticode·PE version·SHA-512 |
|
|
| CI | `.gitlab-ci.yml` `package-*` → `publish-release` | 태그 전용 |
|
|
| Forgejo | `.forgejo/workflows/release.yml` | tag 전용 release·feed 게시 (동일 publisher 수렴) |
|
|
| 릴리스 노트 | `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/<version|latest>/ .../generic/d3ro-voice/<version|latest>/
|
|
│
|
|
▼
|
|
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`에 최소한의 경고를 남긴다.
|