diff --git a/product-source/hololake-platform/.gitignore b/product-source/hololake-platform/.gitignore index bd0d42c..0683f75 100644 --- a/product-source/hololake-platform/.gitignore +++ b/product-source/hololake-platform/.gitignore @@ -46,6 +46,7 @@ src-tauri/target # Generated mcp-server bundle (built by scripts/bundle-mcp-server.mjs) src-tauri/resources/mcp-server/ +src-tauri/resources/public-architecture/build-provenance.json # Python cache __pycache__/ diff --git a/product-source/hololake-platform/docs/skills/internal-release-packaging/SKILL.md b/product-source/hololake-platform/docs/skills/internal-release-packaging/SKILL.md index f1ab276..62ca4d1 100644 --- a/product-source/hololake-platform/docs/skills/internal-release-packaging/SKILL.md +++ b/product-source/hololake-platform/docs/skills/internal-release-packaging/SKILL.md @@ -26,7 +26,7 @@ pnpm package:internal:windows ## 构建前检查 -1. 工作树干净,当前分支包含目标 UI 和资源。 +1. 工作树干净,当前分支包含目标 UI 和资源,并显式传入当前 `HOLOLAKE_DEVELOPMENT_ID`。 2. `tauri.conf.json`、`Cargo.toml`、`package.json` 版本一致。 3. 运行相关测试、lint、TypeScript/Vite 构建。 4. 确认公共研发入口和 GLS 文件已经进入源码: @@ -45,6 +45,13 @@ pnpm package:internal:windows - Windows 载荷必须为 `PE32+ x86-64`,NSIS 安装包必须存在; - 每个安装包必须生成同名 `.sha256`,回传后再次校验。 +`scripts/internal-release-provenance.mjs` 是来源硬门: + +- 构建前只接受干净工作树,并把 REPO-014 完整提交、根树、`main` 分支、当前架构编号与版本、应用版本、开发编号、分发类型和目标架构写入包内 `build-provenance.json`; +- 打包后必须从应用包中精确找到且只找到一份来源记录,并逐字段回读一致; +- 来源记录在签名前进入应用资源,签名后补写任何文件都会破坏封印,不能作为发布流程; +- 来源记录只证明制品从哪份源码构建,不证明安装、部署、运行健康或人格主控权限。 + 内测 Mac 包是 ad-hoc 签名、未公证;Windows 包在没有商业证书时未做 Authenticode 签名。交付时必须明确说明,不能描述成正式签名发行版。 ## Windows 构建恢复顺序 diff --git a/product-source/hololake-platform/package.json b/product-source/hololake-platform/package.json index 02f376e..e2e755a 100644 --- a/product-source/hololake-platform/package.json +++ b/product-source/hololake-platform/package.json @@ -33,7 +33,7 @@ "playwright:integration": "playwright test --config playwright.integration.config.ts", "test:coverage": "node scripts/run-vitest-coverage.mjs", "verify:internal-package": "node scripts/verify-internal-package-content.mjs", - "test:internal-release": "node --test scripts/internal-release-version.test.mjs", + "test:internal-release": "node --test scripts/internal-release-version.test.mjs scripts/internal-release-provenance.test.mjs", "guard:deployment-source": "node scripts/deployment-source-guard.mjs", "test:deployment-source": "node --test scripts/deployment-source-guard.test.mjs", "test:native-authority": "bash scripts/test-guanghu-native-authority.sh && node scripts/validate-guanghu-native-profile.mjs", diff --git a/product-source/hololake-platform/scripts/build-macos-internal.sh b/product-source/hololake-platform/scripts/build-macos-internal.sh index 8d59604..614386f 100755 --- a/product-source/hololake-platform/scripts/build-macos-internal.sh +++ b/product-source/hololake-platform/scripts/build-macos-internal.sh @@ -7,6 +7,8 @@ cd "$repo_root" [[ "$(uname -s)" == "Darwin" ]] || { echo "macOS internal packages must be built on macOS." >&2; exit 2; } target="${HOLOLAKE_MAC_TARGET:-aarch64-apple-darwin}" +development_id="${HOLOLAKE_DEVELOPMENT_ID:?HOLOLAKE_DEVELOPMENT_ID is required for source-bound packaging}" +distribution="${HOLOLAKE_DISTRIBUTION:-internal}" version="$(node scripts/internal-release-version.mjs src-tauri/tauri.conf.json "${HOLOLAKE_VERSION:-}")" product_name="$(node -e "const fs=require('node:fs'); const value=JSON.parse(fs.readFileSync('src-tauri/tauri.conf.json','utf8')).productName; if(!value) process.exit(1); process.stdout.write(value)")" output_dir="${HOLOLAKE_OUTPUT_DIR:-$repo_root/artifacts/internal/$version}" @@ -14,6 +16,7 @@ target_root="${CARGO_TARGET_DIR:-$repo_root/src-tauri/target}" bundle_dir="$target_root/$target/release/bundle/macos" app="$bundle_dir/${HOLOLAKE_APP_NAME:-$product_name}.app" installer="$output_dir/HoloLake-Era-$version-Mac-internal-aarch64.dmg" +provenance_source="$repo_root/src-tauri/resources/public-architecture/build-provenance.json" tauri_config="${HOLOLAKE_TAURI_CONFIG:-}" if [[ -n "${HOLOLAKE_INSTALLER_BASENAME:-}" ]]; then installer_basename="${HOLOLAKE_INSTALLER_BASENAME//\{version\}/$version}" @@ -30,6 +33,15 @@ else fi command -v hdiutil >/dev/null || { echo "hdiutil is required." >&2; exit 2; } mkdir -p "$output_dir" +[[ ! -e "$provenance_source" ]] || { echo "Stale generated build provenance exists: $provenance_source" >&2; exit 1; } +cleanup_provenance() { rm -f "$provenance_source"; } +trap cleanup_provenance EXIT + +node scripts/internal-release-provenance.mjs generate \ + --output "$provenance_source" \ + --development-id "$development_id" \ + --distribution "$distribution" \ + --target-triple "$target" if [[ "${HOLOLAKE_SKIP_INSTALL:-0}" != "1" ]]; then "${pnpm_cmd[@]}" install --frozen-lockfile @@ -41,6 +53,11 @@ if [[ -n "$tauri_config" ]]; then tauri_args+=(--config "$tauri_config"); fi [[ -d "$app" ]] || { echo "No macOS application bundle was produced: $app" >&2; exit 1; } node scripts/verify-internal-package-content.mjs "$app" +node scripts/internal-release-provenance.mjs verify \ + --bundle "$app" \ + --development-id "$development_id" \ + --distribution "$distribution" \ + --target-triple "$target" codesign --force --deep --sign - "$app" codesign --verify --deep --strict "$app" hdiutil create -volname "HoloLake Era $version Internal" -srcfolder "$app" -ov -format UDZO "$installer" diff --git a/product-source/hololake-platform/scripts/internal-release-provenance.mjs b/product-source/hololake-platform/scripts/internal-release-provenance.mjs new file mode 100644 index 0000000..a7c9b1b --- /dev/null +++ b/product-source/hololake-platform/scripts/internal-release-provenance.mjs @@ -0,0 +1,174 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process' +import { mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises' +import { dirname, join, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' + +const COMMIT_PATTERN = /^[0-9a-f]{40}$/u +const VERSION_PATTERN = /^\d+\.\d+\.\d+$/u +const ARCHITECTURE_VERSION_PATTERN = /^\d{4}-\d{2}-\d{2}\.\d+$/u +const DEVELOPMENT_ID_PATTERN = /^DEV-\d{8}-\d{3}$/u +const CANONICAL_REPOSITORY = 'repo://guanghulab.com/code/bingshuo/hololake-system-architecture' +const CANONICAL_REPOSITORY_ID = 'REPO-014' +const CANONICAL_SOURCE_PATH = 'product-source/hololake-platform' +const PROVENANCE_FILE_NAME = 'build-provenance.json' + +function requireExactString(value, label, pattern) { + if (typeof value !== 'string' || !pattern.test(value)) { + throw new Error(`invalid ${label}`) + } + return value +} + +export function buildInternalReleaseProvenance(input) { + if (input.worktreeClean !== true) { + throw new Error('internal release provenance requires a clean source worktree') + } + if (input.sourceRepository !== CANONICAL_REPOSITORY) { + throw new Error('invalid source repository') + } + if (input.sourceRepositoryId !== CANONICAL_REPOSITORY_ID) { + throw new Error('invalid source repository id') + } + if (input.sourcePath !== CANONICAL_SOURCE_PATH) { + throw new Error('invalid source path') + } + + return { + schema: 'hololake.internal-release-provenance/v1', + architecture: { + id: requireExactString(input.architectureId, 'architecture id', /^HLP-[A-Z0-9-]+$/u), + version: requireExactString(input.architectureVersion, 'architecture version', ARCHITECTURE_VERSION_PATTERN), + }, + build: { + application_version: requireExactString(input.applicationVersion, 'application version', VERSION_PATTERN), + development_id: requireExactString(input.developmentId, 'development id', DEVELOPMENT_ID_PATTERN), + distribution: requireExactString(input.distribution, 'distribution', /^[a-z][a-z0-9-]+$/u), + recorded_at: requireExactString(input.recordedAt, 'recorded at', /^\d{4}-\d{2}-\d{2}T/u), + target_triple: requireExactString(input.targetTriple, 'target triple', /^[a-z0-9-]+$/u), + }, + source: { + branch: requireExactString(input.sourceBranch, 'source branch', /^[A-Za-z0-9._/-]+$/u), + commit: requireExactString(input.sourceCommit, 'source commit', COMMIT_PATTERN), + path: input.sourcePath, + repository: input.sourceRepository, + repository_id: input.sourceRepositoryId, + tree: requireExactString(input.sourceTree, 'source tree', COMMIT_PATTERN), + worktree_clean: true, + }, + } +} + +function expectEqual(actual, expected, label) { + if (actual !== expected) throw new Error(`${label} mismatch`) +} + +export function validateInternalReleaseProvenance(provenance, expected) { + if (!provenance || provenance.schema !== 'hololake.internal-release-provenance/v1') { + throw new Error('provenance schema mismatch') + } + expectEqual(provenance.source?.commit, expected.sourceCommit, 'source commit') + expectEqual(provenance.source?.tree, expected.sourceTree, 'source tree') + expectEqual(provenance.source?.branch, expected.sourceBranch, 'source branch') + expectEqual(provenance.source?.repository, expected.sourceRepository, 'source repository') + expectEqual(provenance.source?.repository_id, expected.sourceRepositoryId, 'source repository id') + expectEqual(provenance.source?.path, expected.sourcePath, 'source path') + expectEqual(provenance.source?.worktree_clean, expected.worktreeClean, 'source worktree state') + expectEqual(provenance.architecture?.id, expected.architectureId, 'architecture id') + expectEqual(provenance.architecture?.version, expected.architectureVersion, 'architecture version') + expectEqual(provenance.build?.application_version, expected.applicationVersion, 'application version') + expectEqual(provenance.build?.development_id, expected.developmentId, 'development id') + expectEqual(provenance.build?.distribution, expected.distribution, 'distribution') + expectEqual(provenance.build?.target_triple, expected.targetTriple, 'target triple') + return true +} + +function git(repositoryRoot, ...args) { + return execFileSync('git', ['-C', repositoryRoot, ...args], { encoding: 'utf8' }).trim() +} + +async function repositoryExpectation({ repositoryRoot, developmentId, distribution, targetTriple }) { + const architecture = JSON.parse(await readFile(join(repositoryRoot, 'routing/hololake-current-architecture.json'), 'utf8')) + const application = JSON.parse(await readFile(join(repositoryRoot, CANONICAL_SOURCE_PATH, 'src-tauri/tauri.conf.json'), 'utf8')) + return { + sourceCommit: git(repositoryRoot, 'rev-parse', 'HEAD'), + sourceTree: git(repositoryRoot, 'rev-parse', 'HEAD^{tree}'), + sourceBranch: git(repositoryRoot, 'branch', '--show-current'), + sourceRepository: CANONICAL_REPOSITORY, + sourceRepositoryId: CANONICAL_REPOSITORY_ID, + sourcePath: CANONICAL_SOURCE_PATH, + architectureId: architecture.architecture_id, + architectureVersion: architecture.version, + applicationVersion: application.version, + developmentId, + distribution, + targetTriple, + worktreeClean: git(repositoryRoot, 'status', '--porcelain=v1', '--untracked-files=all') === '', + } +} + +async function writeJsonAtomic(outputPath, value) { + await mkdir(dirname(outputPath), { recursive: true }) + const temporary = `${outputPath}.${process.pid}.tmp` + await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' }) + try { + await rename(temporary, outputPath) + } catch (error) { + await rm(temporary, { force: true }) + throw error + } +} + +async function findProvenanceFiles(directory) { + const found = [] + for (const entry of await readdir(directory, { withFileTypes: true })) { + const entryPath = join(directory, entry.name) + if (entry.isDirectory()) found.push(...await findProvenanceFiles(entryPath)) + else if (entry.isFile() && entry.name === PROVENANCE_FILE_NAME) found.push(entryPath) + } + return found +} + +function option(args, name) { + const index = args.indexOf(name) + if (index === -1 || !args[index + 1]) throw new Error(`${name} is required`) + return args[index + 1] +} + +async function main() { + const [command, ...args] = process.argv.slice(2) + if (!['generate', 'verify'].includes(command)) { + throw new Error('Usage: internal-release-provenance.mjs [options]') + } + + const repositoryRoot = git(process.cwd(), 'rev-parse', '--show-toplevel') + const developmentId = option(args, '--development-id') + const distribution = option(args, '--distribution') + const targetTriple = option(args, '--target-triple') + const expected = await repositoryExpectation({ repositoryRoot, developmentId, distribution, targetTriple }) + + if (command === 'generate') { + const outputPath = resolve(option(args, '--output')) + const provenance = buildInternalReleaseProvenance({ + ...expected, + recordedAt: new Date().toISOString(), + }) + await writeJsonAtomic(outputPath, provenance) + process.stdout.write(`${outputPath}\n`) + return + } + + const bundlePath = resolve(option(args, '--bundle')) + const provenanceFiles = await findProvenanceFiles(bundlePath) + if (provenanceFiles.length !== 1) { + throw new Error(`expected exactly one packaged ${PROVENANCE_FILE_NAME}, found ${provenanceFiles.length}`) + } + const provenance = JSON.parse(await readFile(provenanceFiles[0], 'utf8')) + validateInternalReleaseProvenance(provenance, expected) + process.stdout.write(`${provenanceFiles[0]}\n`) +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + await main() +} diff --git a/product-source/hololake-platform/scripts/internal-release-provenance.test.mjs b/product-source/hololake-platform/scripts/internal-release-provenance.test.mjs new file mode 100644 index 0000000..ea0770a --- /dev/null +++ b/product-source/hololake-platform/scripts/internal-release-provenance.test.mjs @@ -0,0 +1,77 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { + buildInternalReleaseProvenance, + validateInternalReleaseProvenance, +} from './internal-release-provenance.mjs' + +const sourceCommit = '1'.repeat(40) +const sourceTree = '2'.repeat(40) + +function input(overrides = {}) { + return { + sourceCommit, + sourceTree, + sourceBranch: 'main', + sourceRepository: 'repo://guanghulab.com/code/bingshuo/hololake-system-architecture', + sourceRepositoryId: 'REPO-014', + sourcePath: 'product-source/hololake-platform', + architectureId: 'HLP-CURRENT-ARCH-001', + architectureVersion: '2026-08-12.16', + applicationVersion: '0.4.6', + developmentId: 'DEV-20260811-010', + distribution: 'local-candidate', + targetTriple: 'aarch64-apple-darwin', + recordedAt: '2026-08-12T07:00:00+08:00', + worktreeClean: true, + ...overrides, + } +} + +test('builds a source-bound local candidate provenance record', () => { + assert.deepEqual(buildInternalReleaseProvenance(input()), { + schema: 'hololake.internal-release-provenance/v1', + architecture: { + id: 'HLP-CURRENT-ARCH-001', + version: '2026-08-12.16', + }, + build: { + application_version: '0.4.6', + development_id: 'DEV-20260811-010', + distribution: 'local-candidate', + recorded_at: '2026-08-12T07:00:00+08:00', + target_triple: 'aarch64-apple-darwin', + }, + source: { + branch: 'main', + commit: sourceCommit, + path: 'product-source/hololake-platform', + repository: 'repo://guanghulab.com/code/bingshuo/hololake-system-architecture', + repository_id: 'REPO-014', + tree: sourceTree, + worktree_clean: true, + }, + }) +}) + +test('refuses to issue provenance for a dirty source tree', () => { + assert.throws( + () => buildInternalReleaseProvenance(input({ worktreeClean: false })), + /clean source worktree/, + ) +}) + +test('validates exact source, architecture, application and development bindings', () => { + const provenance = buildInternalReleaseProvenance(input()) + + assert.equal(validateInternalReleaseProvenance(provenance, input()), true) + assert.throws( + () => validateInternalReleaseProvenance(provenance, input({ sourceCommit: '3'.repeat(40) })), + /source commit mismatch/, + ) + assert.throws( + () => validateInternalReleaseProvenance(provenance, input({ architectureVersion: '2026-08-12.17' })), + /architecture version mismatch/, + ) +})