Skip to content

Build Dist Tarball

Build Dist Tarball #6

Workflow file for this run

name: Build Dist Tarball
# 方案 B:CI 只 build 一次(dist 跨平台),ship dist+lockfile(不 ship node_modules)+ pnpm.cjs
# + 4 平台 portable Node → 4 个瘦 tarball。
# 用户侧 install.sh 跑 `pnpm install --prod --frozen-lockfile`(用 bundled pnpm + portable Node)
# 拉依赖+native prebuilt,无 tsc/无 OOM。
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build + attach (e.g. v5.6.0);由 release.yml 推 tag 后触发'
required: true
type: string
permissions:
contents: write
concurrency:
group: build-dist-${{ github.ref }}
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-24.04
steps:
- name: Checkout xiaobei
uses: actions/checkout@v5
- name: Read pinned openclaw version
id: pin
run: |
source openclaw.version
echo "commit=$OPENCLAW_COMMIT" >> "$GITHUB_OUTPUT"
echo "version=$OPENCLAW_VERSION" >> "$GITHUB_OUTPUT"
- name: Setup Node + pnpm (build env)
# v6:package-manager-cache 限 npm-only,避免 pnpm 装好前自动开缓存报
# "Unable to locate executable file: pnpm"(setup-node#1351)
uses: actions/setup-node@v6
with:
node-version: '24.15.0'
- uses: pnpm/action-setup@v6
with:
version: 10.30.2
- name: Clone openclaw at pinned commit
run: |
git init openclaw
git -C openclaw remote add origin https://github.com/openclaw/openclaw.git
git -C openclaw fetch --depth=1 origin ${{ steps.pin.outputs.commit }}
git -C openclaw checkout FETCH_HEAD
- name: Apply patches + pnpm install (for build)
run: bash scripts/apply-addons.sh --no-build --no-restart --skip-crew
- name: Build openclaw dist
run: |
cd openclaw
NODE_OPTIONS=--max-old-space-size=8192 pnpm build
NODE_OPTIONS=--max-old-space-size=8192 pnpm ui:build
env:
CI: 'true'
- name: Build camoufox-cli fork dist (no global install)
run: |
cd patches/camoufox-cli
npm install --registry=https://registry.npmmirror.com
npm run build
- name: Fetch standalone pnpm (npm package, self-contained)
run: |
curl -fsSL -o /tmp/pnpm.tgz https://registry.npmmirror.com/pnpm/-/pnpm-11.2.2.tgz
rm -rf /tmp/pnpm-pkg
mkdir -p /tmp/pnpm-pkg
tar -xzf /tmp/pnpm.tgz -C /tmp/pnpm-pkg
# 入口 bin/pnpm.mjs(main 字段),dist/pnpm.mjs 是自包含 bundle
test -f /tmp/pnpm-pkg/package/bin/pnpm.mjs
test -f /tmp/pnpm-pkg/package/dist/pnpm.mjs
- name: Stage engine (no node_modules)
id: stage
run: |
set -euo pipefail
REPO="$GITHUB_WORKSPACE"
STAGE="$RUNNER_TEMP/engine"
rm -rf "$STAGE"
mkdir -p "$STAGE/bin" "$STAGE/tools"
# 1. openclaw 引擎:dist + 源码 + lockfile + package.jsons,排除 node_modules / .git / 缓存
# 用户侧 pnpm install --prod 按 lockfile 重建 node_modules
rsync -a \
--exclude='node_modules' \
--exclude='.git' \
--exclude='*.tsbuildinfo' \
--exclude='coverage' \
"$REPO/openclaw/" "$STAGE/openclaw/"
# 2. standalone pnpm(用户侧 pnpm install --prod 用,入口 tools/pnpm/bin/pnpm.mjs)
cp -a /tmp/pnpm-pkg/package "$STAGE/tools/pnpm"
# 3. camoufox-cli fork(dist 已 build,用户侧 npm install -g .)
rsync -a --exclude='node_modules' --exclude='.git' \
"$REPO/patches/camoufox-cli/" "$STAGE/camoufox-cli/"
# 4. xiaobei 模板与脚本
cp -a "$REPO/crews" "$STAGE/crews"
cp -a "$REPO/skills" "$STAGE/skills"
cp -a "$REPO/config-templates" "$STAGE/config-templates"
cp -a "$REPO/scripts" "$STAGE/scripts"
# awada 本地插件(TS,jiti 加载;ship 源码 + lockfile,排除 node_modules,
# 用户侧 install.sh 在 awada/ 下 npm install --omit=dev 装 ws+zod)
rsync -a --exclude='node_modules' --exclude='.git' \
"$REPO/awada/" "$STAGE/awada/"
cp "$REPO/openclaw.version" "$REPO/version" "$STAGE/"
# 仓根 requirements.txt(install.sh install_python_deps 扫 skills/crews/仓根,
# 不打进则用户侧 Step 4 报 "No requirements.txt found")
[ -f "$REPO/requirements.txt" ] && cp "$REPO/requirements.txt" "$STAGE/" || true
# openclaw-weixin 插件 pin(install.sh 据此装插件,国内 npmmirror)
[ -f "$REPO/openclaw-weixin.version.json" ] && cp "$REPO/openclaw-weixin.version.json" "$STAGE/" || true
# 5. 体积 breakdown(诊断)
(
set +e +o pipefail
echo "📊 engine top-level:"
du -sh "$STAGE"/* 2>/dev/null | sort -rh
echo "📊 openclaw top-level:"
du -sh "$STAGE/openclaw"/* 2>/dev/null | sort -rh | head -15
) || true
echo "stage_dir=$STAGE" >> "$GITHUB_OUTPUT"
- name: Build 4 platform tarballs (engine + portable Node)
id: buildtars
run: |
set -euo pipefail
STAGE="${{ steps.stage.outputs.stage_dir }}"
XIAOBEI_VER="$(tr -d '[:space:]' < "$GITHUB_WORKSPACE/version")"
# asset tag 优先级:workflow_dispatch inputs.tag > git tag(push 触发)> version 文件
INPUT_TAG="${{ inputs.tag }}"
REF_NAME="${GITHUB_REF##*/}"
if [[ -n "$INPUT_TAG" ]]; then ASSET_TAG="$INPUT_TAG"
elif [[ "$REF_NAME" == v* ]]; then ASSET_TAG="$REF_NAME"
else ASSET_TAG="$XIAOBEI_VER"; fi
echo "ASSET_TAG=$ASSET_TAG"
mkdir -p "$RUNNER_TEMP/out"
# 平台 → (node_url, subdir)
declare -A PLATFORMS=(
[linux-x64]="https://nodejs.org/dist/v24.15.0/node-v24.15.0-linux-x64.tar.xz|node-v24.15.0-linux-x64"
[mac-arm64]="https://nodejs.org/dist/v24.15.0/node-v24.15.0-darwin-arm64.tar.xz|node-v24.15.0-darwin-arm64"
[mac-x64]="https://nodejs.org/dist/v24.15.0/node-v24.15.0-darwin-x64.tar.xz|node-v24.15.0-darwin-x64"
[win-x64]="https://nodejs.org/dist/v24.15.0/node-v24.15.0-win-x64.zip|node-v24.15.0-win-x64"
)
for plat in linux-x64 mac-arm64 mac-x64 win-x64; do
IFS='|' read -r url subdir <<< "${PLATFORMS[$plat]}"
echo "=== packaging $plat ==="
PKG="$RUNNER_TEMP/pkg-$plat"
rm -rf "$PKG"
cp -a "$STAGE" "$PKG"
# asset 名用 git tag(release 的 source of truth),与 install.sh 按 tag 拉对齐
TAG="$ASSET_TAG"
# portable Node
if [[ "$plat" == win-x64 ]]; then
curl -fsSL -o /tmp/node.zip "$url"
unzip -q /tmp/node.zip -d /tmp/
mv "/tmp/$subdir" "$PKG/tools/node"
else
curl -fsSL -o /tmp/node.tar.xz "$url"
tar -xJf /tmp/node.tar.xz -C /tmp/
mv "/tmp/$subdir" "$PKG/tools/node"
fi
# wrapper: <ROOT>/bin/openclaw → portable node + openclaw.mjs(ROOT 默认 ~/.xiaobei)
cat > "$PKG/bin/openclaw" <<'EOF'
#!/usr/bin/env bash
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$HERE/tools/node/bin/node" "$HERE/openclaw/openclaw.mjs" "$@"
EOF
chmod +x "$PKG/bin/openclaw"
# Windows 原生 .cmd wrapper(WSL/Git Bash 用户可用上面 bash 版)
if [[ "$plat" == win-x64 ]]; then
cat > "$PKG/bin/openclaw.cmd" <<'EOF'
@echo off
setlocal
set "HERE=%~dp0.."
"%HERE%\tools\node\node.exe" "%HERE%\openclaw\openclaw.mjs" %*
EOF
fi
# mac/win 用 .tar.gz(bsdtar 原生支持 gzip,小白机无 zstd 二进制);linux 用 .tar.zst(压缩率更好)
if [[ "$plat" == linux-x64 ]]; then
ASSET="xiaobei-${TAG}-${plat}.tar.zst"
tar --zstd -cf "$RUNNER_TEMP/out/$ASSET" -C "$PKG" .
else
ASSET="xiaobei-${TAG}-${plat}.tar.gz"
tar -czf "$RUNNER_TEMP/out/$ASSET" -C "$PKG" .
fi
ls -lh "$RUNNER_TEMP/out/$ASSET"
done
# 给后续步骤用
echo "out_dir=$RUNNER_TEMP/out" >> "$GITHUB_OUTPUT"
echo "asset_tag=$ASSET_TAG" >> "$GITHUB_OUTPUT"
ls -lh "$RUNNER_TEMP/out/"
- name: Smoke test linux tarball (extract + install --skip-browser + openclaw --version)
run: |
set -euo pipefail
ASSET_TAG="${{ steps.buildtars.outputs.asset_tag }}"
if [[ "$ASSET_TAG" != v* ]]; then
echo "无 release tag(asset_tag=$ASSET_TAG),跳过冒烟自检"
exit 0
fi
TB="$RUNNER_TEMP/out/xiaobei-$ASSET_TAG-linux-x64.tar.zst"
[[ -f "$TB" ]] || { echo "❌ tarball 不存在:$TB"; exit 1; }
SMOKE="$RUNNER_TEMP/smoke"
rm -rf "$SMOKE"
mkdir -p "$SMOKE/extract" "$SMOKE/program" "$SMOKE/runtime"
# 解压一份拿到 scripts/install.sh(install.sh 会再按 XIAOBEI_TARBALL 解压到 program)
tar --zstd -xf "$TB" -C "$SMOKE/extract"
[[ -f "$SMOKE/extract/scripts/install.sh" ]] || { echo "❌ tarball 缺 scripts/install.sh"; exit 1; }
export XIAOBEI_HOME="$SMOKE/program"
export OPENCLAW_HOME="$SMOKE/runtime"
export XIAOBEI_TARBALL="$TB"
export XIAOBEI_TAG="$ASSET_TAG"
export XIAOBEI_SOURCE=github
export AWK_API_KEY="smoke-test-key"
bash "$SMOKE/extract/scripts/install.sh" \
--no-prompt --skip-bind --skip-browser --root "$XIAOBEI_HOME"
"$XIAOBEI_HOME/bin/openclaw" --version
echo "✅ 冒烟自检通过:tarball 可解压 + 依赖可装 + 引擎可起"
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: xiaobei-tarballs
path: |
${{ runner.temp }}/out/*.tar.zst
${{ runner.temp }}/out/*.tar.gz
retention-days: 14
- name: Attach to release (on real tag, skip disttest)
if: ${{ startsWith(steps.buildtars.outputs.asset_tag, 'v') && !contains(steps.buildtars.outputs.asset_tag, 'disttest') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ steps.buildtars.outputs.asset_tag }}"
if ! gh release view "$TAG" >/dev/null 2>&1; then
gh release create "$TAG" --title "$TAG" --generate-notes
fi
for f in "$RUNNER_TEMP"/out/*.tar.zst "$RUNNER_TEMP"/out/*.tar.gz; do
[ -e "$f" ] || continue
gh release upload "$TAG" "$f" --clobber
done