From 49a4c97923e80e0cfc20d05c5238fd3d3e938977 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Wed, 16 Sep 2026 23:49:37 +0900 Subject: [PATCH 1/3] fix(release): refuse to re-publish a version that already shipped The feed publisher overwrote whatever version-specific assets it found, so a re-run of an old release tag could quietly replace the installer that customers already downloaded under that version number. Publication now compares the bytes already in the version-specific registry path and stops when they differ, while still allowing an identical re-run to finish. The metadata verifier gained a negative case so the guard cannot be removed unnoticed. --- scripts/ci/publish-forgejo-release.mjs | 23 +++++++++++++++++++++++ scripts/ci/verify-release-metadata.mjs | 13 ++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/scripts/ci/publish-forgejo-release.mjs b/scripts/ci/publish-forgejo-release.mjs index ea06e57..eb4b1fb 100644 --- a/scripts/ci/publish-forgejo-release.mjs +++ b/scripts/ci/publish-forgejo-release.mjs @@ -98,6 +98,11 @@ if (dryRun) { process.exit(0); } +// 0) 이미 게시된 버전은 재게시하지 않는다 (SemVer 동일 버전 재릴리스 금지). +// 버전별 경로에 같은 크기의 자산이 있으면 재시도/재실행으로 보고 통과시키고, +// 다른 바이트를 가진 자산이 있으면 fail-closed로 중단한다. +await assertVersionNotRepublished(sorted); + // 1) 버전별(immutable) 패키지 업로드 for (const file of sorted) { await uploadToRegistry(file, `${packageVersionedUrl}/${encodeURIComponent(safeAssetName(file.name))}`, { @@ -188,6 +193,24 @@ async function sha256(path) { return hash.digest("hex"); } +async function assertVersionNotRepublished(localFiles) { + for (const file of localFiles) { + const url = `${packageVersionedUrl}/${encodeURIComponent(safeAssetName(file.name))}`; + const head = await forgejoFetch(url, { method: "HEAD" }).catch(() => null); + if (!head || !head.ok) continue; + + const remoteLength = Number(head.headers.get("content-length")); + const localLength = (await stat(file.path)).size; + if (!Number.isFinite(remoteLength) || remoteLength === localLength) continue; + + throw new Error( + `${tag} is already published with different bytes (${safeAssetName(file.name)}: ` + + `remote ${remoteLength} bytes, local ${localLength} bytes). Releases are immutable — ` + + "lift the version and publish a new tag instead of re-publishing this one.", + ); + } +} + async function uploadToRegistry(file, url, { replace = false, immutable = false } = {}) { const fileStat = await stat(file.path).catch(() => null); diff --git a/scripts/ci/verify-release-metadata.mjs b/scripts/ci/verify-release-metadata.mjs index a2a5337..7e0438d 100644 --- a/scripts/ci/verify-release-metadata.mjs +++ b/scripts/ci/verify-release-metadata.mjs @@ -196,6 +196,10 @@ function validate(surfaces) { fail(surfaces.forgejoPublisher.includes('verifyPublicFile'), 'forgejo_publisher_public_verification_missing') fail(surfaces.forgejoPublisher.includes('update-policy.json'), 'forgejo_publisher_policy_upload_missing') fail(surfaces.forgejoPublisher.includes('CHANGELOG.md'), 'forgejo_publisher_changelog_gate_missing') + fail( + surfaces.forgejoPublisher.includes('assertVersionNotRepublished'), + 'forgejo_publisher_rerelease_guard_missing', + ) fail( surfaces.forgejoPublisher.includes('/api/packages/') && surfaces.forgejoPublisher.includes('generic'), @@ -309,6 +313,13 @@ if (process.argv.includes('--self-test')) { }, 'forgejo_publisher_asset_first_order_missing', ) + expectRejected( + surfaces, + (candidate) => { + candidate.forgejoPublisher = candidate.forgejoPublisher.replaceAll('assertVersionNotRepublished', 'uploadAnyway') + }, + 'forgejo_publisher_rerelease_guard_missing', + ) expectRejected( surfaces, (candidate) => { @@ -373,7 +384,7 @@ if (process.argv.includes('--self-test')) { if (!missingDesktopKeyRejected) { throw new Error('release_metadata_self_test_failed:desktop_license_public_key_missing') } - result.negativeCases = 13 + result.negativeCases = 14 } process.stdout.write(`${JSON.stringify(result, null, 2)}\n`) From 035d0a76f54747a3244ebc46668bb13f9d2b7866 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Wed, 16 Sep 2026 23:49:50 +0900 Subject: [PATCH 2/3] feat(release): ship the current feature set as 1.2.0 Version 1.1.0 was published on 2026-09-15, so its tag is closed. Every commit since then, the update-feed rework, dictionary import and export, the extra push transports, and entitlement gating, needs its own immutable version. Product version, Android version code, iOS build number, and all package and store surfaces move to 1.2.0 / 1020001, with Korean and English store notes for the new version code. The download centers read that version and its release date from one place instead of repeating them inline. --- apps/admin-swagger/openapi.json | 2 +- apps/admin/package.json | 2 +- apps/api-server/D3ROVoice.Api.csproj | 2 +- apps/desktop/package.json | 2 +- apps/mobile-rn/android/app/build.gradle | 4 ++-- .../ios/D3ROVoice.xcodeproj/project.pbxproj | 8 +++---- .../android/en-US/changelogs/1020001.txt | 1 + .../android/ko-KR/changelogs/1020001.txt | 1 + apps/mobile-rn/package-lock.json | 14 ++++++------ apps/mobile-rn/package.json | 2 +- apps/web/package.json | 2 +- apps/web/src/app/download/page.tsx | 3 ++- apps/web/src/components/layout/sidebar.tsx | 2 +- apps/web/src/lib/desktop-release.ts | 5 ++++- package-lock.json | 22 +++++++++---------- package.json | 2 +- packages/api-client/package.json | 2 +- packages/core/package.json | 2 +- packages/i18n/package.json | 2 +- packages/ui-native/package.json | 2 +- packages/ui/package.json | 2 +- release/product-version.json | 8 +++---- site/package-lock.json | 4 ++-- site/package.json | 2 +- site/src/release.ts | 5 ++++- 25 files changed, 56 insertions(+), 47 deletions(-) create mode 100644 apps/mobile-rn/metadata/android/en-US/changelogs/1020001.txt create mode 100644 apps/mobile-rn/metadata/android/ko-KR/changelogs/1020001.txt diff --git a/apps/admin-swagger/openapi.json b/apps/admin-swagger/openapi.json index 385711a..140aded 100644 --- a/apps/admin-swagger/openapi.json +++ b/apps/admin-swagger/openapi.json @@ -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.1.0" + "version": "1.2.0" }, "servers": [ { diff --git a/apps/admin/package.json b/apps/admin/package.json index c1e58ae..74e1de4 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/admin", - "version": "1.1.0", + "version": "1.2.0", "private": true, "description": "D3RO Voice Admin CRM — SaaS 관리 도구", "scripts": { diff --git a/apps/api-server/D3ROVoice.Api.csproj b/apps/api-server/D3ROVoice.Api.csproj index d7685cf..c62425c 100644 --- a/apps/api-server/D3ROVoice.Api.csproj +++ b/apps/api-server/D3ROVoice.Api.csproj @@ -2,7 +2,7 @@ net10.0 - 1.1.0 + 1.2.0 enable enable diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ebf4195..376115a 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/desktop", - "version": "1.1.0", + "version": "1.2.0", "productName": "d3ro-voice", "description": "로컬 AI 음성 어시스턴트 (Electron)", "main": "./out/main/index.js", diff --git a/apps/mobile-rn/android/app/build.gradle b/apps/mobile-rn/android/app/build.gradle index 76c9498..8f1c98a 100644 --- a/apps/mobile-rn/android/app/build.gradle +++ b/apps/mobile-rn/android/app/build.gradle @@ -151,8 +151,8 @@ def versionSettingsValid = configuredVersionName != null && configuredVersionName ==~ strictSemver && configuredVersionCodeValue != null && configuredVersionCodeValue <= 2100000000L -def resolvedVersionName = versionSettingsValid ? configuredVersionName : "1.1.0" -def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1010001 +def resolvedVersionName = versionSettingsValid ? configuredVersionName : "1.2.0" +def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1020001 def requiredReleaseSettings = [ D3RO_RELEASE_STORE_FILE: releaseStoreFilePath, diff --git a/apps/mobile-rn/ios/D3ROVoice.xcodeproj/project.pbxproj b/apps/mobile-rn/ios/D3ROVoice.xcodeproj/project.pbxproj index cd5c985..2eaba84 100644 --- a/apps/mobile-rn/ios/D3ROVoice.xcodeproj/project.pbxproj +++ b/apps/mobile-rn/ios/D3ROVoice.xcodeproj/project.pbxproj @@ -257,7 +257,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 1010001; + CURRENT_PROJECT_VERSION = 1020001; ENABLE_BITCODE = NO; INFOPLIST_FILE = D3ROVoice/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; @@ -265,7 +265,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.2.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -287,14 +287,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 1010001; + CURRENT_PROJECT_VERSION = 1020001; INFOPLIST_FILE = D3ROVoice/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.2.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/apps/mobile-rn/metadata/android/en-US/changelogs/1020001.txt b/apps/mobile-rn/metadata/android/en-US/changelogs/1020001.txt new file mode 100644 index 0000000..7750638 --- /dev/null +++ b/apps/mobile-rn/metadata/android/en-US/changelogs/1020001.txt @@ -0,0 +1 @@ +Team, meeting, memo, template, command, and dictionary screens now use server responses directly, so lists and edits stay in sync. Reporting a generated document confirms completion instead of hanging. \ No newline at end of file diff --git a/apps/mobile-rn/metadata/android/ko-KR/changelogs/1020001.txt b/apps/mobile-rn/metadata/android/ko-KR/changelogs/1020001.txt new file mode 100644 index 0000000..0961c21 --- /dev/null +++ b/apps/mobile-rn/metadata/android/ko-KR/changelogs/1020001.txt @@ -0,0 +1 @@ +팀·회의·메모·템플릿·명령·사전 화면이 서버 응답을 그대로 사용해 목록과 수정 내용이 어긋나지 않습니다. 생성 문서 신고가 멈추지 않고 완료 확인을 표시합니다. \ No newline at end of file diff --git a/apps/mobile-rn/package-lock.json b/apps/mobile-rn/package-lock.json index c274b58..6d68878 100644 --- a/apps/mobile-rn/package-lock.json +++ b/apps/mobile-rn/package-lock.json @@ -1,12 +1,12 @@ { "name": "@d3ro/mobile-rn", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@d3ro/mobile-rn", - "version": "1.1.0", + "version": "1.2.0", "dependencies": { "@d3ro/api-client": "file:../../packages/api-client", "@d3ro/core": "file:../../packages/core", @@ -62,7 +62,7 @@ }, "../..": { "name": "d3ro-voice-monorepo", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "workspaces": [ "apps/desktop", @@ -81,7 +81,7 @@ }, "../../packages/api-client": { "name": "@d3ro/api-client", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "dependencies": { "@d3ro/core": "*", @@ -98,7 +98,7 @@ }, "../../packages/core": { "name": "@d3ro/core", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "dependencies": { "docx": "^9.6.1" @@ -109,7 +109,7 @@ }, "../../packages/i18n": { "name": "@d3ro/i18n", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "devDependencies": { "@types/react": "^19.0.0" @@ -120,7 +120,7 @@ }, "../../packages/ui-native": { "name": "@d3ro/ui-native", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "devDependencies": { "@types/react": "*" diff --git a/apps/mobile-rn/package.json b/apps/mobile-rn/package.json index 25f1180..7ea08ee 100644 --- a/apps/mobile-rn/package.json +++ b/apps/mobile-rn/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/mobile-rn", - "version": "1.1.0", + "version": "1.2.0", "private": true, "scripts": { "android": "react-native run-android", diff --git a/apps/web/package.json b/apps/web/package.json index 48b54a6..c6cbf8b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/web", - "version": "1.1.0", + "version": "1.2.0", "private": true, "description": "D3RO Voice 웹 앱 — Next.js 기반 SaaS 인터페이스", "scripts": { diff --git a/apps/web/src/app/download/page.tsx b/apps/web/src/app/download/page.tsx index 1b5cbb1..1135786 100644 --- a/apps/web/src/app/download/page.tsx +++ b/apps/web/src/app/download/page.tsx @@ -24,6 +24,7 @@ import { DESKTOP_FEED_URL, DESKTOP_RELEASE_HUB_URL, DESKTOP_RELEASES_URL, + DESKTOP_RELEASE_DATE, DESKTOP_VERSION, DESKTOP_WINDOWS_INSTALLER_FILENAME, DESKTOP_WINDOWS_INSTALLER_URL, @@ -539,7 +540,7 @@ export default function DownloadPage(): React.ReactElement { fontWeight: 500, }} /> - 2026-08-29 + {DESKTOP_RELEASE_DATE}