feature(macos): keep polishing onboard steps #53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: macOS Latest Release | |
| on: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| allow_unsigned: | |
| description: "Allow ad-hoc signed macOS artifacts when Apple signing secrets are unavailable" | |
| required: false | |
| default: "false" | |
| type: choice | |
| options: | |
| - "true" | |
| - "false" | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: macos-latest-release | |
| cancel-in-progress: true | |
| jobs: | |
| build-macos: | |
| name: Build macOS ${{ matrix.target }} | |
| runs-on: ${{ matrix.runner }} | |
| outputs: | |
| signing_mode: ${{ steps.prepare-signing.outputs.signing_mode }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: aarch64-apple-darwin | |
| runner: macos-15 | |
| - target: x86_64-apple-darwin | |
| runner: macos-15-intel | |
| env: | |
| ALLOW_UNSIGNED: ${{ github.event_name == 'workflow_dispatch' && inputs.allow_unsigned || 'false' }} | |
| MACOS_BUNDLE_RUNTIME: "1" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Setup uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: | | |
| pyproject.toml | |
| uv.lock | |
| - name: Cache macOS runtime payloads | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/Library/Caches/ElephantAgent/macos-runtime/${{ matrix.target }} | |
| key: macos-runtime-${{ matrix.target }}-${{ hashFiles('pyproject.toml', 'uv.lock', 'apps/macos/Scripts/build-app.sh', 'apps/macos/Scripts/package-runtime.sh') }} | |
| restore-keys: | | |
| macos-runtime-${{ matrix.target }}- | |
| - name: Prepare Apple signing | |
| id: prepare-signing | |
| shell: bash | |
| env: | |
| APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_PASSWORD_FALLBACK: ${{ secrets.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| set -euo pipefail | |
| notary_password="${APPLE_APP_SPECIFIC_PASSWORD:-${APPLE_PASSWORD_FALLBACK:-}}" | |
| missing=() | |
| for var in APPLE_CERTIFICATE_BASE64 APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_ID APPLE_TEAM_ID; do | |
| if [ -z "${!var:-}" ]; then | |
| missing+=("$var") | |
| fi | |
| done | |
| if [ -z "${notary_password}" ]; then | |
| missing+=("APPLE_APP_SPECIFIC_PASSWORD or APPLE_PASSWORD") | |
| fi | |
| if [ "${#missing[@]}" -gt 0 ]; then | |
| if [ "${ALLOW_UNSIGNED}" = "true" ]; then | |
| echo "::warning::Apple signing secrets are missing; publishing ad-hoc signed artifacts. These are useful for CI artifacts but are not Gatekeeper-smooth for general users." | |
| printf 'Missing: %s\n' "${missing[*]}" | |
| echo "MACOS_SIGNING_IDENTITY=-" >> "$GITHUB_ENV" | |
| echo "MACOS_NOTARIZE=0" >> "$GITHUB_ENV" | |
| echo "MACOS_RELEASE_SIGNING_MODE=ad-hoc" >> "$GITHUB_ENV" | |
| echo "signing_mode=ad-hoc" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "::error::Apple signing/notarization secrets are required for automatic latest releases." | |
| printf 'Missing: %s\n' "${missing[*]}" | |
| exit 1 | |
| fi | |
| echo "::add-mask::${notary_password}" | |
| keychain_path="${RUNNER_TEMP}/elephant-signing.keychain-db" | |
| keychain_password="$(openssl rand -base64 32)" | |
| certificate_path="${RUNNER_TEMP}/apple-signing.p12" | |
| echo "::add-mask::${keychain_password}" | |
| echo "${APPLE_CERTIFICATE_BASE64}" | base64 --decode > "${certificate_path}" | |
| 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 "${certificate_path}" \ | |
| -k "${keychain_path}" \ | |
| -P "${APPLE_CERTIFICATE_PASSWORD}" \ | |
| -T /usr/bin/codesign \ | |
| -T /usr/bin/security | |
| security set-key-partition-list -S apple-tool:,apple: -s -k "${keychain_password}" "${keychain_path}" | |
| security list-keychains -d user -s "${keychain_path}" $(security list-keychains -d user | tr -d '"') | |
| echo "Available codesigning identities:" | |
| security find-identity -v -p codesigning "${keychain_path}" | |
| resolved_identity="$( | |
| security find-identity -v -p codesigning "${keychain_path}" \ | |
| | awk '/Developer ID Application|Mac Developer ID Application/ { print $2; exit }' | |
| )" | |
| if [ -z "${resolved_identity}" ]; then | |
| echo "::error::No Developer ID Application identity is available after importing the p12 certificate." | |
| exit 1 | |
| fi | |
| echo "MACOS_SIGNING_IDENTITY=${resolved_identity}" >> "$GITHUB_ENV" | |
| echo "MACOS_NOTARIZE=1" >> "$GITHUB_ENV" | |
| echo "MACOS_RELEASE_SIGNING_MODE=developer-id-notarized" >> "$GITHUB_ENV" | |
| echo "signing_mode=developer-id-notarized" >> "$GITHUB_OUTPUT" | |
| echo "APPLE_ID=${APPLE_ID}" >> "$GITHUB_ENV" | |
| echo "APPLE_PASSWORD=${notary_password}" >> "$GITHUB_ENV" | |
| echo "APPLE_TEAM_ID=${APPLE_TEAM_ID}" >> "$GITHUB_ENV" | |
| - name: Build macOS artifact | |
| shell: bash | |
| env: | |
| MACOS_APP_BUILD_NUMBER: ${{ github.run_number }} | |
| run: | | |
| make macos-build MACOS_TARGET="${{ matrix.target }}" MACOS_BUNDLE_RUNTIME="${MACOS_BUNDLE_RUNTIME}" | |
| - name: Verify self-contained runtime | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| app="apps/macos/.build/release/${{ matrix.target }}/Elephant Agent.app" | |
| runtime="${app}/Contents/Resources/Runtime" | |
| test -x "${runtime}/python/bin/python3.12" | |
| test -d "${runtime}/site-packages" | |
| test -d "${runtime}/ms-playwright" | |
| test -f "${runtime}/manifest.json" | |
| du -sh "${app}" "${runtime}" | |
| - name: Verify signing and notarization | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| app="apps/macos/.build/release/${{ matrix.target }}/Elephant Agent.app" | |
| dmg="$(find "apps/macos/.build/artifacts/${{ matrix.target }}" -maxdepth 1 -name 'ElephantAgent_*_${{ matrix.target }}.dmg' -print -quit)" | |
| test -n "${dmg}" | |
| codesign --verify --deep --strict --verbose=2 "${app}" | |
| codesign --verify --verbose=2 "${dmg}" | |
| if [ "${MACOS_SIGNING_IDENTITY}" != "-" ]; then | |
| xcrun stapler validate "${app}" | |
| xcrun stapler validate "${dmg}" | |
| spctl -a -vvv -t exec "${app}" | |
| spctl -a -vvv -t open --context context:primary-signature "${dmg}" | |
| fi | |
| - name: Upload macOS artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: macos-${{ matrix.target }} | |
| path: apps/macos/.build/artifacts/${{ matrix.target }}/* | |
| if-no-files-found: error | |
| retention-days: 14 | |
| publish-latest: | |
| name: Publish latest release | |
| runs-on: ubuntu-latest | |
| needs: | |
| - build-macos | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download macOS artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: macos-* | |
| path: release-assets | |
| merge-multiple: true | |
| - name: Replace latest release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| MACOS_ASSET_DIR: ${{ github.workspace }}/release-assets | |
| MACOS_RELEASE_TAG: latest | |
| MACOS_RELEASE_TITLE: Elephant Agent latest | |
| MACOS_RELEASE_SIGNING_MODE: ${{ needs.build-macos.outputs.signing_mode || 'unknown' }} | |
| run: | | |
| bash apps/macos/Scripts/release-latest.sh |