fix(release): keep the updater configuration in the installer
Some checks failed
deploy-site / deploy (push) Failing after 1m6s

Installing the previous build left an app that could not update itself: the
packaging path used to guarantee the native module build does not create the
updater configuration file, so the update client had nothing to read.

That file is now written from the single feed source and its presence in the
packaged app is checked before anything is published, so an installer that
cannot update can no longer be released.
This commit is contained in:
Yun Chan 2026-09-18 16:35:03 +09:00
parent 1af3cf75c7
commit f14341ace4
28 changed files with 141 additions and 47 deletions

View file

@ -13,6 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Cloud-optional backup (encrypted, opt-in)
- Plugin system for custom pipelines
## [1.3.4] - 2026-09-18
### Fixed
- **Automatic updates could not start on 1.3.3**: its installer was missing the updater
configuration file, because the packaging path used to guarantee the native module build
does not create it. The file is now written from the single feed source and its presence
in the packaged app is verified before publishing, so an installer that cannot update is
never shipped.
## [1.3.3] - 2026-09-18
### Fixed

View file

@ -3,7 +3,7 @@
"info": {
"title": "D3RO-VOICE Admin API",
"description": "Admin CRM Edge Functions for user management, subscription CRUD, payment history, and audit logs.",
"version": "1.3.3"
"version": "1.3.4"
},
"servers": [
{

View file

@ -1,6 +1,6 @@
{
"name": "@d3ro/admin",
"version": "1.3.3",
"version": "1.3.4",
"private": true,
"description": "D3RO Voice Admin CRM — SaaS 관리 도구",
"scripts": {

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Version>1.3.3</Version>
<Version>1.3.4</Version>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

View file

@ -1,6 +1,6 @@
{
"name": "@d3ro/desktop",
"version": "1.3.3",
"version": "1.3.4",
"productName": "d3ro-voice",
"description": "로컬 AI 음성 어시스턴트 (Electron)",
"main": "./out/main/index.js",

View file

@ -151,8 +151,8 @@ def versionSettingsValid = configuredVersionName != null &&
configuredVersionName ==~ strictSemver &&
configuredVersionCodeValue != null &&
configuredVersionCodeValue <= 2100000000L
def resolvedVersionName = versionSettingsValid ? configuredVersionName : "1.3.3"
def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1031003
def resolvedVersionName = versionSettingsValid ? configuredVersionName : "1.3.4"
def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1031004
def requiredReleaseSettings = [
D3RO_RELEASE_STORE_FILE: releaseStoreFilePath,

View file

@ -257,7 +257,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1031003;
CURRENT_PROJECT_VERSION = 1031004;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = D3ROVoice/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
@ -265,7 +265,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.3.3;
MARKETING_VERSION = 1.3.4;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -287,14 +287,14 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1031003;
CURRENT_PROJECT_VERSION = 1031004;
INFOPLIST_FILE = D3ROVoice/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.3.3;
MARKETING_VERSION = 1.3.4;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",

View file

@ -0,0 +1 @@
Fixed an installer that was missing its update configuration.

View file

@ -0,0 +1 @@
자동 업데이트 설정이 누락되던 문제를 고쳤습니다.

View file

@ -1,12 +1,12 @@
{
"name": "@d3ro/mobile-rn",
"version": "1.3.3",
"version": "1.3.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@d3ro/mobile-rn",
"version": "1.3.3",
"version": "1.3.4",
"dependencies": {
"@d3ro/api-client": "file:../../packages/api-client",
"@d3ro/core": "file:../../packages/core",
@ -62,7 +62,7 @@
},
"../..": {
"name": "d3ro-voice-monorepo",
"version": "1.3.3",
"version": "1.3.4",
"license": "MIT",
"workspaces": [
"apps/desktop",
@ -81,7 +81,7 @@
},
"../../packages/api-client": {
"name": "@d3ro/api-client",
"version": "1.3.3",
"version": "1.3.4",
"license": "MIT",
"dependencies": {
"@d3ro/core": "*",
@ -98,7 +98,7 @@
},
"../../packages/core": {
"name": "@d3ro/core",
"version": "1.3.3",
"version": "1.3.4",
"license": "MIT",
"dependencies": {
"docx": "^9.6.1"
@ -109,7 +109,7 @@
},
"../../packages/i18n": {
"name": "@d3ro/i18n",
"version": "1.3.3",
"version": "1.3.4",
"license": "MIT",
"devDependencies": {
"@types/react": "^19.0.0"
@ -120,7 +120,7 @@
},
"../../packages/ui-native": {
"name": "@d3ro/ui-native",
"version": "1.3.3",
"version": "1.3.4",
"license": "MIT",
"devDependencies": {
"@types/react": "*"

View file

@ -1,6 +1,6 @@
{
"name": "@d3ro/mobile-rn",
"version": "1.3.3",
"version": "1.3.4",
"private": true,
"scripts": {
"android": "react-native run-android",

View file

@ -1,6 +1,6 @@
{
"name": "@d3ro/web",
"version": "1.3.3",
"version": "1.3.4",
"private": true,
"description": "D3RO Voice 웹 앱 — Next.js 기반 SaaS 인터페이스",
"scripts": {

View file

@ -139,7 +139,7 @@ export function Sidebar(): React.ReactElement {
</PhosphorText>
</Box>
<Box sx={{ fontSize: '10px', fontFamily: 'ui-monospace, monospace', color: d3roPalette.text.muted }}>
v1.3.3
v1.3.4
</Box>
</Box>

View file

@ -1,15 +1,15 @@
{
"version": "1.3.3",
"version": "1.3.4",
"description": "로컬 AI 음성 어시스턴트 (faster-whisper + Ollama, 100% 오프라인 지원)",
"homepage": "https://d3ro.chanpaca.net",
"license": "MIT",
"architecture": {
"64bit": {
"url": [
"https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-1.3.3/D3RO-Voice-1.3.3-x64-portable.7z.001"
"https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-1.3.4/D3RO-Voice-1.3.4-x64-portable.7z.001"
],
"hash": [
"b4bb847a7f25732cf0abf8516f6c708d6d9e6ab7d1fd813abf7795a0466c17b0"
"259e4803d3edbc1d4e6fd79ff8d034e75fb55f2ebac9e0f6459d0880531852e4"
]
}
},
@ -27,7 +27,7 @@
"architecture": {
"64bit": {
"url": [
"https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-$version/D3RO-Voice-1.3.3-x64-portable.7z.001"
"https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-$version/D3RO-Voice-1.3.4-x64-portable.7z.001"
]
}
}

View file

@ -195,7 +195,7 @@ Status quick-reference: `[x]` done+verified · `[~]` partial/unverified · `[ ]`
| INFRA-16 | Desktop STT engine packaging | [x] | `apps/desktop/scripts/setup-sidecar.mjs` + `build-sidecar.mjs`, `electron-builder.yml` `extraResources` (`sidecar-dist/sidecar``resources/sidecar`, `resources/ffmpeg`), and `scripts/ci/verify-sidecar-bundle.mjs` run in `package-windows`/`package-macos` before electron-builder. Verified on the real bundle: `sidecar.exe` + `_internal` including `faster_whisper/assets/silero_vad_v6.onnx`, plus a packaged-engine transcription round-trip on GPU. |
| INFRA-17 | 서명 없는 배포 채널 (portable + Scoop) | [x] | `scripts/ci/build-portable.mjs` (95MiB 7z 분할 볼륨 + Scoop 매니페스트), `scripts/ci/publish-portable-release.mjs`, `scripts/local/install-d3ro-voice.ps1`, `bucket/` 버킷, `.forgejo/workflows/portable.yml`; 7z 분할 볼륨(Scoop, 162MiB) + zip 분할 부품(수동 설치, 243MiB, 7-Zip 불필요); updater feed와 분리. 2026-09-18 `portable-1.3.1` 게시 + 실제 설치 검증. |
| INFRA-18 | 로컬 런타임 온디맨드 설치 | [x] | `RuntimeProvisioner`(부품 다운로드 + SHA-256 검증 + tar 해제, `%APPDATA%/d3ro-voice/runtime`), `POST runtime:ensure` / `runtime:progress` IPC, 설정 > STT 상태/내려받기 UI. 설치본에서 엔진/ffmpeg를 분리해 189MB → 90.6MiB, 업데이트 피드 게시 복구. 2026-09-18 실제 feed 통합 검증(엔진 94.4MiB/17초, ffmpeg 21.7MiB/5초). |
| INFRA-19 | 네이티브 ABI 패키징 게이트 | [x] | `scripts/ci/verify-native-abi.mjs`(패키징된 `better_sqlite3.node`가 Electron ABI인지 호스트 Node 로드 거부로 판별) + `scripts/ci/fix-native-abi.mjs`(로컬 잠금 우회용 주입). GitLab/Forgejo/GitHub 패키징 단계에 검증 삽입. 2026-09-18: Node ABI 모듈이 설치본에 들어가 앱이 시작 즉시 죽은 사고의 재발 방지. |
| INFRA-19 | 네이티브 ABI + updater 설정 게이트 | [x] | `scripts/ci/verify-native-abi.mjs`(패키징된 `better_sqlite3.node`가 Electron ABI인지 호스트 Node 로드 거부로 판별) + `scripts/ci/fix-native-abi.mjs`(로컬 잠금 우회용 주입). GitLab/Forgejo/GitHub 패키징 단계에 검증 삽입. 2026-09-18: Node ABI 모듈로 앱이 시작 즉시 죽은 사고 + 누락으로 자동 업데이트가 죽은 사고를 함께 방지(). |
---

20
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "d3ro-voice-monorepo",
"version": "1.3.3",
"version": "1.3.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "d3ro-voice-monorepo",
"version": "1.3.3",
"version": "1.3.4",
"license": "MIT",
"workspaces": [
"apps/desktop",
@ -25,7 +25,7 @@
},
"apps/admin": {
"name": "@d3ro/admin",
"version": "1.3.3",
"version": "1.3.4",
"dependencies": {
"@d3ro/api-client": "*",
"@d3ro/core": "*",
@ -109,7 +109,7 @@
},
"apps/desktop": {
"name": "@d3ro/desktop",
"version": "1.3.3",
"version": "1.3.4",
"license": "MIT",
"dependencies": {
"@d3ro/core": "*",
@ -158,7 +158,7 @@
},
"apps/web": {
"name": "@d3ro/web",
"version": "1.3.3",
"version": "1.3.4",
"dependencies": {
"@d3ro/api-client": "*",
"@d3ro/core": "*",
@ -16861,7 +16861,7 @@
},
"packages/api-client": {
"name": "@d3ro/api-client",
"version": "1.3.3",
"version": "1.3.4",
"license": "MIT",
"dependencies": {
"@d3ro/core": "*",
@ -16878,7 +16878,7 @@
},
"packages/core": {
"name": "@d3ro/core",
"version": "1.3.3",
"version": "1.3.4",
"license": "MIT",
"dependencies": {
"docx": "^9.6.1"
@ -16889,7 +16889,7 @@
},
"packages/i18n": {
"name": "@d3ro/i18n",
"version": "1.3.3",
"version": "1.3.4",
"license": "MIT",
"devDependencies": {
"@types/react": "^19.0.0"
@ -16900,7 +16900,7 @@
},
"packages/ui": {
"name": "@d3ro/ui",
"version": "1.3.3",
"version": "1.3.4",
"license": "MIT",
"dependencies": {
"@d3ro/core": "*"
@ -16920,7 +16920,7 @@
},
"packages/ui-native": {
"name": "@d3ro/ui-native",
"version": "1.3.3",
"version": "1.3.4",
"license": "MIT",
"devDependencies": {
"@types/react": "*"

View file

@ -1,6 +1,6 @@
{
"name": "d3ro-voice-monorepo",
"version": "1.3.3",
"version": "1.3.4",
"private": true,
"description": "D3RO Voice — 멀티플랫폼 AI 음성 어시스턴트 (Monorepo)",
"author": "D3RO",

View file

@ -1,6 +1,6 @@
{
"name": "@d3ro/api-client",
"version": "1.3.3",
"version": "1.3.4",
"private": true,
"description": "D3RO Voice API 클라이언트 — Supabase 래퍼 (web/desktop/mobile 공유)",
"license": "MIT",

View file

@ -1,6 +1,6 @@
{
"name": "@d3ro/core",
"version": "1.3.3",
"version": "1.3.4",
"private": true,
"description": "D3RO Voice 공유 비즈니스 로직 — 타입, 에러, IPC 채널, 상수, 유틸",
"license": "MIT",

View file

@ -1,6 +1,6 @@
{
"name": "@d3ro/i18n",
"version": "1.3.3",
"version": "1.3.4",
"private": true,
"description": "D3RO Voice 공유 i18n — 12개 locale, 타입 안전 키, Context, 포맷 유틸",
"license": "MIT",

View file

@ -1,6 +1,6 @@
{
"name": "@d3ro/ui-native",
"version": "1.3.3",
"version": "1.3.4",
"private": true,
"description": "D3RO Voice React Native DS — MetalCard/PhosphorText/Led/WaveBars etc.",
"license": "MIT",

View file

@ -1,6 +1,6 @@
{
"name": "@d3ro/ui",
"version": "1.3.3",
"version": "1.3.4",
"private": true,
"description": "D3RO Voice 공유 UI — 디자인 시스템 컴포넌트 + 테마 토큰 + CSS 변수 맵",
"license": "MIT",

View file

@ -1,8 +1,8 @@
{
"schemaVersion": 1,
"version": "1.3.3",
"androidVersionCode": 1031003,
"iosBuildNumber": 1031003,
"version": "1.3.4",
"androidVersionCode": 1031004,
"iosBuildNumber": 1031004,
"releaseDate": "2026-09-18",
"desktopLicensePublicKeyId": "5c52b765135ee2531c681b53cd4dc0e96fff8737f496c0b04ba8334fc81a887f"
}

View file

@ -146,6 +146,9 @@ if (!existsSync(appDir)) {
// (실측 사고: better_sqlite3.node가 NODE_MODULE_VERSION 131 → Electron 130 요구)
runNodeScript('scripts/ci/fix-native-abi.mjs', ['--dir', appDir])
runNodeScript('scripts/ci/verify-native-abi.mjs', ['--dir', appDir])
// electron-builder의 --dir/--prepackaged 경로는 app-update.yml을 만들지 않는다.
// 이 파일이 없으면 electron-updater가 설정을 읽지 못해 자동 업데이트가 죽는다(실측).
runNodeScript('scripts/ci/write-app-update-yml.mjs', ['--dir', appDir])
// 이전 볼륨 정리 (같은 버전 재드 시 잔여 볼륨이 섞이지 않도록)
for (const name of readdirSync(releaseDir)) {

View file

@ -70,6 +70,9 @@ if (build) {
runNodeScript('scripts/ci/fix-native-abi.mjs', ['--dir', appDir])
runNodeScript('scripts/ci/verify-native-abi.mjs', ['--dir', appDir])
// electron-updater 설정 파일을 보장한다(없으면 자동 업데이트가 동작하지 않는다)
runNodeScript('scripts/ci/write-app-update-yml.mjs', ['--dir', appDir])
// 3) 검증된 트리에서 설치본 생성 (--prepackaged = 재빌드 없이 그대로 패키징)
console.log('[updater] electron-builder --prepackaged (NSIS x64)')
const packageResult = spawnSync(
@ -157,6 +160,13 @@ if (installerSize > MAX_UPLOAD_BYTES) {
process.exit(1)
}
// 설치본에 app-update.yml이 없으면 electron-updater가 설정을 읽지 못해 자동 업데이트가 죽는다.
const packagedUpdateConfig = join(releaseDir, 'win-unpacked', 'resources', 'app-update.yml')
if (!existsSync(packagedUpdateConfig)) {
console.error('[updater] app-update.yml 누락 — write-app-update-yml.mjs를 먼저 실행하세요.')
process.exit(1)
}
const metadata = readFileSync(metadataPath, 'utf8')
if (!metadata.includes(`version: ${version}`)) {
console.error('[updater] latest.yml의 버전이 product-version.json과 다릅니다.')

View file

@ -0,0 +1,70 @@
// scripts/ci/write-app-update-yml.mjs
// 패키징된 앱 트리에 electron-updater 설정 파일(resources/app-update.yml)을 보장한다.
//
// 배경(실측 사고): electron-builder는 `--dir`/`--prepackaged` 경로에서 app-update.yml을
// 생성하지 않는다. 그래서 1.3.3 설치본에는 이 파일이 없었고, electron-updater가 설정을
// 읽지 못해 **자동 업데이트가 동작하지 않는다**.
// (일반 `electron-builder --win nsis` 경로에서는 생성되지만, 우리는 네이티브 ABI 검증을 위해
// --dir → 검증 → --prepackaged 순서를 쓰므로 직접 만들어 준다.)
//
// 값의 출처는 런타임 SSOT인 apps/desktop/src/main/update-feed.ts의 UPDATE_FEED_URL 하나뿐이다.
//
// 사용: node scripts/ci/write-app-update-yml.mjs --dir <packagedDir>
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..')
const dirFlagIndex = process.argv.indexOf('--dir')
const packagedDir = dirFlagIndex >= 0 ? process.argv[dirFlagIndex + 1] : null
if (!packagedDir || !existsSync(packagedDir)) {
console.error('사용: node scripts/ci/write-app-update-yml.mjs --dir <packagedDir>')
process.exit(1)
}
// 피드 URL은 update-feed.ts가 정본이다 (electron-builder.yml의 publish.url과 동일해야 한다).
const feedSource = readFileSync(join(root, 'apps', 'desktop', 'src', 'main', 'update-feed.ts'), 'utf8')
const feedMatch = feedSource.match(/export const UPDATE_FEED_URL\s*=\s*'([^']+)'/)
if (!feedMatch) {
console.error('[app-update] update-feed.ts에서 UPDATE_FEED_URL을 찾을 수 없습니다.')
process.exit(1)
}
const feedUrl = feedMatch[1]
const electronBuilderConfig = readFileSync(
join(root, 'apps', 'desktop', 'electron-builder.yml'),
'utf8',
)
const publishMatch = electronBuilderConfig.match(/^publish:\s*$[\s\S]*?url:\s*"([^"]+)"/m)
if (publishMatch && publishMatch[1] !== feedUrl) {
console.error(
[
'[app-update] feed URL 불일치:',
` update-feed.ts : ${feedUrl}`,
` electron-builder.yml : ${publishMatch[1]}`,
' 두 값은 같아야 합니다(자동 업데이트 계약).',
].join('\n'),
)
process.exit(1)
}
const target = join(packagedDir, 'resources', 'app-update.yml')
const contents = [
'provider: generic',
`url: ${feedUrl}`,
// electron-builder가 일반 경로에서 생성하는 값과 동일한 규칙(제품명 기반)
"updaterCacheDirName: 'd3ro-voice-updater'",
'',
].join('\n')
const existing = existsSync(target) ? readFileSync(target, 'utf8') : null
if (existing === contents) {
console.log('[app-update] 이미 최신 상태입니다')
process.exit(0)
}
writeFileSync(target, contents, 'utf8')
console.log(`[app-update] 작성: ${target}`)
console.log(contents.trimEnd())

View file

@ -1,12 +1,12 @@
{
"name": "d3ro-voice-site",
"version": "1.3.3",
"version": "1.3.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "d3ro-voice-site",
"version": "1.3.3",
"version": "1.3.4",
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"

View file

@ -1,7 +1,7 @@
{
"name": "d3ro-voice-site",
"private": true,
"version": "1.3.3",
"version": "1.3.4",
"type": "module",
"scripts": {
"dev": "vite --port 5199 --host",