Merge remote-tracking branch 'origin/master' into worktree/pi-ai-1.1.0
# Conflicts: # apps/desktop/tests/prepare-runtime-patches.spec.ts
This commit is contained in:
commit
40f9bccd78
33 files changed
+486
-66
No files matched your search
@@ -9,6 +9,11 @@ on:
|
||||
options: [test, production]
|
||||
default: test
|
||||
required: true
|
||||
skip_notarization:
|
||||
description: 'Skip notarization/stapling: internal debugging only, not for distribution or updates'
|
||||
type: boolean
|
||||
default: false
|
||||
required: true
|
||||
build_version:
|
||||
description: 'Version: auto recommended; explicit version must match the checkout product prefix'
|
||||
type: string
|
||||
@@ -30,6 +35,7 @@ jobs:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
PACKAGE_SKIP_NOTARIZATION: ${{ inputs.skip_notarization }}
|
||||
DSH_TELEMETRY_DISABLED: '1'
|
||||
DSH_PACKAGING_PRESET: ${{ inputs.preset }}
|
||||
steps:
|
||||
@@ -54,11 +60,17 @@ jobs:
|
||||
- name: Check packaging configuration and toolchain
|
||||
env:
|
||||
PACKAGE_VERSION: ${{ steps.settings.outputs.version }}
|
||||
run: pnpm run package:desktop:mac:arm64 --check --build-version "$PACKAGE_VERSION"
|
||||
- name: Build signed and notarized installers
|
||||
run: |
|
||||
args=()
|
||||
if [[ "$PACKAGE_SKIP_NOTARIZATION" = true ]]; then args+=(--skip-notarization); fi
|
||||
pnpm run package:desktop:mac:arm64 --check --build-version "$PACKAGE_VERSION" "${args[@]}"
|
||||
- name: Build signed installers
|
||||
env:
|
||||
PACKAGE_VERSION: ${{ steps.settings.outputs.version }}
|
||||
run: pnpm run package:desktop:mac:arm64 --build-version "$PACKAGE_VERSION"
|
||||
run: |
|
||||
args=()
|
||||
if [[ "$PACKAGE_SKIP_NOTARIZATION" = true ]]; then args+=(--skip-notarization); fi
|
||||
pnpm run package:desktop:mac:arm64 --build-version "$PACKAGE_VERSION" "${args[@]}"
|
||||
- name: Retain packaging diagnostics
|
||||
if: always() && steps.checkout.outcome == 'success'
|
||||
uses: actions/[email protected]
|
||||
@@ -68,14 +80,14 @@ jobs:
|
||||
path: apps/desktop/.desktop-build/packaging-runs/
|
||||
if-no-files-found: warn
|
||||
retention-days: 14
|
||||
- name: Retain verified installers
|
||||
- name: Retain installers for the selected mode
|
||||
if: success()
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
name: macos-arm64-signed-${{ steps.settings.outputs.version }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
name: macos-arm64-${{ steps.settings.outputs.mode }}-${{ steps.settings.outputs.version }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
include-hidden-files: true
|
||||
path: |
|
||||
apps/desktop/.desktop-build/targets/mac-arm64/artifacts/*.dmg
|
||||
apps/desktop/.desktop-build/targets/mac-arm64/artifacts/*.zip
|
||||
apps/desktop/.desktop-build/targets/mac-arm64/${{ steps.settings.outputs.artifacts }}/*.dmg
|
||||
apps/desktop/.desktop-build/targets/mac-arm64/${{ steps.settings.outputs.artifacts }}/*.zip
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
@@ -146,6 +146,7 @@ pnpm applies local patches to the following packages at install time, so shipped
|
||||
- `@fortune-sheet/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
|
||||
- `@fortune-sheet/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
|
||||
- `@yao-pkg/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
|
||||
- `[email protected]` — [`patches/[email protected]`](patches/[email protected])
|
||||
- `[email protected]` — [`patches/[email protected]`](patches/[email protected])
|
||||
- `[email protected]` — [`patches/[email protected]`](patches/[email protected])
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface DesktopElectronBuilderConfig {
|
||||
readonly signIgnore: readonly string[]
|
||||
}
|
||||
readonly dmg: {
|
||||
readonly format: 'ULMO'
|
||||
readonly sign: boolean
|
||||
readonly writeUpdateInfo: boolean
|
||||
}
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
en: d80a92c02a583b80
|
||||
zh: b27f75ff29fcea1a
|
||||
/desktop-packaging/configuration-and-local-builds:
|
||||
en: e8b8bb1e287bff80
|
||||
zh: 09b38712c8e974bc
|
||||
en: e4f526604231ed43
|
||||
zh: d412179ffdc402ba
|
||||
/desktop-packaging/windows-actions-packaging:
|
||||
en: e3f51e7130a0fe05
|
||||
zh: 70fd6a40d5899871
|
||||
/desktop-packaging/macos-self-hosted-packaging:
|
||||
en: 1929f652d7654d7e
|
||||
zh: bab90dfb9523ef6a
|
||||
en: a3af9523fdc60036
|
||||
zh: 5e18d4740e9b7f57
|
||||
/desktop-packaging/unsigned-windows-test-installer:
|
||||
en: 6843a0fdc03ead58
|
||||
zh: 957f92fc8126e23e
|
||||
|
||||
@@ -93,6 +93,8 @@ pnpm run package:desktop:win:x64
|
||||
|
||||
The macOS arm64 command requires Apple Silicon. The macOS x64 command runs on Intel macOS or Apple Silicon with Rosetta. The Windows x64 command requires Windows x64. Linux is not a supported Desktop release target.
|
||||
|
||||
Both release and local unsigned macOS DMGs use LZMA compression (`dmg.format: 'ULMO'`) in [the electron-builder configuration](../scripts/electron-builder-config.mjs). A [build-time patch](../../../patches/[email protected]) adds this format to the pinned builder's validation and TypeScript declarations.
|
||||
|
||||
Each target owns its packed package inputs, prepared runtime, package set, dsh tree, pnpm preparation state, unpacked application, update metadata, and final artifacts under `apps/desktop/.desktop-build/targets/<target>/`. The Electron archive cache remains shared under `.desktop-build/downloads` because every archive name includes its version, platform, and architecture and is verified before extraction. A target build never consumes another target's mutable preparation state.
|
||||
|
||||
<a id="windows-actions"></a>
|
||||
@@ -111,11 +113,23 @@ Open the completed run in Actions and download its Artifacts. `windows-x64-signe
|
||||
|
||||
The `Package Desktop · macOS` [manual workflow](../../../.github/workflows/macos-package.yml) targets an Apple Silicon runner labeled `dsh-mac-package-trial`. It selects `test` or `production`, resolves `build_version=auto` from the UTC date, run number and attempt, and uses the existing signing, notarization and packaged-runtime checks. Successful DMG/ZIP files and diagnostic logs are retained for 14 days; this workflow does not publish a release. The operator must register the runner and verify a complete Actions run before relying on it.
|
||||
|
||||
Run `node apps/desktop/scripts/setup-macos-packaging.mjs "$HOME/ci"` to prepare private `runner/`, `packaging/{presets,secrets,certificates}` and `cache/macos-signatures` directories without overwriting existing files; append `--check` to inspect them. Fill the [preset](../config/macos-packaging/test.env.example) and [signing](../config/macos-packaging/macos-signing.env.example) templates outside the checkout. Set secret and certificate files to mode `0600`. `DSH_PACKAGING_CONFIG_DIR` selects the absolute `packaging` directory and `DSH_PACKAGING_PRESET` selects its preset; missing files fail without falling back to `.env.macos`. Signed and preparation-only Mac builds read `secrets/macos-signing.env`; relative credential paths resolve beside that file. Public presets reject signing credentials, and signing files cannot override public settings. Local unsigned Mac builds retain their configuration-free behavior.
|
||||
Run `node apps/desktop/scripts/setup-macos-packaging.mjs "$HOME/dsh-ci"` to prepare private `runner/`, `packaging/{presets,secrets,certificates}` and `cache/macos-signatures` directories without overwriting existing files; append `--check` to inspect them. Fill the [preset](../config/macos-packaging/test.env.example) and [signing](../config/macos-packaging/macos-signing.env.example) templates outside the checkout. Set secret and certificate files to mode `0600`. `DSH_PACKAGING_CONFIG_DIR` selects the absolute `packaging` directory and `DSH_PACKAGING_PRESET` selects its preset; missing files fail without falling back to `.env.macos`. Signed and preparation-only Mac builds read `secrets/macos-signing.env`; relative credential paths resolve beside that file. Public presets reject signing credentials, and signing files cannot override public settings. Local unsigned Mac builds retain their configuration-free behavior.
|
||||
|
||||
Run `bash apps/desktop/scripts/start-macos-packaging-runner.sh "$HOME/ci/runner" "$HOME/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; service installation is separate. 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).
|
||||
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, 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`:
|
||||
|
||||
```sh
|
||||
bash apps/desktop/scripts/macos-runner-service.sh install "$HOME/dsh-ci/runner" "$HOME/dsh-ci/packaging" /opt/homebrew/opt/node@24/bin
|
||||
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. 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>
|
||||
|
||||
## Unsigned Windows test installer
|
||||
|
||||
@@ -93,6 +93,8 @@ pnpm run package:desktop:win:x64
|
||||
|
||||
macOS arm64 命令要求 Apple Silicon。macOS x64 命令可以在 Intel macOS 或带 Rosetta 的 Apple Silicon 上运行。Windows x64 命令要求 Windows x64。Linux 不是受支持的 Desktop 发布目标。
|
||||
|
||||
macOS 正式发布和本地未签名 DMG 都通过 [electron-builder 配置](../scripts/electron-builder-config.mjs)中的 `dmg.format: 'ULMO'` 使用 LZMA 压缩。[构建时补丁](../../../patches/[email protected])为固定版本 builder 的校验规则和 TypeScript 声明添加此格式。
|
||||
|
||||
每个目标都在 `apps/desktop/.desktop-build/targets/<target>/` 下持有自己的打包输入、已准备运行时、包集合、dsh 依赖树、pnpm 准备状态、未打包应用、更新元数据和最终产物。Electron 归档缓存继续由 `.desktop-build/downloads` 共享,因为每个归档文件名都包含版本、平台和架构,并且在解包前经过验证。目标构建绝不读取其他目标的可变准备状态。
|
||||
|
||||
<a id="windows-actions"></a>
|
||||
@@ -111,11 +113,23 @@ macOS arm64 命令要求 Apple Silicon。macOS x64 命令可以在 Intel macOS
|
||||
|
||||
`Package Desktop · macOS` [手动工作流](../../../.github/workflows/macos-package.yml) 使用带有 `dsh-mac-package-trial` 标签的 Apple Silicon runner。它选择 `test` 或 `production`,以 UTC 日期、运行编号和重试次数解析 `build_version=auto`,并复用现有签名、公证和打包运行时检查。成功的 DMG/ZIP 与诊断日志保留 14 天;工作流不发布 release。操作人员须注册 runner,并验收完整 Actions 运行后再依赖该流程。
|
||||
|
||||
运行 `node apps/desktop/scripts/setup-macos-packaging.mjs "$HOME/ci"` 创建私有 `runner/`、`packaging/{presets,secrets,certificates}` 和 `cache/macos-signatures` 目录,不覆盖已有文件;追加 `--check` 仅检查。在 checkout 外填写 [preset](../config/macos-packaging/test.env.example) 和[签名](../config/macos-packaging/macos-signing.env.example)模板,将秘密和证书文件权限设为 `0600`。`DSH_PACKAGING_CONFIG_DIR` 指定 `packaging` 的绝对目录,`DSH_PACKAGING_PRESET` 选择其中的 preset;缺少文件立即失败,不回退到 `.env.macos`。Mac 签名和仅准备构建读取 `secrets/macos-signing.env`,相对凭据路径以该文件所在目录为基准。公开 preset 拒绝签名凭据,签名文件不能覆盖公开配置。本地未签名 Mac 构建保留无需配置的行为。
|
||||
运行 `node apps/desktop/scripts/setup-macos-packaging.mjs "$HOME/dsh-ci"` 创建私有 `runner/`、`packaging/{presets,secrets,certificates}` 和 `cache/macos-signatures` 目录,不覆盖已有文件;追加 `--check` 仅检查。在 checkout 外填写 [preset](../config/macos-packaging/test.env.example) 和[签名](../config/macos-packaging/macos-signing.env.example)模板,将秘密和证书文件权限设为 `0600`。`DSH_PACKAGING_CONFIG_DIR` 指定 `packaging` 的绝对目录,`DSH_PACKAGING_PRESET` 选择其中的 preset;缺少文件立即失败,不回退到 `.env.macos`。Mac 签名和仅准备构建读取 `secrets/macos-signing.env`,相对凭据路径以该文件所在目录为基准。公开 preset 拒绝签名凭据,签名文件不能覆盖公开配置。本地未签名 Mac 构建保留无需配置的行为。
|
||||
|
||||
运行 `bash apps/desktop/scripts/start-macos-packaging-runner.sh "$HOME/ci/runner" "$HOME/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)。
|
||||
运行 `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 签名、DMG 签名验证和打包运行时验证,跳过 Apple 公证和 stapling,不要求 Apple 公证凭据。DMG/ZIP 文件名带 `-unnotarized`,存放于 `.desktop-build/targets/<target>/unnotarized-artifacts/`,上传产物标为 `unnotarized-debug`。这些产物仅供内部调试:Gatekeeper 可能阻止下载副本运行,不能视为可分发包或用于更新发布。该模式不写入发布完成记录,不能与未签名、Windows 或仅准备构建组合。
|
||||
|
||||
在 checkout 中,以注册 runner 的同一账户安装用户服务,不使用 `sudo`:
|
||||
|
||||
```sh
|
||||
bash apps/desktop/scripts/macos-runner-service.sh install "$HOME/dsh-ci/runner" "$HOME/dsh-ci/packaging" /opt/homebrew/opt/node@24/bin
|
||||
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`;重新安装会替换启动快照和 PATH。
|
||||
|
||||
<a id="unsigned-windows-test-installer"></a>
|
||||
|
||||
## 未签名 Windows 测试安装包
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface DesktopTargetBuildPaths {
|
||||
readonly root: string
|
||||
readonly artifacts: string
|
||||
readonly unsignedArtifacts: string
|
||||
readonly unnotarizedArtifacts: string
|
||||
readonly runtime: string
|
||||
readonly packageSet: string
|
||||
readonly dsh: string
|
||||
@@ -72,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 }
|
||||
@@ -38,7 +38,7 @@ function assertSupportedTarget(target) {
|
||||
/**
|
||||
* Return the mutable preparation and artifact directories owned by one release target.
|
||||
* @param {'mac-arm64' | 'mac-x64' | 'win-x64'} target - Supported Desktop target name.
|
||||
* @returns {{ root: string, artifacts: string, unsignedArtifacts: string, runtime: string, packageSet: string, dsh: string, dshPnpm: string, electron: string, packedDsh: string, packedVendor: string, packedLandlock: string, downloads: string }} Target paths plus the shared immutable download cache.
|
||||
* @returns {{ root: string, artifacts: string, unsignedArtifacts: string, unnotarizedArtifacts: string, runtime: string, packageSet: string, dsh: string, dshPnpm: string, electron: string, packedDsh: string, packedVendor: string, packedLandlock: string, downloads: string }} Target paths plus the shared immutable download cache.
|
||||
*/
|
||||
export function desktopTargetBuildPaths(target) {
|
||||
assertSupportedTarget(target)
|
||||
@@ -48,6 +48,7 @@ export function desktopTargetBuildPaths(target) {
|
||||
root,
|
||||
artifacts: join(root, 'artifacts'),
|
||||
unsignedArtifacts: join(root, 'unsigned-artifacts'),
|
||||
unnotarizedArtifacts: join(root, 'unnotarized-artifacts'),
|
||||
runtime: join(root, 'runtime'),
|
||||
packageSet: join(root, 'package-set'),
|
||||
dsh: join(root, 'dsh'),
|
||||
@@ -105,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 {
|
||||
|
||||
@@ -32,5 +32,5 @@ export function loadDesktopPackageEnvironment(
|
||||
export function validateDesktopPackageEnvironment(
|
||||
environment: NodeJS.ProcessEnv,
|
||||
target: { platform: 'win32' | 'darwin', arch: string },
|
||||
options?: { unsigned?: boolean, prepareOnly?: boolean },
|
||||
options?: { unsigned?: boolean, prepareOnly?: boolean, skipNotarization?: boolean },
|
||||
): void
|
||||
@@ -122,7 +122,7 @@ function requireReadableFile(environment, name) {
|
||||
* Validate the packaging mode before preparation without invoking a token or Apple's services.
|
||||
* @param {NodeJS.ProcessEnv} environment Selected local defaults or file-owned release settings.
|
||||
* @param {{ platform: 'win32' | 'darwin', arch: string }} target Selected release target.
|
||||
* @param {{ unsigned?: boolean, prepareOnly?: boolean }} options Explicit packaging mode.
|
||||
* @param {{ unsigned?: boolean, prepareOnly?: boolean, skipNotarization?: boolean }} options Explicit packaging mode.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function validateDesktopPackageEnvironment(environment, target, options = {}) {
|
||||
@@ -144,17 +144,19 @@ export function validateDesktopPackageEnvironment(environment, target, options =
|
||||
} else {
|
||||
if (environment.DSH_DESKTOP_MACOS_SIGNATURE_CACHE_DIR !== undefined) validateMacOSSignatureCacheDirectory(environment.DSH_DESKTOP_MACOS_SIGNATURE_CACHE_DIR)
|
||||
resolveMacOSSigningEnvironment(environment)
|
||||
const strategies = [
|
||||
['APPLE_ID', 'APPLE_APP_SPECIFIC_PASSWORD', 'APPLE_TEAM_ID'],
|
||||
['APPLE_API_KEY', 'APPLE_API_KEY_ID', 'APPLE_API_ISSUER'],
|
||||
['APPLE_KEYCHAIN_PROFILE', 'APPLE_KEYCHAIN'],
|
||||
]
|
||||
if (strategies.filter(names => names.some(name => environment[name] !== undefined)).length > 1) {
|
||||
throw new Error('desktop package: configure exactly one macOS notarization strategy; comment out the other strategies')
|
||||
if (!options.skipNotarization) {
|
||||
const strategies = [
|
||||
['APPLE_ID', 'APPLE_APP_SPECIFIC_PASSWORD', 'APPLE_TEAM_ID'],
|
||||
['APPLE_API_KEY', 'APPLE_API_KEY_ID', 'APPLE_API_ISSUER'],
|
||||
['APPLE_KEYCHAIN_PROFILE', 'APPLE_KEYCHAIN'],
|
||||
]
|
||||
if (strategies.filter(names => names.some(name => environment[name] !== undefined)).length > 1) {
|
||||
throw new Error('desktop package: configure exactly one macOS notarization strategy; comment out the other strategies')
|
||||
}
|
||||
const credentials = resolveMacOSNotarizationEnvironment(environment)
|
||||
if ('appleApiKey' in credentials) requireReadableFile(environment, 'APPLE_API_KEY')
|
||||
if ('keychain' in credentials) requireReadableFile(environment, 'APPLE_KEYCHAIN')
|
||||
}
|
||||
const credentials = resolveMacOSNotarizationEnvironment(environment)
|
||||
if ('appleApiKey' in credentials) requireReadableFile(environment, 'APPLE_API_KEY')
|
||||
if ('keychain' in credentials) requireReadableFile(environment, 'APPLE_KEYCHAIN')
|
||||
requireReadableFile(environment, 'CSC_LINK')
|
||||
if (environment.CSC_KEY_PASSWORD === undefined) {
|
||||
throw new Error('desktop package: CSC_KEY_PASSWORD must be set to the p12 export password (use an explicit empty value for an unencrypted p12)')
|
||||
|
||||
@@ -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'
|
||||
@@ -58,13 +58,19 @@ export function createElectronBuilderConfig(
|
||||
throw new Error('desktop package: DSH_DESKTOP_UNSIGNED must be 0 or 1')
|
||||
}
|
||||
const unsigned = env.DSH_DESKTOP_UNSIGNED === '1'
|
||||
const skipNotarization = env.DSH_DESKTOP_MACOS_SKIP_NOTARIZATION === '1'
|
||||
if (env.DSH_DESKTOP_MACOS_SKIP_NOTARIZATION !== undefined && !['0', '1'].includes(env.DSH_DESKTOP_MACOS_SKIP_NOTARIZATION)) {
|
||||
throw new Error('desktop package: DSH_DESKTOP_MACOS_SKIP_NOTARIZATION must be 0 or 1')
|
||||
}
|
||||
if (skipNotarization && (unsigned || resolvedPlatform !== 'darwin')) throw new Error('Skipping notarization requires a signed macOS build')
|
||||
const packagesMacOS = targetPlatform === 'darwin' || (targetPlatform === undefined && hostPlatform === 'darwin')
|
||||
const packagesWindows = resolvedPlatform === 'win32'
|
||||
const policy = packagesMacOS && unsigned ? undefined : resolveDesktopPolicyEnvironment(env)
|
||||
if (resolvedPlatform === 'win32') installWindowsDirectoryInstaller()
|
||||
const macOSSigning = packagesMacOS && !unsigned ? resolveMacOSSigningEnvironment(env) : undefined
|
||||
if (packagesMacOS && !unsigned) resolveMacOSNotarizationEnvironment(env)
|
||||
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 = []
|
||||
@@ -107,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' : ''}.\${ext}`,
|
||||
directories: { output: unsigned ? buildPaths.unsignedArtifacts : buildPaths.artifacts },
|
||||
artifactName: `deepseek-harness-\${version}-\${os}-\${arch}${output.suffix}.\${ext}`,
|
||||
directories: { output: output.directory },
|
||||
asar: true,
|
||||
electronDist: buildPaths.electron,
|
||||
electronFuses: { runAsNode: true },
|
||||
@@ -162,10 +168,11 @@ export function createElectronBuilderConfig(
|
||||
entitlementsInherit: fileURLToPath(new URL('./macos-entitlements.plist', import.meta.url)),
|
||||
// Prepared runtime files retain their signatures; PAK resources are sealed by their enclosing bundle.
|
||||
signIgnore: ['/Contents/Resources/app\\.asar\\.unpacked/dsh(?:/|$)', '/Contents/Resources/runtime/primary-runtime(?:/|$)', '\\.pak$'],
|
||||
notarize: !unsigned,
|
||||
notarize: !unsigned && !skipNotarization,
|
||||
target: unsigned ? ['dmg'] : ['dmg', 'zip'],
|
||||
},
|
||||
dmg: {
|
||||
format: 'ULMO',
|
||||
sign: !unsigned,
|
||||
writeUpdateInfo: false,
|
||||
},
|
||||
@@ -216,6 +223,10 @@ export function createElectronBuilderConfig(
|
||||
},
|
||||
artifactBuildCompleted: artifact => {
|
||||
if (unsigned || !artifact.file.endsWith('.dmg')) return
|
||||
if (skipNotarization) {
|
||||
verifyMacOSDiskImageSignature(artifact.file, macOSSigning ?? resolveMacOSSigningEnvironment(env))
|
||||
return
|
||||
}
|
||||
return notarizeMacOSDiskImageArtifact(
|
||||
artifact,
|
||||
env,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
# Manage a registered runner in the current user's logged-in macOS session.
|
||||
set -euo pipefail
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo 'Usage: bash macos-runner-service.sh install RUNNER_ROOT CONFIG_ROOT NODE_BIN | start|stop|status|uninstall RUNNER_ROOT' >&2
|
||||
exit 1
|
||||
fi
|
||||
action=$1
|
||||
runner_root=$2
|
||||
[[ $(uname -s) = Darwin && $EUID -ne 0 ]] || { echo 'Run as the registered macOS runner user, without sudo' >&2; exit 1; }
|
||||
[[ "$runner_root" = /* && -f "$runner_root/.runner" && -x "$runner_root/svc.sh" ]] || { echo 'Provide an absolute registered runner directory' >&2; exit 1; }
|
||||
source_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
cd "$runner_root"
|
||||
case "$action" in
|
||||
install)
|
||||
[[ $# = 4 ]] || { echo 'install requires RUNNER_ROOT CONFIG_ROOT NODE_BIN' >&2; exit 1; }
|
||||
config_root=$3
|
||||
node_bin=$4
|
||||
[[ ! -f .service ]] || { echo 'Service already installed; stop and uninstall it before reinstalling' >&2; exit 1; }
|
||||
launchctl print "gui/$(id -u)" >/dev/null || { echo 'Log into the macOS desktop as this user first' >&2; exit 1; }
|
||||
bash "$source_dir/start-macos-packaging-runner.sh" "$runner_root" "$config_root" "$node_bin" --check
|
||||
umask 077
|
||||
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; 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'
|
||||
const [directory, runner, config, node] = process.argv.slice(2)
|
||||
const xml = value => value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''')
|
||||
// svc.sh substitutes its own placeholders with sed; prohibit template syntax in paths.
|
||||
for (const value of [directory, runner, config, node]) {
|
||||
if (/[\r\n]|\{\{/u.test(value)) throw new Error('Service paths cannot contain newlines or template placeholders')
|
||||
}
|
||||
const args = ['/bin/bash', `${directory}/start.sh`, runner, config, node, '--service']
|
||||
writeFileSync(`${directory}/service.plist.template`, `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>Label</key><string>{{SvcName}}</string>
|
||||
<key>ProgramArguments</key><array>${args.map(arg => `<string>${xml(arg)}</string>`).join('')}</array>
|
||||
<key>WorkingDirectory</key><string>${xml(runner)}</string>
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>StandardOutPath</key><string>${xml(directory)}/stdout.log</string>
|
||||
<key>StandardErrorPath</key><string>${xml(directory)}/stderr.log</string>
|
||||
<key>EnvironmentVariables</key><dict><key>ACTIONS_RUNNER_SVC</key><string>1</string></dict>
|
||||
<key>ProcessType</key><string>Interactive</string>
|
||||
<key>SessionCreate</key><true/>
|
||||
</dict></plist>
|
||||
`, { mode: 0o600 })
|
||||
NODE
|
||||
GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE="$service_dir/service.plist.template" ./svc.sh install
|
||||
/usr/bin/plutil -lint "$(cat .service)"
|
||||
echo 'Installed. Run start, then status. This LaunchAgent requires a desktop login after reboot.'
|
||||
;;
|
||||
start|stop|status|uninstall)
|
||||
[[ $# = 2 && -f .service ]] || { echo 'Provide an installed runner service directory' >&2; exit 1; }
|
||||
./svc.sh "$action"
|
||||
;;
|
||||
*) echo "Unknown action: $action" >&2; exit 1 ;;
|
||||
esac
|
||||
@@ -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'
|
||||
@@ -196,6 +196,7 @@ interface DesktopPackageInvocation {
|
||||
readonly directory: boolean
|
||||
readonly prepareOnly: boolean
|
||||
readonly unsigned: boolean
|
||||
readonly skipNotarization: boolean
|
||||
readonly check: boolean
|
||||
/** Build identifier to publish under, when this build does not publish the product version. */
|
||||
readonly requestedBuildVersion: string | undefined
|
||||
@@ -228,6 +229,7 @@ export function parseDesktopPackageInvocation(
|
||||
dir: { type: 'boolean', default: false },
|
||||
'prepare-only': { type: 'boolean', default: false },
|
||||
unsigned: { type: 'boolean', default: false },
|
||||
'skip-notarization': { type: 'boolean', default: false },
|
||||
check: { type: 'boolean', default: false },
|
||||
'build-version': { type: 'string' },
|
||||
},
|
||||
@@ -235,6 +237,9 @@ export function parseDesktopPackageInvocation(
|
||||
if (positionals.length > 1) throw new Error('desktop package: expected at most one target')
|
||||
const name = positionals[0] ?? hostTargetName(hostPlatform, hostArch)
|
||||
if (values.unsigned && values['prepare-only']) throw new Error('desktop package: --unsigned cannot use --prepare-only')
|
||||
if (values['skip-notarization'] && (name === 'win-x64' || values.unsigned || values['prepare-only'])) {
|
||||
throw new Error('desktop package: --skip-notarization requires a signed macOS build, without --prepare-only')
|
||||
}
|
||||
const requestedBuildVersion = values['build-version']?.trim()
|
||||
if (values['build-version'] !== undefined && (requestedBuildVersion === undefined || requestedBuildVersion === '')) {
|
||||
throw new Error('desktop package: --build-version requires a value')
|
||||
@@ -244,6 +249,7 @@ export function parseDesktopPackageInvocation(
|
||||
directory: values.dir,
|
||||
prepareOnly: values['prepare-only'],
|
||||
unsigned: values.unsigned,
|
||||
skipNotarization: values['skip-notarization'],
|
||||
check: values.check,
|
||||
requestedBuildVersion,
|
||||
}
|
||||
@@ -326,13 +332,16 @@ 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 : paths.artifacts,
|
||||
artifactsRoot: desktopArtifactOutput(paths, invocation).directory,
|
||||
})
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const invocation = parseDesktopPackageInvocation(process.argv.slice(2))
|
||||
const { target } = invocation
|
||||
if (invocation.skipNotarization) {
|
||||
process.stdout.write('desktop package: skipping notarization and stapling; internal debugging only, not qualified for distribution or update publishing\n')
|
||||
}
|
||||
const environment = target.platform === 'darwin' && invocation.unsigned
|
||||
? localMacOSPackageEnvironment()
|
||||
: loadDesktopPackageEnvironment(target.platform, process.env, APP_ROOT, {
|
||||
@@ -354,7 +363,8 @@ async function main(): Promise<void> {
|
||||
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,
|
||||
target: target.name, unsigned: invocation.unsigned, skipNotarization: invocation.skipNotarization,
|
||||
directory: invocation.directory, prepareOnly: invocation.prepareOnly,
|
||||
version: buildVersion, productVersion, node: process.version,
|
||||
commit: packaged.commit,
|
||||
dirty: packaged.dirty,
|
||||
@@ -389,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' : ''}.dmg`
|
||||
console.log(`DMG: ${join(invocation.unsigned ? paths.unsignedArtifacts : 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)}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,14 +425,16 @@ export async function packageTarget(
|
||||
const packArguments = mac === undefined ? [] : ['--concurrency', String(mac.packConcurrency)]
|
||||
const buildPaths = desktopTargetBuildPaths(target.name)
|
||||
const releaseRecordPath = join(buildPaths.artifacts, desktopBuildRecordFilename(target.name))
|
||||
if (!invocation.prepareOnly && !invocation.unsigned) {
|
||||
if (!invocation.prepareOnly && !invocation.unsigned && !invocation.skipNotarization) {
|
||||
rmSync(releaseRecordPath, { force: true })
|
||||
rmSync(`${releaseRecordPath}.tmp`, { force: true })
|
||||
}
|
||||
const artifactsRoot = desktopArtifactOutput(buildPaths, invocation).directory
|
||||
const buildEnv = withoutWindowsSigningEnvironment(withoutDesktopUploadCredentials(
|
||||
desktopElectronBuilderEnvironment(environment, invocation.unsigned)))
|
||||
const targetEnv: NodeJS.ProcessEnv = {
|
||||
...buildEnv,
|
||||
DSH_DESKTOP_MACOS_SKIP_NOTARIZATION: invocation.skipNotarization ? '1' : '0',
|
||||
DSH_DESKTOP_TARGET_PLATFORM: target.platform,
|
||||
DSH_DESKTOP_TARGET_ARCH: target.arch,
|
||||
}
|
||||
@@ -485,7 +498,7 @@ export async function packageTarget(
|
||||
await execute(['run', 'prepare:dsh', ...(signPrimaryRuntime ? ['--defer-runtime-smoke'] : [])], downloadEnv)
|
||||
if (signPrimaryRuntime) await execute(['run', 'sign:primary-runtime', '--dsh'], electronBuilderEnv)
|
||||
if (invocation.prepareOnly) return
|
||||
if (target.platform === 'darwin' && !invocation.unsigned && !invocation.directory) {
|
||||
if (target.platform === 'darwin' && !invocation.unsigned && !invocation.skipNotarization && !invocation.directory) {
|
||||
await execute([
|
||||
...desktopElectronBuilderArguments(target, true),
|
||||
'--config.mac.notarize=false',
|
||||
@@ -498,7 +511,7 @@ export async function packageTarget(
|
||||
artifactsRoot: buildPaths.artifacts,
|
||||
environment: electronBuilderEnv,
|
||||
}, artifact => execute(desktopElectronBuilderArguments(target, false, artifact), electronBuilderEnv)), undefined, undefined, proxyEvent)
|
||||
} else if (target.platform === 'darwin' && !invocation.unsigned) {
|
||||
} else if (target.platform === 'darwin' && !invocation.unsigned && !invocation.skipNotarization) {
|
||||
await execute([...desktopElectronBuilderArguments(target, true), '--config.mac.notarize=false'], electronBuilderEnv)
|
||||
await execute(['exec', 'tsx', 'scripts/smoke-packaged-runtime.ts'], targetEnv)
|
||||
const appPath = join(buildPaths.artifacts, target.arch === 'arm64' ? 'mac-arm64' : 'mac', 'DeepSeek Harness.app')
|
||||
@@ -506,10 +519,13 @@ export async function packageTarget(
|
||||
() => notarizeMacOS({ appPath, ...resolveMacOSNotarizationEnvironment(environment) }), undefined, undefined, proxyEvent)
|
||||
} else {
|
||||
await signedStage('artifacts', () => execute(desktopElectronBuilderArguments(target, invocation.directory), electronBuilderEnv))
|
||||
await execute(['exec', 'tsx', 'scripts/smoke-packaged-runtime.ts', ...(invocation.unsigned ? ['--unsigned'] : [])], targetEnv)
|
||||
await execute(['exec', 'tsx', 'scripts/smoke-packaged-runtime.ts',
|
||||
...(invocation.unsigned ? ['--unsigned'] : []), ...(invocation.skipNotarization ? ['--skip-notarization'] : [])], targetEnv)
|
||||
}
|
||||
if (!invocation.directory && !invocation.unsigned) writeReleaseRecord(target, electronBuilderEnv, buildPaths.artifacts)
|
||||
if (journal) recordPackagingEvent(journal, { type: 'artifacts', directory: invocation.unsigned ? buildPaths.unsignedArtifacts : buildPaths.artifacts })
|
||||
if (!invocation.directory && !invocation.unsigned && !invocation.skipNotarization) {
|
||||
writeReleaseRecord(target, electronBuilderEnv, buildPaths.artifacts)
|
||||
}
|
||||
if (journal) recordPackagingEvent(journal, { type: 'artifacts', directory: artifactsRoot })
|
||||
}
|
||||
|
||||
if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) await main()
|
||||
@@ -8,6 +8,8 @@ const appRoot = resolve(import.meta.dirname, '..')
|
||||
const product = JSON.parse(readFileSync(resolve(appRoot, 'package.json'), 'utf8'))
|
||||
const version = desktopCiVersion(process.env, product.version)
|
||||
if (!process.env.DSH_PACKAGING_PRESET) throw new Error('Select a machine preset')
|
||||
const skipNotarization = process.env.PACKAGE_SKIP_NOTARIZATION === 'true'
|
||||
if (!['true', 'false', undefined].includes(process.env.PACKAGE_SKIP_NOTARIZATION)) throw new Error('Invalid skip notarization selection')
|
||||
const environment = loadDesktopPackageEnvironment('darwin', process.env, appRoot)
|
||||
for (const [name, value] of Object.entries(environment)) {
|
||||
if (/KEY|TOKEN|PASSWORD|SECRET|APPLE_ID/iu.test(name) && value) {
|
||||
@@ -15,7 +17,8 @@ for (const [name, value] of Object.entries(environment)) {
|
||||
process.stdout.write(`::add-mask::${escaped}\n`)
|
||||
}
|
||||
}
|
||||
validateDesktopPackageEnvironment(environment, { platform: 'darwin', arch: 'arm64' })
|
||||
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}\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')
|
||||
@@ -13,6 +13,7 @@ export const DESKTOP_RUNTIME_PATCHES: Readonly<Record<string, DesktopRuntimePatc
|
||||
'@fortune-sheet/[email protected]': { scope: 'workspace-only', reason: 'Patched spreadsheet code is embedded in the client bundle.' },
|
||||
'@fortune-sheet/[email protected]': { scope: 'workspace-only', reason: 'Patched spreadsheet code is embedded in the client bundle.' },
|
||||
'@yao-pkg/[email protected]': { scope: 'workspace-only', reason: 'The executable packer executes on the build host.' },
|
||||
'[email protected]': { scope: 'workspace-only', reason: 'DMG format validation executes on the build host.' },
|
||||
'[email protected]': { scope: 'workspace-only', reason: 'The patched parser is embedded in the client bundle and worker.' },
|
||||
'[email protected]': { scope: 'shared', reason: 'Runtime terminals need helper resolution and Windows worker cleanup.' },
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
/** 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'
|
||||
import { resolveDesktopPackageTarget } from './package-target.ts'
|
||||
|
||||
const paths = resolveDesktopTargetBuildPaths()
|
||||
const { values } = parseArgs({ options: { unsigned: { type: 'boolean', default: false } }, allowPositionals: false })
|
||||
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 : 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')
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/bin/bash
|
||||
# Run an already registered Apple Silicon runner with explicit tools and machine configuration.
|
||||
set -euo pipefail
|
||||
if [[ $# -lt 3 || $# -gt 4 || (${4:-} != '' && ${4:-} != '--check') ]]; then
|
||||
echo 'Usage: bash start-macos-packaging-runner.sh RUNNER_ROOT CONFIG_ROOT NODE_BIN [--check]' >&2
|
||||
if [[ $# -lt 3 || $# -gt 4 || (${4:-} != '' && ${4:-} != '--check' && ${4:-} != '--service') ]]; then
|
||||
echo 'Usage: bash start-macos-packaging-runner.sh RUNNER_ROOT CONFIG_ROOT NODE_BIN [--check|--service]' >&2
|
||||
exit 1
|
||||
fi
|
||||
runner_root=$1
|
||||
@@ -29,9 +29,14 @@ if [[ ${4:-} = '--check' ]]; then
|
||||
exit 0
|
||||
fi
|
||||
[[ -f "$runner_root/.runner" && -x "$runner_root/run.sh" ]] || { echo 'Register the runner before starting' >&2; exit 1; }
|
||||
entry=./run.sh
|
||||
if [[ ${4:-} = '--service' ]]; then
|
||||
[[ -x "$runner_root/runsvc.sh" ]] || { echo 'Install the runner service first' >&2; exit 1; }
|
||||
entry=./runsvc.sh
|
||||
fi
|
||||
cd "$runner_root"
|
||||
exec /usr/bin/env -i HOME="$HOME" USER="$(id -un)" LOGNAME="$(id -un)" \
|
||||
PATH="$node_bin:/usr/bin:/bin:/usr/sbin:/sbin" TMPDIR="${TMPDIR:-/tmp}" \
|
||||
DEVELOPER_DIR="$developer_dir" SDKROOT="$sdk_root" \
|
||||
LANG=en_US.UTF-8 DSH_PACKAGING_CONFIG_DIR="$config_root" DSH_TELEMETRY_DISABLED=1 \
|
||||
/usr/bin/caffeinate -i ./run.sh
|
||||
ACTIONS_RUNNER_SVC="${ACTIONS_RUNNER_SVC:-0}" LANG=en_US.UTF-8 DSH_PACKAGING_CONFIG_DIR="$config_root" DSH_TELEMETRY_DISABLED=1 \
|
||||
/usr/bin/caffeinate -i "$entry"
|
||||
@@ -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)
|
||||
}
|
||||
@@ -16,6 +16,7 @@ describe('desktop build paths', () => {
|
||||
'root',
|
||||
'artifacts',
|
||||
'unsignedArtifacts',
|
||||
'unnotarizedArtifacts',
|
||||
'runtime',
|
||||
'packageSet',
|
||||
'dsh',
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -237,3 +237,19 @@ it('rejects an explicit invalid macOS cache before signing while leaving unsigne
|
||||
}
|
||||
expect(() => { validateDesktopPackageEnvironment(environment, MACOS, { unsigned: true }) }).not.toThrow()
|
||||
})
|
||||
|
||||
it('skips Apple credentials but still requires signing credentials for unnotarized packages', async () => {
|
||||
await withDirectory(async (directory) => {
|
||||
const certificate = join(directory, 'signing.p12')
|
||||
await writeFile(certificate, 'fixture')
|
||||
const environment = { ...RELEASE, ...MAC_IDENTITY, CSC_LINK: certificate, CSC_KEY_PASSWORD: '' }
|
||||
expect(() => { validateDesktopPackageEnvironment(environment, MACOS, { skipNotarization: true }) }).not.toThrow()
|
||||
expect(() => { validateDesktopPackageEnvironment(environment, MACOS) }).toThrow()
|
||||
expect(() => {
|
||||
validateDesktopPackageEnvironment({ ...environment, CSC_LINK: undefined }, MACOS, { skipNotarization: true })
|
||||
}).toThrow(/CSC_LINK/u)
|
||||
expect(() => {
|
||||
validateDesktopPackageEnvironment({ ...environment, CSC_KEY_PASSWORD: undefined }, MACOS, { skipNotarization: true })
|
||||
}).toThrow(/CSC_KEY_PASSWORD/u)
|
||||
})
|
||||
})
|
||||
@@ -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',
|
||||
])
|
||||
})
|
||||
@@ -59,3 +59,43 @@ it.skipIf(process.platform !== 'darwin')('prepares private machine directories w
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('records debugging artifact paths only for an explicit CI selection without notarization credentials', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'macos-ci-settings-'))
|
||||
try {
|
||||
await mkdir(join(root, 'presets'))
|
||||
await mkdir(join(root, 'secrets'))
|
||||
await writeFile(join(root, 'certificate.p12'), 'fixture')
|
||||
await writeFile(join(root, 'presets/test.env'), [
|
||||
'DSH_DESKTOP_APP_ID=com.example.fixture', 'DSH_DESKTOP_AUTO_UPDATE_ENV=test',
|
||||
'DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN=https://policy.example.com',
|
||||
'DSH_DESKTOP_MANDATORY_UPDATE_CONFIG={"allowedAuthOrigins":["https://login.example.com"]}',
|
||||
'DOWNLOAD_TEST_ORIGIN=https://updates.example.com', 'DOWNLOAD_TEST_RELEASE_ID=0123456789abcdef0123456789abcdef',
|
||||
].join('\n'))
|
||||
await writeFile(join(root, 'secrets/macos-signing.env'), [
|
||||
'CSC_LINK=../certificate.p12', 'CSC_KEY_PASSWORD=fixture-password',
|
||||
'DSH_DESKTOP_MACOS_SIGNING_IDENTITY=Example Company (TEAMID1234)', 'DSH_DESKTOP_MACOS_TEAM_ID=TEAMID1234',
|
||||
].join('\n'))
|
||||
const output = join(root, 'output')
|
||||
const summary = join(root, 'summary')
|
||||
const run = (skip: string) => spawnSync(process.execPath,
|
||||
[join(import.meta.dirname, '../scripts/prepare-macos-ci.mjs')], { encoding: 'utf8', env: {
|
||||
...process.env, DSH_PACKAGING_CONFIG_DIR: root, DSH_PACKAGING_PRESET: 'test',
|
||||
PACKAGE_SKIP_NOTARIZATION: skip, PACKAGE_BUILD_VERSION: 'auto',
|
||||
GITHUB_RUN_NUMBER: '1', GITHUB_RUN_ATTEMPT: '1', GITHUB_OUTPUT: output, GITHUB_STEP_SUMMARY: summary,
|
||||
} })
|
||||
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)
|
||||
expect(await readFile(output, 'utf8')).toContain('artifacts=unnotarized-artifacts\nmode=unnotarized-debug\n')
|
||||
expect(await readFile(summary, 'utf8')).toContain('Internal debugging only')
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import type { NotarizeOptions } from '@electron/notarize'
|
||||
import {
|
||||
resolveDesktopAppId,
|
||||
@@ -12,6 +13,11 @@ import {
|
||||
assertMacOSSignatureDetails,
|
||||
} from '../scripts/verify-macos-signature.mjs'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const { validateConfiguration } = require('app-builder-lib/out/util/config/config.js') as {
|
||||
validateConfiguration: (config: object, logger: { isEnabled: false }) => Promise<void>
|
||||
}
|
||||
|
||||
const RELEASE_ENVIRONMENT = {
|
||||
DSH_DESKTOP_APP_ID: 'com.example.desktop',
|
||||
DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN: 'https://policy.example.com',
|
||||
@@ -75,6 +81,7 @@ describe('desktop macOS release signature', () => {
|
||||
signIgnore: ['/Contents/Resources/app\\.asar\\.unpacked/dsh(?:/|$)', '/Contents/Resources/runtime/primary-runtime(?:/|$)', '\\.pak$'],
|
||||
},
|
||||
dmg: {
|
||||
format: 'ULMO',
|
||||
sign: true,
|
||||
writeUpdateInfo: false,
|
||||
},
|
||||
@@ -87,6 +94,23 @@ describe('desktop macOS release signature', () => {
|
||||
expect(typeof config.artifactBuildCompleted).toBe('function')
|
||||
})
|
||||
|
||||
it('accepts LZMA DMGs through builder validation while rejecting unknown formats', async () => {
|
||||
const { createElectronBuilderConfig } = await import('../scripts/electron-builder-config.mjs')
|
||||
for (const arch of ['arm64', 'x64']) {
|
||||
for (const unsigned of ['0', '1']) {
|
||||
const config = createElectronBuilderConfig({
|
||||
...RELEASE_ENVIRONMENT,
|
||||
DSH_DESKTOP_TARGET_ARCH: arch,
|
||||
DSH_DESKTOP_UNSIGNED: unsigned,
|
||||
}, 'darwin', arch)
|
||||
expect(config.dmg.format).toBe('ULMO')
|
||||
await expect(validateConfiguration(config, { isEnabled: false })).resolves.toBeUndefined()
|
||||
}
|
||||
}
|
||||
await expect(validateConfiguration({ dmg: { format: 'invalid' } }, { isEnabled: false }))
|
||||
.rejects.toThrow('configuration.dmg.format')
|
||||
})
|
||||
|
||||
it('seals PAK resources with their enclosing bundle while signing executable code', async () => {
|
||||
const { createElectronBuilderConfig } = await import('../electron-builder.config.mjs')
|
||||
const config = createElectronBuilderConfig(RELEASE_ENVIRONMENT, 'darwin', 'arm64')
|
||||
@@ -255,3 +279,19 @@ describe('desktop macOS release signature', () => {
|
||||
expect(verified).toEqual(['/tmp/release.dmg'])
|
||||
})
|
||||
})
|
||||
|
||||
it('signs debugging artifacts without Apple credentials and isolates them from releases', async () => {
|
||||
const { createElectronBuilderConfig } = await import('../scripts/electron-builder-config.mjs')
|
||||
const config = createElectronBuilderConfig({ ...RELEASE_ENVIRONMENT,
|
||||
APPLE_API_KEY: undefined, APPLE_API_KEY_ID: undefined, APPLE_API_ISSUER: undefined,
|
||||
DSH_DESKTOP_MACOS_SKIP_NOTARIZATION: '1',
|
||||
}, 'darwin', 'arm64')
|
||||
expect(config.mac.forceCodeSigning).toBe(true)
|
||||
expect(config.mac.notarize).toBe(false)
|
||||
expect(config.dmg.sign).toBe(true)
|
||||
expect(config.mac).toMatchObject({ target: ['dmg', 'zip'] })
|
||||
expect(config.artifactName).toContain('-unnotarized.')
|
||||
expect(portablePath(config.directories.output)).toContain('/unnotarized-artifacts')
|
||||
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)
|
||||
})
|
||||
@@ -197,3 +197,27 @@ it('does not write a release completion record when Apple proxy cleanup fails',
|
||||
.rejects.toThrow('proxy restoration failed')
|
||||
expect(writeFileSync).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([false, true])('keeps signing and runtime verification when notarization is skipped (directory=%s)', async (directory) => {
|
||||
const { run, stages } = supervisor()
|
||||
await packageTarget(parseDesktopPackageInvocation(['mac-arm64', '--skip-notarization', ...(directory ? ['--dir'] : [])], 'darwin', 'arm64'), {
|
||||
...environment, CSC_LINK: 'signing.p12', CSC_KEY_PASSWORD: 'fixture-password',
|
||||
}, run)
|
||||
expect(stages.at(-2)).toBe(`exec electron-builder --config electron-builder.config.mjs --mac --arm64 --publish never${directory ? ' --dir' : ''}`)
|
||||
expect(stages.at(-1)).toBe('exec tsx scripts/smoke-packaged-runtime.ts --skip-notarization')
|
||||
expect(run.run.mock.calls.at(-2)![3].env).toMatchObject({
|
||||
DSH_DESKTOP_UNSIGNED: '0', DSH_DESKTOP_MACOS_SKIP_NOTARIZATION: '1', CSC_LINK: 'signing.p12',
|
||||
})
|
||||
expect(withMacOSNotarizationProxy).not.toHaveBeenCalled()
|
||||
expect(packageMacOSArtifacts).not.toHaveBeenCalled()
|
||||
expect(writeFileSync).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('fails debugging packages when runtime verification fails', async () => {
|
||||
const failure = 'exec tsx scripts/smoke-packaged-runtime.ts --skip-notarization'
|
||||
const { run, stages } = supervisor(failure)
|
||||
await expect(packageTarget(parseDesktopPackageInvocation(['mac-arm64', '--skip-notarization'], 'darwin', 'arm64'), environment, run))
|
||||
.rejects.toThrow('stage refused')
|
||||
expect(stages.at(-1)).toBe(failure)
|
||||
expect(writeFileSync).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -148,3 +148,12 @@ describe('desktop package target', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('allows skipping notarization only for signed macOS packages', () => {
|
||||
expect(parseDesktopPackageInvocation(['mac-arm64'], 'darwin', 'arm64').skipNotarization).toBe(false)
|
||||
expect(parseDesktopPackageInvocation(['mac-arm64', '--skip-notarization'], 'darwin', 'arm64').skipNotarization).toBe(true)
|
||||
for (const flag of ['--unsigned', '--prepare-only']) {
|
||||
expect(() => parseDesktopPackageInvocation(['mac-arm64', '--skip-notarization', flag], 'darwin', 'arm64')).toThrow(/requires a signed macOS build/u)
|
||||
}
|
||||
expect(() => parseDesktopPackageInvocation(['win-x64', '--skip-notarization'], 'win32', 'x64')).toThrow(/requires a signed macOS build/u)
|
||||
})
|
||||
@@ -43,7 +43,7 @@ it('classifies the current workspace and shares only runtime patch bytes', () =>
|
||||
const before = readFileSync(join(repository, 'pnpm-lock.yaml'))
|
||||
const plan = prepareRuntimePatches(project, repository)
|
||||
expect(plan.patches.map(entry => entry.spec)).toEqual(['@earendil-works/[email protected]', '[email protected]'])
|
||||
expect(plan.workspaceOnly).toHaveLength(5)
|
||||
expect(plan.workspaceOnly).toHaveLength(6)
|
||||
const settings = yaml.load(readFileSync(join(project, 'pnpm-workspace.yaml'), 'utf8')) as { overrides: Record<string, string> }
|
||||
expect(settings.overrides['@earendil-works/pi-ai']).toBeUndefined()
|
||||
expect(settings.overrides['@earendil-works/pi-ai@^1.1.0']).toBe('1.1.0')
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
diff --git a/out/options/macOptions.d.ts b/out/options/macOptions.d.ts
|
||||
index e93987d973399b17f3970c858abb49f5e4b193d6..445a73c81e1d7eb61dc318373d2331487934d3f5 100644
|
||||
--- a/out/options/macOptions.d.ts
|
||||
+++ b/out/options/macOptions.d.ts
|
||||
@@ -258,7 +258,7 @@ export interface DmgOptions extends TargetSpecificOptions {
|
||||
* The disk image format. `ULFO` (lzfse-compressed image (OS X 10.11+ only)).
|
||||
* @default UDZO
|
||||
*/
|
||||
- format?: "UDRW" | "UDRO" | "UDCO" | "UDZO" | "UDBZ" | "ULFO";
|
||||
+ format?: "UDRW" | "UDRO" | "UDCO" | "UDZO" | "UDBZ" | "ULFO" | "ULMO";
|
||||
/**
|
||||
* The filesystem for the DMG volume (e.g. `"APFS"` or `"HFS+"`)
|
||||
* This will be changed to APFS in the next major release, so it is recommended to set it explicitly to HFS+ if you want to keep using HFS+ (e.g. for better compatibility with older macOS versions).
|
||||
diff --git a/scheme.json b/scheme.json
|
||||
index a637bab2668ef88e7382dd4aa5db62b74fdde898..ebcafac58971bdd7c69b78ceddcd401a75ffaa35 100644
|
||||
--- a/scheme.json
|
||||
+++ b/scheme.json
|
||||
@@ -979,7 +979,8 @@
|
||||
"UDRO",
|
||||
"UDRW",
|
||||
"UDZO",
|
||||
- "ULFO"
|
||||
+ "ULFO",
|
||||
+ "ULMO"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
Generated
+6
-5
@@ -15,6 +15,7 @@ patchedDependencies:
|
||||
'@fortune-sheet/[email protected]': 8791a230763dbf853fd65d627c68c31de37209992851377855e16cf08ea70532
|
||||
'@fortune-sheet/[email protected]': 2932e52e79afcd4ced08cfa123b5eded786c85043da32f622f3e80af6fc96c37
|
||||
'@yao-pkg/[email protected]': 75891fc22b277c145cafeab83e41baf4a70f5b8fe6dc0801c6a0b922d7ee88c7
|
||||
[email protected]: b70dc2a2a78ffabb80a4f14fafc7c103237f2b0a207e1f296fa1c1387ea29348
|
||||
[email protected]: c1bf8ac8f319dab382f08e3a669b715e691d46c703ac5a4163eb383fbedd1d48
|
||||
[email protected]: 9d30053266d0f39d3debe894c675867f6782da6bcadbc94a693b3cfb9eff8ed0
|
||||
|
||||
@@ -722,7 +723,7 @@ importers:
|
||||
version: 8.18.1
|
||||
app-builder-lib:
|
||||
specifier: 26.17.0
|
||||
version: 26.17.0([email protected])([email protected])([email protected])
|
||||
version: 26.17.0(patch_hash=b70dc2a2a78ffabb80a4f14fafc7c103237f2b0a207e1f296fa1c1387ea29348)([email protected])([email protected])([email protected])
|
||||
cos-nodejs-sdk-v5:
|
||||
specifier: 3.0.0
|
||||
version: 3.0.0
|
||||
@@ -26194,7 +26195,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]([email protected])([email protected])([email protected]):
|
||||
[email protected](patch_hash=b70dc2a2a78ffabb80a4f14fafc7c103237f2b0a207e1f296fa1c1387ea29348)([email protected])([email protected])([email protected]):
|
||||
dependencies:
|
||||
'@electron/asar': 3.4.1
|
||||
'@electron/fuses': 1.8.0
|
||||
@@ -27057,7 +27058,7 @@ snapshots:
|
||||
|
||||
[email protected]([email protected])([email protected]):
|
||||
dependencies:
|
||||
app-builder-lib: 26.17.0([email protected])([email protected])([email protected])
|
||||
app-builder-lib: 26.17.0(patch_hash=b70dc2a2a78ffabb80a4f14fafc7c103237f2b0a207e1f296fa1c1387ea29348)([email protected])([email protected])([email protected])
|
||||
builder-util: 26.16.0([email protected])
|
||||
fs-extra: 10.1.0
|
||||
js-yaml: 4.3.2
|
||||
@@ -27108,7 +27109,7 @@ snapshots:
|
||||
|
||||
[email protected]([email protected])([email protected]):
|
||||
dependencies:
|
||||
app-builder-lib: 26.17.0([email protected])([email protected])([email protected])
|
||||
app-builder-lib: 26.17.0(patch_hash=b70dc2a2a78ffabb80a4f14fafc7c103237f2b0a207e1f296fa1c1387ea29348)([email protected])([email protected])([email protected])
|
||||
builder-util: 26.16.0([email protected])
|
||||
electron-winstaller: 5.4.0([email protected])
|
||||
transitivePeerDependencies:
|
||||
@@ -27117,7 +27118,7 @@ snapshots:
|
||||
|
||||
[email protected]([email protected])([email protected]):
|
||||
dependencies:
|
||||
app-builder-lib: 26.17.0([email protected])([email protected])([email protected])
|
||||
app-builder-lib: 26.17.0(patch_hash=b70dc2a2a78ffabb80a4f14fafc7c103237f2b0a207e1f296fa1c1387ea29348)([email protected])([email protected])([email protected])
|
||||
builder-util: 26.16.0([email protected])
|
||||
builder-util-runtime: 9.7.0([email protected])
|
||||
chalk: 4.1.2
|
||||
|
||||
@@ -111,5 +111,6 @@ patchedDependencies:
|
||||
'@fortune-sheet/[email protected]': patches/@[email protected]
|
||||
'@fortune-sheet/[email protected]': patches/@[email protected]
|
||||
'@yao-pkg/[email protected]': patches/@[email protected]
|
||||
[email protected]: patches/[email protected]
|
||||
[email protected]: patches/[email protected]
|
||||
[email protected]: patches/[email protected]
|
||||
@@ -25,5 +25,15 @@ it('confines signed Mac packaging to manual dispatch and a dedicated Apple Silic
|
||||
expect(uploads).toHaveLength(2)
|
||||
expect(uploads[0]?.if).toBe("always() && steps.checkout.outcome == 'success'")
|
||||
expect(uploads[1]?.if).toBe('success()')
|
||||
expect(uploads[1]?.with?.path).toBe('apps/desktop/.desktop-build/targets/mac-arm64/artifacts/*.dmg\napps/desktop/.desktop-build/targets/mac-arm64/artifacts/*.zip\n')
|
||||
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,
|
||||
description: 'Skip notarization/stapling: internal debugging only, not for distribution or updates',
|
||||
} } })
|
||||
const steps = workflow.jobs.package!.steps
|
||||
expect(steps.filter(step => step.run?.includes('args+=(--skip-notarization)'))).toHaveLength(2)
|
||||
expect(steps.at(-1)?.with?.name).toContain('${{ steps.settings.outputs.mode }}')
|
||||
})
|
||||
Reference in new issue
Block a user