Archon 发布流水线二进制崩溃事故剖析:构建期常量绕过 `scripts/build-binaries.sh` 的根因、修复与验证
2026/9/13 14:20:00 网站建设 项目流程

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 是一份典型的"发布后才发现二进制不可用"的高危事故调查记录,调查结论可以用下表概括:

MetricValueReasoning
SeverityCRITICAL连续两个版本(v0.2.13、v0.3.0)发布的二进制在执行archon version时崩溃;用户无法运行已发布的 CLI,且没有临时绕行方案
ComplexityMEDIUM涉及 3 个文件(scripts/build-binaries.sh.github/workflows/release.ymltest-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_BINARY=true的地方。结果,发布的二进制烘焙进了开发模式的默认值(BUNDLED_IS_BINARY=falseBUNDLED_VERSION='dev'),运行时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(): Promise<void> { 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_VERSIONis_binary: BUNDLED_IS_BINARY(telemetry.ts),错误的常量会让遥测数据失真;
  • 更新检查packages/paths/src/update-check.ts注释明确"仅在BUNDLED_IS_BINARY为 true 时调用checkForUpdate"(update-check.ts),因为源码运行模式根本不需要远程版本比较;
  • doctor 命令packages/cli/src/commands/doctor.tscheckClaudeBinaryBUNDLED_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.yml(Build 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.yml
  • PR #962/#963此前用运行时探测修过同类 bug,因为不依赖构建脚本执行,恰好能在当时的发布工作流下工作;
  • 结论:这是一次不完整重构造成的回归——CI 路径在发布前从未被真正执行验证过,测试盲区直接转化为线上事故。

这个教训具有普适性:当"逻辑的唯一权威实现"与"实际执行路径"出现分裂时,两条路径必然逐步漂移,直到某次发布把漂移暴露给所有用户。


四、修复方案设计:五步收敛到单一构建入口

issue #986 的实现计划核心思想是让 CI 与本地开发共用同一个构建脚本,从制度上消除"内联命令与规范脚本漂移"的可能性。

Step 1:重构scripts/build-binaries.sh,支持单目标模式

文件scripts/build-binaries.sh(UPDATE)

要求改动:

  1. 接受TARGETOUTFILE环境变量:两者都设置 → 只构建该目标(CI 模式);两者都不设置 → 构建全部 4 个本地目标(保持原有本地开发行为不变);只设置其中一个 → 直接报错退出
  2. 始终传递--minify(与当时 CI 行为保持一致);
  3. Windows 目标跳过--bytecode(Bun 交叉编译存在不一致,匹配当时 CI 行为);
  4. 保留现有 EXIT trap,确保构建结束后恢复packages/paths/src/bundled-build.ts
  5. 保留最小体积校验(MIN_BINARY_SIZE=1000000);
  6. VERSION/GIT_COMMIT环境变量的优先级与默认值保持不变。

设计意图:单一权威构建入口,彻底消除本地开发与 CI 之间的行为漂移风险。

Step 2:修改release.yml改为调用脚本

文件.github/workflows/release.yml(UPDATE,对应原第 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 }} --outfile=dist/${{ matrix.binary }} packages/cli/src/cli.ts else bun build --compile --minify --bytecode --target=${{ matrix.target }} --outfile=dist/${{ 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.10.3.1)、commit 截取前 8 位,然后把全部构建逻辑(包括常量改写)委托给权威脚本。

Step 3:新增构建后冒烟测试

文件.github/workflows/release.yml(CREATE,新增在 "Build binary" 之后的步骤)

只在bun-linux-x64+ Linux runner 上运行(该类 bug 是跨平台的,单一目标即可捕获),断言三条:

  1. 输出中不包含Failed to read versionpackage.json not foundbad installation中的任何一个;
  2. 输出包含Build: binary
  3. 输出包含剥掉v前缀的标签版本号。

设计理由:v0.2.13 与 v0.3.0 的故障模式完全一致,这组断言在发布前就能同时拦住两者。

Step 4:更新test-releaseskill 文档

文件.claude/skills/test-release/SKILL.md(UPDATE)

新增"发布前 QA 的本地构建"小节,记录多目标与单目标两种模式的调用方式,让下一位贡献者在打 tag 之前就能在本地复现 CI 的构建路径。

Step 5:无需修改测试代码

构建脚本是 bash,没有单元测试,验证方式为手工执行(见下文 Validation 章节),无需新增 TypeScript 测试。


五、当前仓库中的落地实现:修复已生效

需要特别说明:截至本文写作,当前仓库源码中这套修复方案已经完整落地,且实现比 issue 计划更进一步。下面逐项对照,读者可以在仓库中直接验证。

5.1scripts/build-binaries.sh现状

scripts/build-binaries.sh 当前实现与计划完全一致,并增加了额外的加固:

  • 双模式入口:TARGET+OUTFILE同时设置走单目标 CI 模式(build-binaries.sh),只设置一个会明确报错退出;均未设置则构建bun-darwin-arm64bun-darwin-x64bun-linux-x64bun-linux-arm64四个本地目标到dist/binaries/
  • 构建前先执行bun run scripts/generate-bundled-defaults.ts重新生成 bundled defaults,保证编译进二进制的默认工作流与磁盘内容一致(build-binaries.sh);
  • EXIT trap 恢复机制(build-binaries.sh),即使构建中途失败也不会弄脏开发树,且用|| echo WARNING兜底防止恢复失败导致步骤失败;
  • 写入常量时额外嵌入BUNDLED_WEB_DIST_SHA256(web 前端产物 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 wrapper(build-binaries.sh);
  • 最小体积校验MIN_BINARY_SIZE=1000000(1MB,Bun 编译产物通常 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):
    1. 版本冒烟:断言无崩溃关键词、输出Build: binary、报告正确的标签版本(第 115-151 行);
    2. bundled defaults 加载冒烟:在临时 git 仓库中执行workflow list,断言能看到archon-assist等内置工作流(第 153-173 行);
    3. Claude 二进制解析器的负向用例:未设置CLAUDE_BIN_PATH时,错误信息必须是面向用户的Claude Code not found,绝不能出现泄漏 CI 主机路径的Module not found(第 175-211 行);
    4. Claude 子进程启动的正向用例:安装官方 CLI 后设置CLAUDE_BIN_PATH运行工作流,只要能证明子进程成功 spawn 即通过(第 213-256 行);
  • 发布矩阵为 5 个目标:bun-linux-x64bun-linux-arm64bun-windows-x64.exe)、bun-darwin-x64bun-darwin-arm64,与 issue 中"5 个目标全部同样损坏"的描述对应;
  • 发布前web-distjob 会以确定性方式打包 web 前端(--sort=name --owner=0 --group=0 --numeric-owner --mtime='@0'),保证同一源码重建出的 tarball 字节一致、SHA-256 可复现,这正是build-binaries.shWEB_DIST_SHA256的来源。

5.3test-releaseskill 现状

.claude/skills/test-release/SKILL.md 已包含"Local build for pre-release QA"小节,完整给出了两种模式的命令(SKILL.md):

# 多目标模式(构建 4 个本地平台到 dist/binaries/) VERSION=0.3.1 GIT_COMMIT=abc12345 bash scripts/build-binaries.sh # 单目标模式(匹配某一个 CI 矩阵任务) VERSION=0.3.1 \ GIT_COMMIT=abc12345 \ TARGET=bun-darwin-arm64 \ OUTFILE=dist/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_FILE="packages/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 2>&1; 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 fi

Bun 编译产物体积异常小,几乎必然是构建参数错误或产物被覆盖,体积校验是成本最低的兜底防线。


七、边界情况与风险缓解

issue 整理的风险矩阵值得在实施前逐条核对:

风险 / 边界情况缓解措施
CI 中 EXIT trap 恢复失败(tag checkout 后处于 detached HEAD)git 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 validate

8.2 本地手工验证(合并前)

  1. 向后兼容bash scripts/build-binaries.sh(不带环境变量),确认 4 个目标构建进dist/binaries/
  2. 单目标模式VERSION=0.3.1-test GIT_COMMIT=test1234 TARGET=bun-darwin-arm64 OUTFILE=/tmp/test-single-target bash scripts/build-binaries.sh,确认二进制存在;
  3. 构建期常量已嵌入/tmp/test-single-target version输出v0.3.1-testBuild: binaryGit commit: test1234
  4. EXIT trap 恢复git status packages/paths/src/bundled-build.ts显示干净;
  5. 错误处理:只设置TARGET(不设OUTFILE)运行,脚本以明确错误退出。

8.3 CI 验证(合并后)

  1. 通过workflow_dispatch用测试 tag(如v0.3.1-rc1)触发发布工作流;
  2. 确认bun-linux-x64的新冒烟测试步骤执行并通过;
  3. gh release view v0.3.1-rc1显示全部 5 个二进制 +checksums.txt
  4. 下载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.rbscripts/update-homebrew.sh仍在持续更新;
  • 遥测 /BUNDLED_POSTHOG_KEY(#980),属独立功能,会从本次重构自动受益(构建期常量机制天然可扩展新的内嵌值,当前脚本已新增BUNDLED_WEB_DIST_SHA256即为例证);
  • Windows / macOS 冒烟测试(Linux runner 上无法运行,单一目标已能捕获该类 bug);
  • 运行时探测回退(#982 已刻意移除,不要重新引入——这正是本次事故的诱因之一);
  • update-homebrewjob 结构(修复后原样可用)。

十、结语:从一次发布事故中沉淀的工程原则

issue #986 的完整调查与修复,为 Archon 乃至所有"构建脚本 + CI 内联命令并存"的项目沉淀了三条可迁移的原则:

  1. 单一权威入口:任何需要在编译期嵌入的元信息(版本、commit、二进制标志、资源校验和),其写入逻辑只能存在于一个脚本,CI 与本地开发都调用它,禁止在 YAML 中复制命令——复制即漂移,漂移即事故;
  2. 测试盲区 = 事故温床:v0.2.13 与 v0.3.0 连续两版损坏,根本原因是"这条代码路径从未被验证过"。发布流水线必须在发布前对产物做最小可用性断言(能执行、版本正确、类型正确),成本远低于一次全量用户的升级事故;
  3. 防御性脚本模式: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),仅供参考

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

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

立即咨询