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.
This commit is contained in:
winewill committed 2026-09-21 15:26:29 +08:00
1 parent ee0fc82267
commit 4ab8fbfacc
7 files changed
+40 -37

No files matched your search

@@ -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<string, string>
export function desktopBuildCommitEnvironment(packaged: DesktopBuildCommit): Record<string, string>
@@ -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<string, string>} 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',
}
}
@@ -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.
*/
@@ -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}',
+7 -7
View File
@@ -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<void> {
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
+1 -1
View File
@@ -96,7 +96,7 @@ async function main(): Promise<void> {
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) {
@@ -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<unknown>) => 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<typeof import('node:fs')>(),
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 },
],
})