关于 release
面向 Archon 项目维护者的发布技能,负责比较 dev 与 main、整理变更日志、更新版本并创建 PR。发布完成后还会同步 Homebrew 公式及校验和,并验证端到端安装流程。
主要功能
- 01检查分支与工作区状态
- 02执行发布前二进制冒烟测试
- 03按提交和差异整理变更日志
- 04按规则提升项目版本
- 05更新版本文件与锁文件
- 06同步 Homebrew 公式并验证安装
适用场景
- 01准备项目版本发布
- 02发现 dev 与 main 的分支漂移
- 03生成并审核版本变更日志
- 04发布后更新 Homebrew 校验和
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ coleam00/ archon --skill release安装后在 Claude Code 中输入/release确认已加载。
详细介绍
Release Skill
通过比较 dev 和 main、根据提交生成变更日志条目、更新版本号并创建 PR 来创建一次发布。标签推送后,发布工作流完成二进制文件构建时,使用已发布的 checksums.txt 中的实际 SHA256 值更新 Homebrew 配方,同步到 coleam00/homebrew-archon tap,并通过 /test-release 验证端到端安装路径。
⚠️ 严重 — 必须等到发布工作流构建完二进制文件后,才能确定 Homebrew 配方的 SHA 值。
homebrew/archon.rb中的version字段和sha256字段必须同时更新。绝不能只更新其中一个。正确顺序如下:
- 推送标签 → 发布工作流启动 → 构建二进制文件 → 上传
checksums.txt- 从已发布的版本中获取
checksums.txt- 解析各个平台对应的 SHA256 值
- 在同一个提交中,用新版本号和新 SHA 值更新
homebrew/archon.rb- 同步到
coleam00/homebrew-archon/Formula/archon.rbtap 仓库如果只更新配方的
version字段而不同时更新sha256值,就会产生一个过时且具有误导性的配方:它看起来有效,但安装时会出现校验和不匹配。之前发生过这种情况(v0.3.0:版本号已更新为 0.3.0,但 SHA 值仍是 v0.2.13 的)。务必两个都更新,否则就都不更新。
流程
第 1 步:验证状态
# Must be on dev branch with clean working tree
git checkout dev
git pull origin dev --no-rebase
git status --porcelain # must be empty
git fetch origin main
如果当前不在 dev 分支上,或工作树不干净,则中止并清楚说明原因。
接着检查 dev 是否确实包含 main。 任何直接提交到 main 的内容——文档中的拼写错误修复、热修复、上一次发布的配方提交——都会 一直滞留在那里,直到有人将它合并回来。这样一来,发布 PR 就会提议 撤销这些更改,而下一次发布则会继承这种分歧。
if git merge-base --is-ancestor origin/main origin/dev; then
echo "dev contains main — OK"
else
echo "DRIFT: commits exist on main that dev does not have:"
git log origin/dev..origin/main --oneline
echo ""
echo "Resync before releasing:"
echo " git checkout dev && git pull origin main --no-rebase && git push origin dev"
exit 1
fi
在 0.7.1 发布时就出现过这种情况:ae704a73 Update docs.mdx (#2155) 在
0.7.0 发布后直接提交到了 main,且从未合并回来。有人发现了内容上的差异,并
通过单独的 PR (#2403) 手动将更改正向移植,因此 dev 中有这些更改,却没有对应的提交——而且 main
仍不是 dev 的祖先。请在第 2 步之前解决此问题;不要将其带入发布 PR。
第 1.5 步:编译后二进制文件冒烟测试(执行任何其他步骤前必须完成)
为何首先执行:曾有发布因为模块初始化崩溃或打包器错误,导致没有任何可用二进制文件;这类问题只会在
bun build --compile的输出中暴露,在bun run中不会出现。CI 会捕获此问题——但要等到标签已推送且 GitHub Release 已创建之后才行。到那时,损害(空发布、损坏的releases/latest、失效的install.sh)已经发生并公开可见。在此处失败、避免任何用户可见的更改,可将影响范围控制在“未创建发布”。
在原生目标平台上本地运行。此操作约需 15-30s,比推送标签并创建发布后再发现问题更省事。
# Guard: only run this for Node/Bun projects with a CLI entry point + build-binaries script.
if [ -f scripts/build-binaries.sh ] && [ -f packages/cli/src/cli.ts ]; then
TMP_BINARY=$(mktemp)
trap "rm -f $TMP_BINARY" EXIT
# Compile for the native target only (not full cross-compile — that's CI's job).
# Match the real release flags so any bundler quirk reproduces locally.
bun build \
--compile \
--minify \
--target=bun \
--outfile="$TMP_BINARY" \
packages/cli/src/cli.ts
# Smoke test: the binary must start and exit 0 on a safe, non-interactive command.
# Use `--help` (NOT `version`). The `version` command's compiled-binary code
# path depends on BUNDLED_IS_BINARY=true, which is set by scripts/build-binaries.sh
# — but we're doing a bare `bun build --compile` here to keep the smoke fast,
# so BUNDLED_IS_BINARY is still `false`. That sends `version` down the dev
# branch of version.ts which tries to read package.json from a path that only
# exists in node_modules, producing a false-positive ENOENT. `--help` has no
# such dev/binary branch and exercises the same module-init graph we're
# actually testing. Must NOT touch network, database, or require env vars.
if ! "$TMP_BINARY" --help > /tmp/archon-preflight.log 2>&1; then
echo "ERROR: compiled binary crashed at startup"
cat /tmp/archon-preflight.log
echo ""
echo "This usually means a dependency has a module-init-time side effect that"
echo "fails in a compiled binary context (readFileSync of a path that only"
echo "exists in node_modules, etc.). Fix before cutting the release — do NOT"
echo "proceed to version bump."
exit 1
fi
# Also grep for known crash markers that exit 0 but print a fatal error
# (some module-init errors are caught by top-level try/catch but still log).
if grep -qE "Expected CommonJS module|TypeError:|ReferenceError:|SyntaxError:" /tmp/archon-preflight.log; then
echo "ERROR: compiled binary emitted a runtime error despite exit 0"
cat /tmp/archon-preflight.log
exit 1
fi
echo "Pre-flight binary smoke: PASSED"
fi
如果此步骤失败,请彻底中止发布——不要更新版本号,不要修改 CHANGELOG,也不要创建 PR。向用户报告错误,指出失败的输出,然后停止。恢复方法:在功能分支上修复打包器或依赖问题,合并到 dev,然后重新运行 /release。
此测试可捕获的常见故障:
- Bun 的
--bytecode标志为当前模块依赖图生成了损坏的字节码 - 某个依赖(例如 SDK)在模块顶层通过某些路径读取
package.json或其他文件;这些路径在node_modules/中可正常解析,但在编译后的二进制文件旁边无法解析 - 循环导入在压缩后会出错,但在直接运行
bun run时正常 - 新增的包使用了带有特殊包装结构的 CJS
步骤 2:检测技术栈和当前版本
检测项目的包管理器和版本文件:
- 检查
pyproject.toml— Python 项目,版本位于version = "x.y.z" - 检查
package.json— Node/Bun 项目,版本位于"version": "x.y.z" - 检查
Cargo.toml— Rust 项目,版本位于version = "x.y.z" - 检查
go.mod— Go 项目(版本仅从 git 标签获取,没有需要更新的文件)
如果均未找到,则中止并提示:"无法检测项目技术栈 — 未找到版本文件。"
从检测到的文件中读取当前版本。
步骤 3:确定版本更新级别
根据参数确定更新规则:
- 未指定参数或指定
patch(默认):0.1.0 -> 0.1.1 minor:0.1.3 -> 0.2.0major:0.3.5 -> 1.0.0
步骤 4:收集提交
不要使用
git log main..dev。 此仓库使用 squash 合并将发布 PR 合并到 main,因此 main 每次发布只会新增一个提交,不会包含 dev 上的 各个独立提交。因此,main..dev会累积所有曾经被 squash 合并的提交, 并且随着每次发布不断增长。发布 0.7.1 时,它返回了 61 个提交, 但其中只有 25 个是新增的——其余 36 个已在 0.7.0 中发布, 并已写入其变更日志。照此操作,会把上个版本的大部分内容再次记录进去。
真正的边界是 dev 上最近一个 Release x.y.z 提交:
# The previous release commit ON DEV (not the squashed one on main).
#
# The `$` anchor is load-bearing. Both commits exist on dev after Step 9's sync:
# 6c6945ce Release 0.7.1 <- dev's own version-bump commit (WANT)
# c71f7f52 Release 0.7.1 (#2435) <- main's squash, pulled back (WRONG)
# Picking the squash commit is catastrophic: its ancestry does not include dev's
# individual history, so the range balloons to the whole repo (383 commits when
# tested). Match the bare subject only.
LAST_RELEASE=$(git log origin/dev \
--grep='^Release [0-9]+\.[0-9]+\.[0-9]+$' --extended-regexp \
--format='%H' -n 1)
# First-ever release: fall back to the root commit. Without this the range
# becomes `..origin/dev`, which git resolves against HEAD and which returns
# ZERO commits — the skill would report "nothing to release" and stop.
if [ -z "$LAST_RELEASE" ]; then
LAST_RELEASE=$(git rev-list --max-parents=0 origin/dev | tail -n 1)
fi
echo "Previous release commit: $(git log -1 --oneline "$LAST_RELEASE")"
# Everything after it is genuinely new, minus the release-plumbing commits
# described below.
git log "$LAST_RELEASE"..origin/dev --oneline --no-merges \
--grep='^Release [0-9]+\.[0-9]+\.[0-9]+' \
--grep='^chore: update Homebrew formula for v' \
--grep='^chore\(homebrew\):' \
--extended-regexp --invert-grep
^chore\(homebrew\):中的反斜杠是必需的。在--extended-regexp下, 未转义的(和)是分组运算符,因此未转义的形式会匹配字面文本chorehomebrew:,并且不会报错地漏掉所有过滤结果。
此范围内有两种提交属于发布流程提交,而非变更日志内容,上面的
--invert-grep 会将它们排除:
Release x.y.z (#NNNN)— main 上的 squash 提交,由步骤 9 的同步操作拉回chore: update Homebrew formula for vx.y.z/chore(homebrew): …— CI 作业和步骤 10 的提交;注意,这些提交会以内容相同、SHA 不同的形式出现, 每个分支各有一个
Release模式特意要求完整的x.y.z版本号。若只使用^Release, 也会过滤掉主题以该词开头的有效功能提交。如果确实有提交被排除, 应手动扩大范围,而不是放宽模式。
起草前先检查边界是否合理。 与上一版本已记录的内容进行交叉核对—— 任何重叠都意味着范围有误。从当前版本推导两个标题,避免内容过时:
# CURRENT_VERSION is the value read in Step 2, before the bump (e.g. 0.7.1)
PREV_MAJOR_MINOR_PATCH="$CURRENT_VERSION"
# The heading immediately above the one we are about to write:
awk -v prev="## [$PREV_MAJOR_MINOR_PATCH]" '
index($0, prev) == 1 { on = 1; next }
on && /^## \[/ { exit }
on { print }
' CHANGELOG.md | grep -oE '#[0-9]{3,5}' | sort -u
如果提交范围内的 PR 编号出现在该列表中,请停止并重新推导边界——不要重复记录。
还要阅读 [Unreleased] 部分。 贡献者可能会手动在此处添加条目。
这些条目是本次发布的输入,不代表边界有误:
awk '
/^## \[Unreleased\]/ { on = 1; next }
on && /^## \[/ { exit }
on { print }
' CHANGELOG.md
如果没有新提交,则中止并提示:"没有内容可发布 — dev 与 main 已保持同步。"
步骤 5:起草变更日志条目
阅读提交消息和实际差异(git diff "$LAST_RELEASE"..origin/dev),了解发生了哪些变化。
优先参考 PR 标题及其 ## Summary 部分,而不是原始提交主题——
它们说明了用户可见的问题,这正是变更日志条目需要描述的内容:
gh pr view <N> --repo coleam00/Archon --json number,title,body,closingIssuesReferences
仅为第 4 步中 [Unreleased] 条目尚未描述的变更起草条目,确保每项变更只出现一次。手写条目通常引用 PR 关闭的问题(closingIssuesReferences),而不是 PR,也可能不引用任何内容;请根据条目所述内容判断,而不要依据编号。保留手写条目的实质内容(迁移表、准确细节),只整理措辞和位置。如果无法判断某个条目是否涵盖某个 PR,请在第 7 步询问。
按 Keep a Changelog 的类别分类,顺序如下(空类别省略):
- Breaking — 会破坏用户可能依赖的现有行为的变更
- Security — 漏洞修复和与安全相关的强化
- Added — 新功能、新文件、新能力
- Changed — 对现有行为的修改
- Deprecated — 仍可使用但不再建议使用或计划移除的功能
- Removed — 已删除的功能或代码
- Fixed — Bug 修复
撰写规则:
- 像人一样撰写条目,清晰、简洁,面向用户
- 不要直接照抄提交消息,改写成规范的变更日志条目
- 适当时将相关提交合并为一条
- 如果有 PR 编号,请用括号注明:
(#12) - 每条都应以描述变更内容的名词或动名词开头
- 跳过仅影响内部的变更(CI 调整、拼写错误修正),除非它们会影响行为
- 各类别之间空一行
步骤 6:更新文件
-
版本文件 — 将版本更新为新值:
package.json:更新"version": "x.y.z"pyproject.toml:更新version = "x.y.z"Cargo.toml:更新version = "x.y.z"
-
工作区版本同步(仅限 monorepo):
- 如果存在
scripts/sync-versions.sh,运行bash scripts/sync-versions.sh,将所有packages/*/package.json版本同步为与根目录版本一致。该脚本也会刷新bun.lock,因此工作区版本也会更新到锁文件中。
- 如果存在
-
锁文件刷新(依技术栈而定)— 如果第 2 步中的脚本已刷新锁文件,则跳过对应行:
package.json+bun.lock:运行bun installpackage.json+package-lock.json:运行npm install --package-lock-onlypyproject.toml+uv.lock:运行uv lock --quietCargo.toml:运行cargo update --workspace
-
CHANGELOG.md— 根据[Unreleased]条目和第 5 步起草的内容,在[Unreleased]下方写入一个新的版本部分;每个非空类别都添加一个### <Category>标题,顺序遵循第 5 步的类别顺序:
## [x.y.z] - YYYY-MM-DD
One-line summary of the release.
### Breaking
- Entry one (#PR)
### Added
- Entry one (#PR)
- Entry two (#PR)
### Fixed
- Entry one (#PR)
### Breaking 必须排在第一位,紧接在单行摘要之后。第 9 步的发布工作流会将此部分原样写入 GitHub 发布说明,因此该文件自身的类别顺序决定了发布页面上破坏性变更的显示顺序。
保留 [Unreleased] 标题,其下不添加任何内容。
步骤 7:提交审核
向用户展示:
- 检测到的技术栈和版本文件
- 版本升级(旧版本 -> 新版本)
- 即将添加的完整变更日志部分
- 将纳入的提交列表
- 你不确定是否涵盖某个 PR 的任何
[Unreleased]条目,并将该条目与相应 PR 并列展示
询问:“看起来可以吗?我会提交更改并创建 PR。”
步骤 8:提交并创建 PR
仅在用户批准后执行:
# Stage version file, workspace packages, lockfile, and changelog
git add <version-file> packages/*/package.json <lockfile> CHANGELOG.md
git commit -m "Release x.y.z"
# Push dev
git push origin dev
# Create PR: dev -> main
gh pr create --base main --head dev \
--title "Release x.y.z" \
--body "$(cat <<'EOF'
## Release x.y.z
{changelog section content}
---
Merging this PR releases x.y.z to main.
EOF
)"
将 PR URL 返回给用户。
步骤 9:合并后打标签、发布并同步
PR 合并后(由用户合并或通过 gh pr merge 合并):
# Fetch the merge commit on main
git fetch origin main
# Tag the merge commit
git tag vx.y.z origin/main
git push origin vx.y.z
# Create a GitHub Release from the tag. --generate-notes is a placeholder: the
# push above already triggered .github/workflows/release.yml, and once its
# `release` job finishes building binaries (5-10 min), its "Build release
# notes" step overwrites this release's body with CHANGELOG.md's own
# `## [x.y.z]` section (Breaking first, per Step 6) plus install instructions.
# That job is the single source of truth for the body, so it does not matter
# that this step's placeholder is throwaway, or which of the two finishes
# first — do NOT hand-craft curated notes here, they would just be discarded.
gh release create vx.y.z --title "vx.y.z" --generate-notes
# Sync dev with main so both branches are identical
git checkout dev
git pull origin main --no-rebase
git push origin dev
# Verify the sync actually converged — do not assume it did. FAIL CLOSED:
# continuing past a failed sync publishes a formula and tap from a tree that
# does not match what was released.
git fetch origin
if git merge-base --is-ancestor origin/main origin/dev; then
echo "dev contains main — OK"
else
echo "STILL DIVERGED — stranded on main:"
git log origin/dev..origin/main --oneline
exit 1
fi
必须使用
--no-rebase,不能省略。 否则,在未设置pull.rebase的机器上,git 会因fatal: Need to specify how to reconcile divergent branches而中止。 它还会确保行为符合此步骤的实际需求——否则,pull.rebase=true配置会改写 dev 的 历史记录,这正是下方警告所禁止的。
预计要执行两次。 CI 的
update-homebrew作业会在你操作期间将自己的 formula 提交推送到 dev,因此这里的git push origin dev(以及步骤 10 中的同一命令)可能会因Updates were rejected被拒绝。这是 正常情况:运行git pull origin dev --no-rebase,然后再次推送。在 0.7.1 版本中,此步骤和 步骤 10 都需要执行第二次。
重要:此同步可确保 dev 包含来自 main 的合并提交。否则,dev 和 main 会出现分歧。CI 的
update-homebrew作业只会将 formula 提交推送到 dev,不会把 PR 合并提交带到 dev。手动运行此处的git pull origin main --no-rebase才能确保 dev 包含该合并提交。
不要在此同步中使用
git pull origin main --ff-only或git reset --hard origin/main。 squash 合并无法快进——main 的 squash 提交与 dev 的发布提交具有不同的 SHA,因此 dev 永远无法 快进到 main。而且,将 dev 重置到 main 会改写 dev 的历史记录,使每个未关闭 PR 的合并基点都与其原始提交 脱节,并导致差异膨胀到数千行(根据 v0.3.10 的发布情况确认:在 dev 上运行git reset --hard origin/main后,PR 的差异从+80/-1增至+6626/-300)。上面的git pull origin main --no-rebase会在 dev 上创建一个常规合并提交。dev 的git log中的合并 气泡,是为了维护未关闭 PR 的正常状态而付出的合理代价。如果在恢复流程中,合并导致homebrew/archon.rb冲突,请使用git checkout origin/main -- homebrew/archon.rb解决 (注意:是origin/main,不是main——本地 main 通常已过时,因为发布时会通过git push origin dev:main推送,而不会快进本地分支)。
GitHub Release 与 git 标签不同——如果没有 GitHub Release,发布内容就不会显示在仓库的 Releases 页面上。务必创建它。
如果用户自行合并 PR 后再回来,仍应主动提出打标签、发布并同步。
步骤 10:等待发布工作流并更新 Homebrew Formula
注意:
.github/workflows/release.yml中的update-homebrewCI 任务会在发布任务之后自动运行,并负责更新 formula 并推送到 dev(属于步骤 10)。 步骤 11(将 tap 同步到coleam00/homebrew-archon)始终需要手动操作。手动运行步骤 10 前,请检查 Actions 选项卡。
推送 tag 后,.github/workflows/release.yml 会构建各平台的二进制文件并将其上传到 GitHub release。这需要 5-10 分钟。只有这些二进制文件生成后,才能确定 Homebrew formula 的 SHA256 值。
等待所有资源出现在 release 中:
echo "Waiting for release workflow to finish uploading binaries..."
WORKFLOW_FAILED=0
for i in {1..30}; do
ASSET_COUNT=$(gh release view "vx.y.z" --repo coleam00/Archon --json assets --jq '.assets | length')
# Expect 7 assets: 5 binaries (darwin-arm64, darwin-x64, linux-arm64, linux-x64, windows-x64.exe) + archon-web.tar.gz + checksums.txt
if [ "$ASSET_COUNT" -ge 7 ]; then
echo "All $ASSET_COUNT assets uploaded"
break
fi
# Short-circuit: if the release workflow itself has failed, stop waiting.
# Hanging for 15 min when CI already crashed just delays the recovery path.
WORKFLOW_STATUS=$(gh run list --workflow release.yml --event push --limit 1 --json conclusion,status --jq '.[0] | "\(.status)|\(.conclusion)"')
if [[ "$WORKFLOW_STATUS" == "completed|failure" ]]; then
echo "Release workflow FAILED — no point waiting longer"
WORKFLOW_FAILED=1
break
fi
echo " Assets so far: $ASSET_COUNT/7 — waiting 30s (attempt $i/30)..."
sleep 30
done
if [ "$WORKFLOW_FAILED" -eq 1 ] || [ "$ASSET_COUNT" -lt 7 ]; then
# Triage: rerun once in case it's transient, then check again.
RUN_ID=$(gh run list --workflow release.yml --event push --limit 1 --json databaseId --jq '.[0].databaseId')
echo "Release workflow failed on run $RUN_ID. Rerunning failed jobs once to confirm..."
gh run rerun "$RUN_ID" --failed
gh run watch "$RUN_ID" --exit-status --interval 30 || true
# Re-check asset count + run status after rerun.
ASSET_COUNT=$(gh release view "vx.y.z" --repo coleam00/Archon --json assets --jq '.assets | length')
if [ "$ASSET_COUNT" -ge 7 ]; then
echo "Rerun succeeded — all assets now present"
else
echo ""
echo "===== DETERMINISTIC CI FAILURE ====="
echo "The release workflow failed on two consecutive runs. This is NOT a flake."
echo "The tag and release exist but have no (or incomplete) assets."
echo ""
echo "install.sh and similar 'releases/latest' paths are now 404-ing."
echo "Proceeding with Homebrew/tap sync would publish a formula pointing at"
echo "missing or inconsistent binaries."
echo ""
echo "Jump to the 'Recovery: deterministic release CI failure' section at the"
echo "bottom of this skill and execute it. Do NOT continue past this point."
exit 1
fi
fi
获取 checksums.txt 并提取 SHA256 值:
TMP_DIR=$(mktemp -d)
gh release download "vx.y.z" --repo coleam00/Archon --pattern "checksums.txt" --dir "$TMP_DIR"
DARWIN_ARM64_SHA=$(awk '/archon-darwin-arm64$/ {print $1}' "$TMP_DIR/checksums.txt")
DARWIN_X64_SHA=$(awk '/archon-darwin-x64$/ {print $1}' "$TMP_DIR/checksums.txt")
LINUX_ARM64_SHA=$(awk '/archon-linux-arm64$/ {print $1}' "$TMP_DIR/checksums.txt")
LINUX_X64_SHA=$(awk '/archon-linux-x64$/ {print $1}' "$TMP_DIR/checksums.txt")
# Sanity check — all four must be present and non-empty
for var in DARWIN_ARM64_SHA DARWIN_X64_SHA LINUX_ARM64_SHA LINUX_X64_SHA; do
if [ -z "${!var}" ]; then
echo "ERROR: $var is empty — checksums.txt may be malformed"
cat "$TMP_DIR/checksums.txt"
exit 1
fi
done
rm -rf "$TMP_DIR"
在主仓库中同时更新 homebrew/archon.rb 的版本和 SHA 值:
使用下面的精确模板重写 formula 文件。不要用 sed 原地编辑——应根据此模板重新生成整个文件,从而完全避免不完整更新的风险。
cat > homebrew/archon.rb << EOF
# Homebrew formula for Archon CLI
# To install: brew install coleam00/archon/archon
#
# This formula downloads pre-built binaries from GitHub releases.
# For development, see: https://github.com/coleam00/Archon
class Archon < Formula
desc "Remote agentic coding platform - control AI assistants from anywhere"
homepage "https://github.com/coleam00/Archon"
version "x.y.z"
license "MIT"
on_macos do
on_arm do
url "https://github.com/coleam00/Archon/releases/download/v#{version}/archon-darwin-arm64"
sha256 "${DARWIN_ARM64_SHA}"
end
on_intel do
url "https://github.com/coleam00/Archon/releases/download/v#{version}/archon-darwin-x64"
sha256 "${DARWIN_X64_SHA}"
end
end
on_linux do
on_arm do
url "https://github.com/coleam00/Archon/releases/download/v#{version}/archon-linux-arm64"
sha256 "${LINUX_ARM64_SHA}"
end
on_intel do
url "https://github.com/coleam00/Archon/releases/download/v#{version}/archon-linux-x64"
sha256 "${LINUX_X64_SHA}"
end
end
def install
binary_name = case
when OS.mac? && Hardware::CPU.arm?
"archon-darwin-arm64"
when OS.mac? && Hardware::CPU.intel?
"archon-darwin-x64"
when OS.linux? && Hardware::CPU.arm?
"archon-linux-arm64"
when OS.linux? && Hardware::CPU.intel?
"archon-linux-x64"
end
bin.install binary_name => "archon"
end
test do
# Basic version check - archon version should exit with 0 on success
assert_match version.to_s, shell_output("#{bin}/archon version")
end
end
EOF
将 formula 更新提交到 main,然后同步回 dev:
git checkout main
git pull origin main --no-rebase
git add homebrew/archon.rb
git commit -m "chore(homebrew): update formula to vx.y.z"
git push origin main
# Sync dev with main so the formula update is on both branches
git checkout dev
git pull origin main --no-rebase
git push origin dev # may be rejected — see below
CI 的 update-homebrew 任务会在此处与你发生竞态。 当你将同一项更改提交到 main 时,它会直接向 dev 写入自己的 formula 提交(例如 chore: update Homebrew formula for vx.y.z)。两者都会修改相同的四行 sha256。因此,上述推送会失败并提示 Updates were rejected:
git pull origin dev --no-rebase # merges CI's commit; usually resolves clean
git push origin dev
合并干净并不能证明 SHA 值正确。 两个提交修改相同的行时,可能在没有冲突的情况下合并,但最终仍留下错误的值。推送前请对照已发布的校验和进行验证——这是唯一一个值错误就会导致所有用户无法安装的文件:
VERSION=x.y.z # without the leading v
gh release download "v$VERSION" --repo coleam00/Archon --pattern checksums.txt --dir /tmp/rel
fail=0
# Assert the formula version matches the release. A stale version points every
# URL at the wrong release while carrying the new digests.
formula_version=$(awk -F'"' '/^[[:space:]]*version "/ {print $2; exit}' homebrew/archon.rb)
if [ "$formula_version" != "$VERSION" ]; then
echo " BAD version: formula says '$formula_version', release is '$VERSION'"
fail=1
fi
# Compare each digest against the sha256 that FOLLOWS ITS OWN url line. A naive
# `grep -q "$digest" formula` only asks whether the value appears anywhere, so
# two platforms with swapped hashes both pass — and an empty digest degenerates
# to `grep -q ""`, which matches every file.
for p in archon-darwin-arm64 archon-darwin-x64 archon-linux-arm64 archon-linux-x64; do
real=$(awk -v p="$p" '$2 ~ "(^|/)" p "$" {print $1}' /tmp/rel/checksums.txt | head -1)
if ! printf '%s' "$real" | grep -qE '^[0-9a-f]{64}$'; then
echo " BAD $p: no 64-char digest in checksums.txt (got '$real')"
fail=1
continue
fi
in_formula=$(awk -v p="$p" '
index($0, "/" p "\"") { seen = 1; next }
seen && /sha256 "/ { gsub(/.*sha256 "|".*/, ""); print; exit }
' homebrew/archon.rb)
if [ "$in_formula" = "$real" ]; then
echo " OK $p"
else
echo " BAD $p: formula has '$in_formula', release has '$real'"
fail=1
fi
done
[ "$fail" -eq 0 ] || { echo "formula verification FAILED — do not push"; exit 1; }
echo "formula verified against v$VERSION checksums"
每一行都必须输出 OK,且版本必须匹配。出现任何 BAD 时,都应根据步骤 10 的模板重新生成 formula,而不要手动编辑——手动编辑正是摘要被放到错误平台下的根本原因。
最后,确认两者已收敛。失败时应默认停止——步骤 11 会发布到用户安装时使用的 tap,因此不要在代码树尚未收敛时继续:
git fetch origin
converged=1
git merge-base --is-ancestor origin/main origin/dev \
|| { echo "DIVERGED: dev does not contain main"; converged=0; }
stranded=$(git log origin/dev..origin/main --oneline | wc -l | tr -d ' ')
[ "$stranded" -eq 0 ] || { echo "STRANDED: $stranded commit(s) on main not on dev:"; \
git log origin/dev..origin/main --oneline; converged=0; }
[ "$converged" -eq 1 ] || { echo "sync incomplete — resolve before Step 11"; exit 1; }
echo "branches converged — OK"
步骤 11:同步 Homebrew Tap 仓库
coleam00/homebrew-archon 仓库托管着 Homebrew 实际读取的 tap formula,用户运行 brew tap coleam00/archon && brew install coleam00/archon/archon 时就会读取该文件。文件 coleam00/Archon/homebrew/archon.rb 是事实来源模板;文件 coleam00/homebrew-archon/Formula/archon.rb 则是用户实际安装时使用的文件。两者必须保持同步。
TAP_DIR=$(mktemp -d)
git clone [email protected]:coleam00/homebrew-archon.git "$TAP_DIR"
cp homebrew/archon.rb "$TAP_DIR/Formula/archon.rb"
cd "$TAP_DIR"
if git diff --quiet; then
echo "Tap formula already matches — no sync needed"
else
git add Formula/archon.rb
git commit -m "chore: sync formula to vx.y.z"
git push origin main
fi
cd -
rm -rf "$TAP_DIR"
如果 git clone 因权限错误而失败,则运行发布技能的用户没有 coleam00/homebrew-archon 的推送权限。请让他们向仓库所有者申请推送权限,或通过 GitHub 网页界面手动完成同步。不要默默跳过此步骤——tap 同步完成后,发布才算完成。
步骤 12:端到端验证发布
公式同步后,最后一步验证是通过 Homebrew 实际安装已发布的二进制文件,并运行冒烟测试。使用 test-release 技能:
/test-release brew x.y.z
这将:
- 通过
brew tap coleam00/archon && brew install coleam00/archon/archon安装 - 验证二进制文件报告了正确的版本和
Build: binary - 验证捆绑的工作流能够加载
- 验证 SDK 的生成路径可用(运行最简的 archon-investigate)
- 验证环境变量泄漏防护会从含敏感键的 .env 文件中移除这些键(测试 4)
- 干净地卸载
- 生成 PASS/FAIL 报告
如果 /test-release brew 失败,就不能宣布发布。 针对出问题的部分提交一个热修复问题,发布带有修复的 x.y.z+1,然后重新运行此技能。切勿宣传未通过 test-release 的发布。
另外,运行 /test-release curl-mac x.y.z,覆盖 curl 安装路径。两种安装路径测试的内容略有不同(Homebrew 测试 tap 公式,curl 测试 install.sh 和发布版本中的校验和),为了确保用户能够可靠地安装,两种路径都必须正常工作。
如果你有可用的 VPS,也运行 /test-release curl-vps x.y.z <vps-target>,验证 Linux 二进制文件。
恢复:确定性发布 CI 失败
你会来到这里,是因为步骤 10 检测到连续两次工作流失败。标签 vx.y.z 已推送,GitHub 发布版本已创建,但资源文件缺失或不完整。当前每次运行 install.sh 都会将 releases/latest 解析到这个有问题的发布版本,并在下载时遇到 404。由于步骤 10 中公式的原子更新已被阻止,Homebrew 用户不受影响。
不要寄希望于第三次重跑发布工作流就能成功。 如果同一故障可复现两次,那就是代码缺陷——你需要发布代码来修复它。
立即缓解(恢复 install.sh)
删除 GitHub 发布版本,让 releases/latest 回退到上一个版本。保留 git 标签——标签不可变性很重要,而且目前没有已发布的构建产物指向该标签。
gh release delete "vx.y.z" --yes
# Do NOT delete the tag:
# git push --delete origin vx.y.z ← do not run
# Tag stays so git history records the attempt; no release means no assets
# means releases/latest resolves to the prior working release.
验证:
gh api repos/coleam00/Archon/releases/latest --jq '.tag_name'
# should now print the prior version (e.g. v0.3.6), not vx.y.z
诊断
发布工作流日志会指出哪个目标失败,以及失败发生在哪个阶段(编译、冒烟测试还是上传):
gh run list --workflow release.yml --limit 2 --json databaseId,conclusion
gh run view <RUN_ID> --log-failed
常见原因:
- 打包器/字节码缺陷——Bun 的
--bytecode会为当前模块图生成无效输出。症状:二进制文件启动时出现TypeError: Expected CommonJS module to have a function wrapper。历史上,这是新依赖的 CJS/ESM 结构与--bytecode交互导致的;去掉该标记或延迟导入问题模块即可修复。 - 模块初始化崩溃——某个依赖在模块顶层通过特定路径执行
readFileSync('package.json')或类似操作。该路径在node_modules/中存在,但编译后的二进制文件旁边不存在。症状:二进制文件的每个子命令都会立即崩溃;错误通常会提到process.execPath旁边缺少文件。修复方法是在实际使用该依赖的代码路径中延迟导入它。 - Windows 上冒烟测试超时——这并非代码本身的缺陷,而是 Windows 运行器速度较慢。重跑一次;如果再次出现,就延长测试超时时间。
现在,步骤 1.5 会在任何面向用户的步骤之前,先对本地编译后的二进制文件运行冒烟测试。如果上述故障模式能在本地复现,你就找到了问题。如果不能,说明缺陷与平台有关(Windows 交叉编译、Linux glibc 等),需要查看 CI 日志。
修复并作为下一个补丁版本重新发布
不要重复使用 vx.y.z。 修复后发布 vx.y.(z+1)(或视情况发布下一个次要版本)。原因:
- 标签不可变:
vx.y.z已记录在 git 历史和发布缓存中 - Semver 清晰明了:版本内容发生变化时,用户和工具应看到新的版本号
- 审计记录:与其改写
v0.3.7,不如记录“已发布v0.3.7,但没有随附已发布的二进制文件;v0.3.8是首个包含 的版本”,这样更清晰
步骤:
- 从 dev 分支切出修复分支,实现修复,向 dev 提交 PR 并合并。
- 重新运行
/release(它会自动将版本号递增到下一个补丁版本,例如0.3.8)。 - 第 1.5 步的发布前冒烟测试会在本地发现同一问题(如果修复未真正生效)。反复迭代,直到测试通过后再打标签。
热修复版本的 CHANGELOG 说明
在新版本的 CHANGELOG 中添加一行,引用之前有问题的版本,以便用户了解为什么该标签下没有二进制文件:
### Fixed
- **First release with working compiled binaries after vx.y.z's <bug>.** vx.y.z was tagged but its binary smoke test failed deterministically (see RUN_ID in CI history). The tag is preserved for history; this release (vx.y.(z+1)) is the first with shipped binaries. `install.sh` and Homebrew were never updated to vx.y.z, so users were not exposed to the broken state.
不要做的事
- 不要强制推送或改写标签。 标签一旦存在,就代表对该 SHA 的公开承诺。删除标签并用不同的 SHA 重新创建属于标签欺骗,还会影响缓存了原始标签的下游项目。
- 不要跳过此恢复流程,“直接给有问题的版本多推送一些二进制文件”。 该版本对应特定的提交 SHA;上传基于更新 SHA 构建的二进制文件会导致二进制文件与源代码不一致,之后很难诊断。
- 在
v0.3.(z+1)完整发布之前,不要更新 Homebrew formula。 该 formula 应始终指向所有 7 个资源都已上传且/test-release brew通过的版本。
重要规则
- 绝不强制推送
- 绝不能根据
git log main..dev推导变更日志。 main 使用 squash merge,因此该范围会累积此前所有已发布的提交,并且每次发布都会变长(0.7.1 版本中,该范围有 61 个提交,而实际只有 25 个)。请使用 dev 上最近一次Release x.y.z提交作为边界,并对照上一版本的变更日志条目进行交叉核对——如果同一个 PR 编号同时出现在两处,说明边界有误。 - 执行
git pull时始终传入--no-rebase。 不带参数的命令会报错并中止,提示Need to specify how to reconcile divergent branches,除非已配置pull.rebase;而配置pull.rebase=true会改写 dev 的历史。 - 始终验证同步是否真正收敛,不能想当然。
每次同步操作后执行
git merge-base --is-ancestor origin/main origin/dev。 否则,直接提交到 main 的提交会悄无声息地滞留。 - 绝不能因为合并干净就信任
homebrew/archon.rb。 CI 的update-homebrew任务会在 dev 上编辑与你在 main 上编辑的同一组sha256行;即使合并成功,结果仍可能有误。推送前,将这四个值与已发布的checksums.txt进行对比。 - 如果版本升级与提交范围矛盾,请明确指出。 如果要求升级
patch,但提交范围中包含feat:提交或面向用户的 CLI 新功能,请在第 7 步审查时说明,并让用户决定——不要悄悄将功能作为补丁发布。 - 绝不能跳过第 1.5 步(发布前的已编译二进制冒烟测试)。 如果项目栈使用 Bun/Node,且有构建二进制文件的脚本,那么必须在升级版本、创建 PR 或打标签之前运行
bun build --compile冒烟测试。跳过这一步意味着,打包器回归或模块初始化崩溃只会在标签推送后才暴露——届时releases/latest已经对所有用户返回 404。花费约 30 秒运行测试,是为了将故障留在本地发现。 - 如果第 1.5 步失败,中止发布,并在功能分支上修复根本问题。不要“干脆跳过”,寄希望于 CI 不会复现问题。
- 绝不能跳过审查步骤——提交前始终展示变更日志
- 提交中绝不能包含
Co-Authored-By: Claude或任何 AI 署名 - 除非用户要求,否则绝不能在变更日志条目中添加表情符号
- 如果用户只说“ship it”而未指定升级类型,默认升级
patch - 提交消息只能是
Release x.y.z——简洁明了 - 更新
homebrew/archon.rb的版本字段时,绝不能不同时更新sha256值。 两者必须原子性地一起更新。只有发布工作流完成二进制文件构建后,才能获取正确的 SHA——请参阅第 10 步。只更新版本字段会产生一个过时的 formula,看似有效,却会在安装时导致校验和不匹配。 - 绝不能跳过第 11 步(tap 同步)。
coleam00/Archon/homebrew/archon.rb文件只是模板;用户实际从coleam00/homebrew-archon/Formula/archon.rb安装。如果只更新其中一个,用户拿到的内容就会过时或有误。 - 如果
/test-release brew失败,绝不能宣布发布。 安装成功却在首次调用时崩溃,比不发布更糟糕——这会损害用户信任。如果发布验证失败,请先发布热修复,再告知任何人该版本已经发布。