From 6e8d5cd5786bcc0060cb3f1666f3dffcf4c8da6d Mon Sep 17 00:00:00 2001 From: Onur Date: Mon, 23 Mar 2026 22:35:51 +0100 Subject: [PATCH] Release: privatize macOS publish flow (#53166) --- .../openclaw-release-maintainer/SKILL.md | 75 ++-- .github/workflows/macos-release.yml | 320 +----------------- 2 files changed, 59 insertions(+), 336 deletions(-) diff --git a/.agents/skills/openclaw-release-maintainer/SKILL.md b/.agents/skills/openclaw-release-maintainer/SKILL.md index ea0ab5d87f3..063c79503a7 100644 --- a/.agents/skills/openclaw-release-maintainer/SKILL.md +++ b/.agents/skills/openclaw-release-maintainer/SKILL.md @@ -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-` 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 diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml index 3b16ce2365a..6af1b552493 100644 --- a/.github/workflows/macos-release.yml +++ b/.github/workflows/macos-release.yml @@ -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"