openclaw/.github/workflows/macos-release.yml

307 lines
11 KiB
YAML

name: macOS Release
on:
workflow_dispatch:
inputs:
tag:
description: Existing release tag to build macOS artifacts for (for example v2026.3.22 or v2026.3.22-beta.1)
required: true
type: string
preflight_only:
description: Run validation/build only and skip the gated publish job
required: true
default: false
type: boolean
concurrency:
group: macos-release-${{ inputs.tag }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.23.0"
SPARKLE_FEED_URL: https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml
jobs:
preflight_macos_release:
runs-on: macos-latest
permissions:
contents: read
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]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]]; then
echo "Invalid release tag format: ${RELEASE_TAG}"
exit 1
fi
- name: Checkout selected tag
uses: actions/checkout@v6
with:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
use-sticky-disk: "false"
- name: Ensure matching GitHub release exists
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
run: gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null
- name: Validate release tag and package metadata
env:
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_MAIN_REF: origin/main
run: |
set -euo pipefail
RELEASE_SHA=$(git rev-parse HEAD)
export RELEASE_SHA RELEASE_TAG RELEASE_MAIN_REF
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
pnpm release:openclaw:npm:check
- name: Resolve package version
id: package_version
run: echo "value=$(node -p 'require(\"./package.json\").version')" >> "$GITHUB_OUTPUT"
- name: Check
run: pnpm check
- name: Build
run: pnpm build
- name: Build Control UI
run: node scripts/ui.js build
- name: Verify release contents
run: pnpm release:check
- name: Swift build
run: swift build --package-path apps/macos --configuration release
- name: Swift test
run: swift test --package-path apps/macos --parallel
- name: Package macOS release with ad-hoc signing
env:
APP_VERSION: ${{ steps.package_version.outputs.value }}
BUNDLE_ID: ai.openclaw.mac
BUILD_CONFIG: release
CODESIGN_TIMESTAMP: "off"
SIGN_IDENTITY: "-"
SKIP_NOTARIZE: "1"
SKIP_PNPM_INSTALL: "1"
SKIP_TSC: "1"
SKIP_UI_BUILD: "1"
SPARKLE_FEED_URL: ${{ env.SPARKLE_FEED_URL }}
run: scripts/package-mac-dist.sh
publish_macos_release:
needs: [preflight_macos_release]
if: ${{ !inputs.preflight_only }}
runs-on: macos-latest
environment: mac-release
concurrency:
# Stable releases all derive the same shared appcast.xml; serialize those
# runs so each artifact starts from the latest stable feed snapshot.
group: macos-release-publish-${{ contains(inputs.tag, '-beta.') && inputs.tag || 'stable-feed' }}
cancel-in-progress: false
permissions:
contents: write
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]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]]; then
echo "Invalid release tag format: ${RELEASE_TAG}"
exit 1
fi
- name: Checkout selected tag
uses: actions/checkout@v6
with:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
use-sticky-disk: "false"
- name: Ensure matching GitHub release exists
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
run: gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null
- name: Resolve package version
id: package_version
run: echo "value=$(node -p 'require(\"./package.json\").version')" >> "$GITHUB_OUTPUT"
- name: Determine release channel
id: release_channel
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
if [[ "$RELEASE_TAG" == *-beta.* ]]; then
echo "is_beta=true" >> "$GITHUB_OUTPUT"
else
echo "is_beta=false" >> "$GITHUB_OUTPUT"
fi
- name: Import Developer ID certificate
env:
MACOS_DEVELOPER_ID_P12_BASE64: ${{ secrets.MACOS_DEVELOPER_ID_P12_BASE64 }}
MACOS_DEVELOPER_ID_P12_PASSWORD: ${{ secrets.MACOS_DEVELOPER_ID_P12_PASSWORD }}
run: |
set -euo pipefail
CERT_PATH="$RUNNER_TEMP/openclaw-macos-release.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/openclaw-release.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -hex 32)"
echo "::add-mask::$KEYCHAIN_PASSWORD"
export CERT_PATH MACOS_DEVELOPER_ID_P12_BASE64
python3 - <<'PY'
import base64
import os
from pathlib import Path
Path(os.environ["CERT_PATH"]).write_bytes(
base64.b64decode(os.environ["MACOS_DEVELOPER_ID_P12_BASE64"])
)
PY
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" \
-k "$KEYCHAIN_PATH" \
-P "$MACOS_DEVELOPER_ID_P12_PASSWORD" \
-T /usr/bin/codesign \
-T /usr/bin/security
EXISTING_KEYCHAINS="$(security list-keychains -d user | tr -d '"')"
security list-keychains -d user -s "$KEYCHAIN_PATH" $EXISTING_KEYCHAINS
security default-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
- name: Resolve signing identity
run: |
set -euo pipefail
SIGN_IDENTITY="$(security find-identity -p codesigning -v "$KEYCHAIN_PATH" 2>/dev/null | awk -F'\"' '/Developer ID Application/ { print $2; exit }')"
if [[ -z "${SIGN_IDENTITY}" ]]; then
echo "Developer ID Application identity not found in imported keychain." >&2
exit 1
fi
echo "SIGN_IDENTITY=$SIGN_IDENTITY" >> "$GITHUB_ENV"
- name: Write notary and Sparkle key files
env:
APP_STORE_CONNECT_API_KEY_P8: ${{ secrets.APP_STORE_CONNECT_API_KEY_P8 }}
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
run: |
set -euo pipefail
NOTARYTOOL_KEY_PATH="$RUNNER_TEMP/openclaw-notary.p8"
SPARKLE_PRIVATE_KEY_PATH="$RUNNER_TEMP/openclaw-sparkle-ed25519.pem"
export NOTARYTOOL_KEY_PATH SPARKLE_PRIVATE_KEY_PATH
python3 - <<'PY'
import os
from pathlib import Path
def write_secret(path_env: str, value_env: str) -> None:
value = os.environ[value_env].replace("\\n", "\n")
Path(os.environ[path_env]).write_text(value, encoding="utf-8")
write_secret("NOTARYTOOL_KEY_PATH", "APP_STORE_CONNECT_API_KEY_P8")
write_secret("SPARKLE_PRIVATE_KEY_PATH", "SPARKLE_PRIVATE_KEY")
PY
echo "NOTARYTOOL_KEY=$NOTARYTOOL_KEY_PATH" >> "$GITHUB_ENV"
echo "NOTARYTOOL_KEY_ID=$APP_STORE_CONNECT_KEY_ID" >> "$GITHUB_ENV"
echo "NOTARYTOOL_ISSUER=$APP_STORE_CONNECT_ISSUER_ID" >> "$GITHUB_ENV"
echo "SPARKLE_PRIVATE_KEY_FILE=$SPARKLE_PRIVATE_KEY_PATH" >> "$GITHUB_ENV"
- name: Build, sign, notarize, and package macOS release
env:
APP_VERSION: ${{ steps.package_version.outputs.value }}
BUNDLE_ID: ai.openclaw.mac
BUILD_CONFIG: release
SIGN_IDENTITY: ${{ env.SIGN_IDENTITY }}
SKIP_PNPM_INSTALL: "1"
SPARKLE_FEED_URL: ${{ env.SPARKLE_FEED_URL }}
run: scripts/package-mac-dist.sh
- name: Checkout main branch for appcast seed
if: ${{ steps.release_channel.outputs.is_beta != 'true' }}
uses: actions/checkout@v6
with:
path: openclaw-main
ref: main
fetch-depth: 0
- name: Seed appcast from main
if: ${{ steps.release_channel.outputs.is_beta != 'true' }}
run: |
set -euo pipefail
APPCAST_SOURCE="openclaw-main/appcast.xml"
if [[ -f "$APPCAST_SOURCE" ]]; then
cp "$APPCAST_SOURCE" appcast.xml
else
echo "No existing appcast at $APPCAST_SOURCE; generating a fresh feed."
fi
- name: Generate signed appcast artifact
if: ${{ steps.release_channel.outputs.is_beta != 'true' }}
env:
SPARKLE_DOWNLOAD_URL_PREFIX: https://github.com/openclaw/openclaw/releases/download/${{ inputs.tag }}/
SPARKLE_RELEASE_VERSION: ${{ steps.package_version.outputs.value }}
run: scripts/make_appcast.sh "dist/OpenClaw-${{ steps.package_version.outputs.value }}.zip" "${{ env.SPARKLE_FEED_URL }}"
- name: Upload stable appcast artifact
if: ${{ steps.release_channel.outputs.is_beta != 'true' }}
uses: actions/upload-artifact@v7
with:
name: macos-appcast-${{ inputs.tag }}
path: appcast.xml
if-no-files-found: error
- name: Skip shared appcast for beta releases
if: ${{ steps.release_channel.outputs.is_beta == 'true' }}
run: echo "Beta release detected; skip shared production appcast artifact generation."
- name: Upload macOS assets to GitHub release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
VERSION: ${{ steps.package_version.outputs.value }}
run: |
set -euo pipefail
gh release upload "$RELEASE_TAG" \
"dist/OpenClaw-$VERSION.zip" \
"dist/OpenClaw-$VERSION.dmg" \
"dist/OpenClaw-$VERSION.dSYM.zip" \
--clobber \
--repo "$GITHUB_REPOSITORY"
- name: Clean up signing keychain
if: always()
run: |
if [[ -n "${KEYCHAIN_PATH:-}" ]]; then
security delete-keychain "$KEYCHAIN_PATH" >/dev/null 2>&1 || true
fi