diff --git a/.github/actions/macos-code-sign/action.yml b/.github/actions/macos-code-sign/action.yml new file mode 100644 index 00000000..273a433e --- /dev/null +++ b/.github/actions/macos-code-sign/action.yml @@ -0,0 +1,163 @@ +name: macos-code-sign +description: Sign and notarize macOS PyInstaller binaries + +inputs: + binary-path: + description: Path to the binary to sign + required: true + apple-certificate-p12: + description: Base64-encoded Apple signing certificate (P12) + required: true + apple-certificate-password: + description: Password for the signing certificate + required: true + apple-notarization-key-p8: + description: Base64-encoded Apple notarization key (P8) + required: true + apple-notarization-key-id: + description: Apple notarization key ID + required: true + apple-notarization-issuer-id: + description: Apple notarization issuer ID + required: true + +runs: + using: composite + steps: + - name: Import signing certificate + shell: bash + env: + APPLE_CERTIFICATE_P12: ${{ inputs.apple-certificate-p12 }} + APPLE_CERTIFICATE_PASSWORD: ${{ inputs.apple-certificate-password }} + KEYCHAIN_PASSWORD: actions + run: | + set -euo pipefail + + # Decode certificate + cert_path="${RUNNER_TEMP}/certificate.p12" + echo "$APPLE_CERTIFICATE_P12" | base64 -d > "$cert_path" + + # Create temporary keychain + keychain_path="${RUNNER_TEMP}/signing.keychain-db" + security create-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path" + security set-keychain-settings -lut 21600 "$keychain_path" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path" + + # Add to keychain search list + security list-keychains -d user -s "$keychain_path" $(security list-keychains -d user | tr -d '"') + security default-keychain -s "$keychain_path" + + # Import certificate + security import "$cert_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" > /dev/null + + # Find signing identity + IDENTITY=$(security find-identity -v -p codesigning "$keychain_path" | grep "Developer ID Application" | head -1 | sed -n 's/.*"\(Developer ID Application[^"]*\)".*/\1/p') + + if [[ -z "$IDENTITY" ]]; then + echo "❌ No Developer ID Application identity found" + security find-identity -v -p codesigning "$keychain_path" + exit 1 + fi + + echo "✅ Found signing identity: $IDENTITY" + echo "APPLE_SIGNING_IDENTITY=$IDENTITY" >> "$GITHUB_ENV" + echo "APPLE_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV" + + rm -f "$cert_path" + + - name: Sign PyInstaller binary and embedded libraries + shell: bash + env: + BINARY_PATH: ${{ inputs.binary-path }} + run: | + set -euo pipefail + + echo "Signing PyInstaller binary: $BINARY_PATH" + + # PyInstaller onefile binaries embed libraries that get extracted at runtime. + # We need to unpack, sign everything, and repack. + + # First, try signing the binary directly with --deep + # For single-file PyInstaller executables, this should work + codesign --deep --force --options runtime --timestamp \ + --sign "$APPLE_SIGNING_IDENTITY" \ + --keychain "$APPLE_KEYCHAIN_PATH" \ + "$BINARY_PATH" + + echo "✅ Binary signed" + codesign -dv --verbose=2 "$BINARY_PATH" + + - name: Notarize binary + shell: bash + env: + BINARY_PATH: ${{ inputs.binary-path }} + APPLE_NOTARIZATION_KEY_P8: ${{ inputs.apple-notarization-key-p8 }} + APPLE_NOTARIZATION_KEY_ID: ${{ inputs.apple-notarization-key-id }} + APPLE_NOTARIZATION_ISSUER_ID: ${{ inputs.apple-notarization-issuer-id }} + run: | + set -euo pipefail + + # Save API key + key_path="${RUNNER_TEMP}/AuthKey.p8" + echo "$APPLE_NOTARIZATION_KEY_P8" | base64 -d > "$key_path" + + # Create zip for notarization + binary_name=$(basename "$BINARY_PATH") + zip_path="${RUNNER_TEMP}/${binary_name}.zip" + ditto -c -k --keepParent "$BINARY_PATH" "$zip_path" + + echo "Submitting for notarization..." + + # Submit and wait + result=$(xcrun notarytool submit "$zip_path" \ + --key "$key_path" \ + --key-id "$APPLE_NOTARIZATION_KEY_ID" \ + --issuer "$APPLE_NOTARIZATION_ISSUER_ID" \ + --wait \ + --timeout 10m \ + --output-format json 2>&1) || true + + echo "$result" + + status=$(echo "$result" | grep -o '"status":"[^"]*"' | cut -d'"' -f4 || echo "unknown") + + if [[ "$status" == "Accepted" ]]; then + echo "✅ Notarization successful" + else + echo "⚠️ Notarization status: $status" + # Get detailed log + submission_id=$(echo "$result" | grep -o '"id":"[^"]*"' | cut -d'"' -f4 || echo "") + if [[ -n "$submission_id" ]]; then + echo "Fetching notarization log..." + xcrun notarytool log "$submission_id" \ + --key "$key_path" \ + --key-id "$APPLE_NOTARIZATION_KEY_ID" \ + --issuer "$APPLE_NOTARIZATION_ISSUER_ID" || true + fi + exit 1 + fi + + # Cleanup + rm -f "$key_path" "$zip_path" + + - name: Verify signature + shell: bash + env: + BINARY_PATH: ${{ inputs.binary-path }} + run: | + set -euo pipefail + + echo "Verifying signature and notarization..." + codesign -dv --verbose=2 "$BINARY_PATH" + echo "" + echo "Gatekeeper check:" + spctl -a -vv "$BINARY_PATH" 2>&1 || true + + - name: Cleanup keychain + if: always() + shell: bash + run: | + if [[ -n "${APPLE_KEYCHAIN_PATH:-}" && -f "${APPLE_KEYCHAIN_PATH}" ]]; then + security delete-keychain "$APPLE_KEYCHAIN_PATH" || true + fi diff --git a/.github/workflows/release-kimi-cli.yml b/.github/workflows/release-kimi-cli.yml index 175110ad..ba58c350 100644 --- a/.github/workflows/release-kimi-cli.yml +++ b/.github/workflows/release-kimi-cli.yml @@ -73,9 +73,103 @@ jobs: - name: Prepare building environment run: make prepare-build + # macOS: Setup signing certificate before build + - name: Setup macOS signing certificate + if: runner.os == 'macOS' + env: + APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: actions + run: | + set -euo pipefail + + # Decode certificate + cert_path="${RUNNER_TEMP}/certificate.p12" + echo "$APPLE_CERTIFICATE_P12" | base64 -d > "$cert_path" + + # Create temporary keychain + keychain_path="${RUNNER_TEMP}/signing.keychain-db" + security create-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path" + security set-keychain-settings -lut 21600 "$keychain_path" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path" + + # Add to keychain search list + security list-keychains -d user -s "$keychain_path" $(security list-keychains -d user | tr -d '"') + security default-keychain -s "$keychain_path" + + # Import certificate + security import "$cert_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" > /dev/null + + # Find signing identity + IDENTITY=$(security find-identity -v -p codesigning "$keychain_path" | grep "Developer ID Application" | head -1 | sed -n 's/.*"\(Developer ID Application[^"]*\)".*/\1/p') + + if [[ -z "$IDENTITY" ]]; then + echo "❌ No Developer ID Application identity found" + security find-identity -v -p codesigning "$keychain_path" + exit 1 + fi + + echo "✅ Found signing identity: $IDENTITY" + echo "APPLE_SIGNING_IDENTITY=$IDENTITY" >> "$GITHUB_ENV" + echo "APPLE_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV" + + rm -f "$cert_path" + + # Build with signing on macOS (APPLE_SIGNING_IDENTITY is read by kimi.spec) + - name: Build standalone binary (macOS with signing) + if: runner.os == 'macOS' + run: make build-bin + + # Build without signing on other platforms - name: Build standalone binary + if: runner.os != 'macOS' run: make build-bin + # macOS: Notarize the signed binary + - name: Notarize macOS binary + if: runner.os == 'macOS' + env: + APPLE_NOTARIZATION_KEY_P8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }} + APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} + APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} + run: | + set -euo pipefail + + # Save API key + key_path="${RUNNER_TEMP}/AuthKey.p8" + echo "$APPLE_NOTARIZATION_KEY_P8" | base64 -d > "$key_path" + + # Create zip for notarization + zip_path="${RUNNER_TEMP}/kimi.zip" + ditto -c -k --keepParent dist/kimi "$zip_path" + + echo "Submitting for notarization..." + + xcrun notarytool submit "$zip_path" \ + --key "$key_path" \ + --key-id "$APPLE_NOTARIZATION_KEY_ID" \ + --issuer "$APPLE_NOTARIZATION_ISSUER_ID" \ + --wait \ + --timeout 15m + + echo "✅ Notarization completed" + + # Verify + echo "Verifying signature..." + codesign -dv --verbose=2 dist/kimi + + # Cleanup + rm -f "$key_path" "$zip_path" + + # macOS: Cleanup keychain + - name: Cleanup macOS keychain + if: always() && runner.os == 'macOS' + run: | + if [[ -n "${APPLE_KEYCHAIN_PATH:-}" && -f "${APPLE_KEYCHAIN_PATH}" ]]; then + security delete-keychain "$APPLE_KEYCHAIN_PATH" || true + fi + - name: Package artifact shell: python env: diff --git a/kimi.spec b/kimi.spec index 5e31656f..c63e2f7f 100644 --- a/kimi.spec +++ b/kimi.spec @@ -1,7 +1,11 @@ # -*- mode: python ; coding: utf-8 -*- +import os from kimi_cli.utils.pyinstaller import datas, hiddenimports +# Read codesign identity from environment variable (for macOS signing in CI) +codesign_identity = os.environ.get("APPLE_SIGNING_IDENTITY", None) + a = Analysis( ["src/kimi_cli/cli/__main__.py"], pathex=[], @@ -34,6 +38,6 @@ exe = EXE( disable_windowed_traceback=False, argv_emulation=False, target_arch=None, - codesign_identity=None, + codesign_identity=codesign_identity, entitlements_file=None, )