Release: privatize macOS publish flow (#53166)

This commit is contained in:
Onur 2026-03-23 22:35:51 +01:00 committed by GitHub
parent ade0182ae0
commit 6e8d5cd578
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 59 additions and 336 deletions

View File

@ -85,8 +85,9 @@ OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke
- `pnpm release:check`
- `OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke`
- Check all release-related build surfaces touched by the release, not only the npm package.
- Include mac release readiness in preflight by running or inspecting the mac
packaging, notarization, and appcast flow for every release.
- Include mac release readiness in preflight by running the public validation
workflow in `openclaw/openclaw` and the real mac preflight in
`openclaw/releases-private` for every release.
- Treat the `appcast.xml` update on `main` as part of mac release readiness, not an optional follow-up.
- The workflows remain tag-based. The agent is responsible for making sure
preflight runs complete successfully before any publish run starts.
@ -104,34 +105,42 @@ OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke
- OpenClaw publish uses GitHub trusted publishing.
- The publish run must be started manually with `workflow_dispatch`.
- Both release workflows accept `preflight_only=true` to run CI
validation/build steps without entering the gated publish job.
- The npm workflow and the private mac publish workflow accept
`preflight_only=true` to run validation/build/package steps without uploading
public release assets.
- `preflight_only=true` on the npm workflow is also the right way to validate an
existing tag after publish; it should keep running the build checks even when
the npm version is already published.
- Validation-only runs may be dispatched from a branch when you are testing a
workflow change before merge.
- macOS release workflows run on GitHub's xlarge macOS runner and use a
- `.github/workflows/macos-release.yml` in `openclaw/openclaw` is now a
public validation-only handoff. It validates the tag/release state and points
operators to the private repo; it does not build or publish macOS artifacts.
- Real mac preflight and real mac publish both use
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml`.
- The private mac workflow runs on GitHub's xlarge macOS runner and uses a
SwiftPM cache because the Swift build/test/package path is CPU-heavy.
- macOS preflight uploads the ad-hoc `.zip` output as a workflow artifact so
maintainers can download and inspect the built package before any real
publish run.
- npm preflight and macOS preflight must both pass before any publish run
starts.
- Private mac preflight uploads notarized build artifacts as workflow artifacts
instead of uploading public GitHub release assets.
- npm preflight, public mac validation, and private mac preflight must all pass
before any real publish run starts.
- Real publish runs must be dispatched from `main`; branch-dispatched publish
attempts should fail before the protected environment is reached.
- The release workflows stay tag-based; rely on the documented release sequence
rather than workflow-level SHA pinning.
- The `npm-release` environment must be approved by `@openclaw/openclaw-release-managers` before publish continues.
- Mac publish uses `.github/workflows/macos-release.yml` for build, signing,
notarization, stable-feed `appcast.xml` artifact generation, and release-asset
upload.
- Mac publish uses
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml` for
build, signing, notarization, stable-feed `appcast.xml` artifact generation,
and release-asset upload.
- The agent must download the signed `appcast.xml` artifact from a successful
stable mac workflow and then update `appcast.xml` on `main`.
stable private mac workflow and then update `appcast.xml` on `main`.
- For beta mac releases, do not update the shared production `appcast.xml`
unless a separate beta Sparkle feed exists.
- `.github/workflows/macos-release.yml` still requires the `mac-release`
environment approval.
- The private repo targets a dedicated `mac-release` environment. If the GitHub
plan does not yet support required reviewers there, do not assume the
environment alone is the approval boundary; rely on private repo access and
CODEOWNERS until those settings can be enabled.
- Do not use `NPM_TOKEN` or the plugin OTP flow for OpenClaw releases.
- `@openclaw/*` plugin publishes use a separate maintainer-only flow.
- Only publish plugins that already exist on npm; bundled disk-tree-only plugins stay unpublished.
@ -165,25 +174,27 @@ OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke
8. Create or refresh the matching GitHub release.
9. Start `.github/workflows/openclaw-npm-release.yml` with `preflight_only=true`
and wait for it to pass.
10. Start `.github/workflows/macos-release.yml` with `preflight_only=true` and
wait for it to pass.
11. If either preflight fails, fix the issue on a new commit, delete the tag
and matching GitHub release, recreate them from the fixed commit, and rerun
both preflights from scratch before continuing. Never reuse old preflight
results after the commit changes.
12. Start `.github/workflows/openclaw-npm-release.yml` with the same tag for
10. Start `.github/workflows/macos-release.yml` in `openclaw/openclaw` and wait
for the public validation-only run to pass.
11. Start
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml`
with `preflight_only=true` and wait for it to pass.
12. If any preflight or validation run fails, fix the issue on a new commit,
delete the tag and matching GitHub release, recreate them from the fixed
commit, and rerun all relevant preflights from scratch before continuing.
Never reuse old preflight results after the commit changes.
13. Start `.github/workflows/openclaw-npm-release.yml` with the same tag for
the real publish.
13. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`.
14. Start `.github/workflows/macos-release.yml` for the real publish and wait
for `mac-release` approval and success.
15. For stable releases, let the mac workflow generate the signed
`appcast.xml` artifact before it uploads the public mac assets, then
download that artifact from the successful run, update `appcast.xml` on
`main`, and verify the feed.
16. For beta releases, publish the mac assets but expect no shared production
14. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`.
15. Start
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml`
for the real publish and wait for success.
16. For stable releases, download `macos-appcast-<tag>` from the successful
private mac run, update `appcast.xml` on `main`, and verify the feed.
17. For beta releases, publish the mac assets but expect no shared production
`appcast.xml` artifact and do not update the shared production feed unless a
separate beta feed exists.
17. After publish, verify npm and any attached release artifacts.
18. After publish, verify npm and any attached release artifacts.
## GHSA advisory work

View File

@ -4,13 +4,13 @@ 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)
description: Existing release tag to validate for macOS release handoff (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
description: Retained for operator compatibility; this public workflow is validation-only
required: true
default: false
default: true
type: boolean
concurrency:
@ -21,13 +21,10 @@ 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:
# Use GitHub's xlarge macOS runner because release packaging is
# Swift-heavy and benefits from the faster hosted hardware tier.
runs-on: macos-latest-xlarge
validate_macos_release_request:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
@ -55,20 +52,6 @@ jobs:
install-bun: "false"
use-sticky-disk: "false"
- name: Select Xcode 26.1
run: |
sudo xcode-select -s /Applications/Xcode_26.1.app
xcodebuild -version
swift --version
- name: Cache SwiftPM
uses: actions/cache@v5
with:
path: ~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-swiftpm-release-${{ hashFiles('apps/macos/Package.resolved') }}
restore-keys: |
${{ runner.os }}-swiftpm-release-
- name: Ensure matching GitHub release exists
env:
GH_TOKEN: ${{ github.token }}
@ -86,288 +69,17 @@ jobs:
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
env:
NODE_OPTIONS: --max-old-space-size=4096
run: pnpm release:check
- name: Swift test
run: |
set -euo pipefail
for attempt in 1 2 3; do
if swift test --package-path apps/macos --parallel; then
exit 0
fi
echo "swift test failed (attempt $attempt/3). Retrying…"
sleep $((attempt * 20))
done
exit 1
- name: Package macOS release with ad-hoc signing
id: package_preflight
env:
BUNDLE_ID: ai.openclaw.mac
BUILD_CONFIG: release
CODESIGN_TIMESTAMP: "off"
SIGN_IDENTITY: "-"
SKIP_PNPM_INSTALL: "1"
SKIP_TSC: "1"
SKIP_UI_BUILD: "1"
SPARKLE_FEED_URL: ${{ env.SPARKLE_FEED_URL }}
run: |
set -euo pipefail
scripts/package-mac-app.sh
VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" dist/OpenClaw.app/Contents/Info.plist)
ZIP_PATH="dist/OpenClaw-${VERSION}.zip"
rm -f "$ZIP_PATH"
ditto -c -k --sequesterRsrc --keepParent dist/OpenClaw.app "$ZIP_PATH"
echo "zip_path=$ZIP_PATH" >> "$GITHUB_OUTPUT"
- name: Upload preflight macOS artifacts
uses: actions/upload-artifact@v7
with:
name: macos-preflight-${{ inputs.tag }}
path: ${{ steps.package_preflight.outputs.zip_path }}
if-no-files-found: error
validate_publish_dispatch_ref:
if: ${{ !inputs.preflight_only }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Require main workflow ref for publish
env:
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]]; then
echo "Real publish runs must be dispatched from main. Use preflight_only=true for branch validation."
exit 1
fi
publish_macos_release:
needs: [preflight_macos_release, validate_publish_dispatch_ref]
if: ${{ !inputs.preflight_only }}
runs-on: macos-latest-xlarge
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
- name: Summarize next step
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: Select Xcode 26.1
run: |
sudo xcode-select -s /Applications/Xcode_26.1.app
xcodebuild -version
swift --version
- name: Cache SwiftPM
uses: actions/cache@v5
with:
path: ~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-swiftpm-release-${{ hashFiles('apps/macos/Package.resolved') }}
restore-keys: |
${{ runner.os }}-swiftpm-release-
- 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
{
echo "## Public macOS validation only"
echo
echo "This workflow no longer builds, signs, notarizes, or uploads macOS assets."
echo
echo "Next step:"
echo "- Run \`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml\` with tag \`${RELEASE_TAG}\`."
echo "- Use \`preflight_only=true\` there for the full private mac preflight."
echo "- For stable releases, download \`macos-appcast-${RELEASE_TAG}\` from the successful private run and commit \`appcast.xml\` back to \`main\` in \`openclaw/openclaw\`."
} >> "$GITHUB_STEP_SUMMARY"