Add a watchdog for OpenClaw gateway recovery #362
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |