From 4563707cc8ffe2dffc68b0e518501d3c286f2924 Mon Sep 17 00:00:00 2001 From: Derek Bourgeois Date: Thu, 19 Mar 2026 19:18:25 -0400 Subject: [PATCH 1/2] fix(dist): sign and notarize macos desktop builds --- .github/workflows/tagged-release.yml | 216 ++++++++++++++++++++------- README.md | 3 +- docs/development/nativephp.md | 17 ++- 3 files changed, 175 insertions(+), 61 deletions(-) diff --git a/.github/workflows/tagged-release.yml b/.github/workflows/tagged-release.yml index 6048d0c..184da74 100644 --- a/.github/workflows/tagged-release.yml +++ b/.github/workflows/tagged-release.yml @@ -147,7 +147,12 @@ jobs: NATIVEPHP_APP_VERSION: ${{ needs.context.outputs.version }} NATIVEPHP_UPDATER_ENABLED: false SURREAL_VERSION: v3.0.4 - CSC_IDENTITY_AUTO_DISCOVERY: 'false' + CSC_IDENTITY_AUTO_DISCOVERY: 'true' + NATIVEPHP_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }} + NATIVEPHP_APPLE_ID_PASS: ${{ secrets.MACOS_NOTARY_APP_SPECIFIC_PASSWORD }} + NATIVEPHP_APPLE_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }} + KATRA_MACOS_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_BASE64 }} + KATRA_MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD }} steps: - name: Checkout @@ -215,91 +220,181 @@ jobs: tar -xzf "$archive_path" -C "$runtime_dir" chmod +x "$runtime_dir/surreal" - - name: Force consistent ad-hoc macOS signing + - name: Validate macOS signing inputs + run: | + set -euo pipefail + + missing=() + + for required_env in \ + NATIVEPHP_APPLE_ID \ + NATIVEPHP_APPLE_ID_PASS \ + NATIVEPHP_APPLE_TEAM_ID \ + KATRA_MACOS_CERTIFICATE_P12_BASE64 \ + KATRA_MACOS_CERTIFICATE_PASSWORD + do + if [[ -z "${!required_env:-}" ]]; then + missing+=("$required_env") + fi + done + + if (( ${#missing[@]} > 0 )); then + { + echo "::error title=Missing macOS signing inputs::Tagged releases now require Developer ID signing and Apple notarization." + echo "Missing environment values: ${missing[*]}" + echo "Expected GitHub secrets:" + echo "- MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_BASE64" + echo "- MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD" + echo "- MACOS_NOTARY_APPLE_ID" + echo "- MACOS_NOTARY_APP_SPECIFIC_PASSWORD" + echo "- MACOS_NOTARY_TEAM_ID" + } >&2 + + exit 1 + fi + + - name: Import Developer ID certificate run: | node <<'NODE' const { readFileSync, writeFileSync } = require('node:fs'); - const path = 'vendor/nativephp/desktop/resources/electron/electron-builder.mjs'; + const path = 'vendor/nativephp/desktop/resources/electron/build/notarize.js'; const current = readFileSync(path, 'utf8'); const normalized = current.replace(/\r\n/g, '\n'); - const pattern = /^(\s*mac:\s*\{\n)([\s\S]*?)(^\s*\},\n)/m; - const match = normalized.match(pattern); + const pattern = /(\s+console\.error\(error\)\n)(\s+\})/; - if (! match) { - throw new Error('Unable to locate the NativePHP mac builder block.'); + if (! pattern.test(normalized)) { + throw new Error('Unable to locate the NativePHP notarization error handler.'); } - const macBlock = match[2]; + const updated = normalized.replace(pattern, "$1 throw error\n$2"); - if (! macBlock.includes("entitlementsInherit: 'build/entitlements.mac.plist'")) { - throw new Error('NativePHP mac builder block is missing entitlementsInherit.'); + if (updated === normalized) { + throw new Error('Unable to harden the NativePHP notarization hook.'); } - const adHocSettings = [ - " identity: '-',", - ' hardenedRuntime: false,', - ' notarize: false,', - ' gatekeeperAssess: false,', - ]; - - const updatedMacBlock = macBlock.includes("identity: '-'") - ? macBlock - : macBlock.replace( - /(^\s*entitlementsInherit:\s*'build\/entitlements\.mac\.plist',\n)/m, - `$1${adHocSettings.join('\n')}\n`, - ); - - if (updatedMacBlock === macBlock) { - throw new Error('Unable to apply preview macOS signing overrides.'); - } - - writeFileSync(path, normalized.replace(pattern, `$1${updatedMacBlock}$3`)); + writeFileSync(path, updated); NODE + keychain_path="$RUNNER_TEMP/katra-signing.keychain-db" + certificate_path="$RUNNER_TEMP/katra-developer-id-application.p12" + keychain_password="$(openssl rand -hex 24)" + + if ! printf '%s' "$KATRA_MACOS_CERTIFICATE_P12_BASE64" | base64 --decode > "$certificate_path" 2>/dev/null; then + printf '%s' "$KATRA_MACOS_CERTIFICATE_P12_BASE64" | base64 -D > "$certificate_path" + fi + + 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 "$KATRA_MACOS_CERTIFICATE_PASSWORD" \ + -T /usr/bin/codesign \ + -T /usr/bin/security \ + -T /usr/bin/productbuild \ + -T /usr/bin/productsign + security set-key-partition-list \ + -S apple-tool:,apple:,codesign: \ + -s \ + -k "$keychain_password" \ + "$keychain_path" + + existing_keychains="$(security list-keychains -d user | tr -d '"' | tr '\n' ' ')" + security list-keychains -d user -s "$keychain_path" $existing_keychains + security default-keychain -d user -s "$keychain_path" + + if ! security find-identity -v -p codesigning "$keychain_path" | grep -q "Developer ID Application"; then + echo "::error title=Missing Developer ID Application identity::The provided certificate archive did not import a usable macOS distribution identity." >&2 + exit 1 + fi + + echo "KATRA_MACOS_SIGNING_KEYCHAIN=$keychain_path" >> "$GITHUB_ENV" + - name: Validate patched Electron builder config - run: node --check vendor/nativephp/desktop/resources/electron/electron-builder.mjs + run: | + node --check vendor/nativephp/desktop/resources/electron/electron-builder.mjs + node --check vendor/nativephp/desktop/resources/electron/build/notarize.js - name: Build macOS desktop artifact run: php artisan native:build mac ${{ matrix.architecture }} --no-interaction - - name: Stage architecture-specific release assets + - name: Locate macOS build outputs run: | set -euo pipefail - mkdir -p nativephp/electron/release-assets - shopt -s nullglob - - staged_files=0 + app_bundle_path='' + dmg_path='' - for candidate in nativephp/electron/dist/*; do - if [[ ! -f "$candidate" ]]; then - continue + while IFS= read -r candidate; do + if [[ -n "$app_bundle_path" ]]; then + echo "Multiple .app bundles were generated unexpectedly." >&2 + exit 1 fi - filename="$(basename "$candidate")" - staged_name="$filename" - - if [[ "$filename" != *"-${{ matrix.architecture }}."* ]]; then - extension="${filename##*.}" - basename="${filename%.*}" + app_bundle_path="$candidate" + done < <(find nativephp/electron/dist -maxdepth 2 -type d -name '*.app' | sort) - if [[ "$basename" == "$filename" ]]; then - staged_name="${filename}-${{ matrix.architecture }}" - else - staged_name="${basename}-${{ matrix.architecture }}.${extension}" - fi + while IFS= read -r candidate; do + if [[ -n "$dmg_path" ]]; then + echo "Multiple DMG files were generated unexpectedly." >&2 + exit 1 fi - cp "$candidate" "nativephp/electron/release-assets/$staged_name" - staged_files=$((staged_files + 1)) - done + dmg_path="$candidate" + done < <(find nativephp/electron/dist -maxdepth 1 -type f -name '*.dmg' | sort) - if (( staged_files == 0 )); then - echo "No macOS release files were generated." >&2 + if [[ -z "$app_bundle_path" ]]; then + echo "No macOS .app bundle was generated." >&2 + exit 1 + fi + + if [[ -z "$dmg_path" ]]; then + echo "No macOS DMG artifact was generated." >&2 exit 1 fi + echo "KATRA_MACOS_APP_BUNDLE_PATH=$app_bundle_path" >> "$GITHUB_ENV" + echo "KATRA_MACOS_DMG_PATH=$dmg_path" >> "$GITHUB_ENV" + + - name: Verify app code signature + run: | + codesign --verify --deep --strict --verbose=2 "$KATRA_MACOS_APP_BUNDLE_PATH" + codesign --display --verbose=4 "$KATRA_MACOS_APP_BUNDLE_PATH" + + - name: Staple notarized app bundle + run: | + xcrun stapler staple "$KATRA_MACOS_APP_BUNDLE_PATH" + xcrun stapler validate "$KATRA_MACOS_APP_BUNDLE_PATH" + + - name: Notarize release disk image + run: | + xcrun notarytool submit "$KATRA_MACOS_DMG_PATH" \ + --apple-id "$NATIVEPHP_APPLE_ID" \ + --password "$NATIVEPHP_APPLE_ID_PASS" \ + --team-id "$NATIVEPHP_APPLE_TEAM_ID" \ + --wait + + - name: Staple notarized disk image + run: | + xcrun stapler staple "$KATRA_MACOS_DMG_PATH" + xcrun stapler validate "$KATRA_MACOS_DMG_PATH" + + - name: Stage trusted release assets + run: | + set -euo pipefail + + mkdir -p nativephp/electron/release-assets + + dmg_name="$(basename "$KATRA_MACOS_DMG_PATH")" + + cp "$KATRA_MACOS_DMG_PATH" "nativephp/electron/release-assets/$dmg_name" + + ( + cd nativephp/electron/release-assets + shasum -a 256 "$dmg_name" > "$dmg_name.sha256" + ) + - name: Upload workflow artifact uses: actions/upload-artifact@v4 with: @@ -329,6 +424,13 @@ jobs: gh release upload "$RELEASE_TAG" "${release_files[@]}" --clobber + - name: Cleanup signing keychain + if: always() + run: | + if [[ -n "${KATRA_MACOS_SIGNING_KEYCHAIN:-}" ]]; then + security delete-keychain "$KATRA_MACOS_SIGNING_KEYCHAIN" || true + fi + - name: Summarize packaging status run: | { @@ -339,6 +441,8 @@ jobs: echo "- Release tag: \`${{ needs.context.outputs.tag }}\`" echo "- Bundled Surreal runtime: \`${SURREAL_VERSION}\` (\`${{ matrix.surreal_asset_architecture }}\` asset)" echo - echo "- Preview builds are packaged with an explicit ad-hoc macOS signature for launch consistency." - echo "- Developer ID signing and notarization remain tracked separately before trusted distribution." + echo "- Release disk image: \`$(basename "$KATRA_MACOS_DMG_PATH")\`" + echo "- Developer ID signing: enabled" + echo "- Apple notarization: enabled" + echo "- Stapled artifacts: app bundle and release disk image" } >> "$GITHUB_STEP_SUMMARY" diff --git a/README.md b/README.md index b69c6f8..fa8f5a5 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,8 @@ There are two practical ways to try Katra today. - Browse the [GitHub Releases](https://github.com/devoption/katra/releases) page and download the latest macOS desktop asset for your machine. - Choose the architecture-specific asset that matches your Mac when it is available: `x64` for Intel, `arm64` for Apple Silicon. - Desktop preview builds now bundle the local Surreal runtime instead of expecting a separate machine-local `surreal` CLI install. -- Current desktop builds are preview-quality and use ad-hoc macOS signing, so macOS may require `Open Anyway` or a control-click `Open` flow the first time you launch it. +- Recent tagged release builds are signed, notarized, and stapled for macOS distribution, though older preview tags may still require a manual `Open Anyway` flow. +- The app is still preview-quality even when the install path is trusted. ### Run From Source diff --git a/docs/development/nativephp.md b/docs/development/nativephp.md index f237be0..3fee6ab 100644 --- a/docs/development/nativephp.md +++ b/docs/development/nativephp.md @@ -69,7 +69,8 @@ If you want to try Katra without cloning the repository, use the desktop assets - choose the asset that matches your Mac architecture when it is available: `x64` for Intel or `arm64` for Apple Silicon - release builds now bundle the Surreal runtime through NativePHP `extras`, so the desktop shell does not require a separate machine-local `surreal` CLI install - expect preview-quality behavior while the desktop shell and local runtime story are still being built out -- expect Gatekeeper prompts until Developer ID signing and notarization are in place +- recent tagged releases are intended to be signed, notarized, and stapled for normal macOS installation +- older preview releases may still trigger Gatekeeper prompts because they were produced before trusted distribution was added ## Release Artifacts @@ -82,14 +83,22 @@ The current workflow intentionally keeps this first packaging path small: - raw build output: generated under `nativephp/electron/dist` - workflow artifact: preserved from the staged `nativephp/electron/release-assets` directory - bundled local data runtime: the official SurrealDB macOS CLI is downloaded during release builds and packaged under NativePHP `extras` -- release assets: uploaded to the matching GitHub Release with architecture-explicit filenames +- release assets: a notarized architecture-specific DMG plus a matching `sha256` checksum file - current GitHub-hosted runners: `macos-15-intel` for Intel builds and `macos-15` for Apple Silicon builds ### Signing And Notarization -Preview release builds currently force a consistent ad-hoc macOS signature during packaging and disable hardened runtime/notarization in that path so the generated Intel and Apple Silicon apps launch reliably after download. +Tagged macOS releases now import a Developer ID Application certificate in CI, let NativePHP / Electron Builder sign the generated app bundle, notarize the app with Apple, notarize the packaged DMG, and staple the notarization ticket to both artifacts before upload. -That keeps the release artifacts usable for early testing, but they are still not trusted macOS distributions. Gatekeeper prompts remain expected until dedicated Developer ID signing, notarization, and stapling land through the tracked distribution work. +The workflow expects these GitHub repository secrets before a release-worthy merge can publish macOS artifacts: + +- `MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_BASE64` +- `MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD` +- `MACOS_NOTARY_APPLE_ID` +- `MACOS_NOTARY_APP_SPECIFIC_PASSWORD` +- `MACOS_NOTARY_TEAM_ID` + +If any of those values are missing, the `Tagged Release` workflow now fails immediately with a clear configuration error instead of silently falling back to an ad-hoc preview signature. ## Current Bootstrap Behavior From a14f01d44fb9558d337efdd596828e52474e7670 Mon Sep 17 00:00:00 2001 From: Derek Bourgeois Date: Thu, 19 Mar 2026 19:49:55 -0400 Subject: [PATCH 2/2] fix(ci): harden macos signing credential cleanup --- .github/workflows/tagged-release.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/tagged-release.yml b/.github/workflows/tagged-release.yml index 184da74..e92cc7c 100644 --- a/.github/workflows/tagged-release.yml +++ b/.github/workflows/tagged-release.yml @@ -255,6 +255,8 @@ jobs: - name: Import Developer ID certificate run: | + set -euo pipefail + node <<'NODE' const { readFileSync, writeFileSync } = require('node:fs'); @@ -280,6 +282,8 @@ jobs: certificate_path="$RUNNER_TEMP/katra-developer-id-application.p12" keychain_password="$(openssl rand -hex 24)" + echo "KATRA_MACOS_CERTIFICATE_PATH=$certificate_path" >> "$GITHUB_ENV" + if ! printf '%s' "$KATRA_MACOS_CERTIFICATE_P12_BASE64" | base64 --decode > "$certificate_path" 2>/dev/null; then printf '%s' "$KATRA_MACOS_CERTIFICATE_P12_BASE64" | base64 -D > "$certificate_path" fi @@ -294,6 +298,9 @@ jobs: -T /usr/bin/security \ -T /usr/bin/productbuild \ -T /usr/bin/productsign + + rm -f "$certificate_path" + security set-key-partition-list \ -S apple-tool:,apple:,codesign: \ -s \ @@ -431,6 +438,10 @@ jobs: security delete-keychain "$KATRA_MACOS_SIGNING_KEYCHAIN" || true fi + if [[ -n "${KATRA_MACOS_CERTIFICATE_PATH:-}" ]]; then + rm -f "$KATRA_MACOS_CERTIFICATE_PATH" || true + fi + - name: Summarize packaging status run: | {