mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
release: automate macOS publishing (#52853)
* release: automate macOS publishing * release: keep mac appcast in openclaw repo * release: add preflight-only release workflow runs * release: keep appcast updates manual * release: generate signed appcast as workflow artifact * release: require preflight before publish * release: require mac app for every release * docs: clarify every release ships mac app * release: document Sparkle feed and SHA rules * release: keep publish flow tag-based * release: stabilize mac appcast flow * release: document local mac fallback
This commit is contained in:
306
.github/workflows/macos-release.yml
vendored
Normal file
306
.github/workflows/macos-release.yml
vendored
Normal file
@@ -0,0 +1,306 @@
|
||||
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
|
||||
69
.github/workflows/openclaw-npm-release.yml
vendored
69
.github/workflows/openclaw-npm-release.yml
vendored
@@ -7,6 +7,11 @@ on:
|
||||
description: Release tag to publish (for example v2026.3.22, v2026.3.22-beta.1, or fallback v2026.3.22-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: openclaw-npm-release-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}
|
||||
@@ -18,13 +23,10 @@ env:
|
||||
PNPM_VERSION: "10.23.0"
|
||||
|
||||
jobs:
|
||||
publish_openclaw_npm:
|
||||
# npm trusted publishing + provenance requires a GitHub-hosted runner.
|
||||
preflight_openclaw_npm:
|
||||
runs-on: ubuntu-latest
|
||||
environment: npm-release
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Validate tag input format
|
||||
env:
|
||||
@@ -84,5 +86,64 @@ jobs:
|
||||
- name: Verify release contents
|
||||
run: pnpm release:check
|
||||
|
||||
publish_openclaw_npm:
|
||||
# npm trusted publishing + provenance requires a GitHub-hosted runner.
|
||||
needs: [preflight_openclaw_npm]
|
||||
if: ${{ !inputs.preflight_only }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: npm-release
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: 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
|
||||
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: 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
|
||||
# Fetch the full main ref so merge-base ancestry checks keep working
|
||||
# for older tagged commits that are still contained in main.
|
||||
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||
pnpm release:openclaw:npm:check
|
||||
|
||||
- name: Ensure version is not already published
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
||||
|
||||
if npm view "openclaw@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
|
||||
echo "openclaw@${PACKAGE_VERSION} is already published on npm."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Publishing openclaw@${PACKAGE_VERSION}"
|
||||
|
||||
- name: Publish
|
||||
run: bash scripts/openclaw-npm-publish.sh --publish
|
||||
|
||||
Reference in New Issue
Block a user