From 4ab8fbfacc659165ab5647424ff7d5fa8974127d Mon Sep 17 00:00:00 2001 From: winewill <324803222+winewill@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:26:29 +0800 Subject: [PATCH] fix(desktop): name the commit a build records instead of its provenance The concrete-terms gate forbids "provenance": the repository asks for the exact source, field, or identity a value holds. The module records one commit and whether its tree was dirty, so it and its callers say that. The toolchain step also has to stay out of the way of the packaging entry's error-recording test, which fakes the host platform and would otherwise report the absent Windows compilers rather than the failure it exercises. The test mocks the probe, as it already does for the release environment and the signing keychain, and expects the new stage. --- ...nance.d.mts => desktop-build-commit.d.mts} | 18 +++++++------- ...rovenance.mjs => desktop-build-commit.mjs} | 24 +++++++++---------- .../scripts/desktop-toolchain-preflight.ts | 4 ++-- .../scripts/electron-builder-config.mjs | 12 +++++----- apps/desktop/scripts/package-target.ts | 14 +++++------ apps/desktop/scripts/upload-target.ts | 2 +- .../tests/package-target-errors.spec.ts | 3 +++ 7 files changed, 40 insertions(+), 37 deletions(-) rename apps/desktop/scripts/{desktop-build-provenance.d.mts => desktop-build-commit.d.mts} (50%) rename apps/desktop/scripts/{desktop-build-provenance.mjs => desktop-build-commit.mjs} (65%) diff --git a/apps/desktop/scripts/desktop-build-provenance.d.mts b/apps/desktop/scripts/desktop-build-commit.d.mts similarity index 50% rename from apps/desktop/scripts/desktop-build-provenance.d.mts rename to apps/desktop/scripts/desktop-build-commit.d.mts index db6796515a..82118e24e1 100644 --- a/apps/desktop/scripts/desktop-build-provenance.d.mts +++ b/apps/desktop/scripts/desktop-build-commit.d.mts @@ -1,5 +1,5 @@ /** Commit a build came from, and whether its checkout carried uncommitted changes. */ -export interface DesktopBuildProvenance { +export interface DesktopBuildCommit { readonly commit: string readonly dirty: boolean } @@ -13,20 +13,20 @@ export const DESKTOP_BUILD_DIRTY_ENV: 'DSH_DESKTOP_BUILD_DIRTY' /** * Read the checkout's current commit and whether it carries uncommitted changes. * @param repositoryRoot - Directory to inspect. - * @returns Provenance of the tree being packaged. + * @returns The commit being packaged and whether its tree was modified. */ -export function readDesktopBuildProvenance(repositoryRoot: string): DesktopBuildProvenance +export function readDesktopBuildCommit(repositoryRoot: string): DesktopBuildCommit /** - * Resolve provenance a parent packaging process recorded. + * Resolve the commit a parent packaging process recorded. * @param env - Packaging environment. - * @returns Provenance, or undefined outside a packaging run. + * @returns The packaged commit, or undefined outside a packaging run. */ -export function resolveDesktopBuildProvenance(env: NodeJS.ProcessEnv): DesktopBuildProvenance | undefined +export function resolveDesktopBuildCommit(env: NodeJS.ProcessEnv): DesktopBuildCommit | undefined /** - * Describe provenance as the environment variables child processes read. - * @param provenance - Provenance to pass down. + * Describe the packaged commit as the environment variables child processes read. + * @param packaged - Commit and tree state to pass down. * @returns Variables to merge into a child environment. */ -export function desktopBuildProvenanceEnvironment(provenance: DesktopBuildProvenance): Record +export function desktopBuildCommitEnvironment(packaged: DesktopBuildCommit): Record diff --git a/apps/desktop/scripts/desktop-build-provenance.mjs b/apps/desktop/scripts/desktop-build-commit.mjs similarity index 65% rename from apps/desktop/scripts/desktop-build-provenance.mjs rename to apps/desktop/scripts/desktop-build-commit.mjs index b2c45a4789..2f1dc27cfd 100644 --- a/apps/desktop/scripts/desktop-build-provenance.mjs +++ b/apps/desktop/scripts/desktop-build-commit.mjs @@ -19,9 +19,9 @@ export const DESKTOP_BUILD_DIRTY_ENV = 'DSH_DESKTOP_BUILD_DIRTY' /** * Read the checkout's current commit and whether it carries uncommitted changes. * @param {string} repositoryRoot - Directory to inspect. - * @returns {{ commit: string, dirty: boolean }} Provenance of the tree being packaged. + * @returns {{ commit: string, dirty: boolean }} The commit being packaged and whether its tree was modified. */ -export function readDesktopBuildProvenance(repositoryRoot) { +export function readDesktopBuildCommit(repositoryRoot) { const git = (args) => execFileSync('git', args, { cwd: repositoryRoot, encoding: 'utf8' }).trim() return { commit: git(['rev-parse', 'HEAD']), @@ -30,29 +30,29 @@ export function readDesktopBuildProvenance(repositoryRoot) { } /** - * Resolve provenance a parent packaging process recorded. + * Resolve the commit a parent packaging process recorded. * @param {NodeJS.ProcessEnv} env - Packaging environment. - * @returns {{ commit: string, dirty: boolean } | undefined} Provenance, or undefined outside a packaging run. + * @returns {{ commit: string, dirty: boolean } | undefined} The packaged commit, or undefined outside a packaging run. */ -export function resolveDesktopBuildProvenance(env) { +export function resolveDesktopBuildCommit(env) { const commit = env[DESKTOP_BUILD_COMMIT_ENV]?.trim() if (commit === undefined || commit === '') return undefined - if (!/^[0-9a-f]{40}$/u.test(commit)) throw new Error(`desktop build provenance: ${DESKTOP_BUILD_COMMIT_ENV} must be a commit hash`) + if (!/^[0-9a-f]{40}$/u.test(commit)) throw new Error(`desktop build commit: ${DESKTOP_BUILD_COMMIT_ENV} must be a commit hash`) const dirty = env[DESKTOP_BUILD_DIRTY_ENV]?.trim() if (dirty !== undefined && dirty !== '' && dirty !== '0' && dirty !== '1') { - throw new Error(`desktop build provenance: ${DESKTOP_BUILD_DIRTY_ENV} must be 0 or 1`) + throw new Error(`desktop build commit: ${DESKTOP_BUILD_DIRTY_ENV} must be 0 or 1`) } return { commit, dirty: dirty === '1' } } /** - * Describe provenance as the environment variables child processes read. - * @param {{ commit: string, dirty: boolean }} provenance - Provenance to pass down. + * Describe the packaged commit as the environment variables child processes read. + * @param {{ commit: string, dirty: boolean }} packaged - Commit and tree state to pass down. * @returns {Record} Variables to merge into a child environment. */ -export function desktopBuildProvenanceEnvironment(provenance) { +export function desktopBuildCommitEnvironment(packaged) { return { - [DESKTOP_BUILD_COMMIT_ENV]: provenance.commit, - [DESKTOP_BUILD_DIRTY_ENV]: provenance.dirty ? '1' : '0', + [DESKTOP_BUILD_COMMIT_ENV]: packaged.commit, + [DESKTOP_BUILD_DIRTY_ENV]: packaged.dirty ? '1' : '0', } } diff --git a/apps/desktop/scripts/desktop-toolchain-preflight.ts b/apps/desktop/scripts/desktop-toolchain-preflight.ts index 812e7b1828..ff855cfd09 100644 --- a/apps/desktop/scripts/desktop-toolchain-preflight.ts +++ b/apps/desktop/scripts/desktop-toolchain-preflight.ts @@ -70,7 +70,7 @@ async function probeWindowsInstallerToolchain(environment: NodeJS.ProcessEnv): P /** * Probe every external tool one packaging run needs. - * @param platform - Target platform, which decides the installer toolchain probes. + * @param platform - Target platform; a Windows target already requires a Windows build host. * @param environment - Packaging environment used to locate Windows tooling. * @returns Every probe that failed, empty when the host can run the packaging sequence. */ @@ -87,7 +87,7 @@ export async function probeDesktopToolchain( /** * Probe the toolchain and fail with every problem the host has. - * @param platform - Target platform, which decides the installer toolchain probes. + * @param platform - Target platform; a Windows target already requires a Windows build host. * @param environment - Packaging environment used to locate Windows tooling. * @returns Resolves when every probe passes. */ diff --git a/apps/desktop/scripts/electron-builder-config.mjs b/apps/desktop/scripts/electron-builder-config.mjs index 327918073d..dafd999a4f 100644 --- a/apps/desktop/scripts/electron-builder-config.mjs +++ b/apps/desktop/scripts/electron-builder-config.mjs @@ -19,7 +19,7 @@ import { scrubWindowsSigningEnvironment, } from './windows-sign.mjs' import { resolveDesktopAutoUpdateConfig } from './desktop-auto-update-environment.mjs' -import { resolveDesktopBuildProvenance } from './desktop-build-provenance.mjs' +import { resolveDesktopBuildCommit } from './desktop-build-commit.mjs' import { resolveDesktopBuildVersion } from './desktop-build-version.mjs' import { resolveDesktopPolicyEnvironment } from './desktop-policy-environment.mjs' import { desktopTargetBuildPaths, resolveDesktopBuildTarget } from './desktop-build-paths.mjs' @@ -88,18 +88,18 @@ export function createElectronBuilderConfig( } const update = unsigned ? undefined : resolveDesktopAutoUpdateConfig(env, resolvedPlatform, resolvedArch) if (preparedRuntime !== undefined) buildPaths.dsh = preparedRuntime - // electron-builder merges extraMetadata into the packaged manifest, so a build identifier here reaches + // electron-builder merges extraMetadata into the packaged manifest, so a build version here reaches // the artifact names, the update feed, and the installed app.getVersion() the updater compares against. const productVersion = JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8')).version - const releaseId = resolveDesktopBuildVersion(env, productVersion) - const provenance = resolveDesktopBuildProvenance(env) + const buildVersion = resolveDesktopBuildVersion(env, productVersion) + const packaged = resolveDesktopBuildCommit(env) return { appId, extraMetadata: { dshDesktopAppId: appId, dshMandatoryUpdatePolicy: policy, - ...releaseId === productVersion ? {} : { version: releaseId }, - ...provenance === undefined ? {} : { dshBuildCommit: provenance.commit, dshBuildDirty: provenance.dirty }, + ...buildVersion === productVersion ? {} : { version: buildVersion }, + ...packaged === undefined ? {} : { dshBuildCommit: packaged.commit, dshBuildDirty: packaged.dirty }, }, productName: 'DeepSeek Harness', artifactName: 'deepseek-harness-${version}-${os}-${arch}.${ext}', diff --git a/apps/desktop/scripts/package-target.ts b/apps/desktop/scripts/package-target.ts index a038ff1893..8f06ea3c50 100644 --- a/apps/desktop/scripts/package-target.ts +++ b/apps/desktop/scripts/package-target.ts @@ -21,7 +21,7 @@ import { notarizeMacOS } from './notarize-macos.mjs' import { resolveMacOSNotarizationEnvironment } from './desktop-release-environment.mjs' import { DESKTOP_BUILD_VERSION_ENV, resolveDesktopBuildVersion, validateDesktopBuildVersion } from './desktop-build-version.mjs' import { suggestDesktopBuildVersion } from './desktop-build-version-discovery.ts' -import { desktopBuildProvenanceEnvironment, readDesktopBuildProvenance, resolveDesktopBuildProvenance } from './desktop-build-provenance.mjs' +import { desktopBuildCommitEnvironment, readDesktopBuildCommit, resolveDesktopBuildCommit } from './desktop-build-commit.mjs' import { requireDesktopToolchain } from './desktop-toolchain-preflight.ts' import { withMacOSNotarizationProxy } from './macos-notarization-proxy.ts' @@ -144,7 +144,7 @@ function writeReleaseRecord( throw new Error(`desktop package: desktop version ${desktopVersion} does not match dsh version ${dshVersion}`) } const buildVersion = resolveDesktopBuildVersion(environment, dshVersion) - const provenance = resolveDesktopBuildProvenance(environment) + const packaged = resolveDesktopBuildCommit(environment) const update = resolveDesktopAutoUpdateConfig(environment, target.platform, target.arch) const recordPath = join(artifactsRoot, desktopBuildRecordFilename(target.name)) const temporaryPath = `${recordPath}.tmp` @@ -155,7 +155,7 @@ function writeReleaseRecord( environment: update.environment, publicUrl: update.publicUrl, // Upload tags the commit a production release was packaged from, which is no longer discoverable from the build tree. - ...provenance === undefined ? {} : { commit: provenance.commit, dirty: provenance.dirty }, + ...packaged === undefined ? {} : { commit: packaged.commit, dirty: packaged.dirty }, }, null, 2)}\n`) renameSync(temporaryPath, recordPath) } @@ -325,14 +325,14 @@ async function main(): Promise { return } process.stdout.write(`desktop package: ${target.name} publishes ${buildVersion}${buildVersion === productVersion ? '' : ` for product version ${productVersion}`}\n`) - const provenance = readDesktopBuildProvenance(REPOSITORY_ROOT) - Object.assign(environment, desktopBuildProvenanceEnvironment(provenance)) + const packaged = readDesktopBuildCommit(REPOSITORY_ROOT) + Object.assign(environment, desktopBuildCommitEnvironment(packaged)) const secrets = Object.entries(environment).filter(([name]) => /KEY|SECRET|TOKEN|PASSWORD|APPLE_ID/iu.test(name)).map(([, value]) => value ?? '') const run = createPackagingRun(join(APP_ROOT, '.desktop-build', 'packaging-runs'), { target: target.name, unsigned: invocation.unsigned, directory: invocation.directory, prepareOnly: invocation.prepareOnly, version: buildVersion, productVersion, node: process.version, - commit: provenance.commit, - dirty: provenance.dirty, + commit: packaged.commit, + dirty: packaged.dirty, }, { parallel: target.platform === 'darwin', secrets }) console.log(`DESKTOP_PACKAGING_RECORD ${run.directory}`) const previousDirectory = process.env.DSH_DESKTOP_PACKAGING_RUN_DIR diff --git a/apps/desktop/scripts/upload-target.ts b/apps/desktop/scripts/upload-target.ts index e61c552e7b..7942828845 100644 --- a/apps/desktop/scripts/upload-target.ts +++ b/apps/desktop/scripts/upload-target.ts @@ -96,7 +96,7 @@ async function main(): Promise { function recordProductionRelease(plan: DesktopUploadPlan): void { const tag = desktopReleaseTag(plan.version) if (plan.commit === undefined) { - process.stdout.write(`desktop upload: this package predates recorded provenance; tag it by hand as ${tag}\n`) + process.stdout.write(`desktop upload: this package was built before builds recorded their commit; tag it by hand as ${tag}\n`) return } if (plan.dirty === true) { diff --git a/apps/desktop/tests/package-target-errors.spec.ts b/apps/desktop/tests/package-target-errors.spec.ts index 4022973816..9c8297ef20 100644 --- a/apps/desktop/tests/package-target-errors.spec.ts +++ b/apps/desktop/tests/package-target-errors.spec.ts @@ -12,6 +12,8 @@ vi.mock('../scripts/desktop-package-environment.mjs', () => ({ vi.mock('../scripts/macos-signing-keychain.mjs', () => ({ withMacOSSigningKeychain: async (_environment: object, action: (environment: object) => Promise) => action({}), })) +// This test fakes the host platform, so the real probes would report the absent Windows compilers rather than the failure under test. +vi.mock('../scripts/desktop-toolchain-preflight.ts', () => ({ requireDesktopToolchain: async () => {} })) vi.mock('node:fs', async importOriginal => ({ ...await importOriginal(), rmSync: () => { throw new AggregateError([new Error('credential-sentinel')], 'restore:mac-proxy recovery required') }, @@ -51,6 +53,7 @@ it.each(['win32', 'darwin'] as const)('records and prints redacted parent failur expect(JSON.parse(await readFile(join(state.directory, 'result.json'), 'utf8'))).toMatchObject({ success: false, stages: [ { stage: 'configuration', success: true }, + { stage: 'toolchain', success: true }, { stage: platform === 'win32' ? 'windows-package' : 'macos-package', success: false }, ], })