From 2dcbbebf4e7ae3b791a66f7e4d34f952d80aea26 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 22 Jul 2026 18:59:56 +0300 Subject: [PATCH] fix: build macOS x64 releases on Intel --- .github/workflows/release.yml | 4 +- .../workflows/repair-macos-x64-release.yml | 207 ++++++++++++++++++ 2 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/repair-macos-x64-release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eef878e0..d706a73a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -124,7 +124,7 @@ jobs: build-desktop-electron-macos: needs: create-release - runs-on: macos-26 + runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: @@ -132,9 +132,11 @@ jobs: - target: aarch64-apple-darwin arch: arm64 platform: darwin-aarch64 + runner: macos-26 - target: x86_64-apple-darwin arch: x64 platform: darwin-x86_64 + runner: macos-15-intel steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 diff --git a/.github/workflows/repair-macos-x64-release.yml b/.github/workflows/repair-macos-x64-release.yml new file mode 100644 index 00000000..f6e224e8 --- /dev/null +++ b/.github/workflows/repair-macos-x64-release.yml @@ -0,0 +1,207 @@ +name: Repair macOS x64 Release + +on: + workflow_dispatch: + inputs: + version: + description: Existing release version without the v prefix + required: true + type: string + ref: + description: Source ref to package + required: true + type: string + manifest_run_id: + description: Release run containing the successful arm64 latest-mac.yml artifact + required: true + type: string + +permissions: + actions: read + contents: write + +jobs: + repair-macos-x64-release: + runs-on: macos-15-intel + steps: + - name: Checkout release source + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + ref: ${{ inputs.ref }} + + - name: Verify requested version + env: + VERSION: ${{ inputs.version }} + run: | + PACKAGE_VERSION=$(node -p "require('./package.json').version") + if [ "$PACKAGE_VERSION" != "$VERSION" ]; then + echo "Requested version $VERSION does not match package version $PACKAGE_VERSION" + exit 1 + fi + + - name: Setup bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '22' + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Get bundled OpenCode CLI version + id: opencode_cli_version + run: | + VERSION=$(node -p "require('./package.json').dependencies['@opencode-ai/sdk']") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Cache bundled OpenCode CLI artifact + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + with: + path: packages/electron/.cache/opencode-cli + key: opencode-cli-${{ runner.os }}-x64-${{ steps.opencode_cli_version.outputs.version }} + restore-keys: | + opencode-cli-${{ runner.os }}-x64- + + - name: Install Apple Certificate + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + run: | + KEYCHAIN_PATH=$RUNNER_TEMP/electron-signing.keychain-db + KEYCHAIN_PASSWORD=$(openssl rand -base64 32) + + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + echo "$APPLE_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/certificate.p12" + security import "$RUNNER_TEMP/certificate.p12" \ + -P "$APPLE_CERTIFICATE_PASSWORD" \ + -A -t cert -f pkcs12 \ + -k "$KEYCHAIN_PATH" + + security list-keychain -d user -s "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple:,codesign: \ + -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + - name: Build Electron app + working-directory: packages/electron + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + ELECTRON_BUILDER_ARCH: x64 + run: | + bun run build:web-assets + bun run prepare:opencode-cli + bun run verify:opencode-cli + bun run bundle:main + bun run rebuild:native + bunx electron-builder --mac --x64 --publish=never + bun run verify:opencode-cli:packaged + + - name: Verify signature, entitlements, and notarization + run: | + set -euo pipefail + + APP_PATH=$(find packages/electron/dist/mac -maxdepth 2 -name "*.app" -print -quit) + if [ -z "$APP_PATH" ]; then + echo "Error: x64 .app not found" + exit 1 + fi + + codesign -vv --deep --strict "$APP_PATH" + CS_INFO=$(codesign -dv --verbose=4 "$APP_PATH" 2>&1) + echo "$CS_INFO" + if ! echo "$CS_INFO" | grep -q "flags=.*runtime"; then + echo "Error: hardened runtime flag missing" + exit 1 + fi + xcrun stapler validate "$APP_PATH" + + ENTITLEMENTS=$(codesign -d --entitlements :- "$APP_PATH" 2>&1 || true) + if echo "$ENTITLEMENTS" | grep -q "com.apple.security.app-sandbox"; then + echo "Error: app sandbox entitlement is present" + exit 1 + fi + for key in \ + com.apple.security.cs.allow-jit \ + com.apple.security.cs.allow-unsigned-executable-memory \ + com.apple.security.cs.disable-library-validation + do + if ! echo "$ENTITLEMENTS" | grep -q "$key"; then + echo "Error: required entitlement missing: $key" + exit 1 + fi + done + + - name: Download arm64 update manifest + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: latest-yml-aarch64-apple-darwin + path: artifacts/latest-yml-aarch64-apple-darwin + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ inputs.manifest_run_id }} + + - name: Finalize combined macOS update manifest + env: + LATEST_YML_DIR: ${{ github.workspace }}/artifacts + GH_REPO: ${{ github.repository }} + OPENCHAMBER_VERSION: ${{ inputs.version }} + run: | + mkdir -p artifacts/latest-yml-x86_64-apple-darwin + cp packages/electron/dist/latest-mac.yml artifacts/latest-yml-x86_64-apple-darwin/latest-mac.yml + node packages/electron/scripts/finalize-latest-yml.mjs + test -s "$RUNNER_TEMP/latest-mac.yml" + + - name: Upload repaired macOS release files + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 + with: + tag_name: v${{ inputs.version }} + draft: false + files: | + packages/electron/dist/OpenChamber-${{ inputs.version }}-mac-x64.dmg + packages/electron/dist/OpenChamber-${{ inputs.version }}-mac-x64.zip + packages/electron/dist/OpenChamber-${{ inputs.version }}-mac-x64.dmg.blockmap + packages/electron/dist/OpenChamber-${{ inputs.version }}-mac-x64.zip.blockmap + ${{ runner.temp }}/latest-mac.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify repaired release inventory + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPOSITORY: ${{ github.repository }} + VERSION: ${{ inputs.version }} + run: | + node - <<'NODE' + (async () => { + const { GITHUB_TOKEN: token, REPOSITORY: repo, VERSION: version } = process.env; + const expected = [ + `OpenChamber-${version}-mac-arm64.dmg`, + `OpenChamber-${version}-mac-arm64.zip`, + `OpenChamber-${version}-mac-x64.dmg`, + `OpenChamber-${version}-mac-x64.zip`, + 'latest-mac.yml', + ]; + const response = await fetch(`https://api.github.com/repos/${repo}/releases/tags/v${version}`, { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' }, + }); + if (!response.ok) throw new Error(`Failed to inspect release: ${response.status} ${await response.text()}`); + const release = await response.json(); + if (release.draft) throw new Error(`Release v${version} is still a draft`); + for (const name of expected) { + const matches = release.assets.filter((asset) => asset.name === name); + if (matches.length !== 1) throw new Error(`Expected exactly one ${name}, found ${matches.length}`); + if (!Number.isSafeInteger(matches[0].size) || matches[0].size <= 0) { + throw new Error(`Release asset ${name} has invalid size ${matches[0].size}`); + } + } + console.log(`Verified published v${version} with arm64, x64, and combined macOS update assets.`); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE