# ClawX Release Workflow # Builds and publishes releases for macOS, Windows, and Linux name: Release on: push: tags: - 'v*' workflow_dispatch: inputs: version: description: 'Version to release (e.g., 1.0.0)' required: true permissions: contents: write jobs: release: strategy: matrix: include: - os: macos-latest platform: mac - os: windows-latest platform: win - os: ubuntu-latest platform: linux runs-on: ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Setup pnpm uses: pnpm/action-setup@v4 - name: Get pnpm store directory shell: bash run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache uses: actions/cache@v4 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install dependencies run: pnpm install - name: Download uv binaries for macOS if: matrix.platform == 'mac' run: pnpm run uv:download:mac - name: Download uv binaries for Windows if: matrix.platform == 'win' run: pnpm run uv:download:win - name: Download uv binaries for Linux if: matrix.platform == 'linux' run: pnpm run uv:download:linux # macOS specific steps # --publish never: prevent electron-builder from auto-publishing to GitHub. # All artifacts are collected and published atomically in the publish job. - name: Build macOS if: matrix.platform == 'mac' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_LINK: ${{ secrets.MAC_CERTS }} CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | ulimit -n 65536 echo "File descriptor limit: $(ulimit -n)" pnpm run build:vite && zx scripts/bundle-openclaw.mjs && electron-builder --mac --publish never # Windows specific steps - name: Build Windows if: matrix.platform == 'win' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: pnpm run build:vite && zx scripts/bundle-openclaw.mjs && electron-builder --win --publish never # Linux specific steps - name: Build Linux if: matrix.platform == 'linux' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: pnpm run build:vite && zx scripts/bundle-openclaw.mjs && electron-builder --linux --publish never - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: release-${{ matrix.platform }} path: | release/*.dmg release/*.zip release/*.exe release/*.AppImage release/*.deb release/*.rpm release/*.yml !release/builder-debug.yml retention-days: 7 # ────────────────────────────────────────────────────────────── # Job: Publish to GitHub Releases # ────────────────────────────────────────────────────────────── publish: needs: release runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Download all artifacts uses: actions/download-artifact@v4 with: path: release-artifacts - name: List all downloaded artifacts run: | echo "=== All artifacts downloaded ===" find release-artifacts/ -type f -exec ls -lh {} \; echo "" echo "=== File tree ===" tree release-artifacts/ || find release-artifacts/ -print - name: Remove duplicate builder-debug files run: | echo "Removing builder-debug.yml files to avoid duplicate asset upload conflicts..." find release-artifacts/ -name "builder-debug.yml" -delete -print || true - name: Create GitHub Release (as pre-release) uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') with: files: | release-artifacts/**/*.dmg release-artifacts/**/*.zip release-artifacts/**/*.exe release-artifacts/**/*.AppImage release-artifacts/**/*.deb release-artifacts/**/*.rpm release-artifacts/**/*.yml draft: false prerelease: true make_latest: false generate_release_notes: true body: | ## 🚀 ClawX ${{ github.ref_name }} ClawX - Graphical AI Assistant based on OpenClaw ### 📦 Downloads Please select the appropriate installer for your operating system and architecture: #### macOS - **Apple Silicon (M1/M2/M3/M4)**: `ClawX-*-mac-arm64.dmg` - **Intel (x64)**: `ClawX-*-mac-x64.dmg` #### Windows - **Installer (x64)**: `ClawX-*-win-x64.exe` - **Installer (ARM64)**: `ClawX-*-win-arm64.exe` #### Linux - **AppImage (x64)**: `ClawX-*-linux-x86_64.AppImage` (Universal format, recommended) - **AppImage (ARM64)**: `ClawX-*-linux-arm64.AppImage` - **Debian/Ubuntu (x64)**: `ClawX-*-linux-amd64.deb` - **Debian/Ubuntu (ARM64)**: `ClawX-*-linux-arm64.deb` - **RPM (x64)**: `ClawX-*-linux-x86_64.rpm` ### 📝 Release Notes See the auto-generated release notes below for detailed changes. ### ⚠️ Installation Notes - **macOS**: On first launch, you may see "cannot verify developer". Go to System Preferences → Security & Privacy to allow the app to run - **Windows**: SmartScreen may block the app. Click "More info" → "Run anyway" to proceed - **Linux**: AppImage requires executable permission: `chmod +x ClawX-*.AppImage` --- 💬 Found an issue? Please submit an [Issue](https://github.com/${{ github.repository }}/issues) env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ────────────────────────────────────────────────────────────── # Job: Upload to Alibaba Cloud OSS # Uploads all release artifacts to OSS for: # - Official website downloads (via release-info.json) # - electron-updater auto-update (via {channel}-*.yml) # # Directory structure on OSS (channel-separated): # latest/ → stable releases (latest.yml, latest-mac.yml, …) # alpha/ → alpha releases (alpha.yml, alpha-mac.yml, …) # beta/ → beta releases (beta.yml, beta-mac.yml, …) # releases/vX.Y.Z/ → permanent archive, never deleted # ────────────────────────────────────────────────────────────── upload-oss: needs: release runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Download all artifacts uses: actions/download-artifact@v4 with: path: release-artifacts - name: Extract version and channel id: version run: | if [[ "${{ github.ref }}" == refs/tags/v* ]]; then VERSION="${GITHUB_REF#refs/tags/v}" else VERSION="${{ github.event.inputs.version }}" fi # Detect channel from semver prerelease tag # e.g. 0.1.8-alpha.0 → alpha, 1.0.0-beta.1 → beta, 1.0.0 → latest if [[ "$VERSION" =~ -([a-zA-Z]+) ]]; then CHANNEL="${BASH_REMATCH[1]}" else CHANNEL="latest" fi echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "tag=v${VERSION}" >> $GITHUB_OUTPUT echo "channel=${CHANNEL}" >> $GITHUB_OUTPUT echo "Detected version: ${VERSION}, channel: ${CHANNEL}" - name: Prepare upload directories run: | VERSION="${{ steps.version.outputs.version }}" TAG="${{ steps.version.outputs.tag }}" CHANNEL="${{ steps.version.outputs.channel }}" mkdir -p staging/${CHANNEL} mkdir -p staging/releases/${TAG} # Flatten all platform artifacts into staging directories find release-artifacts/ -type f | while read file; do filename=$(basename "$file") cp "$file" "staging/${CHANNEL}/${filename}" cp "$file" "staging/releases/${TAG}/${filename}" done echo "=== staging/${CHANNEL}/ ===" ls -lh staging/${CHANNEL}/ echo "" echo "=== staging/releases/${TAG}/ ===" ls -lh staging/releases/${TAG}/ # Note: Do NOT rename yml files. electron-updater (generic provider) always # requests "latest-mac.yml", "latest.yml", etc. regardless of feed URL. # Channel separation is achieved by directory: /alpha/, /beta/, /latest/. - name: Verify yml files present run: | CHANNEL="${{ steps.version.outputs.channel }}" echo "=== staging/${CHANNEL}/ (update metadata) ===" ls -la staging/${CHANNEL}/*.yml 2>/dev/null || echo "No yml files found (check electron-builder outputs)" - name: Generate release-info.json run: | VERSION="${{ steps.version.outputs.version }}" CHANNEL="${{ steps.version.outputs.channel }}" BASE_URL="https://oss.intelli-spectrum.com/${CHANNEL}" jq -n \ --arg version "$VERSION" \ --arg channel "$CHANNEL" \ --arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ --arg base "$BASE_URL" \ --arg changelog "https://github.com/${{ github.repository }}/releases/tag/v${VERSION}" \ '{ version: $version, channel: $channel, releaseDate: $date, downloads: { mac: { x64: ($base + "/ClawX-" + $version + "-mac-x64.dmg"), arm64: ($base + "/ClawX-" + $version + "-mac-arm64.dmg") }, win: { x64: ($base + "/ClawX-" + $version + "-win-x64.exe"), arm64: ($base + "/ClawX-" + $version + "-win-arm64.exe") }, linux: { deb_amd64: ($base + "/ClawX-" + $version + "-linux-amd64.deb"), deb_arm64: ($base + "/ClawX-" + $version + "-linux-arm64.deb"), appimage_x64: ($base + "/ClawX-" + $version + "-linux-x86_64.AppImage"), appimage_arm64: ($base + "/ClawX-" + $version + "-linux-arm64.AppImage"), rpm_x64: ($base + "/ClawX-" + $version + "-linux-x86_64.rpm") } }, changelog: $changelog }' > staging/${CHANNEL}/release-info.json echo "=== release-info.json ===" cat staging/${CHANNEL}/release-info.json - name: Install and configure ossutil env: OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }} OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }} run: | curl -sL https://gosspublic.alicdn.com/ossutil/install.sh | sudo bash # Write config file for non-interactive use cat > $HOME/.ossutilconfig << EOF [Credentials] language=EN endpoint=oss-cn-hangzhou.aliyuncs.com accessKeyID=${OSS_ACCESS_KEY_ID} accessKeySecret=${OSS_ACCESS_KEY_SECRET} EOF ossutil --version - name: "Upload to OSS: {channel}/ (overwrite)" run: | CHANNEL="${{ steps.version.outputs.channel }}" # Only clean the current channel's directory — never touch other channels ossutil rm -r -f oss://valuecell-clawx/${CHANNEL}/ || true # Upload all files with no-cache so clients always get the freshest version ossutil cp -r -f \ --meta="Cache-Control:no-cache,no-store,must-revalidate" \ staging/${CHANNEL}/ \ oss://valuecell-clawx/${CHANNEL}/ echo "Uploaded to ${CHANNEL}/" - name: "Upload to OSS: releases/vX.Y.Z/ (archive)" run: | TAG="${{ steps.version.outputs.tag }}" # Upload to permanent archive (long cache, immutable) ossutil cp -r \ staging/releases/${TAG}/ \ oss://valuecell-clawx/releases/${TAG}/ \ --meta "Cache-Control:public,max-age=31536000,immutable" echo "Uploaded to releases/${TAG}/" - name: Verify OSS upload run: | TAG="${{ steps.version.outputs.tag }}" CHANNEL="${{ steps.version.outputs.channel }}" echo "=== ${CHANNEL}/ ===" ossutil ls oss://valuecell-clawx/${CHANNEL}/ --short echo "" echo "=== releases/${TAG}/ ===" ossutil ls oss://valuecell-clawx/releases/${TAG}/ --short echo "" echo "=== Verify release-info.json ===" ossutil cp oss://valuecell-clawx/${CHANNEL}/release-info.json /tmp/release-info.json -f jq . /tmp/release-info.json echo "" echo "=== Verify update yml ===" if [ "${CHANNEL}" = "latest" ]; then YML_PREFIX="latest" else YML_PREFIX="${CHANNEL}" fi echo "electron-updater expects ${YML_PREFIX}-mac.yml, ${YML_PREFIX}.yml, etc. in ${CHANNEL}/:" ossutil ls oss://valuecell-clawx/${CHANNEL}/ --short | grep "${YML_PREFIX}.*\\.yml" || echo "(none found)" echo "" echo "All files uploaded and verified successfully!" # ────────────────────────────────────────────────────────────── # Job: Finalize Release # Promotes the GitHub Release from pre-release to latest AFTER # both GitHub Release assets and OSS uploads are fully complete. # This ensures /releases/latest API never returns an incomplete # release — the website and electron-updater only see it when # all platform artifacts are ready. # ────────────────────────────────────────────────────────────── finalize: needs: [publish, upload-oss] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') steps: - name: Promote release from pre-release to latest env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${GITHUB_REF#refs/tags/}" IS_PRERELEASE_CHANNEL=false if [[ "$TAG" == *"alpha"* ]] || [[ "$TAG" == *"beta"* ]]; then IS_PRERELEASE_CHANNEL=true fi if [ "$IS_PRERELEASE_CHANNEL" = "true" ]; then echo "Tag $TAG is an alpha/beta release — keeping as pre-release." else echo "Promoting $TAG from pre-release to latest release..." gh release edit "$TAG" \ --prerelease=false \ --latest \ --repo "${{ github.repository }}" echo "Release $TAG is now the latest release." fi