
Archon 发布流水线二进制崩溃事故剖析构建期常量绕过scripts/build-binaries.sh的根因、修复与验证【免费下载链接】ArchonThe first open-source harness builder for AI coding. Make AI coding deterministic and repeatable.项目地址: https://gitcode.com/GitHub_Trending/archon3/Archon导读本文以 Archon 仓库中关于 issue #986 的完整调查记录为主线还原一次真实的发布事故由于发布流水线GitHub Actionsrelease.yml绕过唯一会改写构建期常量的脚本scripts/build-binaries.sh导致连续两个版本v0.2.13、v0.3.0发布的 CLI 二进制在运行archon version时直接崩溃所有用户无法使用发布的命令行工具。读完本文你将掌握 Archon 构建期常量bundled-build.ts的完整机制、bun build --compile与构建脚本之间的职责边界以及一套可复现的排查方法、单目标构建重构方案和发布前冒烟测试设计可直接迁移到任何基于 Bun 编译分发 CLI 的项目中。一、事故概览连续两个版本发布损坏的二进制issue #986 是一份典型的发布后才发现二进制不可用的高危事故调查记录调查结论可以用下表概括MetricValueReasoningSeverityCRITICAL连续两个版本v0.2.13、v0.3.0发布的二进制在执行archon version时崩溃用户无法运行已发布的 CLI且没有临时绕行方案ComplexityMEDIUM涉及 3 个文件scripts/build-binaries.sh、.github/workflows/release.yml、test-releaseskill存在中等程度的 bash/YAML 重构风险ConfidenceHIGH根因已被验证release.yml内联调用bun build --compile从未改写packages/paths/src/bundled-build.ts问题陈述发布工作流直接在 YAML 中内联执行bun build --compile绕过了scripts/build-binaries.sh——而该脚本是唯一会把packages/paths/src/bundled-build.ts改写为BUNDLED_IS_BINARYtrue的地方。结果发布的二进制烘焙进了开发模式的默认值BUNDLED_IS_BINARYfalse、BUNDLED_VERSIONdev运行时isBinaryBuild()判定失败archon version落入开发模式的package.json读取路径最终在 Bun 编译后不可访问的/$bunfs/虚拟文件系统上读取package.json失败抛出Failed to read version: package.json not found (bad installation?)该错误信息可以在当前仓库的 packages/cli/src/commands/version.ts 中逐字找到属于源码可验证的确定性行为。二、前置知识Archon 的构建期常量机制要理解这次事故必须先理解 Archon 如何区分编译后的二进制与开发模式源码运行。2.1 常量定义与依赖位置常量定义在packages/paths/src/bundled-build.ts开发模式下提交进仓库的默认值如下// packages/paths/src/bundled-build.ts:16-20提交版本 export const BUNDLED_IS_BINARY false; export const BUNDLED_VERSION dev; export const BUNDLED_GIT_COMMIT unknown; export const BUNDLED_WEB_DIST_SHA256 ;该文件位于archon/paths包——整个依赖图的底部任何包都可以 import 这些常量而不会产生依赖环packages/paths/src/index.ts 统一 re-export 了这 4 个常量。文件头部注释明确指出这些值是编译时被scripts/build-binaries.sh改写、编译完成后通过 EXIT trap 恢复的占位符禁止手工编辑。2.2archon version的双路径实现packages/cli/src/commands/version.ts 是事故的直接受害者其逻辑完全围绕BUNDLED_IS_BINARY分支export async function versionCommand(): Promisevoid { if (BUNDLED_IS_BINARY) { // 编译产物直接使用嵌入的版本号与 commit version BUNDLED_VERSION; gitCommit BUNDLED_GIT_COMMIT; } else { // 开发模式读取 package.json git rev-parse const devInfo await getDevVersion(); version devInfo.version; gitCommit await getDevGitCommit(); } const buildType BUNDLED_IS_BINARY ? binary : source (bun); console.log(Archon CLI v${version}); console.log( Build: ${buildType}); console.log( Git commit: ${gitCommit}); }关键在getDevVersion()它通过import.meta.url反推../../../../package.json的路径version.ts。这个路径在源码树里成立但在bun build --compile之后源码被封装进 Bun 的/$bunfs/虚拟文件系统package.json并不在其中于是抛出Failed to read version: package.json not found (bad installation?)。2.3 常量不止影响版本命令从源码检索结果看这 4 个常量被多个模块消费任何一个判定错误都会引发连锁故障遥测packages/paths/src/telemetry.ts上报archon_version: BUNDLED_VERSION与is_binary: BUNDLED_IS_BINARYtelemetry.ts错误的常量会让遥测数据失真更新检查packages/paths/src/update-check.ts注释明确仅在BUNDLED_IS_BINARY为 true 时调用checkForUpdateupdate-check.ts因为源码运行模式根本不需要远程版本比较doctor 命令packages/cli/src/commands/doctor.ts的checkClaudeBinary以BUNDLED_IS_BINARY作为默认参数doctor.ts二进制模式下才去解析宿主机的 Claude Code 二进制开发模式直接 skip。因此二进制判定是一类全局状态一处构建环节漏写会在版本、遥测、升级检查、诊断等多个入口同时出错。三、根因分析发布流水线绕过了唯一入口3.1 完整证据链issue #986 给出了从用户可见症状到根因的逐层证据链WHY: archon version 失败报 Failed to read version: package.json not found ↓ 因为: isBinaryBuild() 在已发布二进制中返回 false版本查询落入开发模式读取 package.json 的路径 ↓ 证据: packages/paths/src/bundled-build.ts:16 —— 提交的默认值是 export const BUNDLED_IS_BINARY false; ↓ 因为: CI 执行 bun build --compile 之前bundled-build.ts 从未被改写 ↓ 证据: .github/workflows/release.ymlBuild binary 步骤直接内联执行 bun build --compile │ 没有任何前置的常量改写步骤 ↓ 根因: 发布工作流没有调用 scripts/build-binaries.sh——它是构建期常量的唯一写入者 ↓ 证据: scripts/build-binaries.sh文件改写 EXIT trap 恢复逻辑只存在于此 │ 而 release.yml 中没有任何对该脚本的引用3.2 事件历史一次不完整的重构引入回归调查的 Git 历史部分给出了事故的来龙去脉PR #982引入了构建期常量方案取代了此前脆弱的运行时探测Bun 在 ESM/CJS 编译模式下行为不一致运行时启发式判定不可靠——这也是bundled-build.ts头注释提到的 issue #979 的动机但只接入了scripts/build-binaries.sh没有接入release.ymlPR #962/#963此前用运行时探测修过同类 bug因为不依赖构建脚本执行恰好能在当时的发布工作流下工作结论这是一次不完整重构造成的回归——CI 路径在发布前从未被真正执行验证过测试盲区直接转化为线上事故。这个教训具有普适性当逻辑的唯一权威实现与实际执行路径出现分裂时两条路径必然逐步漂移直到某次发布把漂移暴露给所有用户。四、修复方案设计五步收敛到单一构建入口issue #986 的实现计划核心思想是让 CI 与本地开发共用同一个构建脚本从制度上消除内联命令与规范脚本漂移的可能性。Step 1重构scripts/build-binaries.sh支持单目标模式文件scripts/build-binaries.shUPDATE要求改动接受TARGET与OUTFILE环境变量两者都设置 → 只构建该目标CI 模式两者都不设置 → 构建全部 4 个本地目标保持原有本地开发行为不变只设置其中一个 → 直接报错退出始终传递--minify与当时 CI 行为保持一致Windows 目标跳过--bytecodeBun 交叉编译存在不一致匹配当时 CI 行为保留现有 EXIT trap确保构建结束后恢复packages/paths/src/bundled-build.ts保留最小体积校验MIN_BINARY_SIZE1000000VERSION/GIT_COMMIT环境变量的优先级与默认值保持不变。设计意图单一权威构建入口彻底消除本地开发与 CI 之间的行为漂移风险。Step 2修改release.yml改为调用脚本文件.github/workflows/release.ymlUPDATE对应原第 51-59 行原代码问题代码- name: Build binary run: | mkdir -p dist # --bytecode excluded for Windows cross-compile (inconsistent Bun support) if [[ ${{ matrix.target }} *windows* ]]; then bun build --compile --minify --target${{ matrix.target }} --outfiledist/${{ matrix.binary }} packages/cli/src/cli.ts else bun build --compile --minify --bytecode --target${{ matrix.target }} --outfiledist/${{ matrix.binary }} packages/cli/src/cli.ts fi改为- name: Build binary env: VERSION: ${{ github.ref_name }} GIT_COMMIT: ${{ github.sha }} TARGET: ${{ matrix.target }} OUTFILE: dist/${{ matrix.binary }} run: | # Strip v prefix from tag (e.g. v0.3.1 → 0.3.1) VERSION${VERSION#v} # Short commit (first 8 chars of SHA) GIT_COMMIT${GIT_COMMIT::8} mkdir -p dist VERSION$VERSION GIT_COMMIT$GIT_COMMIT TARGET$TARGET OUTFILE$OUTFILE bash scripts/build-binaries.sh要点版本号剥v前缀v0.3.1→0.3.1、commit 截取前 8 位然后把全部构建逻辑包括常量改写委托给权威脚本。Step 3新增构建后冒烟测试文件.github/workflows/release.ymlCREATE新增在 Build binary 之后的步骤只在bun-linux-x64 Linux runner 上运行该类 bug 是跨平台的单一目标即可捕获断言三条输出中不包含Failed to read version、package.json not found、bad installation中的任何一个输出包含Build: binary输出包含剥掉v前缀的标签版本号。设计理由v0.2.13 与 v0.3.0 的故障模式完全一致这组断言在发布前就能同时拦住两者。Step 4更新test-releaseskill 文档文件.claude/skills/test-release/SKILL.mdUPDATE新增发布前 QA 的本地构建小节记录多目标与单目标两种模式的调用方式让下一位贡献者在打 tag 之前就能在本地复现 CI 的构建路径。Step 5无需修改测试代码构建脚本是 bash没有单元测试验证方式为手工执行见下文 Validation 章节无需新增 TypeScript 测试。五、当前仓库中的落地实现修复已生效需要特别说明截至本文写作当前仓库源码中这套修复方案已经完整落地且实现比 issue 计划更进一步。下面逐项对照读者可以在仓库中直接验证。5.1scripts/build-binaries.sh现状scripts/build-binaries.sh 当前实现与计划完全一致并增加了额外的加固双模式入口TARGETOUTFILE同时设置走单目标 CI 模式build-binaries.sh只设置一个会明确报错退出均未设置则构建bun-darwin-arm64、bun-darwin-x64、bun-linux-x64、bun-linux-arm64四个本地目标到dist/binaries/构建前先执行bun run scripts/generate-bundled-defaults.ts重新生成 bundled defaults保证编译进二进制的默认工作流与磁盘内容一致build-binaries.shEXIT trap 恢复机制build-binaries.sh即使构建中途失败也不会弄脏开发树且用|| echo WARNING兜底防止恢复失败导致步骤失败写入常量时额外嵌入BUNDLED_WEB_DIST_SHA256web 前端产物 tarball 的校验和并在 release/CI 模式下对缺失或非法校验和采取 fail-closed 策略build-binaries.sh——这关联到 issue 范围边界中提到的 telemetry/前端资源校验工作--minify恒开--bytecode当前完全禁用注释解释了原因Bun 1.3.11 对本仓库模块图涉及earendil-works/pi-coding-agent的 CJS/ESM 互操作形态会产生损坏的 bytecode运行时报TypeError: Expected CommonJS module to have a function wrapperbuild-binaries.sh最小体积校验MIN_BINARY_SIZE10000001MBBun 编译产物通常 50MB并使用stat -f%z/stat --printf双写法保证 macOS 与 Linux 均可运行build-binaries.sh。5.2release.yml现状.github/workflows/release.yml 当前已经Build binary 步骤第 99-113 行改为向bash scripts/build-binaries.sh传递VERSION/GIT_COMMIT/TARGET/OUTFILE并在脚本内完成VERSION${VERSION#v}与GIT_COMMIT${GIT_COMMIT::8}的处理VERSION的值在workflow_dispatch时回退到用户输入的version输入参数因为此时github.ref_name是分支名而非 tag冒烟测试远超 issue 计划中的一条共有四条均限定bun-linux-x64 Linux runner版本冒烟断言无崩溃关键词、输出Build: binary、报告正确的标签版本第 115-151 行bundled defaults 加载冒烟在临时 git 仓库中执行workflow list断言能看到archon-assist等内置工作流第 153-173 行Claude 二进制解析器的负向用例未设置CLAUDE_BIN_PATH时错误信息必须是面向用户的Claude Code not found绝不能出现泄漏 CI 主机路径的Module not found第 175-211 行Claude 子进程启动的正向用例安装官方 CLI 后设置CLAUDE_BIN_PATH运行工作流只要能证明子进程成功 spawn 即通过第 213-256 行发布矩阵为 5 个目标bun-linux-x64、bun-linux-arm64、bun-windows-x64.exe、bun-darwin-x64、bun-darwin-arm64与 issue 中5 个目标全部同样损坏的描述对应发布前web-distjob 会以确定性方式打包 web 前端--sortname --owner0 --group0 --numeric-owner --mtime0保证同一源码重建出的 tarball 字节一致、SHA-256 可复现这正是build-binaries.sh中WEB_DIST_SHA256的来源。5.3test-releaseskill 现状.claude/skills/test-release/SKILL.md 已包含Local build for pre-release QA小节完整给出了两种模式的命令SKILL.md# 多目标模式构建 4 个本地平台到 dist/binaries/ VERSION0.3.1 GIT_COMMITabc12345 bash scripts/build-binaries.sh # 单目标模式匹配某一个 CI 矩阵任务 VERSION0.3.1 \ GIT_COMMITabc12345 \ TARGETbun-darwin-arm64 \ OUTFILEdist/test-archon-darwin-arm64 \ bash scripts/build-binaries.sh # 验证二进制 ./dist/test-archon-darwin-arm64 version # 期望输出: Archon CLI v0.3.1, Build: binary, Git commit: abc12345该 skill 的 Phase 4 冒烟测试清单Test 1 版本报告 / Test 2 内置工作流加载 / Test 3 SDK 路径 / Test 5 隔离列表与release.yml中的 CI 冒烟测试形成互补CI 在 Linux 上覆盖skill 在 macOS / VPS / Homebrew 三条安装路径上覆盖真实用户环境。六、保留的既有模式重构必须继承的防御性写法issue 明确要求重构时保留脚本中两个经过验证的模式这两段代码在 scripts/build-binaries.sh 中均可找到模式一EXIT trap 恢复保证失败也不弄脏开发树# scripts/build-binaries.sh:33-34 BUNDLED_BUILD_FILEpackages/paths/src/bundled-build.ts trap echo Restoring ${BUNDLED_BUILD_FILE}...; git checkout -- ${BUNDLED_BUILD_FILE} || echo WARNING: failed to restore ... 2 EXIT这个模式的价值在于构建失败是常态依赖、网络、Bun 版本问题如果改写后的常量文件残留在工作树里下一次开发运行会误判为二进制模式trap 保证无论成功失败都恢复提交版本。模式二可移植的 stat 最小体积校验# scripts/build-binaries.sh:135-145原文案现行为相同 if stat -f%z $outfile /dev/null 21; then size$(stat -f%z $outfile) else size$(stat --printf%s $outfile) fi if [ $size -lt $MIN_BINARY_SIZE ]; then echo ERROR: Build output suspiciously small ($size bytes): $outfile 2 exit 1 fiBun 编译产物体积异常小几乎必然是构建参数错误或产物被覆盖体积校验是成本最低的兜底防线。七、边界情况与风险缓解issue 整理的风险矩阵值得在实施前逐条核对风险 / 边界情况缓解措施CI 中 EXIT trap 恢复失败tag checkout 后处于 detached HEADgit checkout -- file在 detached HEAD 上可用用\|\| true兜底避免构建成功后因恢复失败反而让步骤失败VERSION传给脚本时仍带v前缀在release.yml调用前先执行VERSION${VERSION#v}Windows 冒烟测试无法在 Linux runner 上运行冒烟测试限定bun-linux-x64该 bug 类别跨平台一个目标即可捕获只传TARGET或只传OUTFILE脚本在任何工作开始前明确报错退出本地无环境变量调用bash scripts/build-binaries.sh的向后兼容回退到多目标模式行为不变构建全部 4 个目标到dist/binaries/某个目标的--bytecode支持回归用*windows*模式逐目标匹配精确保持当时 CI 行为八、验证方案自动化、手工、CI 三层8.1 自动化检查# Shell 语法检查 bash -n scripts/build-binaries.sh # 工作流 YAML 有效性有 actionlint 用 actionlint否则用 yamllint actionlint .github/workflows/release.yml || yamllint .github/workflows/release.yml # 仓库整体验证 bun run validate8.2 本地手工验证合并前向后兼容bash scripts/build-binaries.sh不带环境变量确认 4 个目标构建进dist/binaries/单目标模式VERSION0.3.1-test GIT_COMMITtest1234 TARGETbun-darwin-arm64 OUTFILE/tmp/test-single-target bash scripts/build-binaries.sh确认二进制存在构建期常量已嵌入/tmp/test-single-target version输出v0.3.1-test、Build: binary、Git commit: test1234EXIT trap 恢复git status packages/paths/src/bundled-build.ts显示干净错误处理只设置TARGET不设OUTFILE运行脚本以明确错误退出。8.3 CI 验证合并后通过workflow_dispatch用测试 tag如v0.3.1-rc1触发发布工作流确认bun-linux-x64的新冒烟测试步骤执行并通过gh release view v0.3.1-rc1显示全部 5 个二进制 checksums.txt下载archon-darwin-arm64运行./archon-darwin-arm64 version必须报告 tag 版本与Build: binary。8.4 发布后验证/test-release curl-mac 0.3.1通过/test-release curl-linux 0.3.1通过。九、范围边界这次修复刻意不做什么issue 明确划定了本次修复的边界避免范围蔓延IN SCOPE重构scripts/build-binaries.sh支持单目标模式让release.yml调用该脚本为bun-linux-x64增加构建后冒烟测试在test-releaseskill 中记录本地构建的环境变量用法。OUT OF SCOPE刻意不碰Homebrew tap 同步缺口coleam00/homebrew-archonformula 仍停留在 v0.2.0另立 issue 跟踪——当前仓库的homebrew/archon.rb与scripts/update-homebrew.sh仍在持续更新遥测 /BUNDLED_POSTHOG_KEY#980属独立功能会从本次重构自动受益构建期常量机制天然可扩展新的内嵌值当前脚本已新增BUNDLED_WEB_DIST_SHA256即为例证Windows / macOS 冒烟测试Linux runner 上无法运行单一目标已能捕获该类 bug运行时探测回退#982 已刻意移除不要重新引入——这正是本次事故的诱因之一update-homebrewjob 结构修复后原样可用。十、结语从一次发布事故中沉淀的工程原则issue #986 的完整调查与修复为 Archon 乃至所有构建脚本 CI 内联命令并存的项目沉淀了三条可迁移的原则单一权威入口任何需要在编译期嵌入的元信息版本、commit、二进制标志、资源校验和其写入逻辑只能存在于一个脚本CI 与本地开发都调用它禁止在 YAML 中复制命令——复制即漂移漂移即事故测试盲区 事故温床v0.2.13 与 v0.3.0 连续两版损坏根本原因是这条代码路径从未被验证过。发布流水线必须在发布前对产物做最小可用性断言能执行、版本正确、类型正确成本远低于一次全量用户的升级事故防御性脚本模式EXIT trap 恢复构建期改写、可移植的 stat 体积校验、TARGET/OUTFILE成对校验——这些看似琐碎的细节正是把构建脚本从脆弱命令提升为可靠工程组件的关键。如果你正在维护一个用 Bun或任何编译器分发 CLI 的项目可以直接复用本文梳理的完整方案单一构建脚本、环境变量驱动的单目标模式、发布前冒烟测试以及那组价值千金的 EXIT trap 与体积校验。附本文涉及的仓库关键路径事故调查与修复计划.claude/PRPs/issues/completed/issue-986.md构建脚本唯一权威构建入口scripts/build-binaries.sh发布工作流.github/workflows/release.yml构建期常量定义packages/paths/src/bundled-build.ts常量测试packages/paths/src/bundled-build.test.ts版本命令实现packages/cli/src/commands/version.ts发布前 QA 技能文档.claude/skills/test-release/SKILL.md【免费下载链接】ArchonThe first open-source harness builder for AI coding. Make AI coding deterministic and repeatable.项目地址: https://gitcode.com/GitHub_Trending/archon3/Archon创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考