caveman 公共包发布流水线:从注解标签到 npm/PyPI 可信发布的完整机制
【免费下载链接】caveman🪨 why use many token when few token do trick — Claude Code skill that cuts 65% of tokens by talking like caveman项目地址: https://gitcode.com/GitHub_Trending/caveman1/caveman
本文基于 docs/PACKAGE_RELEASES.md 展开,讲解 caveman 如何通过.github/workflows/release-packages.yml工作流,把@caveman-ai/sdk、@caveman-ai/agent、@caveman-ai/create-agent与 PyPIcaveman-sdk四个公共包安全发布到注册表。读完后你将理解:caveman 的发布标签规范、"构建与发布隔离"的 OIDC 安全模型、标签与包元数据的版本一致性校验、npm tarball 与 Python wheel/sdist 的构建及冒烟验证流程,以及发布后的验证与回滚纪律。
发布范围与构件矩阵
caveman 在 2026-08-11 以 Bootstrap 方式发布了四个经过审查的构件,并在干净环境中匿名验证通过:
| 发布渠道 | 包名 | 初始版本 |
|---|---|---|
| npm | @caveman-ai/sdk | 1.0.0 |
| npm | @caveman-ai/agent | 0.1.0 |
| npm | @caveman-ai/create-agent | 0.1.0 |
| PyPI | caveman-sdk | 1.0.0 |
这些版本与当前仓库中的包元数据完全一致:packages/sdk/typescript/package.json中为@caveman-ai/sdk@1.0.0,packages/agent/package.json为@caveman-ai/agent@0.1.0,packages/create-caveman-agent/package.json为0.1.0,而 packages/sdk/python/pyproject.toml 中项目名为caveman-sdk、版本1.0.0(Python 侧的运行时导入名是caveman_cloud,包名与导入名不同,这是发布时需要特别留意的点)。
验证结论包括:全新安装与运行时 import 均通过,注册表下载内容与受审构件的 SHA-256 哈希一致,且发布构件中不包含任何注册表凭证。
需要说明的是,从工作流源码看,.github/workflows/release-packages.yml 的触发标签模式实际还包含第五种pi-v*,对应 packages/pi-extension(npm 名@caveman-ai/pi,当前版本0.1.0,仓库中也存在pi-v0.1.0标签)。文档描述的"四个构件"是 Bootstrap 阶段的受审发布面,工作流本身对 pi 扩展包的发布通道已预留。
工作流架构:构建与发布严格隔离
整个工作流的安全设计核心是一条原则:构建任务没有 OIDC 权限,只有隔离的发布任务可以铸造注册表令牌。具体体现在工作流顶层与三个 job 的划分:
# .github/workflows/release-packages.yml permissions: contents: read # 工作流默认只有只读内容权限 concurrency: group: release-package-${{ github.ref }} cancel-in-progress: false # 同标签的发布任务不允许互相抢占 jobs: build: # 只负责解析包、构建、测试、打包 publish-npm: # environment: npm,permissions: id-token: write publish-pypi: # environment: pypi,permissions: id-token: writebuildjob(30 分钟超时)完成所有重活:校验标签、解析包、构建、测试、审计、打包,最后把 tarball 或 wheel/sdist 上传为一次性 artifact(保留期仅 1 天,retention-days: 1)。publish-npm/publish-pypijob(各 10 分钟超时)只依赖build的输出,下载 artifact 后调用注册表发布接口。它们绑定受保护的 GitHub 环境(npm/pypi),并在environment的url字段中直接写明对应包的注册表页面,方便在 Actions 界面跳转核对。- 只有这两个发布 job 声明了
id-token: write,即只有它们能通过 OIDC 向 npm/PyPI 换取发布令牌;buildjob 即使被恶意代码劫持,也无法触达注册表凭证。 cancel-in-progress: false保证同一个标签一旦开始发布就不会被后续 push 取消,避免半截发布。
标签门禁:注解、签名验证与 main 祖先关系
标签是发布的第一道闸门。文档规定:标签必须是注解标签(annotated)、必须通过 GitHub 签名验证、且必须指向main。工作流中有一个专门的步骤把这三点都硬性校验(release-packages.yml):
set -euo pipefail tag_ref="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$GITHUB_REF_NAME" --jq .object.sha)" tag_type="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$GITHUB_REF_NAME" --jq .object.type)" [[ "$tag_type" == "tag" ]] || { echo "release tag must be annotated" >&2; exit 1; } verified="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$tag_ref" --jq .verification.verified)" [[ "$verified" == "true" ]] || { echo "release tag signature is not GitHub-verified" >&2; exit 1; } target="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$tag_ref" --jq .object.sha)" git cat-file -e "$target^{commit}" git merge-base --is-ancestor "$target" origin/main这段脚本依次检查:
- 标签对象类型为
tag(注解标签),而非轻量的commit指针; - GitHub 对该标签签名的验证结果为
true; - 标签指向的对象确实是一个 commit;
- 该 commit 是
origin/main的祖先(git merge-base --is-ancestor),即发布内容必须来自main分支,杜绝从任意旁支标签直接发布。
标签命名遵循"包前缀 + 版本号"的约定,触发模式为sdk-ts-v*、sdk-python-v*、agent-v*、create-agent-v*(以及上文提到的pi-v*)。
版本一致性校验:标签必须等于包元数据
文档明确"Workflow rejects a tag whose version differs from package metadata(工作流拒绝版本与包元数据不一致的标签)"。工作流的Resolve package and require tag/version parity步骤(release-packages.yml)实现了这一点:
case "$GITHUB_REF_NAME" in sdk-ts-v*) package="packages/sdk/typescript" ecosystem="npm" version="${GITHUB_REF_NAME#sdk-ts-v}" actual="$(node -p "require('./$package/package.json').version")" ;; sdk-python-v*) package="packages/sdk/python" ecosystem="pypi" version="${GITHUB_REF_NAME#sdk-python-v}" actual="$(python -c 'import pathlib,tomllib; print(tomllib.loads(pathlib.Path("packages/sdk/python/pyproject.toml").read_text())["project"]["version"])')" ;; agent-v*) # → packages/agent create-agent-v*)# → packages/create-caveman-agent pi-v*) # → packages/pi-extension esac [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]] || { echo "release version is not semver: $version" >&2; exit 1; } [[ "$actual" == "$version" ]] || { echo "tag version $version does not match package version $actual" >&2; exit 1; }这里有三层防护:
- 标签到包的映射:按前缀把标签解析到具体的包目录,并判定发布生态(
npm或pypi); - Semver 语法校验:版本必须匹配标准 SemVer 正则(允许
+build/-pre后缀),防止agent-v1.0这类非法版本进入流水线; - 精确相等比对:从
package.json(npm 包用node -p读取)或pyproject.toml(用tomllib解析project.version)中读出实际版本,与标签版本做字符串相等比较,任何漂移(比如忘了 bump 版本号就打标签)都会直接exit 1。
当前标签与构件的对应关系(文档表格,已用仓库实际元数据核对):
| 标签 | 构件 | 当前版本 |
|---|---|---|
sdk-ts-v1.0.0 | npm@caveman-ai/sdk | 1.0.0 |
sdk-python-v1.0.0 | PyPIcaveman-sdk | 1.0.0 |
agent-v0.1.0 | npm@caveman-ai/agent | 0.1.0 |
create-agent-v0.1.0 | npm@caveman-ai/create-agent | 0.1.0 |
npm 构建流水线:安装、审计、测试与按包定制的冒烟验证
npm 生态的构建步骤(Build and test npm package,release-packages.yml)在对应包的 working directory 中执行:
npm ci --ignore-scripts --no-audit --no-fund npm audit --package-lock-only --omit=dev --audit-level=low npm audit --package-lock-only --audit-level=high npm test npm pack --pack-destination "$RUNNER_TEMP/package-dist" mapfile -t tarballs < <(find "$RUNNER_TEMP/package-dist" -maxdepth 1 -type f -name '*.tgz' -print) [[ "${#tarballs[@]}" == 1 ]] || { echo "expected exactly one npm tarball" >&2; exit 1; }关键设计点:
- 可移植的提交锁文件:使用
npm ci --ignore-scripts基于已提交的 lockfile 安装,保证"锁文件里写什么就装什么",且禁用 install 脚本以防安装期代码执行; - 双层安全审计:对运行时依赖图做
low级别审计(任何运行时漏洞即失败),再对完整依赖图做high级别审计。这与 packages/agent/README.md 中描述的安全边界一致——发布 CI 拒绝任何运行时 advisory 以及完整图中的 high/critical advisory; - 全量测试:
npm test运行该包的完整测试套件; - 恰好一个 tarball:
npm pack产物必须唯一,防止意外打包了多余文件。
随后工作流把 tarball 装进一个临时干净项目($RUNNER_TEMP/npm-artifact-smoke),按包类型执行不同的"真机冒烟":
case "${{ steps.package.outputs.package }}" in packages/sdk/typescript) (cd "$smoke" && node --input-type=module -e 'const m=await import("@caveman-ai/sdk"); if(typeof m.Cave!=="function") process.exit(1)') ;; packages/agent) (cd "$smoke" && node --input-type=module -e 'const m=await import("@caveman-ai/agent"); if(typeof m.agent!=="function") process.exit(1)') "$smoke/node_modules/.bin/caveman-agent" --version ;; packages/pi-extension) # 发布的 tarball 必须真正携带 package.json `pi.extensions` 所命名的打包扩展 test -s "$smoke/node_modules/@caveman-ai/pi/dist/index.mjs" ;; packages/create-caveman-agent) generated="$smoke/generated-agent" "$smoke/node_modules/.bin/create-caveman-agent" "$generated" --provider openai --no-install test -f "$generated/src/agent.ts" test -f "$generated/src/run.ts" node -e '... 断言生成项目依赖 "@caveman-ai/agent": "^0.1.0" ...' "$generated/package.json" node -e '... 断言 run 脚本为 "node --experimental-strip-types src/run.ts" ...' "$generated/package.json" grep -F 'entryPath: "src/agent.ts"' "$generated/src/run.ts" grep -F 'rootDir' "$generated/src/run.ts" ;; esac这些冒烟测试验证的是"用户拿到 tarball 之后真正会发生什么":
@caveman-ai/sdk:干净项目 import 后必须能拿到Cave构造函数。对照 packages/sdk/typescript/src/index.ts 的导出,Cave确实是 SDK 的核心入口类(CaveOptions支持apiKey、baseURL、agent、retention、timeoutMs、signal等选项),断言的是真实 API 面而非占位导出;@caveman-ai/agent:除agent()导出外,还实际执行caveman-agent --version,验证bin可执行文件在 tarball 中可用;@caveman-ai/pi:检查dist/index.mjs非空,确保打包扩展真的随 tarball 分发——工作流注释直言"这正是关键",因为package.json的pi.extensions字段指向的构建产物若缺失,插件市场安装后会是空壳;@caveman-ai/create-agent:实际运行 initializer 生成一个项目,然后逐项断言生成的src/agent.ts、src/run.ts存在、package.json依赖锁定在^0.1.0、run 脚本为node --experimental-strip-types src/run.ts、run.ts内含entryPath: "src/agent.ts"。这是对"脚手架产物契约"的端到端验证。
Python 构建流水线:wheel 与 sdist 双工件
Python 包发布"wheel plus sdist",构建步骤(release-packages.yml):
python -m pip install --disable-pip-version-check build==1.3.0 pytest==9.0.3 python -m pytest python -m build --sdist --wheel --outdir "$RUNNER_TEMP/package-dist" # 断言恰好 1 个 wheel + 1 个 sdist # wheel 冒烟: python -m venv "$RUNNER_TEMP/wheel-smoke" "$RUNNER_TEMP/wheel-smoke/bin/python" -m pip install --no-deps "${wheels[0]}" "$RUNNER_TEMP/wheel-smoke/bin/python" -c 'import caveman_cloud,importlib.metadata; assert importlib.metadata.version("caveman-sdk") == "${{ steps.package.outputs.version }}"' # sdist 冒烟: python -m venv "$RUNNER_TEMP/sdist-smoke" "$RUNNER_TEMP/sdist-smoke/bin/python" -m pip install setuptools==80.9.0 "$RUNNER_TEMP/sdist-smoke/bin/python" -m pip install --no-deps --no-build-isolation "${sdists[0]}" "$RUNNER_TEMP/sdist-smoke/bin/python" -c 'import caveman_cloud,importlib.metadata; assert importlib.metadata.version("caveman-sdk") == "${{ steps.package.outputs.version }}"'两个独立的 venv 分别验证 wheel 和 sdist 的安装路径:wheel 直装,sdist 用固定版本 setuptools 且--no-build-isolation源码构建。两者都要满足"能import caveman_cloud且importlib.metadata读到的发行版名/版本与标签一致"。注意这里再次体现了 PyPI 发行名(caveman-sdk)与 Python 导入名(caveman_cloud)的分离——对应源码目录 packages/sdk/python/caveman_cloud,测试位于 packages/sdk/python/tests。
OIDC 可信发布:发布任务的令牌从哪来
两个发布 job 都不带长期凭证。publish-npm(release-packages.yml)的流程:
permissions: contents: read id-token: write steps: - uses: actions/download-artifact@... # 拉取 build 产出的 tarball - uses: actions/setup-node@... with: node-version: "24.16.0" registry-url: https://registry.npmjs.org - name: Publish with npm trusted publishing run: | set -euo pipefail npm install --global npm@11.5.1 npm publish ./dist/*.tgz --access publicpublish-pypi则使用pypa/gh-action-pypi-publish@v1.14.0动作(release-packages.yml)。发布时 GitHub Actions 用id-token: write权限向注册表出示 OIDC 身份(owner / repo / workflow / environment / tag),注册表侧配置的"可信发布者"校验该身份后自动铸造一次性令牌——这正是文档所说"only isolated publish jobs can mint registry tokens"的实现方式:构建机上的任何秘密都无法被用来发布,因为 build job 根本拿不到 OIDC 权限。
文档同时给出了可信发布者的精确身份字段(注册表身份字段大小写敏感),这是未来切到受信发布者发布时的核对清单:
- npm:确认 founder 拥有
@caveman-aiscope;为每个包配置可信发布者:ownerJuliusBrussee、repositorycaveman、workflowrelease-packages.yml、environmentnpm、actionnpm publish,随后禁用长期发布令牌; - PyPI:为项目
caveman-sdk配置可信发布者:ownerJuliusBrussee、repositorycaveman、workflowrelease-packages.yml、environmentpypi;Python 导入名保持caveman_cloud不变。
从源码结构看,当前工作流中两个发布 job 已绑定npm/pypi两个 GitHub 环境(含指向注册表页面的url字段),环境侧的受保护规则(限制部署到main上的签名发布标签、要求 founder 审批)是文档"剩余工作"中需要补齐的配套设置。
后续切换受信发布的四步计划
文档列出了把未来发布迁移到 trusted publishers 的完整步骤,值得作为发布工程参考:
- 用
tools/publish-public.sh --apply镜像受审源码,人工检查 diff 后再 commit 并 push 到公共仓库JuliusBrussee/caveman。脚本本身从不commit 或 push——推送动作必须由人完成。需要说明:当前仓库中未见tools/publish-public.sh,该脚本属于文档描述的外部发布仓库侧工具,本仓库作为受审源码镜像不承载它; - 在公共仓库创建受保护的 GitHub 环境
npm与pypi:限制部署到main上的签名发布标签,并要求 founder 审批; - 在 npm 侧确认
@caveman-aiscope 归属,按上文身份字段配置各包的可信发布者,并禁用长期发布令牌; - 在 PyPI 侧为
caveman-sdk配置可信发布者。
发布后验证:证据先行,绝不覆盖
文档对"发布成功"的定义非常严格——不要在注册表端点从干净环境解析回本项目之前,切换任何公共安装命令。每个发布版本都要证明五件事:确切版本号、包归属(owner/repository)、全新安装可用、import 可用、initializer 输出正确。对 Agent SDK,具体做法是在生成项目中运行零 provider 调用的caveman-agent doctor,再执行一次带凭证的陌生人(stranger)响应。这与 packages/agent/README.md 中的说明吻合:npx caveman-agent doctor是"零 provider 调用"的就绪检查,Engine/CLI/gateway 缺失只给 WARN 且退出码 0(observe-only 仍可运行),而 Node 版本、沙箱隔离、配置或锁漂移等问题才会使其失败。provider 费用明确属于发布工作流之外。
失败处理路径同样是强制性的:
- 冒烟失败时,对受影响的 npm 版本执行deprecate,或yankPyPI 发布;
- 移除公共安装命令;
- 用新版本前向修复(fix forward);
- 保留失败的构件与工作流日志作为证据;
- 绝不覆盖已发布的版本。
相关文件索引
- docs/PACKAGE_RELEASES.md:本文的主体文档,记录注册表状态、剩余设置、标签规范与发布后证明要求
- .github/workflows/release-packages.yml:发布工作流完整实现(标签门禁、版本一致性、npm/Python 构建与冒烟、OIDC 发布)
- packages/sdk/typescript/package.json:
@caveman-ai/sdk@1.0.0元数据 - packages/sdk/python/pyproject.toml:
caveman-sdk@1.0.0元数据(导入名caveman_cloud) - packages/agent/package.json:
@caveman-ai/agent@0.1.0元数据 - packages/create-caveman-agent/package.json:
@caveman-ai/create-agent@0.1.0元数据与bin定义 - packages/pi-extension/package.json:
@caveman-ai/pi@0.1.0(工作流已覆盖的第五个发布通道) - packages/agent/README.md:
caveman-agent doctor的就绪检查语义,与发布后证明步骤对应
【免费下载链接】caveman🪨 why use many token when few token do trick — Claude Code skill that cuts 65% of tokens by talking like caveman项目地址: https://gitcode.com/GitHub_Trending/caveman1/caveman
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考