feat(hololake): prepare signed 1.2.0 update artifacts

This commit is contained in:
冰朔 2026-09-03 21:33:32 +08:00
commit d05a118f64
9 changed files with 118 additions and 10 deletions

View file

@ -1,12 +1,12 @@
{
"name": "hololake-clean-desktop",
"version": "1.1.0",
"version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hololake-clean-desktop",
"version": "1.1.0",
"version": "1.2.0",
"dependencies": {
"@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-dialog": "2.7.2",

View file

@ -1,12 +1,13 @@
{
"name": "hololake-clean-desktop",
"private": true,
"version": "1.1.0",
"version": "1.2.0",
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1",
"build": "tsc -b && vite build",
"test": "npm run build",
"test": "npm run build && node --test scripts/*.test.mjs",
"release:manifest": "node scripts/build-update-manifest.mjs",
"tauri": "tauri"
},
"dependencies": {

View file

@ -49,7 +49,7 @@
"kind": "SYSTEM_FOUNDATION",
"audience": "SYSTEM",
"summary_zh": "只接收通过光湖信任根验证的软件更新。",
"state": "CLIENT_TRUST_ROOT_BOUND_SERVER_HTTP_204_NO_PUBLIC_RELEASE",
"state": "CLIENT_TRUST_ROOT_BOUND_MANIFEST_BUILDER_VERIFIED_SERVER_HTTP_204_NO_PUBLIC_RELEASE",
"source": "src-tauri/tauri.conf.json#plugins.updater"
},
{

View file

@ -0,0 +1,67 @@
#!/usr/bin/env node
import crypto from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
function required(value, name) {
if (!value) throw new Error(`${name}_REQUIRED`)
return value
}
export function buildManifest({ version, artifact, signatureFile, baseUrl, notes, publishedAt }) {
if (!/^\d+\.\d+\.\d+$/.test(required(version, 'VERSION'))) throw new Error('VERSION_INVALID')
const artifactPath = path.resolve(required(artifact, 'ARTIFACT'))
const signaturePath = path.resolve(required(signatureFile, 'SIGNATURE'))
if (!fs.statSync(artifactPath, { throwIfNoEntry: false })?.isFile()) throw new Error('ARTIFACT_NOT_READABLE')
if (!fs.statSync(signaturePath, { throwIfNoEntry: false })?.isFile()) throw new Error('SIGNATURE_NOT_READABLE')
const signature = fs.readFileSync(signaturePath, 'utf8').trim()
if (signature.length < 32) throw new Error('SIGNATURE_INVALID')
const root = new URL(required(baseUrl, 'BASE_URL'))
if (root.protocol !== 'https:') throw new Error('HTTPS_BASE_URL_REQUIRED')
if (root.username || root.password || root.search || root.hash) throw new Error('BASE_URL_MUST_NOT_CONTAIN_CREDENTIALS_OR_QUERY')
const bytes = fs.readFileSync(artifactPath)
const fileName = path.basename(artifactPath)
const url = new URL(fileName, root.href.endsWith('/') ? root : new URL(`${root.href}/`)).href
return {
version,
notes: notes || 'HoloLake signed update',
pub_date: publishedAt || new Date().toISOString(),
platforms: {
'darwin-aarch64': {
signature,
url,
sha256: crypto.createHash('sha256').update(bytes).digest('hex'),
bytes: bytes.length,
},
},
}
}
function parseArgs(values) {
const result = {}
for (let index = 0; index < values.length; index += 2) {
if (!values[index]?.startsWith('--') || values[index + 1] === undefined) throw new Error('ARGUMENTS_INVALID')
result[values[index].slice(2)] = values[index + 1]
}
return result
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
const args = parseArgs(process.argv.slice(2))
const manifest = buildManifest({
version: args.version,
artifact: args.artifact,
signatureFile: args.signature,
baseUrl: args['base-url'],
notes: args.notes,
publishedAt: args['published-at'],
})
const output = path.resolve(required(args.output, 'OUTPUT'))
fs.mkdirSync(path.dirname(output), { recursive: true })
const temporary = `${output}.tmp`
fs.writeFileSync(temporary, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o644 })
fs.renameSync(temporary, output)
process.stdout.write(`${JSON.stringify({ outcome: 'MANIFEST_WRITTEN', output, version: manifest.version, platform: 'darwin-aarch64' })}\n`)
}

View file

@ -0,0 +1,37 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { buildManifest } from './build-update-manifest.mjs'
function fixture() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-update-manifest-'))
const artifact = path.join(root, 'HoloLake.app.tar.gz')
const signatureFile = `${artifact}.sig`
fs.writeFileSync(artifact, 'signed-update-bytes')
fs.writeFileSync(signatureFile, 'trusted-signature-material-that-is-long-enough')
return { artifact, signatureFile }
}
test('builds an HTTPS arm64 manifest with immutable evidence', () => {
const files = fixture()
const manifest = buildManifest({
version: '1.2.0', ...files,
baseUrl: 'https://guanghulab.com/hololake/releases/1.2.0/',
notes: 'Current direct-language baseline',
publishedAt: '2026-09-03T00:00:00Z',
})
assert.equal(manifest.version, '1.2.0')
assert.equal(manifest.platforms['darwin-aarch64'].bytes, 19)
assert.match(manifest.platforms['darwin-aarch64'].url, /^https:\/\/guanghulab\.com\//)
assert.equal(manifest.platforms['darwin-aarch64'].sha256.length, 64)
})
test('rejects HTTP, missing signatures and malformed versions', () => {
const files = fixture()
assert.throws(() => buildManifest({ version: '1.2', ...files, baseUrl: 'https://guanghulab.com/' }), /VERSION_INVALID/)
assert.throws(() => buildManifest({ version: '1.2.0', ...files, baseUrl: 'http://guanghulab.com/' }), /HTTPS_BASE_URL_REQUIRED/)
fs.writeFileSync(files.signatureFile, 'short')
assert.throws(() => buildManifest({ version: '1.2.0', ...files, baseUrl: 'https://guanghulab.com/' }), /SIGNATURE_INVALID/)
})

View file

@ -1328,7 +1328,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hololake-clean-desktop"
version = "1.1.0"
version = "1.2.0"
dependencies = [
"base64 0.22.1",
"chrono",

View file

@ -1,6 +1,6 @@
[package]
name = "hololake-clean-desktop"
version = "1.1.0"
version = "1.2.0"
description = "HoloLake clean personal language operating system shell"
authors = ["HoloLake"]
license = "AGPL-3.0-or-later"

View file

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "HoloLake",
"version": "1.1.0",
"version": "1.2.0",
"identifier": "world.guanghu.hololake",
"build": { "frontendDist": "../dist", "devUrl": "http://127.0.0.1:5211", "beforeDevCommand": "npm run dev", "beforeBuildCommand": "npm run build" },
"app": {