Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 171 additions & 56 deletions .github/workflows/tagged-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -215,91 +220,188 @@ 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: |
set -euo pipefail

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];

if (! macBlock.includes("entitlementsInherit: 'build/entitlements.mac.plist'")) {
throw new Error('NativePHP mac builder block is missing entitlementsInherit.');
}
const updated = normalized.replace(pattern, "$1 throw error\n$2");

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.');
if (updated === normalized) {
throw new Error('Unable to harden the NativePHP notarization hook.');
}

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)"

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

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

rm -f "$certificate_path"

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:
Expand Down Expand Up @@ -329,6 +431,17 @@ 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

if [[ -n "${KATRA_MACOS_CERTIFICATE_PATH:-}" ]]; then
rm -f "$KATRA_MACOS_CERTIFICATE_PATH" || true
fi

- name: Summarize packaging status
run: |
{
Expand All @@ -339,6 +452,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"
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 13 additions & 4 deletions docs/development/nativephp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
Loading