Skip to content

Build OpenClaw Standalone #22

Build OpenClaw Standalone

Build OpenClaw Standalone #22

Workflow file for this run

name: Build OpenClaw Standalone
on:
# Manual trigger with version input
workflow_dispatch:
inputs:
openclaw_version:
description: 'OpenClaw version to package (leave empty for latest)'
required: false
default: ''
openclaw_pkg:
description: 'NPM package name (openclaw or @qingchencloud/openclaw-zh)'
required: false
default: '@qingchencloud/openclaw-zh'
upload_r2:
description: 'Upload to Cloudflare R2'
required: false
type: boolean
default: true
# Auto-trigger on tag push
push:
branches:
- main
tags:
- 'v*'
env:
NODE_VERSION: '22'
OPENCLAW_PKG: ${{ github.event.inputs.openclaw_pkg || '@qingchencloud/openclaw-zh' }}
NPM_REGISTRY: 'https://registry.npmmirror.com'
jobs:
# --- Resolve version ---
resolve-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
pkg_spec: ${{ steps.version.outputs.pkg_spec }}
steps:
- name: Resolve OpenClaw version
id: version
run: |
INPUT_VER="${{ github.event.inputs.openclaw_version }}"
if [ -n "$INPUT_VER" ]; then
echo "version=$INPUT_VER" >> $GITHUB_OUTPUT
echo "pkg_spec=${{ env.OPENCLAW_PKG }}@$INPUT_VER" >> $GITHUB_OUTPUT
else
# Get latest version from npm
VER=$(npm view "${{ env.OPENCLAW_PKG }}" version --registry "${{ env.NPM_REGISTRY }}" 2>/dev/null || echo "")
if [ -z "$VER" ]; then
echo "Failed to resolve latest version"
exit 1
fi
echo "version=$VER" >> $GITHUB_OUTPUT
echo "pkg_spec=${{ env.OPENCLAW_PKG }}@$VER" >> $GITHUB_OUTPUT
fi
echo "Resolved version: $(cat $GITHUB_OUTPUT | grep version= | head -1)"
# --- Build matrix ---
build:
needs: resolve-version
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
platform: win-x64
archive_ext: zip
- os: macos-15
platform: mac-arm64
archive_ext: tar.gz
- os: ubuntu-latest
platform: linux-x64
archive_ext: tar.gz
- os: ubuntu-24.04-arm
platform: linux-arm64
archive_ext: tar.gz
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Show environment
run: |
node --version
npm --version
echo "Platform: ${{ matrix.platform }}"
echo "Version: ${{ needs.resolve-version.outputs.version }}"
# ===== Build =====
- name: Enable Windows Long Paths
if: runner.os == 'Windows'
shell: pwsh
run: |
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" `
-Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force | Out-Null
git config --system core.longpaths true
- name: Create build directory
shell: bash
run: mkdir -p build/${{ matrix.platform }}
- name: Install OpenClaw
shell: bash
working-directory: build/${{ matrix.platform }}
run: |
echo '{ "name": "openclaw-standalone-build", "private": true }' > package.json
npm install "${{ needs.resolve-version.outputs.pkg_spec }}" \
--registry "${{ env.NPM_REGISTRY }}" \
--include=optional
rm -f package.json package-lock.json
- name: Copy Node.js binary (Unix)
if: runner.os != 'Windows'
run: |
cp "$(which node)" "build/${{ matrix.platform }}/node"
chmod +x "build/${{ matrix.platform }}/node"
- name: Copy Node.js binary (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
Copy-Item (Get-Command node).Source "build/${{ matrix.platform }}/node.exe"
- name: Copy shim (Unix)
if: runner.os != 'Windows'
run: |
cp shims/openclaw "build/${{ matrix.platform }}/openclaw"
chmod +x "build/${{ matrix.platform }}/openclaw"
- name: Copy shim (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
Copy-Item "shims/openclaw.cmd" "build/${{ matrix.platform }}/openclaw.cmd"
- name: Write VERSION file
shell: bash
run: |
cat > "build/${{ matrix.platform }}/VERSION" <<EOF
openclaw_version=${{ needs.resolve-version.outputs.version }}
node_version=$(node --version)
platform=${{ matrix.platform }}
build_date=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
EOF
# ===== Patch upstream bugs =====
- name: Patch missing changelog.js (upstream bug in @mariozechner/pi-coding-agent)
shell: bash
run: |
STUB="build/${{ matrix.platform }}/node_modules/@mariozechner/pi-coding-agent/dist/utils/changelog.js"
if [ ! -f "$STUB" ]; then
echo "Patching: creating missing changelog.js stub"
mkdir -p "$(dirname "$STUB")"
cat > "$STUB" <<'STUBEOF'
export function getChangelogPath() { return null }
export function parseChangelog() { return [] }
export function getNewEntries() { return [] }
STUBEOF
fi
# ===== Cleanup to reduce size =====
- name: Cleanup unnecessary files (Unix)
if: runner.os != 'Windows'
run: |
echo "Before: $(du -sm build/${{ matrix.platform }}/node_modules | cut -f1)MB"
find "build/${{ matrix.platform }}/node_modules" -type f \( \
-name "*.ts" ! -name "*.d.ts" -o \
-name "*.map" -o \
-name "CHANGELOG*" -o \
-name "HISTORY*" -o \
-name "AUTHORS*" -o \
-name ".npmignore" -o \
-name ".eslintrc*" -o \
-name ".prettierrc*" -o \
-name "Makefile" -o \
-name ".editorconfig" -o \
-name ".travis.yml" \
\) -delete 2>/dev/null || true
find "build/${{ matrix.platform }}/node_modules" -type d \( \
-name "test" -o -name "tests" -o -name "__tests__" -o \
-name "spec" -o -name "example" -o -name "examples" -o \
-name ".github" -o -name ".circleci" \
\) -exec rm -rf {} + 2>/dev/null || true
echo "After: $(du -sm build/${{ matrix.platform }}/node_modules | cut -f1)MB"
- name: Cleanup unnecessary files (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$nmDir = "build/${{ matrix.platform }}/node_modules"
$patterns = @("*.ts", "*.map", "CHANGELOG*", "HISTORY*", "AUTHORS*",
".npmignore", ".eslintrc*", ".prettierrc*", "Makefile",
".editorconfig", ".travis.yml")
$dirPatterns = @("test", "tests", "__tests__", "spec", "example", "examples", ".github", ".circleci")
foreach ($p in $patterns) {
Get-ChildItem -Path $nmDir -Recurse -Filter $p -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -notlike "*.d.ts" } |
Remove-Item -Force -ErrorAction SilentlyContinue
}
foreach ($d in $dirPatterns) {
Get-ChildItem -Path $nmDir -Recurse -Directory -Filter $d -ErrorAction SilentlyContinue |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}
# Remove deeply nested node_modules to avoid MAX_PATH overflow in Inno Setup
foreach ($pkgPath in @("$nmDir\@qingchencloud\openclaw-zh", "$nmDir\openclaw")) {
if (Test-Path $pkgPath) {
Get-ChildItem -Path $pkgPath -Directory -Filter "node_modules" -Recurse -ErrorAction SilentlyContinue |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}
}
# ===== Package =====
- name: Create archive (Unix)
if: runner.os != 'Windows'
run: |
mkdir -p output
mv "build/${{ matrix.platform }}" "build/openclaw"
tar -czf "output/openclaw-${{ needs.resolve-version.outputs.version }}-${{ matrix.platform }}.tar.gz" \
-C build openclaw
mv "build/openclaw" "build/${{ matrix.platform }}"
# Generate checksum
cd output
shasum -a 256 *.tar.gz > "openclaw-${{ needs.resolve-version.outputs.version }}-${{ matrix.platform }}.tar.gz.sha256" 2>/dev/null || \
sha256sum *.tar.gz > "openclaw-${{ needs.resolve-version.outputs.version }}-${{ matrix.platform }}.tar.gz.sha256" 2>/dev/null || true
ls -lh
- name: Create archive (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path output | Out-Null
$version = "${{ needs.resolve-version.outputs.version }}"
$zipName = "openclaw-$version-win-x64.zip"
Rename-Item "build/${{ matrix.platform }}" "openclaw"
Compress-Archive -Path "build/openclaw" -DestinationPath "output/$zipName" -CompressionLevel Optimal
Rename-Item "build/openclaw" "${{ matrix.platform }}"
# Generate checksum
$hash = (Get-FileHash "output/$zipName" -Algorithm SHA256).Hash.ToLower()
"$hash $zipName" | Set-Content "output/$zipName.sha256"
Get-ChildItem output
# ===== Inno Setup installer (Windows only) =====
- name: Build Inno Setup installer
if: runner.os == 'Windows'
shell: pwsh
run: |
$version = "${{ needs.resolve-version.outputs.version }}"
$sourceDir = "${{ github.workspace }}\build\${{ matrix.platform }}"
$outputDir = "${{ github.workspace }}\output"
# Inno Setup is pre-installed on windows-latest
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" `
"/DAppVersion=$version" `
"/DSourceDir=$sourceDir" `
"/DOutputDir=$outputDir" `
"installer\setup.iss"
# ===== Upload artifacts =====
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: openclaw-${{ matrix.platform }}
path: output/*
retention-days: 30
# --- Create Release ---
release:
needs: [resolve-version, build]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: false
- name: Collect all files
run: |
mkdir -p release
find artifacts -type f \( -name "*.zip" -o -name "*.tar.gz" -o -name "*.exe" -o -name "*.sha256" \) \
-exec cp {} release/ \;
ls -lh release/
- name: Generate latest.json manifest
run: |
VERSION="${{ needs.resolve-version.outputs.version }}"
cat > release/latest.json <<EOF
{
"version": "$VERSION",
"date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"downloads": {
"win-x64": {
"zip": "openclaw-$VERSION-win-x64.zip",
"installer": "openclaw-$VERSION-win-x64-setup.exe"
},
"mac-arm64": {
"archive": "openclaw-$VERSION-mac-arm64.tar.gz"
},
"linux-x64": {
"archive": "openclaw-$VERSION-linux-x64.tar.gz"
},
"linux-arm64": {
"archive": "openclaw-$VERSION-linux-arm64.tar.gz"
}
}
}
EOF
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.resolve-version.outputs.version }}
name: OpenClaw ${{ needs.resolve-version.outputs.version }}
body: |
## OpenClaw ${{ needs.resolve-version.outputs.version }} 独立安装包
**零依赖安装** — 无需 Node.js,无需 npm,下载即用!
### 📥 下载
| 平台 | 文件 | 说明 |
|------|------|------|
| Windows x64 | `*-win-x64-setup.exe` | **推荐** 引导式安装 |
| Windows x64 | `*-win-x64.zip` | 绿色免安装版 |
| macOS ARM (Apple Silicon) | `*-mac-arm64.tar.gz` | 解压即用 |
| Linux x64 | `*-linux-x64.tar.gz` | 解压即用 |
| Linux ARM64 (树莓派等) | `*-linux-arm64.tar.gz` | 解压即用 |
### 🚀 安装方法
**Windows**: 下载 `.exe` 安装包,双击运行安装向导
**macOS / Linux**:
```bash
curl -fsSL https://dl.qrj.ai/openclaw/install.sh | bash
```
或手动: 下载对应平台的 `.tar.gz` → 解压 → 将目录加入 PATH
---
由 [晴辰云](https://gpt.qt.cool) 构建 · [ClawPanel 图形管理面板](https://github.com/qingchencloud/clawpanel)
files: release/*
draft: false
prerelease: false
# --- Upload to R2 ---
upload-r2:
needs: [resolve-version, build, release]
runs-on: ubuntu-latest
if: >-
(startsWith(github.ref, 'refs/tags/v') || github.event.inputs.upload_r2 == 'true')
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: false
- name: Collect files
run: |
mkdir -p upload
find artifacts -type f \( -name "*.zip" -o -name "*.tar.gz" -o -name "*.exe" -o -name "*.sha256" \) \
-exec cp {} upload/ \;
ls -lh upload/
- name: Setup Node.js for wrangler
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install wrangler
run: npm install -g wrangler
- name: Upload files to R2
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
VERSION="${{ needs.resolve-version.outputs.version }}"
PREFIX="openclaw-standalone/${VERSION}"
FAILED=0
for file in upload/*; do
FILENAME=$(basename "$file")
SIZE_MB=$(( $(stat --format=%s "$file") / 1048576 ))
echo "Uploading $FILENAME (${SIZE_MB}MB) ..."
if [ "$SIZE_MB" -gt 290 ]; then
echo "⚠ Skipping $FILENAME (${SIZE_MB}MB > 290MB wrangler limit, available in GitHub Release)"
continue
fi
if wrangler r2 object put "clawpanel-releases/${PREFIX}/${FILENAME}" --file "$file" --remote; then
echo "✓ $FILENAME uploaded"
else
echo "⚠ Failed to upload $FILENAME, continuing..."
FAILED=$((FAILED + 1))
fi
done
echo "Upload complete (skipped/failed: $FAILED)"
- name: Upload latest.json to R2
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
VERSION="${{ needs.resolve-version.outputs.version }}"
cat > /tmp/latest.json <<EOF
{
"version": "$VERSION",
"date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"base_url": "https://dl.qrj.ai/openclaw-standalone/$VERSION"
}
EOF
wrangler r2 object put "clawpanel-releases/openclaw-standalone/latest.json" \
--file /tmp/latest.json --content-type application/json --remote
echo "latest.json updated to version $VERSION"