mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 09:01:14 +00:00
443 lines
18 KiB
YAML
443 lines
18 KiB
YAML
name: Linux App Release
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: Existing OpenClaw release tag to receive Linux companion bundles, for example v2026.7.1
|
|
required: true
|
|
type: string
|
|
desktop-test-bundles:
|
|
description: Also build unsigned macOS/Windows test bundles
|
|
default: false
|
|
type: boolean
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
concurrency:
|
|
group: linux-app-release-${{ inputs.tag }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
|
|
jobs:
|
|
validate_release:
|
|
name: Validate release tag
|
|
runs-on: ubuntu-22.04
|
|
timeout-minutes: 10
|
|
# Build jobs check out this exact SHA so a tag force-moved mid-run cannot
|
|
# swap in code the ancestry guard never validated.
|
|
outputs:
|
|
tag_sha: ${{ steps.ancestry.outputs.tag_sha }}
|
|
steps:
|
|
- name: Validate tag input format
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*)?$ ]]; then
|
|
# Alpha/beta semver suffixes map to a Debian revision that sorts
|
|
# AFTER the plain stable version, breaking beta-to-stable upgrades;
|
|
# numeric stable revisions (-N) order correctly and stay allowed.
|
|
echo "Linux bundles ship for stable release tags only (vYYYY.M.PATCH or vYYYY.M.PATCH-N); got: ${RELEASE_TAG}"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Checkout selected tag
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
ref: refs/tags/${{ inputs.tag }}
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- name: Ensure tag commit is reachable from main
|
|
id: ancestry
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
timeout --signal=TERM --kill-after=10s 120s git fetch --quiet origin main
|
|
tag_sha=$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")
|
|
if ! git merge-base --is-ancestor "${tag_sha}" origin/main; then
|
|
echo "Tag ${RELEASE_TAG} (${tag_sha}) is not reachable from main; Linux bundles ship for main-based releases only."
|
|
exit 1
|
|
fi
|
|
echo "tag_sha=${tag_sha}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Ensure matching GitHub release exists
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
run: gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --json tagName --jq .tagName
|
|
|
|
build_linux:
|
|
name: Build Linux companion bundles
|
|
needs: validate_release
|
|
# Oldest supported build base: bundles link against this glibc, so newer
|
|
# runners would silently drop Ubuntu 22.04/Debian 12 users.
|
|
runs-on: ubuntu-22.04
|
|
timeout-minutes: 45
|
|
steps:
|
|
- name: Checkout selected tag
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
ref: ${{ needs.validate_release.outputs.tag_sha }}
|
|
persist-credentials: false
|
|
|
|
- name: Install Tauri system dependencies
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y --no-install-recommends \
|
|
build-essential \
|
|
curl \
|
|
file \
|
|
libayatana-appindicator3-dev \
|
|
librsvg2-dev \
|
|
libssl-dev \
|
|
libwebkit2gtk-4.1-dev \
|
|
libxdo-dev \
|
|
wget
|
|
|
|
- name: Install Rust
|
|
run: rustup toolchain install stable --profile minimal
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
install-deps: "false"
|
|
|
|
- name: Build Linux companion bundles
|
|
working-directory: apps/linux/src-tauri
|
|
env:
|
|
# appimagetool strips fail on CI runners; Tauri documents NO_STRIP for Actions.
|
|
NO_STRIP: "true"
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
# Sign the AppImage updater artifact so the companion can verify updates.
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
run: |
|
|
set -euo pipefail
|
|
version="${RELEASE_TAG#v}"
|
|
# Stamp the release version into the bundles; the committed manifests
|
|
# keep a placeholder 0.1.0 so releases stay tag-driven.
|
|
pnpm dlx @tauri-apps/cli@2.11.4 build --bundles deb,appimage \
|
|
--config "{\"version\":\"${version}\"}"
|
|
|
|
- name: Verify and rename Linux bundles
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
shopt -s nullglob
|
|
version="${RELEASE_TAG#v}"
|
|
debs=(apps/linux/src-tauri/target/release/bundle/deb/*.deb)
|
|
appimages=(apps/linux/src-tauri/target/release/bundle/appimage/*.AppImage)
|
|
if [[ ${#debs[@]} -ne 1 || ${#appimages[@]} -ne 1 || ! -f "${appimages[0]}.sig" ]]; then
|
|
echo "Expected one deb, one AppImage, and its updater signature"
|
|
exit 1
|
|
fi
|
|
deb_version=$(dpkg-deb -f "${debs[0]}" Version)
|
|
if [[ "${deb_version}" != "${version}"* ]]; then
|
|
echo "Debian package version '${deb_version}' does not match release version '${version}'"
|
|
exit 1
|
|
fi
|
|
mkdir -p dist/linux-app/release dist/linux-app/signatures
|
|
cp "${debs[0]}" "dist/linux-app/release/OpenClaw-${version}-amd64.deb"
|
|
cp "${appimages[0]}" "dist/linux-app/release/OpenClaw-${version}-amd64.AppImage"
|
|
cp "${appimages[0]}.sig" \
|
|
"dist/linux-app/signatures/OpenClaw-${version}-amd64.AppImage.sig"
|
|
|
|
- name: Upload Linux bundles
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: linux-app-release
|
|
path: dist/linux-app
|
|
if-no-files-found: error
|
|
|
|
# TEST-ONLY bundles: no Apple codesigning/notarization or Authenticode.
|
|
# Users must bypass Gatekeeper or SmartScreen before running them.
|
|
build_macos:
|
|
name: Build unsigned macOS test bundles
|
|
if: ${{ inputs['desktop-test-bundles'] }}
|
|
needs: validate_release
|
|
runs-on: macos-14
|
|
timeout-minutes: 45
|
|
steps:
|
|
- name: Checkout selected tag
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
ref: ${{ needs.validate_release.outputs.tag_sha }}
|
|
persist-credentials: false
|
|
|
|
- name: Install Rust
|
|
run: rustup toolchain install stable --profile minimal
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
install-deps: "false"
|
|
|
|
- name: Build macOS test bundles
|
|
working-directory: apps/linux/src-tauri
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
run: |
|
|
set -euo pipefail
|
|
version="${RELEASE_TAG#v}"
|
|
pnpm dlx @tauri-apps/cli@2.11.4 build --bundles app,dmg \
|
|
--config "{\"version\":\"${version}\"}"
|
|
|
|
- name: Verify and rename macOS bundles
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
shopt -s nullglob
|
|
version="${RELEASE_TAG#v}"
|
|
apps=(apps/linux/src-tauri/target/release/bundle/macos/*.app)
|
|
archives=(apps/linux/src-tauri/target/release/bundle/macos/*.app.tar.gz)
|
|
dmgs=(apps/linux/src-tauri/target/release/bundle/dmg/*.dmg)
|
|
if [[ ${#apps[@]} -ne 1 || ${#archives[@]} -ne 1 || ${#dmgs[@]} -ne 1 || ! -f "${archives[0]}.sig" ]]; then
|
|
echo "Expected one app, updater archive, updater signature, and dmg"
|
|
exit 1
|
|
fi
|
|
bundle_version=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \
|
|
"${apps[0]}/Contents/Info.plist")
|
|
if [[ "${bundle_version}" != "${version}" ]]; then
|
|
echo "macOS bundle version '${bundle_version}' does not match release version '${version}'"
|
|
exit 1
|
|
fi
|
|
mkdir -p dist/macos-app/release dist/macos-app/signatures
|
|
cp "${dmgs[0]}" "dist/macos-app/release/OpenClaw-${version}-darwin-aarch64.dmg"
|
|
cp "${archives[0]}" \
|
|
"dist/macos-app/release/OpenClaw-${version}-darwin-aarch64.app.tar.gz"
|
|
cp "${archives[0]}.sig" \
|
|
"dist/macos-app/signatures/OpenClaw-${version}-darwin-aarch64.app.tar.gz.sig"
|
|
|
|
- name: Upload macOS test bundles
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: macos-app-release
|
|
path: dist/macos-app
|
|
if-no-files-found: error
|
|
|
|
build_windows:
|
|
name: Build unsigned Windows test bundle
|
|
if: ${{ inputs['desktop-test-bundles'] }}
|
|
needs: validate_release
|
|
runs-on: windows-2022
|
|
timeout-minutes: 45
|
|
steps:
|
|
- name: Checkout selected tag
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
ref: ${{ needs.validate_release.outputs.tag_sha }}
|
|
persist-credentials: false
|
|
|
|
- name: Install Rust
|
|
run: rustup toolchain install stable --profile minimal
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
install-deps: "false"
|
|
|
|
- name: Build Windows test bundle
|
|
working-directory: apps/linux/src-tauri
|
|
shell: bash
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
run: |
|
|
set -euo pipefail
|
|
version="${RELEASE_TAG#v}"
|
|
pnpm dlx @tauri-apps/cli@2.11.4 build --bundles nsis \
|
|
--config "{\"version\":\"${version}\"}"
|
|
|
|
- name: Verify and rename Windows bundle
|
|
shell: pwsh
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
run: |
|
|
$ErrorActionPreference = "Stop"
|
|
$version = $env:RELEASE_TAG.Substring(1)
|
|
$installers = @(Get-ChildItem "apps/linux/src-tauri/target/release/bundle/nsis/*.exe")
|
|
if ($installers.Count -ne 1) {
|
|
throw "Expected one NSIS installer; found $($installers.Count)"
|
|
}
|
|
$installer = $installers[0]
|
|
$signature = "$($installer.FullName).sig"
|
|
if (-not (Test-Path -LiteralPath $signature)) {
|
|
throw "Missing NSIS updater signature: $signature"
|
|
}
|
|
if (-not $installer.VersionInfo.ProductVersion.StartsWith($version)) {
|
|
throw "Windows bundle version '$($installer.VersionInfo.ProductVersion)' does not match release version '$version'"
|
|
}
|
|
New-Item -ItemType Directory -Force -Path "dist/windows-app/release", "dist/windows-app/signatures" | Out-Null
|
|
Copy-Item -LiteralPath $installer.FullName -Destination "dist/windows-app/release/OpenClaw-$version-windows-x86_64.exe"
|
|
Copy-Item -LiteralPath $signature -Destination "dist/windows-app/signatures/OpenClaw-$version-windows-x86_64.exe.sig"
|
|
|
|
- name: Upload Windows test bundle
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: windows-app-release
|
|
path: dist/windows-app
|
|
if-no-files-found: error
|
|
|
|
publish:
|
|
name: Publish companion bundles and updater manifest
|
|
if: >-
|
|
${{
|
|
always() &&
|
|
needs.build_linux.result == 'success' &&
|
|
(!inputs['desktop-test-bundles'] ||
|
|
(needs.build_macos.result == 'success' && needs.build_windows.result == 'success'))
|
|
}}
|
|
needs:
|
|
- validate_release
|
|
- build_linux
|
|
- build_macos
|
|
- build_windows
|
|
# One shared desktop-test channel asset must not race across release tags.
|
|
concurrency:
|
|
group: linux-app-release-publish
|
|
cancel-in-progress: false
|
|
runs-on: ubuntu-22.04
|
|
timeout-minutes: 10
|
|
steps:
|
|
- name: Download Linux bundles
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
with:
|
|
name: linux-app-release
|
|
path: dist/input/linux
|
|
|
|
- name: Download macOS test bundles
|
|
if: ${{ inputs['desktop-test-bundles'] }}
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
with:
|
|
name: macos-app-release
|
|
path: dist/input/macos
|
|
|
|
- name: Download Windows test bundle
|
|
if: ${{ inputs['desktop-test-bundles'] }}
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
with:
|
|
name: windows-app-release
|
|
path: dist/input/windows
|
|
|
|
- name: Assemble release assets and updater manifest
|
|
env:
|
|
DESKTOP_TEST_BUNDLES: ${{ inputs['desktop-test-bundles'] }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
version="${RELEASE_TAG#v}"
|
|
mkdir -p dist/release
|
|
cp dist/input/linux/release/* dist/release/
|
|
if [[ "${DESKTOP_TEST_BUNDLES}" == "true" ]]; then
|
|
cp dist/input/macos/release/* dist/release/
|
|
cp dist/input/windows/release/* dist/release/
|
|
fi
|
|
|
|
# Generate this before latest.json so it covers only downloadable bundles.
|
|
(cd dist/release && sha256sum ./* > SHA256SUMS.linux-app.txt)
|
|
cat dist/release/SHA256SUMS.linux-app.txt
|
|
|
|
linux_signature=$(cat "dist/input/linux/signatures/OpenClaw-${version}-amd64.AppImage.sig")
|
|
url_base="https://github.com/${GITHUB_REPOSITORY}/releases/download/${RELEASE_TAG}"
|
|
pub_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
# Capture the full body (no early-closing pipe under pipefail), then
|
|
# truncate to 2000 Unicode chars inside jq so we never split a
|
|
# multibyte character or SIGPIPE the release-view command.
|
|
notes=$(gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --json body --jq '.body // ""')
|
|
# latest.json is always the stable Linux channel. Desktop test builds
|
|
# use a separate manifest that Linux-only releases leave untouched.
|
|
jq -n \
|
|
--arg version "${version}" \
|
|
--arg notes "${notes}" \
|
|
--arg pub_date "${pub_date}" \
|
|
--arg linux_signature "${linux_signature}" \
|
|
--arg linux_url "${url_base}/OpenClaw-${version}-amd64.AppImage" \
|
|
'{version: $version, notes: ($notes | .[0:2000]), pub_date: $pub_date, platforms: {"linux-x86_64": {signature: $linux_signature, url: $linux_url}}}' \
|
|
> dist/release/latest.json
|
|
cat dist/release/latest.json
|
|
|
|
if [[ "${DESKTOP_TEST_BUNDLES}" == "true" ]]; then
|
|
macos_signature=$(cat "dist/input/macos/signatures/OpenClaw-${version}-darwin-aarch64.app.tar.gz.sig")
|
|
windows_signature=$(cat "dist/input/windows/signatures/OpenClaw-${version}-windows-x86_64.exe.sig")
|
|
jq -n \
|
|
--arg version "${version}" \
|
|
--arg notes "${notes}" \
|
|
--arg pub_date "${pub_date}" \
|
|
--arg macos_signature "${macos_signature}" \
|
|
--arg macos_url "${url_base}/OpenClaw-${version}-darwin-aarch64.app.tar.gz" \
|
|
--arg windows_signature "${windows_signature}" \
|
|
--arg windows_url "${url_base}/OpenClaw-${version}-windows-x86_64.exe" \
|
|
'{version: $version, notes: ($notes | .[0:2000]), pub_date: $pub_date, platforms: {"darwin-aarch64": {signature: $macos_signature, url: $macos_url}, "windows-x86_64": {signature: $windows_signature, url: $windows_url}}}' \
|
|
> dist/release/latest-desktop-test.json
|
|
cat dist/release/latest-desktop-test.json
|
|
fi
|
|
|
|
- name: Attach bundles to the release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
gh release upload "${RELEASE_TAG}" \
|
|
--repo "${GITHUB_REPOSITORY}" \
|
|
--clobber \
|
|
dist/release/*
|
|
|
|
- name: Publish desktop test update channel
|
|
if: ${{ inputs['desktop-test-bundles'] }}
|
|
env:
|
|
DESKTOP_TEST_CHANNEL_TAG: desktop-test
|
|
GH_TOKEN: ${{ github.token }}
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
TAG_SHA: ${{ needs.validate_release.outputs.tag_sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
channel_dir="${RUNNER_TEMP}/desktop-test-channel"
|
|
candidate_version="${RELEASE_TAG#v}"
|
|
mkdir -p "${channel_dir}"
|
|
|
|
if gh release view "${DESKTOP_TEST_CHANNEL_TAG}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
|
|
has_manifest=$(gh release view "${DESKTOP_TEST_CHANNEL_TAG}" \
|
|
--repo "${GITHUB_REPOSITORY}" \
|
|
--json assets \
|
|
--jq '[.assets[].name] | index("latest-desktop-test.json") != null')
|
|
if [[ "${has_manifest}" == "true" ]]; then
|
|
gh release download "${DESKTOP_TEST_CHANNEL_TAG}" \
|
|
--repo "${GITHUB_REPOSITORY}" \
|
|
--pattern latest-desktop-test.json \
|
|
--dir "${channel_dir}"
|
|
current_version=$(jq -er '.version | strings' "${channel_dir}/latest-desktop-test.json")
|
|
newest_version=$(printf '%s\n' "${current_version}" "${candidate_version}" | LC_ALL=C sort -V | tail -n 1)
|
|
if [[ "${newest_version}" != "${candidate_version}" ]]; then
|
|
echo "Desktop test channel is already newer (${current_version}); leaving it unchanged."
|
|
exit 0
|
|
fi
|
|
fi
|
|
else
|
|
gh release create "${DESKTOP_TEST_CHANNEL_TAG}" \
|
|
--repo "${GITHUB_REPOSITORY}" \
|
|
--target "${TAG_SHA}" \
|
|
--prerelease \
|
|
--title "OpenClaw desktop test update channel" \
|
|
--notes "Opt-in updater manifest for unsigned macOS and Windows Tauri test builds."
|
|
fi
|
|
gh release upload "${DESKTOP_TEST_CHANNEL_TAG}" \
|
|
--repo "${GITHUB_REPOSITORY}" \
|
|
--clobber \
|
|
dist/release/latest-desktop-test.json
|