Skip to content

Add a watchdog for OpenClaw gateway recovery #362

Add a watchdog for OpenClaw gateway recovery

Add a watchdog for OpenClaw gateway recovery #362

Workflow file for this run

name: Desktop Bundles
on:
# Expensive desktop bundle builds only run on release-track events:
# - push to release/**, hotfix/** (validate before tagging)
# - tag push v* (actual release)
# - PR to main (release/hotfix PRs — verify job only)
# - workflow_dispatch (manual)
# Push to main/develop does NOT trigger this workflow. Test Suite (test.yml)
# handles TypeScript/tests/build validation on those branches.
pull_request:
branches: [main]
paths-ignore:
- '**.md'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
- '.github/PULL_REQUEST_TEMPLATE.md'
push:
branches: ['release/**', 'hotfix/**']
tags: ['v*']
paths-ignore:
- '**.md'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
- '.github/PULL_REQUEST_TEMPLATE.md'
workflow_dispatch:
inputs:
checkout_ref:
description: Git ref to build for manual runs, for example refs/tags/v0.1.0 or main
required: false
type: string
publish_npm:
description: Publish the CLI package to npm for a tagged manual run
required: false
type: boolean
default: false
publish_release:
description: Publish the built installers to GitHub Releases
required: false
type: boolean
default: false
release_tag:
description: Release tag to create or update when publishing manually, for example v0.1.0
required: false
type: string
draft_release:
description: Create the manual GitHub release as a draft
required: false
type: boolean
default: true
prerelease:
description: Mark the manual GitHub release as a prerelease
required: false
type: boolean
default: false
permissions:
contents: read
env:
NODE_VERSION: 'lts/*'
concurrency:
group: desktop-bundles-${{ github.event_name == 'workflow_dispatch' && inputs.checkout_ref || github.ref }}
cancel-in-progress: true
jobs:
verify:
name: Verify Release Gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_ref || github.ref }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- run: npm ci
- name: TypeScript check
run: npx tsc -b --noEmit packages/web
- name: Unit tests
run: npm test
- name: Build web
run: npm run build
bundle:
name: Bundle (${{ matrix.label }})
needs: verify
if: github.event_name != 'pull_request'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
target: x86_64-unknown-linux-gnu
label: Linux x64
artifact_name: clawmaster-linux-x64
bundles: appimage,deb
- os: macos-15-intel
target: x86_64-apple-darwin
label: macOS Intel
artifact_name: clawmaster-macos-intel
- os: macos-14
target: aarch64-apple-darwin
label: macOS Apple Silicon
artifact_name: clawmaster-macos-arm64
- os: windows-latest
target: x86_64-pc-windows-msvc
label: Windows x64
artifact_name: clawmaster-windows-x64
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_ref || github.ref }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- uses: ./.github/actions/setup-rust
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
key: ${{ matrix.target }}
- name: Install Linux build dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential \
curl \
file \
libayatana-appindicator3-dev \
libgtk-3-dev \
librsvg2-dev \
libssl-dev \
libwebkit2gtk-4.1-dev \
libxdo-dev \
patchelf \
wget
- run: npm ci
- name: Normalize version for MSI (numeric-only required)
if: runner.os == 'Windows'
shell: bash
run: |
RAW=$(node -p "require('./package.json').version")
NUMERIC="${RAW%%-*}"
# Rewrite tauri.conf.json in-place — MSI rejects semver pre-release suffixes
node -e "
const fs = require('fs');
const p = 'src-tauri/tauri.conf.json';
const c = JSON.parse(fs.readFileSync(p, 'utf8'));
c.version = '$NUMERIC';
fs.writeFileSync(p, JSON.stringify(c, null, 2) + '\n');
"
echo "Normalized tauri.conf.json version to $NUMERIC"
- name: Build Tauri desktop bundles
run: npx tauri build --target ${{ matrix.target }} ${{ matrix.bundles && format('--bundles {0}', matrix.bundles) || '' }}
- name: Collect workflow bundle files
shell: bash
env:
TARGET_BUNDLE_ROOT: src-tauri/target/${{ matrix.target }}/release/bundle
DEFAULT_BUNDLE_ROOT: src-tauri/target/release/bundle
ARTIFACT_DIR: ci-artifacts/${{ matrix.artifact_name }}
RUNNER_OS: ${{ runner.os }}
run: |
set -euo pipefail
mkdir -p "$ARTIFACT_DIR"
BUNDLE_ROOTS=()
if [ -d "$TARGET_BUNDLE_ROOT" ]; then
BUNDLE_ROOTS+=("$TARGET_BUNDLE_ROOT")
fi
if [ -d "$DEFAULT_BUNDLE_ROOT" ] && [ "$DEFAULT_BUNDLE_ROOT" != "$TARGET_BUNDLE_ROOT" ]; then
BUNDLE_ROOTS+=("$DEFAULT_BUNDLE_ROOT")
fi
if [ "${#BUNDLE_ROOTS[@]}" -eq 0 ]; then
echo "No bundle directory found" >&2
echo "Checked:" >&2
echo " - $TARGET_BUNDLE_ROOT" >&2
echo " - $DEFAULT_BUNDLE_ROOT" >&2
exit 1
fi
# Use -maxdepth to stay in top-level bundle output dirs (e.g. bundle/msi/,
# bundle/deb/). This keeps internal resources (ICU data, npm packages
# bundled via .tauri-resources/) out of the release artifacts.
case "$RUNNER_OS" in
Linux)
for root in "${BUNDLE_ROOTS[@]}"; do
find "$root" -maxdepth 3 -type f \
\( -name '*.AppImage' -o -name '*.deb' -o -name '*.sig' \) \
-exec cp '{}' "$ARTIFACT_DIR"/ \;
done
;;
macOS)
for root in "${BUNDLE_ROOTS[@]}"; do
find "$root" -maxdepth 3 -type f \
\( -name '*.dmg' -o -name '*.app.tar.gz' -o -name '*.sig' \) \
-exec cp '{}' "$ARTIFACT_DIR"/ \;
done
;;
Windows)
for root in "${BUNDLE_ROOTS[@]}"; do
find "$root" -maxdepth 3 -type f \
\( -name '*.msi' -o -name '*.exe' -o -name '*.zip' -o -name '*.sig' \) \
-exec cp '{}' "$ARTIFACT_DIR"/ \;
done
;;
*)
echo "Unsupported runner OS: $RUNNER_OS" >&2
exit 1
;;
esac
if [ -z "$(find "$ARTIFACT_DIR" -maxdepth 1 -type f -print -quit)" ]; then
echo "No bundle files were collected for $RUNNER_OS" >&2
for root in "${BUNDLE_ROOTS[@]}"; do
echo "--- $root ---" >&2
find "$root" -maxdepth 4 -type f | sort >&2
done
exit 1
fi
{
echo "### ${{ matrix.label }}"
while IFS= read -r file; do
echo "- $(basename "$file")"
done < <(find "$ARTIFACT_DIR" -maxdepth 1 -type f | sort)
echo
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload workflow bundle artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: ci-artifacts/${{ matrix.artifact_name }}
if-no-files-found: error
retention-days: 14
publish-npm:
name: Publish to npm
needs: verify
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.publish_npm)
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_ref || github.ref }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
registry-url: https://registry.npmjs.org
- run: npm ci
- name: Resolve npm release tag
id: npm_release
env:
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
INPUT_RELEASE_TAG: ${{ inputs.release_tag }}
INPUT_CHECKOUT_REF: ${{ inputs.checkout_ref }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
if [ -n "$INPUT_RELEASE_TAG" ]; then
TAG="$INPUT_RELEASE_TAG"
elif printf '%s' "$INPUT_CHECKOUT_REF" | grep -Eq '^refs/tags/v'; then
TAG="${INPUT_CHECKOUT_REF#refs/tags/}"
elif printf '%s' "$INPUT_CHECKOUT_REF" | grep -Eq '^v'; then
TAG="$INPUT_CHECKOUT_REF"
else
echo "publish_npm requires release_tag or checkout_ref pointing to refs/tags/v*" >&2
exit 1
fi
else
TAG="$REF_NAME"
fi
if ! printf '%s' "$TAG" | grep -Eq '^v'; then
echo "npm publish only supports version tags like v0.3.0 or v0.3.0-rc.3 (got $TAG)" >&2
exit 1
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Determine npm dist-tag
id: npm_tag
env:
TAG: ${{ steps.npm_release.outputs.tag }}
run: |
VERSION="${TAG#v}"
if printf '%s' "$VERSION" | grep -Eq -- '-(rc|beta|alpha)'; then
echo "tag=rc" >> "$GITHUB_OUTPUT"
else
echo "tag=latest" >> "$GITHUB_OUTPUT"
fi
- name: Check if version already published
id: check_npm
env:
TAG: ${{ steps.npm_release.outputs.tag }}
run: |
EXPECTED_VERSION="${TAG#v}"
VERSION=$(node -p "require('./package.json').version")
if [ "$EXPECTED_VERSION" != "$VERSION" ]; then
echo "Release tag $TAG does not match package.json version $VERSION" >&2
exit 1
fi
if npm view "clawmaster@$VERSION" version >/dev/null 2>&1; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "clawmaster@$VERSION is already on npm; skipping publish"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish
if: steps.check_npm.outputs.skip != 'true'
run: npm publish --tag ${{ steps.npm_tag.outputs.tag }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish-release:
name: Publish Release Assets
needs: bundle
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.publish_release)
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_ref || github.ref }}
# Custom release-notes script walks the full commit range between
# the previous v* tag and the current tag via `git describe` + `git
# log`, which needs complete history and tags present.
fetch-depth: 0
fetch-tags: true
- uses: actions/download-artifact@v5
with:
path: release-bundles
- name: Resolve release tag
id: release_meta
env:
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
INPUT_RELEASE_TAG: ${{ inputs.release_tag }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
TAG="$INPUT_RELEASE_TAG"
else
TAG="$REF_NAME"
fi
if [ -z "$TAG" ]; then
echo "release_tag is required when publish_release is enabled for workflow_dispatch" >&2
exit 1
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Verify tag matches package version
env:
TAG: ${{ steps.release_meta.outputs.tag }}
run: |
set -euo pipefail
EXPECTED_VERSION="${TAG#v}"
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
if [ "$EXPECTED_VERSION" != "$PACKAGE_VERSION" ]; then
echo "Release tag $TAG does not match package.json version $PACKAGE_VERSION" >&2
exit 1
fi
- name: Collect release assets
run: |
set -euo pipefail
mkdir -p release-assets
# Only installer bundles — exclude internal JSON (ICU data, package
# manifests, tsconfig) that gets packaged inside Tauri resources.
find release-bundles -type f \
\( -name '*.AppImage' -o -name '*.deb' \
-o -name '*.dmg' -o -name '*.app.tar.gz' \
-o -name '*.msi' -o -name '*.exe' \
-o -name '*.sig' \) \
-exec cp '{}' release-assets/ \;
if [ -z "$(find release-assets -maxdepth 1 -type f -print -quit)" ]; then
echo "No desktop bundle files were found to publish" >&2
find release-bundles -maxdepth 4 -type f | sort
exit 1
fi
(
cd release-assets
sha256sum * > SHA256SUMS.txt
)
- name: Create release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ steps.release_meta.outputs.tag }}
run: |
set -euo pipefail
# Custom script groups commits by conventional-commit type into
# emoji'd buckets. Beats `gh api .../generate-notes` for git-flow
# repos where the tagged branch only sees the release-merge commit.
GENERATED_BODY="$(node scripts/release-notes.mjs "$TAG")"
VERSION="${TAG#v}"
NPM_TAG="latest"
if printf '%s' "$VERSION" | grep -Eq -- '-(rc|beta|alpha)'; then
NPM_TAG="rc"
fi
RELEASE_URL="https://github.com/${GH_REPO}/releases/tag/${TAG}"
RELEASE_DOWNLOAD_BASE="https://github.com/${GH_REPO}/releases/download/${TAG}"
NPM_PACKAGE_URL="https://www.npmjs.com/package/clawmaster/v/${VERSION}"
NPM_DIST_URL="https://www.npmjs.com/package/clawmaster?activeTab=versions"
# Build direct asset URLs deterministically from the release tag.
linux_appimage_url="${RELEASE_DOWNLOAD_BASE}/ClawMaster_${VERSION}_amd64.AppImage"
linux_deb_url="${RELEASE_DOWNLOAD_BASE}/ClawMaster_${VERSION}_amd64.deb"
macos_intel_url="${RELEASE_DOWNLOAD_BASE}/ClawMaster_${VERSION}_x64.dmg"
macos_arm64_url="${RELEASE_DOWNLOAD_BASE}/ClawMaster_${VERSION}_aarch64.dmg"
windows_msi_url="${RELEASE_DOWNLOAD_BASE}/ClawMaster_${VERSION}_x64_en-US.msi"
windows_exe_url="${RELEASE_DOWNLOAD_BASE}/ClawMaster_${VERSION}_x64-setup.exe"
checksums_url="${RELEASE_DOWNLOAD_BASE}/SHA256SUMS.txt"
# Use printf instead of heredoc because YAML's `run: |` indentation
# leaks into the heredoc content — turning every heading into an
# indented code block in the rendered markdown.
{
printf '## Install\n\n'
printf '### CLI + Web Console (Recommended)\n\n'
printf '```bash\n'
printf 'npm install -g clawmaster\n'
printf 'clawmaster\n'
printf '```\n\n'
printf '`clawmaster` starts the local service and opens the landing page in your browser by default.\n\n'
printf 'If the browser does not open automatically, use the backup local address: http://localhost:16223\n\n'
printf -- '- npm package: [%s](%s)\n' "clawmaster@${VERSION}" "$NPM_PACKAGE_URL"
printf -- '- all published versions: [%s](%s)\n\n' "npm versions" "$NPM_DIST_URL"
printf '> To pin this exact version: `npm install -g clawmaster@%s`\n\n' "$VERSION"
printf '### Desktop App (Beta)\n\n'
printf 'Direct downloads for this release. Checksums: [`SHA256SUMS.txt`](%s)\n\n' "$checksums_url"
printf '| Platform | Downloads |\n'
printf '|---|---|\n'
printf '| Linux x64 | [AppImage](%s) · [deb](%s) |\n' "$linux_appimage_url" "$linux_deb_url"
printf '| macOS Intel | [dmg](%s) |\n' "$macos_intel_url"
printf '| macOS Apple Silicon | [dmg](%s) |\n' "$macos_arm64_url"
printf '| Windows x64 | [msi](%s) · [exe](%s) |\n\n' "$windows_msi_url" "$windows_exe_url"
printf '> Desktop builds are in beta. The CLI + Web Console is the recommended install method.\n\n'
printf "## What's Changed\n\n"
printf '%s\n' "$GENERATED_BODY"
} > RELEASE_NOTES.md
- name: Create or update GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.release_meta.outputs.tag }}
EVENT_NAME: ${{ github.event_name }}
INPUT_DRAFT: ${{ inputs.draft_release }}
INPUT_PRERELEASE: ${{ inputs.prerelease }}
run: |
set -euo pipefail
DRAFT_FLAG=""
PRERELEASE_FLAG=""
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
if [ "$INPUT_DRAFT" = "true" ]; then
DRAFT_FLAG="--draft"
fi
if [ "$INPUT_PRERELEASE" = "true" ]; then
PRERELEASE_FLAG="--prerelease"
fi
else
if printf '%s' "$TAG" | grep -Eq -- '-(beta|rc)'; then
PRERELEASE_FLAG="--prerelease"
fi
fi
# Pre-releases must not become the "latest" release on GitHub
LATEST_FLAG="--latest"
if [ -n "$PRERELEASE_FLAG" ]; then
LATEST_FLAG="--latest=false"
fi
if gh release view "$TAG" >/dev/null 2>&1; then
# Force un-draft so tag re-pushes produce a publicly-visible release
gh release edit "$TAG" \
--title "ClawMaster $TAG" \
--notes-file RELEASE_NOTES.md \
--draft=false \
$LATEST_FLAG
gh release upload "$TAG" release-assets/* --clobber
else
gh release create "$TAG" release-assets/* \
--title "ClawMaster $TAG" \
--notes-file RELEASE_NOTES.md \
$DRAFT_FLAG \
$PRERELEASE_FLAG \
$LATEST_FLAG
fi
- name: Add workflow summary
env:
REPOSITORY: ${{ github.repository }}
TAG: ${{ steps.release_meta.outputs.tag }}
run: |
{
echo "## Desktop Downloads"
echo
echo "GitHub Release: https://github.com/$REPOSITORY/releases/tag/$TAG"
echo
echo "Artifacts in this release:"
while IFS= read -r file; do
echo "- $(basename "$file")"
done < <(find release-assets -maxdepth 1 -type f | sort)
} >> "$GITHUB_STEP_SUMMARY"