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:
Yichen Jiang committed 2026-10-12 20:56:10 +08:00
commit 40f9bccd78
33 files changed
+486 -66

No files matched your search

+19 -7
View File
@@ -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
+1
View File
@@ -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
}
+4 -4
View File
@@ -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
+16 -2
View File
@@ -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
+16 -2
View File
@@ -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 }
+14 -1
View File
@@ -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('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&apos;')
// 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
+27 -11
View File
@@ -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()
+5 -2
View File
@@ -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')
+27
View File
@@ -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"
},
+6 -5
View File
@@ -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
+1
View File
@@ -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]
+11 -1
View File
@@ -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 }}')
})