Skip to content

pi-mono 深度導讀 16:Release 流程、Lockstep Versioning、Binary Build、Trusted Publishing——從代碼到 npm 發佈的完自動化管線

2026年8月31日1 分鐘
TL;DRRelease 完整流程:Lockstep Versioning(所有套件同版本)、CHANGELOG 更新、Local Smoke Test 驗證、Release Script 自動化、Binary Build (Bun + Node)、npm-shrinkwrap.json 鎖死傳遞依賴、GitHub Actions OIDC Trusted Publishing、R2 Release Marker 驗證、pi.dev/api/latest-version 公告、Announcement Verification 確保發佈成功。

🌏 English version

TL;DR

  • Lockstep Versioning:所有 7+ 套件共用同一版本號、Patch=修復+新增、Minor=Breaking、無 Major
  • Release Script:release.mjs 自動化 Bump→Changelog→Build→Check→Commit→Tag→Push
  • Local Smoke:Node + Bun 雙平台、Interactive/Print/JSON/RPC 模式全測
  • Binary Build:build-binaries.sh 產出 Bun Executable + Node Package、跨平台
  • Shrinkwrap:npm-shrinkwrap.json 鎖死傳遞依賴、發佈給 npm 使用者
  • Trusted Publishing:GitHub Actions OIDC → npm、免 Token、免 OTP
  • R2 Marker:發佈後寫入 R2、pi.dev/api/latest-version 讀取、Announcement Verification

Lockstep Versioning:單一版本號

策略

// 根 package.json
{
  "version": "0.12.3",
  "workspaces": ["packages/*", "packages/session-backends/*", ...],
  "scripts": {
    "version:patch": "npm version patch --workspaces --no-git-tag-version --no-workspaces-update && node scripts/sync-versions.js && npm install --package-lock-only --ignore-scripts",
    "version:minor": "npm version minor --workspaces --no-git-tag-version --no-workspaces-update && node scripts/sync-versions.js && npm install --package-lock-only --ignore-scripts",
    "release:patch": "node scripts/release.mjs patch",
    "release:minor": "node scripts/release.mjs minor"
  }
}

版本語義

版本類型觸發條件範例
Patch (0.12.3 → 0.12.4)Bug Fix、新功能、相容性改善新增 Tool、修正 Bug、效能優化
Minor (0.12.3 → 0.13.0)Breaking ChangesAPI 變更、移除功能、Schema 變更
Major不使用永遠不釋出 Major 版本

為什麼 Lockstep?

優點說明
避免 Diamond Dependency所有套件同版本、無版本衝突
單一發佈一次 npm publish 發佈所有套件
心智負擔低使用者只需記住一個版本號
CHANGELOG 統一每個套件獨立 CHANGELOG 但版本同步

Release Script:完全自動化

release.mjs 流程

// scripts/release.mjs
async function release(type) {
  // 1. 前置檢查
  await run("npm run check");  // Lint + Type Check + Tests
  
  // 2. 更新 CHANGELOG (要求使用者先跑 /cl prompt)
  await verifyChangelogsUpdated();
  
  // 3. Bump 版本
  await run(`npm run version:${type}`);  // 更新所有 workspace 版本
  
  // 4. 重新產生 Artifacts
  await run("npm run build");
  await run("npm run generate:model-catalog");
  await run("npm run generate:coding-agent-shrinkwrap");
  await run("npm run generate:coding-agent-install-lock");
  
  // 5. 再次檢查
  await run("npm run check");
  
  // 6. Commit
  const version = getVersion();
  await run(`git commit -am "Release v${version}"`);
  await run(`git tag v${version}`);
  
  // 7. Push (觸發 CI Publish)
  await run("git push origin main --tags");
  
  console.log(`✅ Release v${version} initiated. CI will handle publishing.`);
}

sync-versions.js

// scripts/sync-versions.js
// 確保所有 workspace 版本一致
const rootPkg = JSON.parse(readFileSync("package.json"));
const version = rootPkg.version;

for (const workspace of getWorkspaces()) {
  const pkgPath = join(workspace, "package.json");
  const pkg = JSON.parse(readFileSync(pkgPath));
  pkg.version = version;
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
}

Local Smoke Test:雙平台驗證

測試矩陣

# scripts/local-release.mjs 執行流程

# 1. 建構 Local Release
npm run release:local -- --out /tmp/pi-local-release --force

# 2. Node Package Smoke Tests
cd /tmp/pi-local-release/node
/tmp/pi-local-release/node/pi --help
/tmp/pi-local-release/node/pi --version
/tmp/pi-local-release/node/pi --list-models
/tmp/pi-local-release/node/pi -p "Say exactly: ok"

# 3. Node Interactive Smoke Test (tmux)
tmux new-session -d -s pi-test-node -x 80 -y 24
tmux send-keys -t pi-test-node "/tmp/pi-local-release/node/pi" Enter
sleep 3
tmux send-keys -t pi-test-node "Say hello" Enter
tmux send-keys -t pi-test-node Escape
sleep 5
tmux capture-pane -t pi-test-node -p  # 驗證回應
tmux kill-session -t pi-test-node

# 4. Bun Binary Smoke Tests
cd /tmp/pi-local-release/bun
/tmp/pi-local-release/bun/pi --help
/tmp/pi-local-release/bun/pi --version
/tmp/pi-local-release/bun/pi --list-models
/tmp/pi-local-release/bun/pi -p "Say exactly: ok"

# 5. Bun Interactive Smoke Test
tmux new-session -d -s pi-test-bun -x 80 -y 24
tmux send-keys -t pi-test-bun "/tmp/pi-local-release/bun/pi" Enter
sleep 3
tmux send-keys -t pi-test-bun "Say hello" Enter
tmux send-keys -t pi-test-bun Escape
sleep 5
tmux capture-pane -t pi-test-bun -p
tmux kill-session -t pi-test-bun

驗證標準

測試項目通過標準
--help顯示正確幫助資訊
--version版本號正確
--list-models列出模型、無錯誤
Print Mode輸出預期文字
Interactive ModeTUI 啟動、接受輸入、模型回應

Binary Build:Bun + Node 雙平台

build-binaries.sh

#!/bin/bash
# scripts/build-binaries.sh

set -euo pipefail

VERSION=$1
PLATFORM=$2  # linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64
OUT_DIR=$3

# 1. 準備
npm ci --ignore-scripts
npm run build

# 2. Node Package (通用)
mkdir -p "$OUT_DIR/node"
cp -r packages/coding-agent "$OUT_DIR/node/"
# 處理 package.json、bin 入口點等

# 3. Bun Executable (平台特定)
if command -v bun &> /dev/null; then
  mkdir -p "$OUT_DIR/bun"
  
  # 編譯 Bun Executable
  bun build packages/coding-agent/src/main.ts \
    --compile \
    --target="bun-$PLATFORM" \
    --outfile "$OUT_DIR/bun/pi" \
    --external="@earendil-works/pi-*" \
    --define="process.env.NODE_ENV='production'"
  
  # 驗證
  "$OUT_DIR/bun/pi" --version
fi

# 4. 產生 SHA256SUMS
cd "$OUT_DIR"
sha256sum node/pi bun/pi > SHA256SUMS

Binary 結構

/tmp/pi-release/
├── node/
│   ├── pi                    # Node 入口腳本
│   ├── package.json
│   ├── node_modules/         # 依賴
│   └── @earendil-works/      # Workspace 套件
└── bun/
    └── pi                    # Bun Executable (單檔、~30MB)

跨平台編譯

# Linux x64
./scripts/build-binaries.sh 0.12.3 linux-x64 /tmp/out

# Linux ARM64
./scripts/build-binaries.sh 0.12.3 linux-arm64 /tmp/out

# macOS x64
./scripts/build-binaries.sh 0.12.3 darwin-x64 /tmp/out

# macOS ARM64 (Apple Silicon)
./scripts/build-binaries.sh 0.12.3 darwin-arm64 /tmp/out

# Windows x64
./scripts/build-binaries.sh 0.12.3 win32-x64 /tmp/out

npm-shrinkwrap.json:傳遞依賴鎖死

生成流程

// scripts/generate-coding-agent-shrinkwrap.mjs
async function generateShrinkwrap() {
  // 1. 讀取根 package-lock.json
  const lockfile = JSON.parse(readFileSync("package-lock.json", "utf8"));
  
  // 2. 解析 coding-agent 依賴樹
  const codingAgentDeps = extractDependencyTree(lockfile, "@earendil-works/pi-coding-agent");
  
  // 3. 生成 shrinkwrap (包含所有傳遞依賴精確版本)
  const shrinkwrap = {
    name: "@earendil-works/pi-coding-agent",
    version: getVersion(),
    lockfileVersion: 3,
    requires: true,
    dependencies: codingAgentDeps,
  };
  
  // 4. 驗證 Lifecycle Scripts Allowlist
  validateLifecycleScripts(shrinkwrap);
  
  // 5. 寫入
  writeFileSync(
    "packages/coding-agent/npm-shrinkwrap.json",
    JSON.stringify(shrinkwrap, null, 2)
  );
}

Allowlist 驗證

const ALLOWED_LIFECYCLE_PACKAGES = new Set([
  "@biomejs/biome",      // 二進位
  "@anthropic-ai/sandbox-runtime",
  "esbuild",             // 二進位
  // ... 經審核的套件
]);

function validateLifecycleScripts(shrinkwrap) {
  for (const [name, pkg] of Object.entries(shrinkwrap.dependencies)) {
    if (pkg.scripts && Object.keys(pkg.scripts).length > 0) {
      if (!ALLOWED_LIFECYCLE_PACKAGES.has(name)) {
        throw new Error(`Package ${name} has lifecycle scripts but not in allowlist`);
      }
    }
  }
}

Trusted Publishing:OIDC 免 Token

GitHub Actions Workflow

# .github/workflows/build-binaries.yml
jobs:
  publish-npm:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # 關鍵:OIDC Token
      contents: read
      attestations: write  # Provenance
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          registry-url: 'https://registry.npmjs.org'
      - name: Install
        run: npm ci --ignore-scripts
      - name: Build
        run: npm run build
      - name: Verify
        run: npm run check
      - name: Publish to npm
        run: npm publish --provenance --access public
        env:
          # 不需要 NODE_AUTH_TOKEN!OIDC 自動處理

OIDC 流程

GitHub Actions          npm Registry
     │                       │
     ├─ Request OIDC Token ──┤
     │◄──────── Token ───────┤
     │                       │
     ├─ npm publish (with token) ─►
     │                       │
     │◄──── Success ────────┤
     │                       │

Provenance 驗證

# 發佈後驗證
npm view @earendil-works/pi-coding-agent@0.12.3 --json | jq '.provenance'
# 顯示 SLSA Provenance、GitHub Actions 來源、Commit SHA

R2 Release Marker:發佈驗證

問題

npm 發佈是最終一致的——npm view 可能暫時查不到新版本。

解法:R2 Marker

# .github/workflows/build-binaries.yml
jobs:
  announce-pi-dev-release:
    needs: publish-npm
    runs-on: ubuntu-latest
    steps:
      - name: Wait for npm availability
        run: |
          for i in {1..30}; do
            if npm view @earendil-works/pi-coding-agent@${VERSION} version 2>/dev/null; then
              echo "Available on npm"
              break
            fi
            sleep 10
          done
      
      - name: Write release marker to R2
        run: |
          # 寫入 marker 到 Cloudflare R2
          cat > marker.json <<EOF
          {
            "version": "${VERSION}",
            "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
            "packages": [
              "@earendil-works/pi-ai",
              "@earendil-works/pi-agent-core",
              "@earendil-works/pi-coding-agent",
              "@earendil-works/pi-tui",
              "@earendil-works/pi-telemetry",
              "@earendil-works/pi-client",
              "@earendil-works/pi-server",
              "@earendil-works/pi-protocol"
            ],
            "binaries": {
              "node": "https://github.com/earendil-works/pi/releases/download/v${VERSION}/pi-${VERSION}-node.tar.gz",
              "bun": "https://github.com/earendil-works/pi/releases/download/v${VERSION}/pi-${VERSION}-bun.tar.gz"
            }
          }
          EOF
          
          # 上傳到 R2
          aws s3 cp marker.json s3://pi-release-markers/v${VERSION}.json \
            --endpoint-url https://<account>.r2.cloudflarestorage.com

pi.dev 版本 API

// pi.dev/api/latest-version 端點
export async function getLatestVersion(): Promise<VersionInfo> {
  const marker = await r2.get("latest.json");
  if (!marker) throw new Error("No release marker");
  
  // 驗證所有套件在 npm 可用
  for (const pkg of marker.packages) {
    const npmVersion = await fetch(`https://registry.npmjs.org/${pkg}/latest`);
    if (npmVersion.version !== marker.version) {
      throw new Error(`Package ${pkg} not yet available at version ${marker.version}`);
    }
  }
  
  return marker;
}

Announcement Verification

# Announcement Job: 確保公告只在驗證通過後發送
announce-release:
  needs: announce-pi-dev-release
  runs-on: ubuntu-latest
  steps:
    - name: Post to Discord
      run: |
        curl -X POST $DISCORD_WEBHOOK \
          -H "Content-Type: application/json" \
          -d '{"content": "🚀 Pi v${VERSION} released! ..."}'
    - name: Tweet
      run: |
        # 發推文
    - name: Update pi.dev banner
      run: |
        # 更新網站橫幅

完整 Release Checklist

# Release Checklist v0.12.3

## Pre-release
- [ ] 所有 PR merged 到 main
- [ ] CHANGELOG 已更新 (`/cl` prompt)
- [ ] 本地 `npm run check` 通過
- [ ] 沒有未提交的變更

## Release
- [ ] `npm run release:patch` (或 minor)
- [ ] 等待 CI 完成
- [ ] 確認 GitHub Release 建立
- [ ] 確認 npm 套件發佈
- [ ] 確認 Binary Release 上傳
- [ ] 確認 R2 Marker 寫入
- [ ] 確認 pi.dev/api/latest-version 返回新版本
- [ ] 確認 Announcement 發送

## Post-release
- [ ] 手動驗證 `npm install -g @earendil-works/pi-coding-agent@latest`
- [ ] 手動驗證 `pi --version`
- [ ] 更新文件版本參考
- [ ] 通知社群

系列總結

至此,pi-mono 深度導讀系列 全 17 篇完結:

篇數標題核心主題
0系列導覽與專案概覽專案全景、閱讀地圖
1CLI 使用者視角安裝、模式、Session、模型切換
2Monorepo 架構7 套件分工、依賴單向流
3pi-ai 統一 LLM APIProvider Factory、Lazy Loading、Model Catalog
4Agent Loop 雙層循環Steering/Follow-up、事件流
5Session TreeAppend-only、Branching、Compaction
6Tool System定義、執行、Parallel/Sequential、Hooks
7Extension SystemHooks、Custom Tools、UI、Lifecycle
8TUI 架構Differential Rendering、Layout、CSI 2026
9Model Catalog/OAuth自動生成、Lazy Loading、Credential Sync
10Remote SessionJSON-RPC、WebSocket、Reconnection
11TelemetrySchema、Conformance、Vendor-neutral
12Compaction 深度Token Estimation、Cut Point、Structured
13Agent Harness/SkillsSystem Prompt、Prompt Templates
14Testing/Quality GatesFaux Provider、Browser Smoke、Biome、tsgo
15Containerization/SandboxGondolin、Docker、OpenShell
16Release PipelineLockstep、Binary、Trusted Publishing

參考資料