Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f95acdc000 |
@@ -248,7 +248,7 @@ jobs:
|
||||
|
||||
- name: Build, sign and notarize macOS release packages
|
||||
if: matrix.platform == 'macos' && steps.macos-signing.outputs.enabled == 'true'
|
||||
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --skip-build
|
||||
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --format dmg --skip-build
|
||||
env:
|
||||
ELECTRON_CACHE: ${{ runner.temp }}/electron
|
||||
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/electron-builder
|
||||
@@ -261,7 +261,7 @@ jobs:
|
||||
|
||||
- name: Build unsigned macOS release packages
|
||||
if: matrix.platform == 'macos' && steps.macos-signing.outputs.enabled == 'false'
|
||||
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --skip-build --unsigned
|
||||
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --format dmg --skip-build --unsigned
|
||||
env:
|
||||
ELECTRON_CACHE: ${{ runner.temp }}/electron
|
||||
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/electron-builder
|
||||
|
||||
@@ -219,6 +219,7 @@ package format v1 归档已经包含固定 Agent 和 Node,可直接用于 Host
|
||||
桌面包会携带经过版本与完整性校验的 OpenCode、Continue 和 DSH 插件安装 Runtime:
|
||||
|
||||
- OpenCode 平台二进制来自 `.runtime-resources/<arch>`。
|
||||
- Desktop 的离线配置复制以 `.runtime-resources` 为源、`opencode-config{,/**/*}` 为过滤器,避免 electron-builder 对复制源根目录下 `node_modules` 的固定排除。最终包逐文件核对离线配置与依赖;DSH bootstrap 和 chunk 不得残留外部 `@deepseek-ai/*` 导入。Windows x64 CI 在删除解包目录前实际导入包内 OpenCode 插件并运行隔离的 DSH UtilityProcess 握手与 npm 探针,失败即停止发布。
|
||||
- OpenCode 离线配置位于 `.runtime-resources/opencode-config`,构建时按已安装 `@opencode-ai/plugin/package.json` 的版本复用,不依赖根 lockfile 或自定义 ready/integrity 标记。缺失、不可读或版本不匹配时,在 staging 中执行精确版本 npm 安装,成功后替换缓存;安装失败保留旧缓存并清理 staging。本机共享配置同样比较随包插件与缓存插件的版本,版本不同时通过 staging 替换。
|
||||
- 离线依赖安装禁用 npm bin links;远程 Runtime 组包排除不使用的 `.bin` 命令链接,并把复制后的普通依赖文件统一为 `0644`,OpenCode 入口仍为 `0755`。目录元数据、每个依赖文件及许可证继续纳入签名校验。
|
||||
- Continue Runtime 来自锁定版本的 `@continuedev/cli`。
|
||||
@@ -397,8 +398,8 @@ DeepSeek Harness Runtime、`app.asar`、目标架构和安装包签名。
|
||||
npm run release:package -- --platform macos --arch <x64|arm64> --unsigned
|
||||
```
|
||||
|
||||
默认发布产物为 Windows 的 NSIS 安装包与 portable ZIP、macOS 的 DMG 与
|
||||
ZIP,以及 Linux 的 AppImage、DEB 与 RPM。Linux 原生 Runner 必须安装
|
||||
默认发布产物为 Windows 的 NSIS 安装包与 portable ZIP、macOS 的 DMG,
|
||||
以及 Linux 的 AppImage、DEB 与 RPM。Linux 原生 Runner 必须安装
|
||||
`rpm`/`rpmbuild` 工具后再调用 electron-builder。Windows portable ZIP 解压后可直接
|
||||
运行 `GoodBuddy.exe`,并包含启用便携数据目录的
|
||||
`.goodbuddy-portable.json`。每个目标目录都包含带文件大小和 SHA-256 的
|
||||
@@ -434,8 +435,8 @@ Desktop 正式发布必须把候选分支推送与标签推送拆成两个阶段
|
||||
全部目标成功后,才会严格校验并聚合所有平台产物,生成按平台重命名的
|
||||
manifests、总 `release-manifest.json` 和 `SHA256SUMS`。随后工作流通过
|
||||
GitHub OIDC 获取短期 STS 凭据,将发布资产和 `site-release.json` 上传到
|
||||
北京 OSS 的不可变版本目录,并公开校验 14 个安装包。验证通过后才创建或
|
||||
更新 draft GitHub Release、上传 22 个 Release 资产并正式发布,最后原子
|
||||
北京 OSS 的不可变版本目录,并公开校验 12 个安装包。验证通过后才创建或
|
||||
更新 draft GitHub Release、上传 20 个 Release 资产并正式发布,最后原子
|
||||
切换官网 `latest.json`。任一步失败都不会提前切换官网最新版本。
|
||||
|
||||
同一标签重跑时,工作流会根据 `resources/release-notes.json` 重新生成并
|
||||
@@ -482,7 +483,7 @@ git push github "$tag"
|
||||
macOS 发布 job 会先原子判断 Apple 凭据状态:以下五项 Actions Secrets 全部
|
||||
存在时,使用 Developer ID Application 证书签名,并通过 App Store Connect
|
||||
API Key 提交 Apple notarization;五项全部缺失时,明确生成未签名、未公证的
|
||||
DMG 和 ZIP,并在 Actions 日志与摘要中警告 Gatekeeper 限制;只配置一部分时
|
||||
DMG,并在 Actions 日志与摘要中警告 Gatekeeper 限制;只配置一部分时
|
||||
任务失败,不能静默降级为未签名包。
|
||||
|
||||
- `MACOS_CERTIFICATE_BASE64`:包含证书及私钥的 `.p12` 文件经 Base64 编码后的内容。
|
||||
|
||||
+13
-3
@@ -21,7 +21,7 @@ otherwise.
|
||||
packages target compatible x64 and arm64 environments, including UOS, Kylin,
|
||||
Hygon, Zhaoxin, Kunpeng, and Phytium systems. This is not vendor certification.
|
||||
LoongArch has a separate experimental loong64 preview outside standard
|
||||
releases and automatic updates; no preview is planned for 0.12.7. See the
|
||||
releases and automatic updates; no preview is planned for 0.12.8. See the
|
||||
[preview boundaries](./docs/development/loongarch-preview-build.md).
|
||||
- [x] **Configurable global shortcut**: Enable, disable, or record an Electron
|
||||
accelerator under Platform Features / General. The default remains
|
||||
@@ -174,7 +174,7 @@ otherwise.
|
||||
failure, Agent `SIGKILL`/restart, and recovery from a reopened Desktop SQLite
|
||||
database. Successful tool START/END events appear exactly once, with no
|
||||
Prompt, provider, or tool replay observed. The current Agent source lock is
|
||||
`0.11.21`, while the current Desktop release candidate is `0.12.7`; formal
|
||||
`0.11.21`, while the current Desktop release candidate is `0.12.8`; formal
|
||||
publication status follows the separate Agent and Desktop
|
||||
release channels. Current macOS source has passed native package installation,
|
||||
detached lifecycle, Attach, real Ask/Execute, and cancellation of tools in
|
||||
@@ -567,7 +567,17 @@ otherwise.
|
||||
build commands.
|
||||
- [x] **Six-target native release matrix**: Native runners build Windows,
|
||||
macOS, and Linux `x64` and `arm64` targets with release manifests and SHA-256
|
||||
hashes. Linux also produces AppImage, DEB, and RPM.
|
||||
hashes. Windows provides NSIS and portable ZIP, macOS DMG only, and Linux
|
||||
AppImage, DEB, and RPM, for 12 installers and 20 published assets.
|
||||
- [x] **Desktop Runtime package verification**: Offline OpenCode dependencies
|
||||
are copied without electron-builder's root node_modules exclusion; DSH
|
||||
session projection is bundled into the unpacked Host. Final package checks
|
||||
compare the offline dependency tree and reject external DeepSeek imports.
|
||||
Windows x64 CI additionally imports the packaged OpenCode plugin and checks
|
||||
the real DSH UtilityProcess handshake before publishing.
|
||||
- [x] **DMG-only update manifests**: Current Desktop and website readers accept
|
||||
DMG-only and historical DMG/ZIP manifests. Older macOS GitHub readers and all
|
||||
older mirror readers require a manual upgrade from the download page.
|
||||
|
||||
### Open interfaces, team collaboration, and remote execution
|
||||
|
||||
|
||||
+5
-3
@@ -14,7 +14,7 @@
|
||||
### 桌面基础、工作空间与上下文
|
||||
|
||||
- [x] **跨平台桌面应用**:支持 Windows、macOS、Linux,以及 `x64`、`arm64` 发布目标。
|
||||
- [x] **信创软硬件覆盖**:正式 Linux 安装包覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境。龙芯 LoongArch 另行提供 `loong64` 实验预览版,不属于正式发布矩阵,也未纳入自动更新;`0.12.7` 不构建该预览。这里的覆盖范围不等同于厂商或整机兼容认证,具体状态与限制见[龙芯预览版构建说明](./docs/development/loongarch-preview-build.md)。
|
||||
- [x] **信创软硬件覆盖**:正式 Linux 安装包覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境。龙芯 LoongArch 另行提供 `loong64` 实验预览版,不属于正式发布矩阵,也未纳入自动更新;`0.12.8` 不构建该预览。这里的覆盖范围不等同于厂商或整机兼容认证,具体状态与限制见[龙芯预览版构建说明](./docs/development/loongarch-preview-build.md)。
|
||||
- [x] **可配置全局快捷唤起**:在“平台功能 / 通用设置”中启停或录制 Electron accelerator;默认保留 `CommandOrControl+Shift+Space`,冲突或保存失败时继续使用上一组已注册快捷键,并显示可处理的状态。
|
||||
- [x] **Projects、独立对话与会话分支**:按项目隔离上下文,管理会话、附件和 Git 工作区变更;本地会话可在稳定状态下复制当前聊天内容到独立分支,分支持续显示来源徽标且不复制 Task、队列或成果归属;项目选择器区分本地、托管 SSH 与远程消息通道项目,托管 SSH 项目按 Host 分组并在 Host 标题显示真实 Agent 连接状态,项目行只保留远端路径。
|
||||
- [x] **文件、截图、窗口、剪贴板上下文**:用户明确选择后才加入模型上下文。
|
||||
@@ -104,10 +104,12 @@
|
||||
|
||||
### 开源、构建与发布
|
||||
|
||||
- 当前源码候选为 Desktop `0.12.7`、Agent `0.11.21`,OpenCode 固定为 `1.18.29`;正式发布状态以 Desktop 与 Agent 的独立发布渠道为准。
|
||||
- 当前源码候选为 Desktop `0.12.8`、Agent `0.11.21`,OpenCode 固定为 `1.18.29`;正式发布状态以 Desktop 与 Agent 的独立发布渠道为准。
|
||||
- [x] **0BSD 开源许可**:原创代码可自由使用、复制、修改、分发和商用;第三方组件和资源仍遵循各自许可证。
|
||||
- [x] **可复现依赖安装与源码构建**:使用锁定依赖、Node.js 24 和统一的测试、类型检查、Lint、生产构建命令。
|
||||
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希;Linux 同时生成 AppImage、DEB 和 RPM。
|
||||
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希。Windows 提供 NSIS 与 portable ZIP,macOS 仅 DMG,Linux 提供 AppImage、DEB 和 RPM,共 12 个安装包、20 个发布资产。
|
||||
- [x] **桌面 Runtime 包校验**:OpenCode 离线依赖复制避开 electron-builder 对源根目录 node_modules 的排除,DSH 会话投影组件打入解包后的 Host。最终包逐文件核对离线依赖并拒绝外部 DeepSeek 导入;Windows x64 CI 还会导入包内 OpenCode 插件、验证真实 DSH UtilityProcess 握手,通过后才发布。
|
||||
- [x] **DMG-only 更新清单**:当前 Desktop 和官网读取器同时接受仅 DMG 与历史 DMG/ZIP 清单。旧版 macOS 的 GitHub 更新检查及所有旧版镜像更新检查需要从下载页手动升级。
|
||||
|
||||
### 开放接口、团队协作与远程执行
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ const manifestName = 'release-manifest.json'
|
||||
const targetDefinitions = [
|
||||
{ platform: 'windows', arch: 'x64', formats: ['nsis', 'portable'] },
|
||||
{ platform: 'windows', arch: 'arm64', formats: ['nsis', 'portable'] },
|
||||
{ platform: 'macos', arch: 'x64', formats: ['dmg', 'zip'] },
|
||||
{ platform: 'macos', arch: 'arm64', formats: ['dmg', 'zip'] },
|
||||
{ platform: 'macos', arch: 'x64', formats: ['dmg'] },
|
||||
{ platform: 'macos', arch: 'arm64', formats: ['dmg'] },
|
||||
{ platform: 'linux', arch: 'x64', formats: ['AppImage', 'deb', 'rpm'] },
|
||||
{ platform: 'linux', arch: 'arm64', formats: ['AppImage', 'deb', 'rpm'] }
|
||||
]
|
||||
|
||||
+30
-2
@@ -42,6 +42,10 @@ const {
|
||||
const {
|
||||
readZipCentralDirectory
|
||||
} = require('./zip-central-directory.cjs')
|
||||
const {
|
||||
verifyOpenCodeConfig,
|
||||
verifyHarnessBundleImports
|
||||
} = require('./verify-desktop-runtimes.cjs')
|
||||
|
||||
const root = join(__dirname, '..')
|
||||
const packageJson = JSON.parse(
|
||||
@@ -90,6 +94,8 @@ const portableRequiredFiles = [
|
||||
'resources/tray-icon.png',
|
||||
'resources/tool-environment/managed-python-artifacts.json',
|
||||
'resources/runtimes/opencode/opencode.exe',
|
||||
'resources/runtimes/opencode-config/node_modules/@opencode-ai/plugin/package.json',
|
||||
'resources/runtimes/opencode-config/node_modules/@opencode-ai/plugin/dist/index.js',
|
||||
'resources/runtimes/continue/package.json',
|
||||
'resources/runtimes/npm/bin/npm-cli.js',
|
||||
'resources/runtimes/npm/bin/npx-cli.js',
|
||||
@@ -126,7 +132,7 @@ const platformDefinitions = {
|
||||
},
|
||||
macos: {
|
||||
builderFlag: '--mac',
|
||||
defaultFormats: ['dmg', 'zip'],
|
||||
defaultFormats: ['dmg'],
|
||||
supportedFormats: ['dmg', 'zip'],
|
||||
unpackedPattern: /^mac(?:-.+)?$/u,
|
||||
executable: [
|
||||
@@ -820,6 +826,10 @@ function verifyHarnessPackage(
|
||||
join(unpackedRoot, ...harnessHostEntry.split('/')),
|
||||
'DeepSeek Harness 可执行 Host'
|
||||
)
|
||||
verifyHarnessBundleImports(
|
||||
readFileSync(join(unpackedRoot, ...harnessHostEntry.split('/')), 'utf8'),
|
||||
harnessHostEntry
|
||||
)
|
||||
const harnessLlmChunk = [...entries]
|
||||
.map((entry) => entry.slice(1).split(sep).join('/'))
|
||||
.find((entry) =>
|
||||
@@ -868,6 +878,10 @@ function verifyHarnessPackage(
|
||||
join(unpackedRoot, ...chunkPath.split('/')),
|
||||
'DeepSeek Harness 可执行 module chunk'
|
||||
)
|
||||
verifyHarnessBundleImports(
|
||||
readFileSync(join(unpackedRoot, ...chunkPath.split('/')), 'utf8'),
|
||||
chunkPath
|
||||
)
|
||||
}
|
||||
for (const [packageName, expectedVersion] of Object.entries(
|
||||
harnessPackageVersions
|
||||
@@ -1080,6 +1094,10 @@ function verifyUnpackedOutput(directory, options) {
|
||||
'Remote Runtime 锁定清单'
|
||||
)
|
||||
assertFile(runtimeExecutable, 'OpenCode Runtime')
|
||||
verifyOpenCodeConfig(
|
||||
resources,
|
||||
join(root, '.runtime-resources', 'opencode-config')
|
||||
)
|
||||
assertFile(
|
||||
join(resources, 'runtimes', 'continue', 'dist', 'index.js'),
|
||||
'Continue Runtime'
|
||||
@@ -1567,7 +1585,7 @@ function printHelp() {
|
||||
|
||||
默认格式:
|
||||
windows: nsis, portable (ZIP)
|
||||
macos: dmg, zip
|
||||
macos: dmg
|
||||
linux: AppImage, deb, rpm`)
|
||||
}
|
||||
|
||||
@@ -1629,6 +1647,16 @@ async function main(argv = process.argv.slice(2)) {
|
||||
stagingDirectory,
|
||||
options
|
||||
)
|
||||
if (
|
||||
process.env.GITHUB_ACTIONS === 'true' &&
|
||||
options.platform === 'windows' &&
|
||||
options.arch === 'x64'
|
||||
) {
|
||||
await run(process.execPath, [
|
||||
join(root, 'build', 'run-packaged-deepseek-harness-smoke.cjs'),
|
||||
unpackedDirectory
|
||||
])
|
||||
}
|
||||
if (
|
||||
options.platform === 'windows' &&
|
||||
options.formats.includes('portable')
|
||||
|
||||
@@ -13,6 +13,7 @@ const {
|
||||
const { statSync } = require('node:fs')
|
||||
const { tmpdir } = require('node:os')
|
||||
const { delimiter, join, resolve } = require('node:path')
|
||||
const { pathToFileURL } = require('node:url')
|
||||
|
||||
const unpackedPath = process.argv[2]
|
||||
? resolve(process.argv[2])
|
||||
@@ -109,6 +110,20 @@ async function main() {
|
||||
const npmProject = join(root, 'npm-project')
|
||||
const npmFixture = join(root, 'npm-fixture')
|
||||
await mkdir(project, { recursive: true })
|
||||
const pluginUrl = pathToFileURL(join(
|
||||
unpackedPath, 'resources', 'runtimes', 'opencode-config',
|
||||
'node_modules', '@opencode-ai', 'plugin', 'dist', 'index.js'
|
||||
)).href
|
||||
const importedPlugin = await run(
|
||||
executable,
|
||||
['--input-type=module', '--eval', `await import(${JSON.stringify(pluginUrl)})`],
|
||||
{ ...process.env, ELECTRON_RUN_AS_NODE: '1' },
|
||||
root
|
||||
)
|
||||
if (importedPlugin.exitCode !== 0 || importedPlugin.signal) {
|
||||
throw new Error(`Packaged OpenCode plugin import failed: ${importedPlugin.output.trim()}`)
|
||||
}
|
||||
console.log('Packaged OpenCode offline plugin import: ready')
|
||||
|
||||
await copyFile(
|
||||
resolve('build/deepseek-harness-utility-smoke.cjs'),
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
const { readdirSync, readFileSync, statSync } = require('node:fs')
|
||||
const { join, relative } = require('node:path')
|
||||
const { opencodeVersion } = require('./opencode-config.cjs')
|
||||
|
||||
function verifyOpenCodeConfig(resources, source) {
|
||||
const destination = join(resources, 'runtimes', 'opencode-config')
|
||||
const manifestPath = join(destination, 'node_modules', '@opencode-ai', 'plugin', 'package.json')
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
||||
if (manifest.name !== '@opencode-ai/plugin' || manifest.version !== opencodeVersion) {
|
||||
throw new Error('Packaged OpenCode plugin version is invalid')
|
||||
}
|
||||
const pending = [source]
|
||||
let files = 0
|
||||
while (pending.length > 0) {
|
||||
const directory = pending.pop()
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
const file = join(directory, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
pending.push(file)
|
||||
} else if (entry.isFile()) {
|
||||
const target = join(destination, relative(source, file))
|
||||
if (!statSync(target, { throwIfNoEntry: false })?.isFile() ||
|
||||
!readFileSync(target).equals(readFileSync(file))) {
|
||||
throw new Error(`Packaged OpenCode dependency is missing or changed: ${relative(source, file)}`)
|
||||
}
|
||||
files++
|
||||
}
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
function verifyHarnessBundleImports(source, name) {
|
||||
if (/(?:\bfrom\s*|\bimport\s*(?:\(\s*)?|\brequire\s*\(\s*)["']@deepseek-ai\//u.test(source)) {
|
||||
throw new Error(`DeepSeek Harness bundle contains an external DeepSeek dependency: ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { verifyOpenCodeConfig, verifyHarnessBundleImports }
|
||||
@@ -38,9 +38,19 @@ below.
|
||||
`npm run release:package -- --platform <platform> --arch <arch>`. It only
|
||||
packages for the native host and writes to
|
||||
`dist/release/<platform>-<arch>`.
|
||||
- Default deliverables are NSIS and portable ZIP for Windows, DMG and ZIP for
|
||||
- Default deliverables are NSIS and portable ZIP for Windows, DMG for
|
||||
macOS, and AppImage, DEB, and RPM for Linux. Every target includes
|
||||
`release-manifest.json` with SHA-256 hashes.
|
||||
- Starting with Desktop 0.12.8, macOS releases contain DMG only. Older macOS
|
||||
GitHub update readers and all older mirror readers require ZIP and cannot
|
||||
validate this new matrix; affected users must upgrade manually from the
|
||||
download page. The user approved this immediate format change. Current
|
||||
readers accept both the historical DMG/ZIP matrix and the DMG-only matrix.
|
||||
- Final Desktop package verification compares every bundled OpenCode
|
||||
configuration file with the prepared dependency tree and rejects external
|
||||
DeepSeek imports in the unpacked Host and chunks. Windows x64 CI also
|
||||
imports the packaged OpenCode plugin and starts an isolated packaged
|
||||
UtilityProcess before publishing; source-only tests do not replace it.
|
||||
- `build/build-release.cjs` verifies the unpacked application, `app.asar`,
|
||||
bundled Continue and OpenCode runtimes, executable architecture, and package
|
||||
signatures before atomically replacing a release directory.
|
||||
@@ -211,12 +221,12 @@ surface together before a new release.
|
||||
It must be able to write immutable version objects and the final latest
|
||||
pointer without granting unrelated administration privileges.
|
||||
- Upload release assets and `site-release.json` under the immutable
|
||||
`releases/<tag>/` prefix first. Verify all 14 installer URLs publicly before
|
||||
`releases/<tag>/` prefix first. Verify all 12 installer URLs publicly before
|
||||
creating or publishing the GitHub Release. Update
|
||||
`releases/latest.json` only after the GitHub Release is public and all prior
|
||||
checks succeeded.
|
||||
- The expected GitHub Release contains 22 assets: 14 installers (two formats
|
||||
for each Windows/macOS target and three formats for each Linux target), six
|
||||
- The expected GitHub Release contains 20 assets: 12 installers (two formats
|
||||
for each Windows target, DMG for each macOS target, and three formats for each Linux target), six
|
||||
renamed target manifests, one aggregate `release-manifest.json`, and one `SHA256SUMS`.
|
||||
`site-release.json` is an OSS publication artifact, not a GitHub Release
|
||||
asset.
|
||||
@@ -269,17 +279,17 @@ Do not report a release complete until all of the following are verified:
|
||||
2. The public GitHub Release is non-draft, non-prerelease, marked Latest, and
|
||||
uses the expected tag and title. Its body must exactly match the Markdown
|
||||
generated from the approved packaged bilingual notes.
|
||||
3. GitHub release metadata contains exactly the expected 22 uploaded asset
|
||||
3. GitHub release metadata contains exactly the expected 20 uploaded asset
|
||||
names, including the expected Windows x64 and arm64 assets. Do not download,
|
||||
hash, launch, or send individual network probes to published installers
|
||||
unless the user explicitly requests that additional validation.
|
||||
4. The Beijing `releases/latest.json` returns HTTP 200, has the expected stable
|
||||
version, exact six targets and 14 installer entries, the trusted Beijing
|
||||
version, exact six targets and 12 installer entries, the trusted Beijing
|
||||
URLs, and the GitHub fallback URL. It must match the immutable
|
||||
`releases/<tag>/site-release.json`. Fetching and comparing these small JSON
|
||||
metadata files is sufficient; rely on the successful publication job for
|
||||
its per-asset public checks.
|
||||
5. The live website successfully fetches the index and produces the 14 correct
|
||||
5. The live website successfully fetches the index and produces the 12 correct
|
||||
platform/architecture/format links from that metadata.
|
||||
6. Both remote `main` refs and both peeled tag refs still equal the approved
|
||||
release commit, and the local working tree is clean.
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# 工作栏实现与验证进度
|
||||
|
||||
## 2026-09-08 Desktop 0.12.8 安装包修复
|
||||
|
||||
- 真实 `0.12.7` 安装目录确认 OpenCode 配置缺少整个 `node_modules`。新增测试调用实际
|
||||
electron-builder 资源复制器,复现相同 ENOENT;把复制源上移到 `.runtime-resources`
|
||||
后,插件及传递依赖逐文件复制验证通过。
|
||||
- 真实安装的 DSH bootstrap 存在 `dsh-session-projection` 外部导入,而 Host 解包目录没有
|
||||
对应依赖。将其加入 Vite bundling,配置回归从失败转为通过;最终包会拒绝同类外部导入。
|
||||
- Windows x64 CI 已接入包内 OpenCode 插件导入、真实 DSH UtilityProcess 握手及 npm
|
||||
探针,位于解包目录清理和发布之前。用户要求不做本地打包;CI 探针结果尚待本候选执行,
|
||||
不能把当前源码回归通过当作真实发布包已通过。
|
||||
- 本次不改变 Agent `0.11.21` 源码或包。macOS 改为 DMG-only,用户明确接受旧版客户端
|
||||
手动升级影响;格式与发布规则见[发布手册](../../development/release-runbook.md)。
|
||||
|
||||
## 2026-09-08 Execute 目录权限等待修复
|
||||
|
||||
- 本机 OpenCode 的内部配置显式允许默认工具权限;每次 Ask 仍设置 deny-all 会话规则并
|
||||
|
||||
@@ -141,6 +141,9 @@ Harness 子进程内控制面不能取代 Main 控制面,Main 控制面也不
|
||||
Harness Session 只在当前 Runtime 进程生命周期内存在。释放 GoodBuddy 会话时必须同步释放对应 Harness Agent。
|
||||
固定 Host 显式加载 `dsh-session-projection` 的内存 SessionProjectionRegistry,这是
|
||||
`0.1.2-rc.1` AgentLoop 的必需依赖;不加载 Session persistence,也不新增磁盘会话副本。
|
||||
该依赖与其余 DSH 依赖一起由 Vite 打入 Host,不能从 `app.asar.unpacked` 外部导入
|
||||
仅存在于 `app.asar` 的 npm 包。最终打包检查及 Windows CI 的真实 UtilityProcess
|
||||
启动探针见 [Runtime 资源与打包](../../../BUILD.md#runtime-资源)。
|
||||
|
||||
## 6. 总体架构
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Linux x64/arm64、取消和离线 GoodBuddy 传输的真实 Host 验收。
|
||||
Windows 到 Linux x64 的安装、Agent-owned Prompt、Agent 本地模型 gateway、断线恢复、
|
||||
同一 OpenCode Session 续接、取消和终态清理已经使用真实模型与工具验证。Agent
|
||||
`0.11.14` 已通过独立 workflow 发布 Linux x64/arm64 复合包和签名累计目录;当前未发布
|
||||
Agent 源码 lock 为 `0.11.21`,Desktop 候选为 `0.12.7`,等待各自独立发布审批。
|
||||
Agent 源码 lock 为已发布的 `0.11.21`,Desktop 候选为 `0.12.8`;本次桌面打包修复不重发 Agent。
|
||||
现有源码显示本地与远端 OpenCode 原生 Task,并取消 GoodBuddy 对生产 Prompt 的
|
||||
固定墙钟总时限。失败的 `agent-v0.11.3` 保持不可变且未发布。
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ export default defineConfig({
|
||||
'@deepseek-ai/dsh-sandbox',
|
||||
'@deepseek-ai/dsh-sandbox-policy',
|
||||
'@deepseek-ai/dsh-session',
|
||||
'@deepseek-ai/dsh-session-projection',
|
||||
'@deepseek-ai/dsh-shell-env',
|
||||
'@deepseek-ai/dsh-skill',
|
||||
'@deepseek-ai/dsh-subprocess-local',
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "goodbuddy",
|
||||
"version": "0.12.7",
|
||||
"version": "0.12.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "goodbuddy",
|
||||
"version": "0.12.7",
|
||||
"version": "0.12.8",
|
||||
"license": "0BSD",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.25.1",
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "goodbuddy",
|
||||
"version": "0.12.7",
|
||||
"version": "0.12.8",
|
||||
"private": true,
|
||||
"description": "Secure desktop AI workspace with controlled Agent Runtimes",
|
||||
"desktopName": "GoodBuddy",
|
||||
@@ -128,10 +128,10 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": ".runtime-resources/opencode-config",
|
||||
"to": "runtimes/opencode-config",
|
||||
"from": ".runtime-resources",
|
||||
"to": "runtimes",
|
||||
"filter": [
|
||||
"**/*"
|
||||
"opencode-config{,/**/*}"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,38 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"releases": [
|
||||
{
|
||||
"version": "0.12.8",
|
||||
"releasedAt": "2026-09-08",
|
||||
"notes": {
|
||||
"zh-CN": {
|
||||
"highlights": [],
|
||||
"features": [],
|
||||
"fixes": [
|
||||
"**修复 OpenCode 启动失败。** 安装包补齐离线配置依赖,避免因缺少 `@opencode-ai/plugin/package.json` 无法启动。升级后无需手动安装 Node.js 或 npm 依赖。",
|
||||
"**修复 DeepSeek Harness Host 启动退出。** 将必需的会话组件完整打入 Host,避免安装后提示“Host 启动前退出(code 1)”。现有模型配置保持不变。",
|
||||
"**macOS 仅提供 DMG。** Intel 与 Apple 芯片版本不再构建或发布 ZIP。Windows 和 Linux 的安装包格式保持不变。"
|
||||
],
|
||||
"notices": [
|
||||
"旧版 macOS 的 GitHub 更新检查,以及所有平台的旧版镜像更新检查可能无法识别新清单,请从官方下载页手动升级至 `0.12.8`。",
|
||||
"Agent 继续使用 `0.11.21`,本次不重发 Agent,也不构建 LoongArch 实验预览。"
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"highlights": [],
|
||||
"features": [],
|
||||
"fixes": [
|
||||
"**Fixed OpenCode startup failures.** Desktop packages now include the offline configuration dependencies, avoiding startup failures caused by a missing `@opencode-ai/plugin/package.json`. No manual Node.js or npm dependency installation is needed after upgrading.",
|
||||
"**Fixed DeepSeek Harness Host startup exits.** Required session components are bundled into the Host, preventing the “Host exited before startup (code 1)” error after installation. Existing model settings remain unchanged.",
|
||||
"**DMG-only macOS releases.** Intel and Apple silicon versions no longer build or publish ZIP packages. Windows and Linux package formats are unchanged."
|
||||
],
|
||||
"notices": [
|
||||
"Older macOS GitHub update checks and older mirror update checks on all platforms may reject the new manifest. Upgrade manually to `0.12.8` from the official download page.",
|
||||
"Agent remains at `0.11.21`; this release does not republish the Agent or include a LoongArch preview."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.12.7",
|
||||
"releasedAt": "2026-09-08",
|
||||
|
||||
+2
-2
@@ -81,7 +81,7 @@ https://github.com/mesalogo/goodbuddy/releases/latest
|
||||
|
||||
校验规则与桌面更新检查保持一致:索引只能指向稳定 SemVer 版本,必须恰好
|
||||
包含 Windows、macOS、Linux 的 x64 / arm64 六个匹配目标;每个目标必须提供
|
||||
准确的两种格式和扩展名、正的安全整数大小、64 字符小写十六进制 SHA-256、
|
||||
准确的格式和扩展名(Windows 两种、macOS 仅 DMG、Linux 三种),正的安全整数大小、64 字符小写十六进制 SHA-256、
|
||||
全局唯一文件名,以及位于
|
||||
`https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/v${version}/`
|
||||
下、对文件名进行 URL 编码的精确地址。校验清单和 GitHub 回退地址也必须
|
||||
@@ -90,7 +90,7 @@ https://github.com/mesalogo/goodbuddy/releases/latest
|
||||
安装包文件名必须与发布生成器完全一致:Windows 使用
|
||||
`GoodBuddy-${version}-windows-${arch}-setup.exe` 与
|
||||
`GoodBuddy-${version}-windows-${arch}-portable.zip`;macOS 使用
|
||||
`GoodBuddy-${version}-mac-${arch}.dmg|zip`;Linux x64 的 AppImage、DEB 与
|
||||
`GoodBuddy-${version}-mac-${arch}.dmg`;读取器仍接受历史 DMG/ZIP 清单,但当前页面不提供 macOS ZIP 选项。Linux x64 的 AppImage、DEB 与
|
||||
RPM 分别使用 electron-builder 的 `x86_64`、`amd64` 与 `x86_64` 架构名。
|
||||
Linux arm64 的 AppImage/DEB 使用 `arm64`,RPM 使用 `aarch64`。
|
||||
Linux 处理器选择器另外提供龙芯 `loong64` 实验预览;选中后安装包类型固定为
|
||||
|
||||
+1
-1
@@ -261,7 +261,7 @@
|
||||
<path d="M16.8 12.7c0-2.7 2.2-4 2.3-4.1A5 5 0 0 0 15.2 6c-1.7-.2-3.2 1-4.1 1-.9 0-2.2-1-3.6-1-1.8 0-3.5 1.1-4.5 2.7-2 3.5-.5 8.7 1.4 11.5.9 1.4 2 2.8 3.5 2.7 1.4 0 1.9-.9 3.7-.9 1.7 0 2.2.9 3.7.9s2.5-1.4 3.4-2.7a10 10 0 0 0 1.6-3.3 4.6 4.6 0 0 1-3.5-4.2ZM14.1 4.3A4.7 4.7 0 0 0 15.2 1a4.8 4.8 0 0 0-3.1 1.6A4.4 4.4 0 0 0 11 5.8c1.2.1 2.3-.5 3.1-1.5Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div><h3>macOS</h3><p>Apple silicon / Intel · DMG / ZIP</p></div>
|
||||
<div><h3>macOS</h3><p>Apple silicon / Intel · DMG</p></div>
|
||||
<a
|
||||
class="button button--download"
|
||||
href="https://github.com/mesalogo/goodbuddy/releases/latest"
|
||||
|
||||
+1
-2
@@ -285,7 +285,7 @@
|
||||
<path d="M16.8 12.7c0-2.7 2.2-4 2.3-4.1A5 5 0 0 0 15.2 6c-1.7-.2-3.2 1-4.1 1-.9 0-2.2-1-3.6-1-1.8 0-3.5 1.1-4.5 2.7-2 3.5-.5 8.7 1.4 11.5.9 1.4 2 2.8 3.5 2.7 1.4 0 1.9-.9 3.7-.9 1.7 0 2.2.9 3.7.9s2.5-1.4 3.4-2.7a10 10 0 0 0 1.6-3.3 4.6 4.6 0 0 1-3.5-4.2ZM14.1 4.3A4.7 4.7 0 0 0 15.2 1a4.8 4.8 0 0 0-3.1 1.6A4.4 4.4 0 0 0 11 5.8c1.2.1 2.3-.5 3.1-1.5Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div><h3>macOS</h3><p>Apple 芯片 / Intel · DMG / ZIP</p></div>
|
||||
<div><h3>macOS</h3><p>Apple 芯片 / Intel · DMG</p></div>
|
||||
<div class="download-options" data-download-card="macos">
|
||||
<label>
|
||||
<span>处理器</span>
|
||||
@@ -298,7 +298,6 @@
|
||||
<span>类型</span>
|
||||
<select data-download-format aria-label="macOS 安装包类型">
|
||||
<option value="dmg">DMG(推荐)</option>
|
||||
<option value="zip">ZIP</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -27,12 +27,12 @@
|
||||
"macos-x64": Object.freeze({
|
||||
platform: "macos",
|
||||
arch: "x64",
|
||||
formats: Object.freeze(["dmg", "zip"]),
|
||||
formats: Object.freeze(["dmg"]),
|
||||
}),
|
||||
"macos-arm64": Object.freeze({
|
||||
platform: "macos",
|
||||
arch: "arm64",
|
||||
formats: Object.freeze(["dmg", "zip"]),
|
||||
formats: Object.freeze(["dmg"]),
|
||||
}),
|
||||
"linux-x64": Object.freeze({
|
||||
platform: "linux",
|
||||
@@ -143,6 +143,9 @@
|
||||
for (const key of targetKeys) {
|
||||
const definition = targetDefinitions[key];
|
||||
const target = index.targets[key];
|
||||
const formats = definition.platform === "macos" && target?.files?.zip
|
||||
? ["dmg", "zip"]
|
||||
: definition.formats;
|
||||
assert(
|
||||
hasExactKeys(target, ["platform", "arch", "files"]) &&
|
||||
target.platform === definition.platform &&
|
||||
@@ -150,11 +153,11 @@
|
||||
`发布目标与键不匹配:${key}`,
|
||||
);
|
||||
assert(
|
||||
hasExactKeys(target.files, definition.formats),
|
||||
hasExactKeys(target.files, formats),
|
||||
`发布目标文件格式无效:${key}`,
|
||||
);
|
||||
|
||||
for (const format of definition.formats) {
|
||||
for (const format of formats) {
|
||||
const file = target.files[format];
|
||||
assert(
|
||||
hasExactKeys(file, ["name", "size", "sha256", "url"]),
|
||||
|
||||
@@ -128,6 +128,14 @@ test("accepts the canonical stable six-target release index", () => {
|
||||
assert.equal(validateReleaseIndex(index), index);
|
||||
});
|
||||
|
||||
test("accepts DMG-only releases with twelve installer entries", () => {
|
||||
const index = validIndex();
|
||||
delete index.targets["macos-x64"].files.zip;
|
||||
delete index.targets["macos-arm64"].files.zip;
|
||||
assert.equal(validateReleaseIndex(index), index);
|
||||
assert.equal(Object.values(index.targets).flatMap((target) => Object.values(target.files)).length, 12);
|
||||
});
|
||||
|
||||
test("rejects unstable or non-strict versions and extra top-level fields", () => {
|
||||
for (const version of ["v1.2.3", "01.2.3", "1.2", "1.2.3-rc.1"]) {
|
||||
expectRejected((index) => {
|
||||
|
||||
@@ -538,7 +538,7 @@ const expectedDownloadOptions = {
|
||||
},
|
||||
macos: {
|
||||
arches: ["arm64", "x64"],
|
||||
formats: ["dmg", "zip"],
|
||||
formats: ["dmg"],
|
||||
},
|
||||
linux: {
|
||||
arches: ["x64", "arm64", "loong64"],
|
||||
|
||||
@@ -99,6 +99,46 @@ function successfulFetch(): ReturnType<typeof vi.fn<typeof fetch>> {
|
||||
})
|
||||
}
|
||||
|
||||
describe('DMG-only release updates', () => {
|
||||
it.each([false, true])('accepts macOS GitHub releases (legacy ZIP: %s)', async (legacyZip) => {
|
||||
const formats = legacyZip ? ['dmg', 'zip'] : ['dmg']
|
||||
const macFiles = formats.map((format, index) => ({
|
||||
name: `GoodBuddy-${latestVersion}-mac-arm64.${format}`,
|
||||
size: 100 + index,
|
||||
sha256: 'a'.repeat(64)
|
||||
}))
|
||||
const manifest = manifestPayload({
|
||||
targets: [{
|
||||
platform: 'macos', arch: 'arm64', formats,
|
||||
manifest: 'release-manifest-macos-arm64.json', files: macFiles
|
||||
}],
|
||||
files: macFiles.map((file) => ({ ...file, platform: 'macos', arch: 'arm64' }))
|
||||
})
|
||||
const fetch = vi.fn<typeof globalThis.fetch>(async (input) =>
|
||||
jsonResponse(String(input) === GOODBUDDY_LATEST_RELEASE_API_URL
|
||||
? releasePayload() : manifest)
|
||||
)
|
||||
const result = await checkForUpdates({
|
||||
currentVersion: '1.0.0', platform: 'darwin', arch: 'arm64', fetch
|
||||
})
|
||||
expect(result.target.formats).toEqual(formats)
|
||||
})
|
||||
|
||||
it('accepts a DMG-only mirror for Windows and macOS clients', async () => {
|
||||
const index = mirrorIndexPayload()
|
||||
delete index.targets['macos-x64']!.files.zip
|
||||
delete index.targets['macos-arm64']!.files.zip
|
||||
for (const platform of ['win32', 'darwin'] as const) {
|
||||
const result = await checkMirrorForUpdates({
|
||||
currentVersion: '1.0.0', platform, arch: 'x64',
|
||||
fetch: vi.fn(async () => jsonResponse(index))
|
||||
})
|
||||
expect(result.updateAvailable).toBe(true)
|
||||
if (platform === 'darwin') expect(result.target.formats).toEqual(['dmg'])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
type MirrorTestFile = {
|
||||
name: string
|
||||
size: number
|
||||
|
||||
@@ -336,10 +336,16 @@ function isCanonicalReleaseAssetApiUrl(value: string): boolean {
|
||||
|
||||
const expectedFormats: Record<ReleasePlatform, string[]> = {
|
||||
windows: ['nsis', 'portable'],
|
||||
macos: ['dmg', 'zip'],
|
||||
macos: ['dmg'],
|
||||
linux: ['AppImage', 'deb', 'rpm']
|
||||
}
|
||||
|
||||
function formatsForTarget(platform: ReleasePlatform, includesZip: boolean): string[] {
|
||||
return platform === 'macos' && includesZip
|
||||
? ['dmg', 'zip']
|
||||
: expectedFormats[platform]
|
||||
}
|
||||
|
||||
function hasExpectedFileFormats(
|
||||
platform: ReleasePlatform,
|
||||
files: ReleaseFile[]
|
||||
@@ -352,7 +358,7 @@ function hasExpectedFileFormats(
|
||||
}
|
||||
const extensions =
|
||||
platform === 'macos'
|
||||
? ['.dmg', '.zip']
|
||||
? files.length === 2 ? ['.dmg', '.zip'] : ['.dmg']
|
||||
: ['.AppImage', '.deb', '.rpm']
|
||||
return extensions.every(
|
||||
(extension) =>
|
||||
@@ -418,7 +424,7 @@ function validateMirrorIndex(
|
||||
if (!target || `${target.platform}-${target.arch}` !== key) {
|
||||
throw new Error(`Mirror release target is invalid: ${key}`)
|
||||
}
|
||||
const formats = expectedFormats[target.platform]
|
||||
const formats = formatsForTarget(target.platform, Boolean(target.files.zip))
|
||||
if (
|
||||
Object.keys(target.files).length !== formats.length ||
|
||||
formats.some((format) => !target.files[format])
|
||||
@@ -454,7 +460,7 @@ function validateMirrorIndex(
|
||||
if (!target) {
|
||||
throw new Error(`Mirror release target is missing: ${platform}/${arch}`)
|
||||
}
|
||||
const formats = expectedFormats[platform]
|
||||
const formats = formatsForTarget(platform, Boolean(target.files.zip))
|
||||
return {
|
||||
platform,
|
||||
arch,
|
||||
@@ -487,7 +493,7 @@ function validateCurrentTarget(
|
||||
if (!target) {
|
||||
throw new Error('Release manifest target is missing')
|
||||
}
|
||||
const formats = expectedFormats[platform]
|
||||
const formats = formatsForTarget(platform, target.formats.includes('zip'))
|
||||
if (
|
||||
target.formats.length !== formats.length ||
|
||||
!formats.every((format, index) => target.formats[index] === format) ||
|
||||
|
||||
@@ -156,7 +156,7 @@ describe('release asset aggregation', () => {
|
||||
])
|
||||
|
||||
const outputNames = readdirSync(output)
|
||||
expect(outputNames).toHaveLength(22)
|
||||
expect(outputNames).toHaveLength(20)
|
||||
expect(outputNames).toContain('release-manifest.json')
|
||||
expect(outputNames).toContain('SHA256SUMS')
|
||||
expect(outputNames).not.toContain('builder-debug.yml')
|
||||
@@ -180,7 +180,7 @@ describe('release asset aggregation', () => {
|
||||
join(output, 'SHA256SUMS'),
|
||||
'utf8'
|
||||
)
|
||||
expect(sums.trim().split('\n')).toHaveLength(21)
|
||||
expect(sums.trim().split('\n')).toHaveLength(19)
|
||||
expect(sums).toContain(
|
||||
'release-manifest-windows-x64.json'
|
||||
)
|
||||
|
||||
@@ -235,6 +235,10 @@ function machO(cpuType: number): Buffer {
|
||||
|
||||
function portableDirectory(parent: string): string {
|
||||
const directory = join(parent, 'portable')
|
||||
const plugin = join(directory, 'resources', 'runtimes', 'opencode-config', 'node_modules', '@opencode-ai', 'plugin')
|
||||
mkdirSync(join(plugin, 'dist'), { recursive: true })
|
||||
writeFileSync(join(plugin, 'package.json'), '{"name":"@opencode-ai/plugin","version":"1.18.29"}')
|
||||
writeFileSync(join(plugin, 'dist', 'index.js'), 'export const tool = true')
|
||||
mkdirSync(
|
||||
join(directory, 'resources', 'runtimes', 'opencode'),
|
||||
{ recursive: true }
|
||||
@@ -319,7 +323,7 @@ function endOfCentralDirectory(
|
||||
describe('release build arguments', () => {
|
||||
it.each([
|
||||
['win32', 'x64', 'windows', ['nsis', 'portable']],
|
||||
['darwin', 'arm64', 'macos', ['dmg', 'zip']],
|
||||
['darwin', 'arm64', 'macos', ['dmg']],
|
||||
['linux', 'x64', 'linux', ['AppImage', 'deb', 'rpm']]
|
||||
])(
|
||||
'uses %s host defaults',
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// @vitest-environment node
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { Plugin, UserConfig } from 'vite'
|
||||
import config from '../electron.vite.config'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const packageJson = require('../package.json')
|
||||
const { getFileMatchers, copyFiles } = require('app-builder-lib/out/fileMatcher')
|
||||
const { verifyOpenCodeConfig, verifyHarnessBundleImports } = require('../build/verify-desktop-runtimes.cjs')
|
||||
|
||||
describe('desktop Runtime packaging', () => {
|
||||
it('runs packaged Windows Runtime checks in CI before removing the unpacked output', () => {
|
||||
const release = readFileSync(join(process.cwd(), 'build', 'build-release.cjs'), 'utf8')
|
||||
const smoke = readFileSync(join(process.cwd(), 'build', 'run-packaged-deepseek-harness-smoke.cjs'), 'utf8')
|
||||
expect(release).toContain("process.env.GITHUB_ACTIONS === 'true'")
|
||||
expect(release.indexOf('run-packaged-deepseek-harness-smoke.cjs'))
|
||||
.toBeLessThan(release.indexOf('rmSync(unpackedDirectory'))
|
||||
expect(smoke).toContain('Packaged OpenCode offline plugin import: ready')
|
||||
expect(smoke).toContain("result.status !== 'ready'")
|
||||
})
|
||||
|
||||
it('copies offline OpenCode dependencies through the real electron-builder resource copier', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'goodbuddy-desktop-runtime-copy-'))
|
||||
const output = join(root, 'output', 'resources')
|
||||
const source = join(root, '.runtime-resources', 'opencode-config')
|
||||
const files = {
|
||||
'package.json': '{"private":true}',
|
||||
'.gitignore': 'node_modules\n',
|
||||
'node_modules/@opencode-ai/plugin/package.json': '{"name":"@opencode-ai/plugin","version":"1.18.29"}',
|
||||
'node_modules/@opencode-ai/plugin/dist/index.js': 'export const tool = true\n',
|
||||
'node_modules/zod/package.json': '{"name":"zod","version":"4.1.8"}',
|
||||
'node_modules/zod/index.js': 'export const z = {}\n'
|
||||
}
|
||||
try {
|
||||
for (const [name, content] of Object.entries(files)) {
|
||||
const file = join(source, name)
|
||||
mkdirSync(dirname(file), { recursive: true })
|
||||
writeFileSync(file, content)
|
||||
}
|
||||
const resource = packageJson.build.extraResources.find(
|
||||
(entry: { from: string }) => entry.from.includes('.runtime-resources') &&
|
||||
JSON.stringify(entry).includes('opencode-config')
|
||||
)
|
||||
expect(resource).toBeDefined()
|
||||
const matchers = getFileMatchers(
|
||||
{ extraResources: [resource] }, 'extraResources', output,
|
||||
{ defaultSrc: root, globalOutDir: join(root, 'output'), customBuildOptions: {}, macroExpander: (value: string) => value }
|
||||
)
|
||||
await copyFiles(matchers)
|
||||
for (const [name, content] of Object.entries(files)) {
|
||||
expect(readFileSync(join(output, 'runtimes', 'opencode-config', name), 'utf8')).toBe(content)
|
||||
}
|
||||
expect(verifyOpenCodeConfig(output, source)).toBe(Object.keys(files).length)
|
||||
rmSync(join(output, 'runtimes', 'opencode-config', 'node_modules', 'zod', 'index.js'))
|
||||
expect(() => verifyOpenCodeConfig(output, source)).toThrow('dependency is missing or changed')
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('bundles every direct DeepSeek dependency into the unpacked Host instead of leaving ASAR-only imports', () => {
|
||||
const plugin = (config.main?.plugins as Plugin[]).find(
|
||||
(entry) => entry.name === 'vite:externalize-deps'
|
||||
)
|
||||
expect(plugin).toBeDefined()
|
||||
const buildConfig: UserConfig = {}
|
||||
const hook = plugin!.config
|
||||
if (typeof hook !== 'function') throw new Error('Expected externalization config hook')
|
||||
Reflect.apply(hook, {}, [buildConfig, { command: 'build', mode: 'production' }])
|
||||
const external = buildConfig.build?.rollupOptions?.external as Array<string | RegExp>
|
||||
for (const dependency of Object.keys(packageJson.dependencies).filter(
|
||||
(name) => name.startsWith('@deepseek-ai/')
|
||||
)) {
|
||||
expect(external.some((entry) => typeof entry === 'string'
|
||||
? entry === dependency
|
||||
: entry.test(dependency)), dependency).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
'import Registry from "@deepseek-ai/dsh-session-projection";',
|
||||
'export { Registry } from "@deepseek-ai/dsh-session-projection";',
|
||||
'await import("@deepseek-ai/dsh-session-projection");'
|
||||
])('rejects an external DeepSeek dependency in a shipped bootstrap or chunk', (source) => {
|
||||
expect(() => verifyHarnessBundleImports(source, 'host.js')).toThrow('external DeepSeek dependency')
|
||||
expect(() => verifyHarnessBundleImports('import { x } from "./chunks/host.js";', 'host.js')).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -50,6 +50,18 @@ function jobNeeds(name: string): string[] {
|
||||
}
|
||||
|
||||
describe('desktop packages workflow', () => {
|
||||
it('builds only DMG for both signed and unsigned macOS jobs', () => {
|
||||
const steps = parsed.jobs.package?.steps?.filter(
|
||||
(step) => step.name?.endsWith('macOS release packages') &&
|
||||
!step.name.includes('non-macOS')
|
||||
)
|
||||
expect(steps).toHaveLength(2)
|
||||
for (const step of steps ?? []) {
|
||||
expect(step.run).toContain('--format dmg')
|
||||
expect(step.run).not.toContain('--format dmg,zip')
|
||||
}
|
||||
})
|
||||
|
||||
it('uses valid YAML, pinned action majors, and only desktop release jobs', () => {
|
||||
expect(() => parse(workflow)).not.toThrow()
|
||||
expect(Object.keys(parsed.jobs)).toEqual([
|
||||
|
||||
@@ -46,8 +46,8 @@ const verifier = require(
|
||||
const targetDefinitions = [
|
||||
['windows', 'x64', ['nsis', 'portable']],
|
||||
['windows', 'arm64', ['nsis', 'portable']],
|
||||
['macos', 'x64', ['dmg', 'zip']],
|
||||
['macos', 'arm64', ['dmg', 'zip']],
|
||||
['macos', 'x64', ['dmg']],
|
||||
['macos', 'arm64', ['dmg']],
|
||||
['linux', 'x64', ['AppImage', 'deb', 'rpm']],
|
||||
['linux', 'arm64', ['AppImage', 'deb', 'rpm']]
|
||||
] as const
|
||||
@@ -145,7 +145,7 @@ describe('site release manifest', () => {
|
||||
|
||||
await expect(
|
||||
verifier.verifySiteRelease(manifest, request)
|
||||
).resolves.toBe(14)
|
||||
expect(request).toHaveBeenCalledTimes(14)
|
||||
).resolves.toBe(12)
|
||||
expect(request).toHaveBeenCalledTimes(12)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user