feat(desktop): dynamic About version, update manifest date, and a unified publish flow

The About section hardcoded its version; the main process now exposes
get-app-info so it reads app.getVersion() dynamically, and the update result
carries the manifest release date. The manifest generator orders versions
numerically (0.10.0 outranks 0.9.0). A new stage-release script copies
releases.json plus installers into the deepseek-harness-web site root's
updates/, which deploy.py publishes to OSS alongside the download page, so the
app and web share one update channel. The About section also gains links to the
release site and official site.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pine
2026-08-14 23:01:36 +08:00
parent 768bcd4356
commit aaa434afbd
10 changed files with 292 additions and 34 deletions
+18 -1
View File
@@ -26,6 +26,23 @@ function versionOf(filename) {
return match === null ? undefined : match[1]
}
/** Order two versions: numeric segments first, then the pre-release suffix. */
function compareVersions(a, b) {
const num = (v) => v.split(/[-+]/)[0].split('.').map((n) => Number(n))
const pre = (v) => { const i = v.indexOf('-'); return i === -1 ? '' : v.slice(i + 1) }
const an = num(a), bn = num(b)
for (let i = 0; i < 3; i++) {
if (an[i] !== bn[i]) return an[i] - bn[i]
}
// Equal numeric core: a release with a pre-release suffix is OLDER than a
// stable one (e.g. `1.0.0-rc.1` < `1.0.0`); two suffixes compare by string.
const ap = pre(a), bp = pre(b)
if (ap === bp) return 0
if (ap === '') return 1
if (bp === '') return -1
return ap < bp ? -1 : 1
}
const platforms = {}
let latestVersion
let latestDate = process.env.DSH_RELEASE_DATE ?? new Date().toISOString().slice(0, 10)
@@ -38,7 +55,7 @@ for (const entry of readdirSync(dist)) {
? entry.includes('arm64') ? 'mac-arm64' : 'mac-x64'
: 'win-x64'
platforms[key] = { url: `${base}${encodeURIComponent(entry)}` }
if (latestVersion === undefined || version > latestVersion) {
if (latestVersion === undefined || compareVersions(version, latestVersion) > 0) {
latestVersion = version
latestDate = process.env.DSH_RELEASE_DATE ?? new Date().toISOString().slice(0, 10)
}
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env node
/**
* Stage a built release into the `deepseek-harness-web` site so it can be
* published in one deploy. Copies `apps/desktop/dist`'s `releases.json` and the
* platform installers (.dmg/.exe) into `../deepseek-harness-web/updates/` — the
* site root's `updates/` folder (deploy.py uploads the repo root to the bucket
* root) that both the web download page and the desktop update check
* (`DSH_UPDATE_URL`) read from.
*
* Run after `desktop:pack` + `generate-release-json`, then deploy the site:
* cd ../deepseek-harness-web && python3 deploy.py
*
* Flags:
* --dry-run print what would be copied without touching the filesystem.
* --site <path> override the deepseek-harness-web repo path (default: sibling dir).
*/
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')
const dist = join(root, 'apps/desktop/dist')
const args = process.argv.slice(2)
const dryRun = args.includes('--dry-run')
const siteFlag = args.find((a) => a.startsWith('--site='))
const siteRoot = siteFlag === undefined
? resolve(root, '../deepseek-harness-web')
: resolve(root, siteFlag.slice('--site='.length))
/** Names the site root's update channel lives at (`updates/`). */
const updatesDir = join(siteRoot, 'updates')
/** Platform installer and manifest files staged to the site. */
const artifactNames = () => {
if (!existsSync(dist)) return []
return readdirSync(dist).filter((f) => f === 'releases.json' || f.endsWith('.dmg') || f.endsWith('.exe'))
}
const artifacts = artifactNames()
if (artifacts.length === 0) {
console.error(`stage-release: no releases.json or installers found in ${dist}`)
console.error('Run `desktop:pack` and `generate-release-json` first.')
process.exit(1)
}
console.log(`staging ${artifacts.length} artifact(s) from ${dist}`)
console.log(`${updatesDir}${dryRun ? ' (dry run)' : ''}`)
for (const name of artifacts) {
console.log(` ${dryRun ? 'would copy' : 'copying'} ${name}`)
if (dryRun) continue
mkdirSync(updatesDir, { recursive: true })
cpSync(join(dist, name), join(updatesDir, name))
}
// A stale installer left from a previous release should not linger on the site.
const stale = existsSync(updatesDir)
? readdirSync(updatesDir).filter((name) => !artifacts.includes(name) && (name.endsWith('.dmg') || name.endsWith('.exe')))
: []
for (const name of stale) {
console.log(` ${dryRun ? 'would remove stale' : 'removing stale'} ${name}`)
if (!dryRun) rmSync(join(updatesDir, name), { force: true })
}
if (dryRun) console.log('(dry run: no files were written)')
console.log('done. Deploy with `cd ../deepseek-harness-web && python3 deploy.py`.')