From 94c48e9aa7b24a4f54d6724afb0d1417b1bb55c7 Mon Sep 17 00:00:00 2001 From: "Yukeon.Wayne" <32856206+YukeonWayne@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:56:01 +0800 Subject: [PATCH] Feature version ctl 20260715 (#9) * Add unified version management workflow * Fix desktop preload typing in web app * Align desktop artifact naming with release manifest * Update releaseManifest with new version details Updated the release headline and notes to reflect the first version release and new features. --------- Co-authored-by: XukeWei-cc <301152382+XukeWei-cc@users.noreply.github.com> --- .github/workflows/desktop-windows.yml | 25 ++ apps/api/package.json | 2 +- apps/api/src/releaseInfo.ts | 35 +++ apps/api/src/routes/runtimeConfig.test.ts | 18 +- apps/api/src/routes/runtimeConfig.ts | 8 +- apps/desktop/electron-builder.config.mjs | 96 ++++++ apps/desktop/package.json | 72 +---- apps/desktop/src/main.ts | 3 +- apps/desktop/src/preload.ts | 1 + apps/desktop/src/types.d.ts | 1 + apps/web/package.json | 2 +- apps/web/src/api.ts | 4 + apps/web/src/main.tsx | 56 ++++ apps/web/src/styles.css | 3 + ...20-private-binary-bundle-github-actions.md | 22 +- docs/current/README.md | 2 + docs/guides/README.md | 8 + docs/guides/version-management-and-release.md | 285 ++++++++++++++++++ packages/shared/package.json | 2 +- packages/shared/src/contracts.ts | 23 ++ packages/shared/src/index.ts | 1 + packages/shared/src/releaseManifest.ts | 51 ++++ scripts/release-metadata.mjs | 37 +++ 23 files changed, 680 insertions(+), 77 deletions(-) create mode 100644 apps/api/src/releaseInfo.ts create mode 100644 apps/desktop/electron-builder.config.mjs create mode 100644 docs/guides/README.md create mode 100644 docs/guides/version-management-and-release.md create mode 100644 packages/shared/src/releaseManifest.ts create mode 100644 scripts/release-metadata.mjs diff --git a/.github/workflows/desktop-windows.yml b/.github/workflows/desktop-windows.yml index 89bd767..15b8c1b 100644 --- a/.github/workflows/desktop-windows.yml +++ b/.github/workflows/desktop-windows.yml @@ -20,6 +20,7 @@ jobs: env: VIFORGE_POSTGRES_PLATFORM_ARCH: win32-x64 VIFORGE_REQUIRE_PGVECTOR: '1' + VIFORGE_RELEASE_COMMIT: ${{ github.sha }} VIFORGE_POSTGRES_BUNDLE_RELEASE_REPO: YukeonWayne/pg_pgvector_binary VIFORGE_POSTGRES_BUNDLE_RELEASE_TAG: ${{ inputs.bundle_release_tag || 'v18.4-pgvector0.8.3-win32-x64' }} VIFORGE_POSTGRES_BUNDLE_ASSET_NAME: postgres-18.4-pgvector-0.8.3-win32-x64.zip @@ -39,20 +40,44 @@ jobs: node-version: 22 cache: pnpm + - name: Read release metadata + id: release_meta + run: node scripts/release-metadata.mjs --github-output + - name: Install dependencies run: pnpm install --frozen-lockfile + env: + VIFORGE_RELEASE_VERSION: ${{ steps.release_meta.outputs.version }} + VIFORGE_RELEASE_TAG: ${{ steps.release_meta.outputs.tag }} + VIFORGE_RELEASE_CHANNEL: ${{ steps.release_meta.outputs.channel }} - name: Verify or download PostgreSQL bundle run: pnpm --filter @viforge/desktop prepare:postgres + env: + VIFORGE_RELEASE_VERSION: ${{ steps.release_meta.outputs.version }} + VIFORGE_RELEASE_TAG: ${{ steps.release_meta.outputs.tag }} + VIFORGE_RELEASE_CHANNEL: ${{ steps.release_meta.outputs.channel }} - name: Typecheck API run: pnpm --filter @viforge/api typecheck + env: + VIFORGE_RELEASE_VERSION: ${{ steps.release_meta.outputs.version }} + VIFORGE_RELEASE_TAG: ${{ steps.release_meta.outputs.tag }} + VIFORGE_RELEASE_CHANNEL: ${{ steps.release_meta.outputs.channel }} - name: Typecheck web run: pnpm --filter @viforge/web typecheck + env: + VIFORGE_RELEASE_VERSION: ${{ steps.release_meta.outputs.version }} + VIFORGE_RELEASE_TAG: ${{ steps.release_meta.outputs.tag }} + VIFORGE_RELEASE_CHANNEL: ${{ steps.release_meta.outputs.channel }} - name: Build Windows installer run: pnpm desktop:dist + env: + VIFORGE_RELEASE_VERSION: ${{ steps.release_meta.outputs.version }} + VIFORGE_RELEASE_TAG: ${{ steps.release_meta.outputs.tag }} + VIFORGE_RELEASE_CHANNEL: ${{ steps.release_meta.outputs.channel }} - name: Upload Windows artifacts uses: actions/upload-artifact@v7 diff --git a/apps/api/package.json b/apps/api/package.json index 8f3959f..487aa00 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@viforge/api", - "version": "0.0.0", + "version": "0.1.0", "private": true, "license": "MIT", "type": "module", diff --git a/apps/api/src/releaseInfo.ts b/apps/api/src/releaseInfo.ts new file mode 100644 index 0000000..04f1ea5 --- /dev/null +++ b/apps/api/src/releaseInfo.ts @@ -0,0 +1,35 @@ +import os from 'node:os'; +import process from 'node:process'; + +import { RELEASE_CHANNEL, RELEASE_VERSION, normalizeReleaseTag, releaseManifest, type ReleaseInfo } from '@viforge/shared'; + +export function getReleaseInfo(): ReleaseInfo { + const version = process.env.VIFORGE_RELEASE_VERSION?.trim() || releaseManifest.version; + const tag = process.env.VIFORGE_RELEASE_TAG?.trim() || normalizeReleaseTag(version); + const channel = process.env.VIFORGE_RELEASE_CHANNEL?.trim() || RELEASE_CHANNEL; + const commit = process.env.VIFORGE_RELEASE_COMMIT?.trim() || process.env.GITHUB_SHA?.trim() || releaseManifest.commit; + const platform = detectPlatform(); + const currentArtifact = releaseManifest.artifacts.find((artifact) => artifact.platform === platform); + + return { + ...releaseManifest, + version, + tag, + channel: channel === 'dev' || channel === 'beta' || channel === 'stable' ? channel : RELEASE_CHANNEL, + commit, + currentArtifact, + }; +} + +export { RELEASE_VERSION }; + +function detectPlatform(): ReleaseInfo['currentArtifact'] extends infer T + ? T extends { platform: infer P } + ? P + : never + : never { + if (process.platform === 'win32') return 'windows-x64'; + if (process.platform === 'darwin') return process.arch === 'arm64' ? 'macos-arm64' : 'macos-x64'; + if (process.platform === 'linux') return 'linux-x64'; + return os.platform() === 'win32' ? 'windows-x64' : 'linux-x64'; +} diff --git a/apps/api/src/routes/runtimeConfig.test.ts b/apps/api/src/routes/runtimeConfig.test.ts index be3c583..66f37f0 100644 --- a/apps/api/src/routes/runtimeConfig.test.ts +++ b/apps/api/src/routes/runtimeConfig.test.ts @@ -56,6 +56,23 @@ describe('runtime config routes', () => { }); }); + it('returns canonical release info for product and artifact surfaces', async () => { + const root = await mkdtemp(path.join(os.tmpdir(), 'viforge-runtime-config-')); + tempDirs.push(root); + const app = createRuntimeConfigRoutes(createRuntimeConfigStore(path.join(root, 'runtime-config.json'))); + + const response = await app.request('/release-info'); + + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ + productName: 'ViForge', + version: '0.1.0', + tag: 'v0.1.0', + channel: 'beta', + updateHeadline: '建立统一版本管理链路', + }); + }); + it('forces embedded PostgreSQL in desktop mode even when legacy config points to an external database', async () => { process.env.VIFORGE_DESKTOP = '1'; process.env.DATABASE_URL = 'postgresql://legacy:password@db.example.test:5432/viforge'; @@ -289,4 +306,3 @@ function restoreEnv(key: string, value: string | undefined): void { } process.env[key] = value; } - diff --git a/apps/api/src/routes/runtimeConfig.ts b/apps/api/src/routes/runtimeConfig.ts index 7c6a4f1..21e82cc 100644 --- a/apps/api/src/routes/runtimeConfig.ts +++ b/apps/api/src/routes/runtimeConfig.ts @@ -1,13 +1,14 @@ import { Hono } from 'hono'; import { z } from 'zod'; -import type { RuntimeConfig, RuntimeMemoryRebuildResponse, RuntimeModelTestResponse, UpdateRuntimeConfigInput } from '@viforge/shared'; +import type { ReleaseInfo, RuntimeConfig, RuntimeMemoryRebuildResponse, RuntimeModelTestResponse, UpdateRuntimeConfigInput } from '@viforge/shared'; import { buildAigcHubHeaders } from '../aigcHubHeaders'; import type { RuntimeConfigStore } from '../runtimeConfigStore'; import type { WorkspaceStore } from '../storage/workspaceStore'; import { MemoryEmbeddingIndexUnavailableError, MemoryEmbeddingRebuildInProgressError, reindexProjectMemories } from '../runs/langGraphAgents'; +import { getReleaseInfo } from '../releaseInfo'; const updateRuntimeConfigSchema = z.object({ modelProvider: z.object({ @@ -40,6 +41,10 @@ export function createRuntimeConfigRoutes(store: RuntimeConfigStore, workspaceSt return context.json(await store.getConfig() satisfies RuntimeConfig); }); + routes.get('/release-info', (context) => { + return context.json(getReleaseInfo() satisfies ReleaseInfo); + }); + routes.put('/runtime-config', async (context) => { const parsed = updateRuntimeConfigSchema.safeParse(await context.req.json()); if (!parsed.success) { @@ -161,4 +166,3 @@ async function modelTestErrorMessage(response: Response): Promise { function trimTrailingSlashes(value: string): string { return value.replace(/\/+$/, ''); } - diff --git a/apps/desktop/electron-builder.config.mjs b/apps/desktop/electron-builder.config.mjs new file mode 100644 index 0000000..7fbaee4 --- /dev/null +++ b/apps/desktop/electron-builder.config.mjs @@ -0,0 +1,96 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; + +const manifestSource = readFileSync(path.resolve('..', '..', 'packages', 'shared', 'src', 'releaseManifest.ts'), 'utf8'); +const productName = readConstString('RELEASE_PRODUCT_NAME'); +const manifestVersion = readConstString('RELEASE_VERSION'); +const manifestChannel = readConstString('RELEASE_CHANNEL'); +const releaseVersion = process.env.VIFORGE_RELEASE_VERSION?.trim() || manifestVersion; +const releaseChannel = normalizeReleaseChannel(process.env.VIFORGE_RELEASE_CHANNEL?.trim()) || manifestChannel; + +export default { + appId: 'cn.viforge.desktop', + productName: 'ViForge', + executableName: 'viforge', + npmRebuild: false, + directories: { + output: '../../release/desktop', + }, + files: [ + 'dist/**/*', + 'build/**/*', + 'package.json', + ], + extraResources: [ + { + from: '../web/dist', + to: 'web', + }, + { + from: 'dist/api', + to: 'api', + filter: ['**/*'], + }, + { + from: 'resources/postgres', + to: 'postgres', + filter: ['**/*'], + }, + { + from: '../../LICENSE', + to: 'LICENSE', + }, + { + from: '../../NOTICE', + to: 'NOTICE', + }, + { + from: '../../THIRD_PARTY_NOTICES.md', + to: 'THIRD_PARTY_NOTICES.md', + }, + ], + win: { + target: ['nsis'], + icon: 'build/icon.ico', + requestedExecutionLevel: 'asInvoker', + artifactName: buildReleaseArtifactFileName({ + productName, + version: releaseVersion, + channel: releaseChannel, + platform: 'win32-x64', + qualifier: 'installer', + extension: 'exe', + }), + }, + nsis: { + oneClick: false, + perMachine: false, + allowToChangeInstallationDirectory: true, + runAfterFinish: false, + include: 'installer.nsh', + }, + mac: { + target: ['dmg'], + icon: 'build/icon.png', + }, + linux: { + target: ['AppImage'], + icon: 'build/icon.png', + }, +}; + +function readConstString(name) { + const pattern = new RegExp("export\\s+const\\s+" + name + "\\s*=\\s*['\"]([^'\"]+)['\"]"); + const match = manifestSource.match(pattern); + if (!match) throw new Error("Unable to read " + name + " from releaseManifest.ts"); + return match[1]; +} + +function buildReleaseArtifactFileName(input) { + const qualifier = input.qualifier ? '-' + input.qualifier : ''; + return input.productName + '-' + input.version + '-' + input.channel + '-' + input.platform + qualifier + '.' + input.extension; +} + +function normalizeReleaseChannel(value) { + return value === 'dev' || value === 'beta' || value === 'stable' ? value : undefined; +} diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 854996e..2fcaee5 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -3,7 +3,7 @@ "description": "ViForge standalone desktop app", "author": "ViForge contributors", "license": "MIT", - "version": "0.0.0", + "version": "0.1.0", "private": true, "type": "module", "main": "dist/main.js", @@ -14,8 +14,8 @@ "build:postgres": "node scripts/build-postgres-from-source.mjs", "build:pgvector": "node scripts/build-pgvector-from-source.mjs", "prepare:postgres": "node scripts/prepare-postgres.mjs", - "pack": "pnpm --filter @viforge/web build && pnpm --filter @viforge/api build && pnpm build && pnpm prepare:postgres && electron-builder --dir", - "dist": "pnpm --filter @viforge/web build && pnpm --filter @viforge/api build && pnpm build && pnpm prepare:postgres && electron-builder" + "pack": "pnpm --filter @viforge/web build && pnpm --filter @viforge/api build && pnpm build && pnpm prepare:postgres && electron-builder --dir --config electron-builder.config.mjs", + "dist": "pnpm --filter @viforge/web build && pnpm --filter @viforge/api build && pnpm build && pnpm prepare:postgres && electron-builder --config electron-builder.config.mjs" }, "dependencies": { "electron-squirrel-startup": "^1.0.1" @@ -27,71 +27,5 @@ "esbuild": "^0.21.5", "sharp": "^0.34.5", "typescript": "^5.5.3" - }, - "build": { - "appId": "cn.viforge.desktop", - "productName": "ViForge", - "executableName": "viforge", - "npmRebuild": false, - "directories": { - "output": "../../release/desktop" - }, - "files": [ - "dist/**/*", - "build/**/*", - "package.json" - ], - "extraResources": [ - { - "from": "../web/dist", - "to": "web" - }, - { - "from": "dist/api", - "to": "api", - "filter": [ - "**/*" - ] - }, - { - "from": "resources/postgres", - "to": "postgres", - "filter": [ - "**/*" - ] - }, - { - "from": "../../LICENSE", - "to": "LICENSE" - }, - { - "from": "../../NOTICE", - "to": "NOTICE" - }, - { - "from": "../../THIRD_PARTY_NOTICES.md", - "to": "THIRD_PARTY_NOTICES.md" - } - ], - "win": { - "target": ["nsis"], - "icon": "build/icon.ico", - "requestedExecutionLevel": "asInvoker" - }, - "nsis": { - "oneClick": false, - "perMachine": false, - "allowToChangeInstallationDirectory": true, - "runAfterFinish": false, - "include": "installer.nsh" - }, - "mac": { - "target": ["dmg"], - "icon": "build/icon.png" - }, - "linux": { - "target": ["AppImage"], - "icon": "build/icon.png" - } } } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 35e4576..cdca70a 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -53,6 +53,8 @@ ipcMain.handle('viforge:select-data-root', async () => { }; }); +ipcMain.handle('viforge:get-app-version', () => app.getVersion()); + async function startDesktopApp(): Promise { if (startupPromise) return startupPromise; startupPromise = startDesktopAppOnce().finally(() => { @@ -689,4 +691,3 @@ app.on('before-quit', (event) => { event.preventDefault(); quitApp(); }); - diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index d8c6661..189b34b 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -8,4 +8,5 @@ type SelectDataRootResult = { contextBridge.exposeInMainWorld('viforgeDesktop', { selectDataRoot: async (): Promise => ipcRenderer.invoke('viforge:select-data-root') as Promise, + getAppVersion: async (): Promise => ipcRenderer.invoke('viforge:get-app-version') as Promise, }); diff --git a/apps/desktop/src/types.d.ts b/apps/desktop/src/types.d.ts index 5a01499..bdc72e7 100644 --- a/apps/desktop/src/types.d.ts +++ b/apps/desktop/src/types.d.ts @@ -10,5 +10,6 @@ interface Window { dataRoot?: string; restartRequired?: boolean; }>; + getAppVersion(): Promise; }; } diff --git a/apps/web/package.json b/apps/web/package.json index 18b7bcd..be8a631 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@viforge/web", - "version": "0.0.0", + "version": "0.1.0", "private": true, "license": "MIT", "type": "module", diff --git a/apps/web/src/api.ts b/apps/web/src/api.ts index 96eeab6..356acfc 100644 --- a/apps/web/src/api.ts +++ b/apps/web/src/api.ts @@ -36,6 +36,7 @@ import type { PromptBlock, ReferencedChatSnippet, ReferencedFile, + ReleaseInfo, RetrievalPolicy, RuntimeConfig, RuntimeMemoryRebuildResponse, @@ -91,6 +92,7 @@ export type { PromptBlock, ReferencedChatSnippet, ReferencedFile, + ReleaseInfo, RetrievalPolicy, RuntimeConfig, RuntimeMemoryRebuildResponse, @@ -111,6 +113,7 @@ export type { export type ApiClient = { getProductProfile(): Promise; getRuntimeConfig(): Promise; + getReleaseInfo(): Promise; updateRuntimeConfig(input: UpdateRuntimeConfigInput): Promise; rebuildMemoryIndex(): Promise; testRuntimeModel(input: UpdateRuntimeConfigInput['modelProvider']): Promise; @@ -374,6 +377,7 @@ export function createApiClient(options: ApiClientOptions = {}): ApiClient { return { getProductProfile: () => request(fetcher, baseUrl, '/api/product-profile'), getRuntimeConfig: () => request(fetcher, baseUrl, '/api/runtime-config'), + getReleaseInfo: () => request(fetcher, baseUrl, '/api/release-info'), updateRuntimeConfig: (input) => request(fetcher, baseUrl, '/api/runtime-config', { method: 'PUT', diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 3fb7869..432ccd0 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -28,6 +28,7 @@ import { type Project, type ReferencedChatSnippet, type ReferencedFile, + type ReleaseInfo, type RuntimeConfig, type UpdateRuntimeConfigInput, type RunEvent, @@ -124,6 +125,7 @@ declare global { dataRoot?: string; restartRequired?: boolean; }>; + getAppVersion(): Promise; }; } } @@ -457,6 +459,7 @@ function App() { const [collapsedDirectoriesByTemporaryProject, setCollapsedDirectoriesByTemporaryProject] = useState>({}); const [activeToolPanel, setActiveToolPanel] = useState<'connectors' | 'git' | 'harness' | 'settings' | null>(null); const [runtimeConfig, setRuntimeConfig] = useState(null); + const [releaseInfo, setReleaseInfo] = useState(null); const [runtimeConfigState, setRuntimeConfigState] = useState('idle'); const [sidebarContextMenu, setSidebarContextMenu] = useState(null); const [chatSessionContextMenu, setChatSessionContextMenu] = useState(null); @@ -931,6 +934,7 @@ function App() { void loadBrowserStatus(); } if (activeToolPanel === 'settings') { + void loadReleaseInfo(); void loadRuntimeConfig(); } }, [activeToolPanel]); @@ -1161,6 +1165,14 @@ function App() { } } + async function loadReleaseInfo() { + try { + setReleaseInfo(await apiClient.getReleaseInfo()); + } catch (error) { + showToast(`读取版本信息失败:${errorToMessage(error)}`, 'error'); + } + } + async function saveRuntimeConfig(input: UpdateRuntimeConfigInput) { setRuntimeConfigState('loading'); try { @@ -4145,6 +4157,7 @@ function App() { {activeToolPanel === 'settings' ? ( { + let cancelled = false; + void window.viforgeDesktop?.getAppVersion().then((value: string) => { + if (!cancelled) setAppVersion(value); + }).catch(() => {}); + return () => { + cancelled = true; + }; + }, []); + + if (!appVersion) return null; + return

桌面安装包版本 {appVersion}

; +} + function RuntimeSettingsPanel({ config, + releaseInfo, state, chatModelOptions, imageModelOptions, @@ -5770,6 +5801,7 @@ function RuntimeSettingsPanel({ onConfirmEmbeddingChange, }: { config: RuntimeConfig | null; + releaseInfo: ReleaseInfo | null; state: LoadState; chatModelOptions: AigcHubModelMetadata[]; imageModelOptions: AigcHubModelMetadata[]; @@ -5794,6 +5826,7 @@ function RuntimeSettingsPanel({ const [embeddingModel, setEmbeddingModel] = useState(''); const [embeddingDims, setEmbeddingDims] = useState('3072'); const [embeddingAdvancedOpen, setEmbeddingAdvancedOpen] = useState(false); + const [releaseInfoOpen, setReleaseInfoOpen] = useState(false); const [localDataRoot, setLocalDataRoot] = useState(''); const [dataRootRestartRequired, setDataRootRestartRequired] = useState(false); const [modelTestState, setModelTestState] = useState>({ chat: 'idle', image: 'idle', embedding: 'idle' }); @@ -6013,6 +6046,29 @@ function RuntimeSettingsPanel({

工作区、聊天会话、Agent 记忆、Harness 产物和日志默认保存在本机数据目录。

+ {releaseInfo ? ( +
+ + {releaseInfoOpen ? ( +
+

{releaseInfo.productName} {releaseInfo.version} · {releaseInfo.channel} · {releaseInfo.tag}

+

{releaseInfo.updateHeadline}

+

发布日期 {releaseInfo.releaseDate}{releaseInfo.currentArtifact ? ` · 当前制品 ${releaseInfo.currentArtifact.fileName}` : ''}

+
+ {releaseInfo.updateNotes.map((note) =>

- {note}

)} +
+ {config?.desktop.enabled && window.viforgeDesktop?.getAppVersion ? : null} +
+ ) : null} +
+ ) : null} +