DeepChat CI 与发布打包契约:基于可复用工作流与 Fail-Closed 清单的六目标跨平台发布体系
2026/9/17 6:52:48 网站建设 项目流程

DeepChat CI 与发布打包契约:基于可复用工作流与 Fail-Closed 清单的六目标跨平台发布体系

【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat

本篇指南以 docs/architecture/ci-release-packaging/plan.md 为骨架,结合 spec.md 的验收标准与仓库内.github/workflows/scripts/ci/resources/的真实实现,完整剖析 DeepChat 如何用三个操作系统级可复用工作流统一 Build、Release 与 PR 打包门禁,如何用机器可校验的目标清单(manifest)与 19 资产发布契约做到"少一个文件就失败",以及如何用提交进仓库的安装包大小基线与策略替代"每次重跑历史基线"。读完你将掌握这套六目标(Windows/Linux/macOS × x64/ARM64)打包体系的拓扑设计、签名边界、分类门禁与回滚策略,并可直接对照仓库文件复现每一项检查。

1. 架构总览:薄调用者 + 三个 OS 级可复用工作流

在引入本方案之前,Build 与 Release 工作流重复维护六条原生打包路径,历史基线每个目标都要重建一次用于安装包大小检查,发布文件通过宽松的 glob 收集——缺失的目标或 updater payload 很容易被"宽容的 copy 命令"悄悄吞掉,而一次常规打包改动需要在多个工作流文件中平行修改。

新架构的核心原则是让三个操作系统可复用工作流成为原生打包的唯一所有者

  • .github/workflows/_package-windows.yml
  • .github/workflows/_package-linux.yml
  • .github/workflows/_package-macos.yml

build.ymlrelease.ymlpackage-regression.ymlpackage-check.yml一律退化为薄调用者:它们只负责传入不可变的源码 SHA 与架构矩阵,可复用工作流内部完成原生环境准备、打包与包级验证,确定性的 Node 脚本则负责文件发现、清单生成、updater 元数据、大小策略、影响分类与发布组装。

1.1 两类产物目的(artifact-purpose)

打包层把产物严格区分为两种用途(见 scripts/ci/package-contract.mjs 中SUPPORTED_ARTIFACT_PURPOSES = ['distribution', 'verification']):

用途行为调用方
distribution生成"可直接下载"的目标产物;macOS 上强制要求并验证签名与公证Build、Release
verification只在当前 runner 内部打包,强制执行 smoke 与大小策略,仅上传诊断内容,绝不分发未签名的安装包PR 打包检查、定时/手动打包回归

值得强调的是,验证模式刻意保留一个操作系统配置的完整目标集。CLI 虽然可以挑选单个 electron-builder 目标,但引入"仅 updater"的 PR 模式会产生第二套 manifest 与大小策略契约;因此延迟的降低靠"分类更少的变更、运行更少的操作系统"实现,而不是削弱受影响目标的打包覆盖。

1.2 内部产物目录布局

package-output/ manifest.json files/ metadata/ reports/

只有distribution模式的package-output/才包含可分发的文件;验证模式仅在本机保留 reports 与 manifest,并只上传诊断内容(对照 _package-windows.yml 中 "Upload verification diagnostics" 步骤的上传路径:package-output/manifest.jsonpackage-output/reports/与各 smoke 报告,设置retention-days: 7)。

2. 共享打包契约:六目标清单与文件角色的唯一事实源

scripts/ci/package-contract.mjs是整条打包链路的"宪法"。它集中定义六个目标 ID、允许架构、产物角色、原始 updater 元数据文件名与公共发布文件,其他脚本通过导入它而非各自维护扩展名 glob。

六个目标及其角色定义(源码见TARGET_DEFINITIONS,此处整理关键字段):

目标 ID平台架构安装器角色(measured/updaterPayload)其他角色原始 updater 元数据
win32-x64win32x64-windows-x64.exe(updater payload + 大小测量).exe.blockmap(sidecar)latest.yml
win32-arm64win32arm64-windows-arm64.exe.exe.blockmap(sidecar)latest.yml
linux-x64linuxx64-linux-x64.AppImage/-linux-x86_64.AppImage-linux-x64.tar.gz/-linux-x86_64.tar.gz(archive,measured)latest-linux.yml
linux-arm64linuxarm64-linux-arm64.AppImage/-linux-aarch64.AppImage-linux-arm64.tar.gz/-linux-aarch64.tar.gz(archive,measured)latest-linux-arm64.yml
darwin-x64darwinx64-mac-x64.dmg(installer,measured)-mac-x64.zip(updater payload)+.zip.blockmaplatest-mac.yml
darwin-arm64darwinarm64-mac-arm64.dmg(installer,measured)-mac-arm64.zip+.zip.blockmaplatest-mac.yml

注意角色的语义差异:

  • 每个目标的 updater payload恰好一个getUpdaterPayloadRole会强制校验);
  • sidecar(blockmap)通过sidecarFor关联其主角色;
  • latest.yml这类原始元数据角色public: false——它们是过程产物,不属于最终公共发布集;
  • 架构后缀存在兼容变体(如 Linux 的x86_64/aarch64),matchesRoleFileName按后缀集合匹配,并强制文件名必须是纯 basename(拒绝路径穿越)。

scripts/ci/package-manifest.mjs在此基础上完成:

  1. 为每个必需角色精确定位恰好一个文件
  2. 拒绝未知或不安全的条目(绝对路径、..穿越、符号链接、重复 basename、重复角色);
  3. 计算每个文件的字节数与 SHA-256;
  4. 校验 electron-builder 原始元数据;
  5. 暂存自包含的目标产物;
  6. 检查结果全部来自已完成工作流的证据(如 macOS 的cuaMacHelperDistributionmacAppDistributionmacZipDistributionmacDmgDistribution四个分发检查,见DARWIN_DISTRIBUTION_CHECK_NAMES);
  7. macOS distribution 模式还会先调用既有的应用与 DMG 验证辅助脚本,再记录分发状态——分发状态由真实验证命令推导,而非调用方传入的布尔值

2.1 契约中的关键校验模式

package-contract.mjs定义了三个输入模式,三个可复用工作流在安装依赖前就用它们校验输入:

  • SOURCE_SHA_PATTERN = /^[a-f0-9]{40}$/:不可变源码 SHA 必须是 40 位小写十六进制 Git SHA;
  • SHA256_PATTERN = /^[a-f0-9]{64}$/
  • SHA512_BASE64_PATTERN = /^[A-Za-z0-9+/]{86}==$/:updater 元数据的 SHA-512 base64 格式(86 字符 +==结尾)。

3. 安装包大小策略:提交进仓库的基线与策略,替代历史基线重建

3.1 基线文件的职责切分

旧方案中每次打包都要重跑一次历史 commit 来生成对比基线,成本高昂。新方案把事实与策略拆成两个文件:

  • resources/package-size-baseline.json:记录选定包角色在成功的 Actions run29978292769(源码 commitdfb4ba0f34c008c27cfb6bd98a08fdbd36f7b343,版本1.1.0-beta.4)上的字节数与 SHA-256。例如win32-x64DeepChat-1.1.0-beta.4-windows-x64.exe为 314,990,972 字节;Linux 同时测量installerarchive两个角色;
  • resources/package-size-policy.json:策略文件。当前实现中六个目标的所有测量角色统一使用maxGrowthBytesmaxShrinkBytes各 90 MiB(94,371,840 字节)的上下界,并带有一个expectedDelta:针对 commitdfb4ba0f34c008c27cfb6bd98a08fdbd36f7b343期望收缩-52428800字节(即 -50 MiB,对应PACKAGE_SIZE_TRANSIENT_DELTA中的stop-shipping-bundled-node说明)。

createDefaultPackageSizePolicy()展示了默认策略的生成方式:每个目标的每个 measured 角色都获得一对maxGrowthBytes/maxShrinkBytes,默认值来自DEFAULT_INSTALLER_DELTA_BYTES = 90 * MIB

3.2 基线导入与对比命令

  • baseline-import 命令:先校验六个目标目录中的每一个 measured 角色都恰好有一个候选文件,再写入可审查的 JSON——防止"缺文件也被写进基线";
  • compare 命令:按target + role匹配而非按带版本号的文件名匹配(避免文件名变化导致误报);无论策略是否通过,始终输出报告。工作流中的实际调用形如:
node scripts/ci/check-package-size.mjs compare \ --target "darwin-${TARGET_ARCH}" \ --candidate-dir dist \ --candidate-commit "${SOURCE_SHA}" \ --report "dist/package-size-darwin-${TARGET_ARCH}.json"

生成的报告随后通过--installer-size-report传给package-manifest.mjs,成为 manifest 证据的一部分。验证模式下的 macOS 未签名包与签名分发包体积可能略有差异,90 MiB 的宽松上下界正是为了容纳签名开销、同时仍能捕获实质性的内容缺失。

4. 三个可复用工作流:共同行为与平台差异

三个工作流的共同行为高度一致(可对照 _package-windows.yml、_package-linux.yml、_package-macos.yml):

  1. 输入校验前置:在安装依赖前用package-contract.mjs的校验函数验证source-shaartifact-purpose、目标平台/架构;macOS 工作流还会在此时完成签名凭证的存在性/缺失性检查(见下);
  2. 冻结安装pnpm install --frozen-lockfile
  3. Sharp 目标配置 + 第二次冻结安装pnpm run install:sharp(带TARGET_OS/TARGET_ARCH)后再次pnpm install --frozen-lockfile(Windows 还额外设置npm_config_build_from_source=truenpm_config_platform=win32npm_config_arch);
  4. 运行时安装installRuntime:<os>:<arch>(依赖RTK_GITHUB_TOKENGITHUB_TOKEN)、DuckDB VSS 安装与 smoke(installRuntime:duckdb:vss+smoke:duckdb:vss)、OpenDAL native smoke(smoke:opendal:native);
  5. 源码构建与目标打包pnpm run buildplugin:bundle(cua、feishu 两个插件)→electron-builder --<os> --<arch> --publish=never
  6. 打包后 smoke 测试:DuckDB VSS 扩展、OpenDAL、Light OCR(离线 + 性能)、plugin:verify两个插件;
  7. 清单生成可选大小对比enforce-installer-size: true时)。

工作流接口均显式声明:4 个输入(source-shaarchartifact-purposeenforce-installer-size),permissions: contents: read只读,persist-credentials: false,Action 全部固定到不可变 commit SHA,且设有有界超时(Windows 75 分钟、macOS 90 分钟)。

4.1 macOS:签名边界与分发验证

_package-macos.yml 是签名边界最严苛的一个。它在"Validate package request and signing inputs"步骤做了双向强制

  • distribution模式:CSC_LINKCSC_KEY_PASSWORDDEEPCHAT_APPLE_NOTARY_USERNAMEDEEPCHAT_APPLE_NOTARY_TEAM_IDDEEPCHAT_APPLE_NOTARY_PASSWORD全部必须存在,缺一个立即失败——在昂贵的打包工作开始之前;
  • verification模式:上述凭证一个都不能出现,出现即失败;同时打包阶段CSC_IDENTITY_AUTO_DISCOVERY: false关闭证书自动发现,并用unset CSC_LINK CSC_KEY_PASSWORD ...确保环境干净。

打包后的 macOS 验证链路覆盖签名与公证全链条:codesignstaplerspctlsyspolicy_check distribution)与 DMG 完整性检查,且分别作用于暂存应用从 updater ZIP 解压出的真实消费负载(ZIP 的根必须是DeepChat.app)。嵌套的 CUA helper 保留其专用暂存签名,要求 Developer ID 权威、期望的 Team ID、hardened runtime、安全时间戳、精确的 entitlements 白名单与允许的 Mach-O 加载路径。Light OCR 离线 smoke 用sandbox-exec -p '(version 1) (allow default) (deny network*)'实现网络隔离。

4.2 Windows:网络隔离与双插件验证

_package-windows.yml 的 Light OCR 离线 smoke 在 PowerShell 中先用New-NetFirewallRule为 runner 的 Node 24.18.0 可执行文件创建出站拦截规则(规则名带[guid]::NewGuid()避免冲突),验证版本与resources/runtime-versions.json一致后运行 smoke,最后在finally中删除规则。两个插件(cua、feishu)都执行plugin:verify。Windows 使用windows-2025-vs2026(x64)与windows-11-arm(arm64)runner。

4.3 Linux:网络命名空间与 ARM64 上的 CUA 省略

Linux 使用网络命名空间隔离 Light OCR,验证 OpenDAL;ARM64 目标明确省略 CUA(CUA 仅覆盖 x64)。Linux 同时产出 AppImage(installer)与 tar.gz(archive)两个 measured 角色。

4.4 产物上传约定

  • distribution 产物使用唯一命名deepchat-package-<platform>-<arch>(对应各目标定义中的artifactName,如deepchat-package-darwin-x64),if-no-files-found: error强制失败,compression-level: 0关闭冗余压缩;
  • verification 仅上传deepchat-package-diagnostics-<platform>-<arch>诊断包(含 manifest、reports、smoke 报告、大小报告),7 天保留。

5. 调用方:Build、包回归与 PR 门禁

5.1 build.yml 与 release.yml:distribution 调用方

Build 与 Release 均以distribution调用三套 OS 矩阵;Apple 签名凭证只传给 macOS 调用方。Release 的完整拓扑见 release.yml:preflight(校验 tag、main 祖先关系、版本、CHANGELOG)→ 三个package-*矩阵 →assemblepublish

5.2 package-regression.yml:全量六目标安全网

_package-regression.yml 同时支持三种触发方式:

  • workflow_call:接收source-sha输入;
  • workflow_dispatch:手动触发;
  • schedule:每日 18:37 UTC(cron: '37 18 * * *')。

它总是以verification覆盖全部六个目标并强制安装包大小门禁,只传运行时 token 与既有非签名构建配置;它不是嵌套在快速 PR 工作流内部,而是独立的完整回归套件。定时失败共享一个 GitHub issue(标题[CI] Scheduled package regression is failing),重复失败更新该 issue,恢复后由scheduled-status任务关闭(gh issue close --reason completed)。

5.3 prcheck.yml:保持快速门禁独立

_package-prcheck.yml 只保留针对dev分支 PR 的静态检查、主进程/渲染进程完整测试、Native Memory 验证、源码构建与稳定的pr-required聚合任务——原生打包分类与回归从其中移除,因此pr-required在快速代码质量检查完成后即可上报结果,不受原生打包延迟影响。

5.4 package-check.yml:始终启动的 PR 打包门禁

_package-package-check.yml 是针对dev分支 PR 的独立、总是启动的工作流,其流程:

  1. 检出完整历史并校验精确的 base/head commit 对git cat-file -e "${BASE_SHA}^{commit}"等三重校验),计算merge-base
  2. 只从 base 版本加载分类器git cat-file -e "${BASE_SHA}:scripts/ci/classify-package-impact.mjs"。若一次性的契约引导期间 base 不存在分类器,则校验两份package.json快照(必须是合法 JSON 对象)并保守地选择全部六个目标,绝不执行候选分类器代码——防止"分类器自身改动削弱自己的门禁";
  3. 将变更路径分类为 Windows/Linux/macOS 独立决策并携带规则证据;
  4. 对每个受影响的 OS 调用两个架构的完整verification打包 + 安装包大小门禁;
  5. package-requiredalways()运行,要求被选中的 OS 任务成功、未被选中的任务必须为 skippedrequire_platform_resultfalse分支检查result != "skipped"即失败),同时对输出缺失(${required:-missing})失败关闭。

关键设计:该工作流故意不使用paths/paths-ignore触发器——GitHub 可能让被跳过的必需工作流状态保持 pending,而"始终存在"的聚合既能代表被选中的原生任务、也能代表被有意跳过的任务,从而安全地作为 required check 配置。

5.5 分类器规则(显式且有序)

分类规则明确且有序(与prcheck.yml协作):

  • 选择全部 OS:共享 builder 配置、原生依赖 manifest、运行时安装器、插件、包 smoke、manifest/大小工具;
  • 选择全部 OS(package.json 语义比较):仅生产依赖、Electron 工具链、包元数据、lifecycle、build、runtime、plugin、package-smoke 变更被视为相关;test-only 与无关的开发工具变更不算;lockfile 变更保守地视为共享;
  • 只选一个 OS_package-<os>.yml、平台签名/安装器文件、平台图标;
  • 不选任何 OS:发布 preflight/组装、无关工作流、生成的 provider/ACP 注册表、普通应用代码、文档。

分类器自身的路径在 base 版本下选择全部 OS;package.json的 CUA Mach-O 契约、final-helper 验证器与 helper 签名路径属于 macOS 自有打包输入,因此触发 macOS 双架构。输出 key 保持向后兼容,破坏性 schema 变更需要两个 PR 分步迁移(因为分类由 base 版本拥有)。

6. 发布组装:从六份 distribution 清单到 19 资产契约

6.1 Preflight:一切前置

release.ymlpreflight任务在调用任何打包矩阵前完成全部合法性检查:tag 存在且可解析(git rev-parse --verify "refs/tags/${tag}^{commit}")、必须是来自origin/main可达的 commitgit merge-base --is-ancestor)、与package.json版本一致、CHANGELOG 中有非空匹配小节,并生成release-context.jsonrelease-notes.md。tag 用git check-ref-format校验合法性,且绝不回退到 workflow context SHA

6.2 assemble-release.mjs:七步组装

六个 distribution 产物全部下载后,scripts/ci/assemble-release.mjs 依次执行:

  1. 校验目标完整性、源码身份、用途、检查、路径、大小与 SHA-256;
  2. 重算并校验原始 updater 元数据的 SHA-512 与大小;
  3. 只把当前公共契约复制进干净的暂存目录;
  4. 用 electron-updater 架构选择语义合并 Windows 与 macOS 元数据(x64 在前,legacypath/sha512指向 x64);
  5. 保留 Linux 各自的架构元数据(latest-linux.ymllatest-linux-arm64.yml);
  6. 写出release-index.json,包含目标证据与其余 18 个资产的 SHA-256;
  7. 验证最终暂存目录恰好包含 19 个允许的资产

19 个资产 = 六目标的公共角色数(expectedReleaseAssetCount()packageAssetCount + 4 + 1:4 为预发布产物,1 为release-index.json)。publish任务是唯一获得contents: write的作业:上传前 scripts/ci/verify-release-assets.mjs 本地复验索引与全部 19 个文件;若已存在草稿,先校验其资产名是否全部在契约内,上传后要求 GitHub 资产数恰好 19 个且 API 报告的 size 与 SHA-256 与本地契约一致。

6.3 Run ID + Run Attempt 双重绑定

manifest 同时绑定GITHUB_RUN_IDGITHUB_RUN_ATTEMPT。部分重跑若混入了不同 attempt 的成功产物会失败关闭;发布重试必须重跑全部任务,避免"一半新一半旧"的混合发布。

7. 测试与验证:fail-closed 边界的全覆盖

测试策略分为三层:

  • Vitest 契约测试:覆盖成功路径与每个 fail-closed 边界——缺失目标或角色、不安全路径、重复项、摘要变化、无效 updater 引用、错误的架构元数据、Release 中出现 verification manifest、缺失 macOS 分发证据、超出包大小限制;
  • 解析后 YAML 工作流测试:覆盖可复用输入、runner 映射、权限、显式 secret 传递、固定 Action、环境声明、distribution/verification 行为、artifact 上传安全、独立 PR 聚合、package-impact 成功/跳过组合、发布 preflight 顺序;
  • 分类器测试:共享/平台专属/忽略路径、畸形输入、NUL 分隔 CLI 输入、证据输出、base 拥有的执行;另有已安装 electron-updater 的架构选择兼容性测试,确保依赖升级不能悄悄破坏组装后的 Windows/macOS/Linux 元数据约定。

任务记录(tasks.md)给出了实测证据:聚焦的包/工作流/updater 契约共 6 个文件 51 个测试通过;本地主套件 425 个文件 4,889 个测试通过(目标分支合并 ref 在 Actions run30013052661上完整通过test-main);渲染套件 197 个文件 1,561 个测试通过;actionlint1.7.12 接受了prcheck.ymlpackage-check.yml六个 GitHub 托管原生 runner 上的 verification 模式打包已全部通过;真实分发模式的 Apple 签名/公证与草稿发布仍需一次发布或手动 Build 运行来验证——这是文档明确标注的待办项,不是已证实的能力。

8. 回滚与演进安全

实现按文档、确定性工具、可复用打包工作流、PR 门禁集成、发布组装分阶段提交,其中PR 门禁可独立回滚:删除package-check.yml并在prcheck.yml中恢复旧的分类调用,不会改变 Build 或 Release 行为;基线与工具提交不改变应用运行时行为,可在工作流回滚期间保持惰性。

两个 schema 版本(包 manifest v2、release index v2)会拒绝 v1 的生产者与消费者;由于发布任务全部从同一个不可变源码 SHA 执行,不存在混合版本迁移路径——这既是约束也是安全保证。19 资产白名单同样刻意拒绝新包格式,直到契约、测试与 release index 同步更新。

9. 实战启示:把"宽容发布"改造成"可验证契约"

从这份实现中可以提炼出对任何 Electron 桌面项目都适用的经验:

  1. 用"角色 + 后缀"而非 glob 定义产物:glob 会让缺失悄然通过,角色契约会让"该角色的文件不存在"变成硬失败;
  2. 证据必须来自命令而非调用方:macOS 分发状态由codesign/spctl的实际执行结果推导,杜绝布尔值谎言;
  3. 基线与策略分文件、提交进仓库:基线是历史事实(run id + commit + digest),策略是当前约束(上下界),两者分开才可独立演进;
  4. 分类器由 base 版本拥有:防止 PR 用候选代码削弱自己的门禁;引导期缺分类器就保守全选;
  5. 必需的聚合任务用always()+ 显式 skip 校验:绕开"被跳过的必需状态会 pending"的 GitHub 行为陷阱;
  6. secret 在可复用边界显式传递secrets: inherit缺席,每个调用方白名单式传入,PR/回归路径从结构上拿不到 Apple 凭证。

如需进一步深入,可继续阅读 spec.md(验收标准 AC-1~AC-7 与约束、非目标)、tasks.md(执行进度与验证证据)、scripts/ci/package-contract.mjs(契约定义)与 resources/package-size-baseline.json(实测基线数据)。

【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询