fix: verify and number unnotarized macOS artifacts
This commit is contained in:
1 parent
a98c6aae78
commit
1379394224
18 files changed
+145
-33
No files matched your search
@@ -27,8 +27,8 @@
|
||||
en: e3f51e7130a0fe05
|
||||
zh: 70fd6a40d5899871
|
||||
/desktop-packaging/macos-self-hosted-packaging:
|
||||
en: 7fed0945686f721c
|
||||
zh: 821f0ba4ca07f9fc
|
||||
en: a3af9523fdc60036
|
||||
zh: 5e18d4740e9b7f57
|
||||
/desktop-packaging/unsigned-windows-test-installer:
|
||||
en: 6843a0fdc03ead58
|
||||
zh: 957f92fc8126e23e
|
||||
|
||||
@@ -116,7 +116,7 @@ Run `node apps/desktop/scripts/setup-macos-packaging.mjs "$HOME/dsh-ci"` to prep
|
||||
Run `bash apps/desktop/scripts/start-macos-packaging-runner.sh "$HOME/dsh-ci/runner" "$HOME/dsh-ci/packaging" <absolute-node-24-bin-directory> --check` to check the host tools and configuration files. The script selects the active Xcode and its macOS SDK together and compiles a small linker probe. After registration, omit `--check` to run in the foreground with a cleared inherited environment and prevent idle sleep while the runner is alive. Stop it with Ctrl-C before installing the service below. The runner owns its `_work` checkout, separate from daily development. The account can read its signing files, so run only trusted code. The existing notarization proxy restoration rules apply; after forced termination, inspect retained recovery records before another build. New setup templates set `DSH_DESKTOP_MACOS_SIGNATURE_CACHE_DIR` to the absolute `cache/macos-signatures` directory outside checkout. For existing installations, rerun setup and add that field to each preset; existing files are preserved. This retains signatures across clean checkouts; cache verification and limits are described under [macOS signing](#macos-release-signing).
|
||||
|
||||
|
||||
The `skip_notarization` workflow option defaults to `false`; the equivalent CLI flag is `--skip-notarization`. It retains Developer ID signing and packaged-runtime verification, but omits Apple notarization and stapling and does not require Apple notarization credentials. DMG/ZIP filenames carry `-unnotarized`, are stored under `.desktop-build/targets/<target>/unnotarized-artifacts/`, and are uploaded with an `unnotarized-debug` label. These artifacts are for internal debugging only: Gatekeeper may block downloaded copies, and they are not qualified for distribution or update publishing. No release completion record is written. The flag rejects unsigned, Windows and preparation-only builds.
|
||||
The `skip_notarization` workflow option defaults to `false`; the equivalent CLI flag is `--skip-notarization`. It retains Developer ID signing, DMG signature verification and packaged-runtime verification, but omits Apple notarization and stapling and does not require Apple notarization credentials. DMG/ZIP filenames carry `-unnotarized`, are stored under `.desktop-build/targets/<target>/unnotarized-artifacts/`, and are uploaded with an `unnotarized-debug` label. These artifacts are for internal debugging only: Gatekeeper may block downloaded copies, and they are not qualified for distribution or update publishing. No release completion record is written. The flag rejects unsigned, Windows and preparation-only builds.
|
||||
|
||||
Install the user service from the checkout as the same account that registered the runner, without `sudo`:
|
||||
|
||||
@@ -126,7 +126,7 @@ bash apps/desktop/scripts/macos-runner-service.sh start "$HOME/dsh-ci/runner"
|
||||
bash apps/desktop/scripts/macos-runner-service.sh status "$HOME/dsh-ci/runner"
|
||||
```
|
||||
|
||||
The installer copies its startup script into `runner/dsh-service`, sets the service PATH to Node 24 plus system tools, and uses GitHub's `svc.sh` and `runsvc.sh`. The LaunchAgent survives SSH disconnection, but requires that account's desktop login after reboot and stops on desktop logout. FileVault disk unlock remains an operator step; this does not provide unattended boot. Logs are `runner/dsh-service/stdout.log` and `stderr.log`; verify `Connected to GitHub` and `Listening for Jobs`. To update installed startup code or tool paths, wait until the runner is idle, run `stop`, then `uninstall`, and repeat installation. Neither action unregisters the runner or removes packaging credentials.
|
||||
The installer copies its startup script into `runner/dsh-service`, sets the service PATH to Node 24 plus system tools, and uses GitHub's `svc.sh` and `runsvc.sh`. The LaunchAgent survives SSH disconnection, but requires that account's desktop login after reboot and stops on desktop logout. FileVault disk unlock remains an operator step; this does not provide unattended boot. Logs are `runner/dsh-service/stdout.log` and `stderr.log`; verify `Connected to GitHub` and `Listening for Jobs`. To update installed startup code or tool paths, wait until the runner is idle, run `stop`, then `uninstall`, and repeat installation. Neither action unregisters the runner or removes packaging credentials. Uninstall retains `runner/dsh-service` (including logs) and the explicit `.path`; reinstall replaces the startup snapshot and PATH.
|
||||
|
||||
<a id="unsigned-windows-test-installer"></a>
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ macOS arm64 命令要求 Apple Silicon。macOS x64 命令可以在 Intel macOS
|
||||
运行 `bash apps/desktop/scripts/start-macos-packaging-runner.sh "$HOME/dsh-ci/runner" "$HOME/dsh-ci/packaging" <absolute-node-24-bin-directory> --check` 检查宿主工具和配置文件。脚本同时选择当前 Xcode 及其 macOS SDK,并编译小型链接探针。注册后省略 `--check`,清理继承环境并在前台启动 runner,存活期间阻止空闲睡眠。安装下方服务前,先用 Ctrl-C 停止。runner 拥有 `_work` 下的源码目录,与日常开发分开。该账户可读取签名文件,因此只执行可信代码。沿用现有公证代理恢复规则;强制终止后,先检查保留的恢复记录再构建。新建模板将 `DSH_DESKTOP_MACOS_SIGNATURE_CACHE_DIR` 设为 checkout 外 `cache/macos-signatures` 的绝对路径。已有部署重新运行 setup 后,在各 preset 中补上该字段;已有文件保持不变。这样可跨干净检出保留签名;缓存验证和容量限制见 [macOS 签名](#macos-release-signing)。
|
||||
|
||||
|
||||
工作流选项 `skip_notarization` 默认为 `false`;对应 CLI 参数是 `--skip-notarization`。该模式保留 Developer ID 签名和打包运行时验证,跳过 Apple 公证和 stapling,不要求 Apple 公证凭据。DMG/ZIP 文件名带 `-unnotarized`,存放于 `.desktop-build/targets/<target>/unnotarized-artifacts/`,上传产物标为 `unnotarized-debug`。这些产物仅供内部调试:Gatekeeper 可能阻止下载副本运行,不能视为可分发包或用于更新发布。该模式不写入发布完成记录,不能与未签名、Windows 或仅准备构建组合。
|
||||
工作流选项 `skip_notarization` 默认为 `false`;对应 CLI 参数是 `--skip-notarization`。该模式保留 Developer ID 签名、DMG 签名验证和打包运行时验证,跳过 Apple 公证和 stapling,不要求 Apple 公证凭据。DMG/ZIP 文件名带 `-unnotarized`,存放于 `.desktop-build/targets/<target>/unnotarized-artifacts/`,上传产物标为 `unnotarized-debug`。这些产物仅供内部调试:Gatekeeper 可能阻止下载副本运行,不能视为可分发包或用于更新发布。该模式不写入发布完成记录,不能与未签名、Windows 或仅准备构建组合。
|
||||
|
||||
在 checkout 中,以注册 runner 的同一账户安装用户服务,不使用 `sudo`:
|
||||
|
||||
@@ -126,7 +126,7 @@ bash apps/desktop/scripts/macos-runner-service.sh start "$HOME/dsh-ci/runner"
|
||||
bash apps/desktop/scripts/macos-runner-service.sh status "$HOME/dsh-ci/runner"
|
||||
```
|
||||
|
||||
安装脚本将启动脚本复制到 `runner/dsh-service`,将服务 PATH 设为 Node 24 和系统工具,并使用 GitHub 的 `svc.sh` 与 `runsvc.sh`。LaunchAgent 在 SSH 断开后继续运行,但重启后需要该账户登录桌面,注销桌面时停止。FileVault 磁盘解锁仍由操作人员完成,这不提供无人值守开机。日志位于 `runner/dsh-service/stdout.log` 和 `stderr.log`;验收时确认出现 `Connected to GitHub` 与 `Listening for Jobs`。更新已安装的启动脚本或工具路径时,等待 runner 空闲,依次执行 `stop`、`uninstall`,再重新安装。这两个操作都不会注销 runner 或删除打包凭据。
|
||||
安装脚本将启动脚本复制到 `runner/dsh-service`,将服务 PATH 设为 Node 24 和系统工具,并使用 GitHub 的 `svc.sh` 与 `runsvc.sh`。LaunchAgent 在 SSH 断开后继续运行,但重启后需要该账户登录桌面,注销桌面时停止。FileVault 磁盘解锁仍由操作人员完成,这不提供无人值守开机。日志位于 `runner/dsh-service/stdout.log` 和 `stderr.log`;验收时确认出现 `Connected to GitHub` 与 `Listening for Jobs`。更新已安装的启动脚本或工具路径时,等待 runner 空闲,依次执行 `stop`、`uninstall`,再重新安装。这两个操作都不会注销 runner 或删除打包凭据。卸载保留 `runner/dsh-service`(包括日志)和显式 `.path`;重新安装会替换启动快照和 PATH。
|
||||
|
||||
<a id="unsigned-windows-test-installer"></a>
|
||||
|
||||
|
||||
@@ -73,3 +73,14 @@ export function developmentRuntimeDirectory(
|
||||
hostPlatform?: NodeJS.Platform,
|
||||
hostArch?: string,
|
||||
): string
|
||||
|
||||
/**
|
||||
* Select the output directory and filename suffix for one packaging mode.
|
||||
* @param paths Target artifact directories.
|
||||
* @param mode Validated packaging mode.
|
||||
* @returns Matching artifact destination and filename marker.
|
||||
*/
|
||||
export function desktopArtifactOutput(
|
||||
paths: Pick<DesktopTargetBuildPaths, 'artifacts' | 'unsignedArtifacts' | 'unnotarizedArtifacts'>,
|
||||
mode: { unsigned: boolean, skipNotarization: boolean },
|
||||
): { directory: string, suffix: string }
|
||||
@@ -106,3 +106,15 @@ export function developmentRuntimeDirectory(
|
||||
) {
|
||||
return join(resolveDesktopTargetBuildPaths(env, hostPlatform, hostArch).runtime, 'primary-runtime')
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the output directory and filename suffix for one packaging mode.
|
||||
* @param {{ artifacts: string, unsignedArtifacts: string, unnotarizedArtifacts: string }} paths Target artifact directories.
|
||||
* @param {{ unsigned: boolean, skipNotarization: boolean }} mode Validated packaging mode.
|
||||
* @returns {{ directory: string, suffix: string }} Matching artifact destination and filename marker.
|
||||
*/
|
||||
export function desktopArtifactOutput(paths, mode) {
|
||||
if (mode.unsigned) return { directory: paths.unsignedArtifacts, suffix: '-unsigned' }
|
||||
if (mode.skipNotarization) return { directory: paths.unnotarizedArtifacts, suffix: '-unnotarized' }
|
||||
return { directory: paths.artifacts, suffix: '' }
|
||||
}
|
||||
@@ -30,8 +30,8 @@ const LISTING_DEADLINE_MS = 8_000
|
||||
/** Objects one listing page may return. */
|
||||
const LISTING_PAGE_SIZE = 1000
|
||||
|
||||
/** Artifact name electron-builder writes for one build, on either platform; unsigned builds add a suffix. */
|
||||
const ARTIFACT = /(?:^|\/)deepseek-harness-(?<version>.+)-(?:mac|win)-(?:arm64|x64)(?:-unsigned)?\.(?:exe|dmg|zip)$/u
|
||||
/** Artifact name electron-builder writes for one build, on either platform; unsigned and unnotarized builds add a suffix. */
|
||||
const ARTIFACT = /(?:^|\/)deepseek-harness-(?<version>.+)-(?:mac|win)-(?:arm64|x64)(?:-unsigned|-unnotarized)?\.(?:exe|dmg|zip)$/u
|
||||
|
||||
/** Inputs that decide which versions are already taken. */
|
||||
export interface DesktopBuildVersionSuggestionOptions {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
resolveMacOSSigningEnvironment,
|
||||
} from './desktop-release-environment.mjs'
|
||||
import { notarizeMacOSDiskImageArtifact } from './notarize-macos-disk-images.mjs'
|
||||
import { verifyMacOSSignatureAfterSign } from './verify-macos-signature.mjs'
|
||||
import { verifyMacOSDiskImageSignature, verifyMacOSSignatureAfterSign } from './verify-macos-signature.mjs'
|
||||
import {
|
||||
createWindowsTokenSigner,
|
||||
installWindowsNsisBootstrapSigner,
|
||||
@@ -23,7 +23,7 @@ import { resolveDesktopAutoUpdateConfig } from './desktop-auto-update-environmen
|
||||
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'
|
||||
import { desktopArtifactOutput, desktopTargetBuildPaths, resolveDesktopBuildTarget } from './desktop-build-paths.mjs'
|
||||
import { installWindowsDirectoryInstaller } from './windows-directory-installer.mjs'
|
||||
import { preserveWindowsRuntimeSignature, signWindowsCode } from './windows-runtime-signature.mjs'
|
||||
import { prepareWindowsAsarUnpack, verifyWindowsAsarUnpack } from './windows-asar-unpack.mjs'
|
||||
@@ -70,6 +70,7 @@ export function createElectronBuilderConfig(
|
||||
const macOSSigning = packagesMacOS && !unsigned ? resolveMacOSSigningEnvironment(env) : undefined
|
||||
if (packagesMacOS && !unsigned && !skipNotarization) resolveMacOSNotarizationEnvironment(env)
|
||||
const buildPaths = desktopTargetBuildPaths(resolveDesktopBuildTarget(env, hostPlatform, hostArch))
|
||||
const output = desktopArtifactOutput(buildPaths, { unsigned, skipNotarization })
|
||||
let primaryRuntimeDestination
|
||||
let dshDestination
|
||||
let windowsCode = []
|
||||
@@ -112,8 +113,8 @@ export function createElectronBuilderConfig(
|
||||
},
|
||||
productName: 'DeepSeek Harness',
|
||||
// Unsigned builds carry their own suffix so a shared file can never pass for a release artifact.
|
||||
artifactName: `deepseek-harness-\${version}-\${os}-\${arch}${unsigned ? '-unsigned' : skipNotarization ? '-unnotarized' : ''}.\${ext}`,
|
||||
directories: { output: unsigned ? buildPaths.unsignedArtifacts : skipNotarization ? buildPaths.unnotarizedArtifacts : buildPaths.artifacts },
|
||||
artifactName: `deepseek-harness-\${version}-\${os}-\${arch}${output.suffix}.\${ext}`,
|
||||
directories: { output: output.directory },
|
||||
asar: true,
|
||||
electronDist: buildPaths.electron,
|
||||
electronFuses: { runAsNode: true },
|
||||
@@ -220,7 +221,11 @@ export function createElectronBuilderConfig(
|
||||
verifyMacOSSignatureAfterSign(context, macOSSigning ?? resolveMacOSSigningEnvironment(env))
|
||||
},
|
||||
artifactBuildCompleted: artifact => {
|
||||
if (unsigned || skipNotarization || !artifact.file.endsWith('.dmg')) return
|
||||
if (unsigned || !artifact.file.endsWith('.dmg')) return
|
||||
if (skipNotarization) {
|
||||
verifyMacOSDiskImageSignature(artifact.file, macOSSigning ?? resolveMacOSSigningEnvironment(env))
|
||||
return
|
||||
}
|
||||
return notarizeMacOSDiskImageArtifact(
|
||||
artifact,
|
||||
env,
|
||||
|
||||
@@ -23,7 +23,7 @@ case "$action" in
|
||||
service_dir="$runner_root/dsh-service"
|
||||
mkdir -p "$service_dir"
|
||||
cp "$source_dir/start-macos-packaging-runner.sh" "$service_dir/start.sh"
|
||||
# runsvc.sh reloads .path before launching RunnerService.js.
|
||||
# runsvc.sh reloads .path; replace the interactive PATH saved by registration.
|
||||
printf '%s\n' "$node_bin:/usr/bin:/bin:/usr/sbin:/sbin" > .path
|
||||
"$node_bin/node" --input-type=module - "$service_dir" "$runner_root" "$config_root" "$node_bin" <<'NODE'
|
||||
import { writeFileSync } from 'node:fs'
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
desktopBuildRecordFilename,
|
||||
resolveDesktopAutoUpdateConfig,
|
||||
} from './desktop-auto-update-environment.mjs'
|
||||
import { desktopTargetBuildPaths } from './desktop-build-paths.mjs'
|
||||
import { desktopArtifactOutput, desktopTargetBuildPaths } from './desktop-build-paths.mjs'
|
||||
import { packageMacOSArtifacts, type DesktopPrepackagedArtifact } from './package-macos.ts'
|
||||
import { loadDesktopPackageEnvironment, localMacOSPackageEnvironment, validateDesktopPackageEnvironment } from './desktop-package-environment.mjs'
|
||||
import { createPackagingRun, recordPackagingEvent } from './packaging-run.mjs'
|
||||
@@ -332,8 +332,7 @@ async function resolveRequestedBuildVersion(
|
||||
// Machine presets own packaging, not upload credentials; local auto numbering uses existing artifacts.
|
||||
environment: environment.DSH_PACKAGING_PRESET === undefined ? environment : {},
|
||||
// Unsigned builds land beside the signed output, so numbering has to read the directory this run writes.
|
||||
artifactsRoot: invocation.unsigned ? paths.unsignedArtifacts
|
||||
: invocation.skipNotarization ? paths.unnotarizedArtifacts : paths.artifacts,
|
||||
artifactsRoot: desktopArtifactOutput(paths, invocation).directory,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -348,7 +347,6 @@ async function main(): Promise<void> {
|
||||
: loadDesktopPackageEnvironment(target.platform, process.env, APP_ROOT, {
|
||||
unsigned: invocation.unsigned, prepareOnly: invocation.prepareOnly,
|
||||
})
|
||||
environment.DSH_DESKTOP_MACOS_SKIP_NOTARIZATION = invocation.skipNotarization ? '1' : '0'
|
||||
const productVersion = packageVersion(join(APP_ROOT, 'package.json'), 'desktop package')
|
||||
// Release settings come from the selected configuration files, so the version this run publishes is an
|
||||
// argument; the environment variable below only carries it to the child processes that build.
|
||||
@@ -401,8 +399,9 @@ async function main(): Promise<void> {
|
||||
}
|
||||
if (success && target.platform === 'darwin' && !invocation.directory && !invocation.prepareOnly) {
|
||||
const paths = desktopTargetBuildPaths(target.name)
|
||||
const filename = `deepseek-harness-${buildVersion}-mac-${target.arch}${invocation.unsigned ? '-unsigned' : invocation.skipNotarization ? '-unnotarized' : ''}.dmg`
|
||||
console.log(`DMG: ${join(invocation.unsigned ? paths.unsignedArtifacts : invocation.skipNotarization ? paths.unnotarizedArtifacts : paths.artifacts, filename)}`)
|
||||
const output = desktopArtifactOutput(paths, invocation)
|
||||
const filename = `deepseek-harness-${buildVersion}-mac-${target.arch}${output.suffix}.dmg`
|
||||
console.log(`DMG: ${join(output.directory, filename)}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,9 +429,7 @@ export async function packageTarget(
|
||||
rmSync(releaseRecordPath, { force: true })
|
||||
rmSync(`${releaseRecordPath}.tmp`, { force: true })
|
||||
}
|
||||
const artifactsRoot = invocation.unsigned ? buildPaths.unsignedArtifacts
|
||||
: invocation.skipNotarization ? buildPaths.unnotarizedArtifacts : buildPaths.artifacts
|
||||
if (invocation.skipNotarization) rmSync(artifactsRoot, { recursive: true, force: true })
|
||||
const artifactsRoot = desktopArtifactOutput(buildPaths, invocation).directory
|
||||
const buildEnv = withoutWindowsSigningEnvironment(withoutDesktopUploadCredentials(
|
||||
desktopElectronBuilderEnvironment(environment, invocation.unsigned)))
|
||||
const targetEnv: NodeJS.ProcessEnv = {
|
||||
|
||||
@@ -19,6 +19,6 @@ for (const [name, value] of Object.entries(environment)) {
|
||||
}
|
||||
validateDesktopPackageEnvironment(environment, { platform: 'darwin', arch: 'arm64' }, { skipNotarization })
|
||||
if (!process.env.GITHUB_OUTPUT || !process.env.GITHUB_STEP_SUMMARY) throw new Error('Run this adapter from GitHub Actions')
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\nartifacts=${skipNotarization ? 'unnotarized-artifacts' : 'artifacts'}\nmode=${skipNotarization ? 'unnotarized-debug' : 'signed-notarized'}\n`)
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\nartifacts=${skipNotarization ? 'unnotarized-artifacts' : 'artifacts'}\nmode=${skipNotarization ? 'unnotarized-debug' : 'signed'}\n`)
|
||||
appendFileSync(process.env.GITHUB_STEP_SUMMARY, `macOS arm64 signed installer\n\nVersion: ${version}\n\nCommit: ${process.env.GITHUB_SHA}\n\nPreset: ${process.env.DSH_PACKAGING_PRESET}\n`)
|
||||
if (skipNotarization) appendFileSync(process.env.GITHUB_STEP_SUMMARY, '\nWARNING: signed but NOT notarized or stapled. Internal debugging only; Gatekeeper may block downloaded copies. Not qualified for distribution or update publishing.\n')
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Validate the assembled application, including native Office conversion outside ASAR. */
|
||||
import { join } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
|
||||
import { desktopArtifactOutput, resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
|
||||
import { readDesktopRuntime, verifyDesktopRuntime } from '../src/runtime-tree.ts'
|
||||
import { verifyWindowsCode } from './windows-runtime-signature.mjs'
|
||||
import { smokePreparedRuntime } from './smoke-prepared-runtime.ts'
|
||||
@@ -11,7 +11,7 @@ const paths = resolveDesktopTargetBuildPaths()
|
||||
const { values } = parseArgs({ options: { unsigned: { type: 'boolean', default: false }, 'skip-notarization': { type: 'boolean', default: false } }, allowPositionals: false })
|
||||
const target = resolveDesktopBuildTarget()
|
||||
const windows = target === 'win-x64'
|
||||
const artifacts = values.unsigned ? paths.unsignedArtifacts : values['skip-notarization'] ? paths.unnotarizedArtifacts : paths.artifacts
|
||||
const artifacts = desktopArtifactOutput(paths, { unsigned: values.unsigned, skipNotarization: values['skip-notarization'] }).directory
|
||||
const application = windows ? join(artifacts, 'win-unpacked')
|
||||
: join(artifacts, target === 'mac-arm64' ? 'mac-arm64' : 'mac', 'DeepSeek Harness.app', 'Contents')
|
||||
const resources = join(application, windows ? 'resources' : 'Resources')
|
||||
|
||||
@@ -80,3 +80,10 @@ export function verifyMacOSSignatureAfterSign(
|
||||
context: MacOSAfterSignContext,
|
||||
expected: MacOSSigningEnvironment,
|
||||
): void
|
||||
|
||||
/**
|
||||
* Verify a disk image's code signature and Developer ID without requiring a notarization ticket.
|
||||
* @param diskImagePath Path to the packaged `.dmg` file.
|
||||
* @param expected Public signing identity.
|
||||
*/
|
||||
export function verifyMacOSDiskImageSignature(diskImagePath: string, expected: MacOSSigningEnvironment): void
|
||||
@@ -172,9 +172,7 @@ export function verifyMacOSNotarizedApplication(appPath, expected) {
|
||||
* @returns {void}
|
||||
*/
|
||||
export function verifyMacOSDiskImage(diskImagePath, expected) {
|
||||
runCodeSign(['--verify', '--strict', '--verbose=2', diskImagePath])
|
||||
const details = runCodeSign(['--display', '--verbose=4', diskImagePath])
|
||||
assertMacOSSignatureDetails(details, expected)
|
||||
verifyMacOSDiskImageSignature(diskImagePath, expected)
|
||||
runAppleCommand('/usr/bin/xcrun', ['stapler', 'validate', diskImagePath], 'stapler validate')
|
||||
runAppleCommand('/usr/sbin/spctl', ['--assess', '--type', 'install', '--verbose=4', diskImagePath], 'spctl')
|
||||
}
|
||||
@@ -202,3 +200,15 @@ if (process.argv[1] !== undefined && import.meta.filename === resolve(process.ar
|
||||
verifyMacOSSignature(resolve(appPath), expected)
|
||||
process.stdout.write(`desktop macOS signing: verified Developer ID Application: ${expected.signingIdentity} (${expected.teamId})\n`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a disk image's code signature and Developer ID without requiring a notarization ticket.
|
||||
* @param {string} diskImagePath Path to the packaged `.dmg` file.
|
||||
* @param {{ signingIdentity: string, teamId: string }} expected Public signing identity.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function verifyMacOSDiskImageSignature(diskImagePath, expected) {
|
||||
runCodeSign(['--verify', '--strict', '--verbose=2', diskImagePath])
|
||||
const details = runCodeSign(['--display', '--verbose=4', diskImagePath])
|
||||
assertMacOSSignatureDetails(details, expected)
|
||||
}
|
||||
@@ -71,6 +71,16 @@ describe('desktop build version discovery', () => {
|
||||
.resolves.toBe(`${PRERELEASE}.${DATE}.4`)
|
||||
})
|
||||
|
||||
it('numbers after existing unnotarized DMG and ZIP artifacts', async () => {
|
||||
const artifactsRoot = await artifactsWith([
|
||||
`deepseek-harness-${PRERELEASE}.${DATE}.2-mac-arm64-unnotarized.dmg`,
|
||||
`deepseek-harness-${PRERELEASE}.${DATE}.4-mac-arm64-unnotarized.zip`,
|
||||
`deepseek-harness-${PRERELEASE}.${DATE}.9-mac-arm64-unnotarized.zip.blockmap`,
|
||||
])
|
||||
await expect(suggestDesktopBuildVersion({ productVersion: PRERELEASE, target: 'mac-arm64', environment: {}, date: DATE, artifactsRoot }))
|
||||
.resolves.toBe(`${PRERELEASE}.${DATE}.5`)
|
||||
})
|
||||
|
||||
it('counts unsigned Windows artifacts written under their own suffix', async () => {
|
||||
const artifactsRoot = await artifactsWith([
|
||||
`deepseek-harness-${PRERELEASE}.${DATE}.2-win-x64-unsigned.exe`,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { createElectronBuilderConfig } from '../scripts/electron-builder-config.mjs'
|
||||
import { verifyMacOSDiskImage } from '../scripts/verify-macos-signature.mjs'
|
||||
|
||||
vi.mock('node:child_process', async importOriginal => ({
|
||||
...await importOriginal<typeof import('node:child_process')>(), spawnSync: vi.fn(),
|
||||
}))
|
||||
afterEach(() => { vi.resetAllMocks() })
|
||||
|
||||
const signingIdentity = 'Example Company (TEAMID1234)'
|
||||
const teamId = 'TEAMID1234'
|
||||
const environment = {
|
||||
DSH_DESKTOP_APP_ID: 'com.example.desktop', DSH_DESKTOP_AUTO_UPDATE_ENV: 'production',
|
||||
DSH_DESKTOP_MANDATORY_UPDATE_PROD_ORIGIN: 'https://policy.example.com',
|
||||
DOWNLOAD_PROD_ORIGIN: 'https://updates.example.com',
|
||||
DSH_DESKTOP_MACOS_SIGNING_IDENTITY: signingIdentity, DSH_DESKTOP_MACOS_TEAM_ID: teamId,
|
||||
DSH_DESKTOP_MACOS_SKIP_NOTARIZATION: '1',
|
||||
}
|
||||
|
||||
function toolResult(details = `Authority=Developer ID Application: ${signingIdentity}\nTeamIdentifier=${teamId}`) {
|
||||
vi.mocked(spawnSync).mockReturnValue({
|
||||
pid: 1, output: [null, '', details], stdout: '', stderr: details, status: 0, signal: null,
|
||||
})
|
||||
}
|
||||
|
||||
it('verifies an unnotarized DMG signature and identity without invoking Apple notarization tools', () => {
|
||||
toolResult()
|
||||
const config = createElectronBuilderConfig(environment, 'darwin', 'arm64')
|
||||
expect(config.artifactBuildCompleted({ file: '/fixture/debug.dmg' })).toBeUndefined()
|
||||
expect(vi.mocked(spawnSync).mock.calls.map(call => call.slice(0, 2))).toEqual([
|
||||
['/usr/bin/codesign', ['--verify', '--strict', '--verbose=2', '/fixture/debug.dmg']],
|
||||
['/usr/bin/codesign', ['--display', '--verbose=4', '/fixture/debug.dmg']],
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects an unnotarized DMG signed by another team', () => {
|
||||
toolResult('Authority=Developer ID Application: Other (OTHERID123)\nTeamIdentifier=OTHERID123')
|
||||
const config = createElectronBuilderConfig(environment, 'darwin', 'arm64')
|
||||
expect(() => config.artifactBuildCompleted({ file: '/fixture/debug.dmg' })).toThrow(/release identity/u)
|
||||
})
|
||||
|
||||
it('rejects a corrupt unnotarized DMG before inspecting its identity', () => {
|
||||
toolResult()
|
||||
vi.mocked(spawnSync).mockReturnValueOnce({ pid: 1, output: [null, '', 'invalid signature'],
|
||||
stdout: '', stderr: 'invalid signature', status: 1, signal: null })
|
||||
const config = createElectronBuilderConfig(environment, 'darwin', 'arm64')
|
||||
expect(() => config.artifactBuildCompleted({ file: '/fixture/debug.dmg' })).toThrow(/invalid signature/u)
|
||||
expect(spawnSync).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('still requires a stapled ticket and Gatekeeper acceptance for release DMGs', () => {
|
||||
toolResult()
|
||||
verifyMacOSDiskImage('/fixture/release.dmg', { signingIdentity, teamId })
|
||||
expect(vi.mocked(spawnSync).mock.calls.map(call => call[0])).toEqual([
|
||||
'/usr/bin/codesign', '/usr/bin/codesign', '/usr/bin/xcrun', '/usr/sbin/spctl',
|
||||
])
|
||||
})
|
||||
@@ -84,8 +84,12 @@ it('records debugging artifact paths only for an explicit CI selection without n
|
||||
PACKAGE_SKIP_NOTARIZATION: skip, PACKAGE_BUILD_VERSION: 'auto',
|
||||
GITHUB_RUN_NUMBER: '1', GITHUB_RUN_ATTEMPT: '1', GITHUB_OUTPUT: output, GITHUB_STEP_SUMMARY: summary,
|
||||
} })
|
||||
expect(run('false').status).toBe(1)
|
||||
expect(run('yes').status).toBe(1)
|
||||
const missingCredentials = run('false')
|
||||
expect(missingCredentials.status).toBe(1)
|
||||
expect(missingCredentials.stderr).toContain('macOS packaging requires APPLE_API_KEY')
|
||||
const invalidSelection = run('yes')
|
||||
expect(invalidSelection.status).toBe(1)
|
||||
expect(invalidSelection.stderr).toContain('Invalid skip notarization selection')
|
||||
const debugging = run('true')
|
||||
expect(debugging.stderr).toBe('')
|
||||
expect(debugging.status).toBe(0)
|
||||
|
||||
@@ -268,7 +268,6 @@ it('signs debugging artifacts without Apple credentials and isolates them from r
|
||||
expect(config.mac).toMatchObject({ target: ['dmg', 'zip'] })
|
||||
expect(config.artifactName).toContain('-unnotarized.')
|
||||
expect(portablePath(config.directories.output)).toContain('/unnotarized-artifacts')
|
||||
expect(config.artifactBuildCompleted({ file: 'debug.dmg' })).toBeUndefined()
|
||||
expect(() => createElectronBuilderConfig({ ...RELEASE_ENVIRONMENT, DSH_DESKTOP_MACOS_SKIP_NOTARIZATION: 'yes' }, 'darwin', 'arm64')).toThrow(/must be 0 or 1/u)
|
||||
expect(() => createElectronBuilderConfig({ ...RELEASE_ENVIRONMENT, DSH_DESKTOP_MACOS_SKIP_NOTARIZATION: '1', DSH_DESKTOP_UNSIGNED: '1' }, 'darwin', 'arm64')).toThrow(/requires a signed macOS build/u)
|
||||
})
|
||||
@@ -28,7 +28,6 @@ it('confines signed Mac packaging to manual dispatch and a dedicated Apple Silic
|
||||
expect(uploads[1]?.with?.path).toBe('apps/desktop/.desktop-build/targets/mac-arm64/${{ steps.settings.outputs.artifacts }}/*.dmg\napps/desktop/.desktop-build/targets/mac-arm64/${{ steps.settings.outputs.artifacts }}/*.zip\n')
|
||||
})
|
||||
|
||||
|
||||
it('defaults to notarization and labels the explicit debugging option', () => {
|
||||
expect(workflow.on.workflow_dispatch).toMatchObject({ inputs: { skip_notarization: {
|
||||
type: 'boolean', default: false,
|
||||
|
||||
Reference in new issue
Block a user