Revert "ci: remove onedir build from build-test workflow" #4
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: Build Test (macOS signing) | |
| on: | |
| push: | |
| branches: | |
| - fix/macos-code-notary | |
| jobs: | |
| build: | |
| name: Build binaries (${{ matrix.target }}) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - runner: macos-14 | |
| target: aarch64-apple-darwin | |
| runs-on: ${{ matrix.runner }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Python 3.14 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.14" | |
| allow-prereleases: true | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v1 | |
| with: | |
| version: "0.8.5" | |
| - 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" | |
| # macOS: Build both onefile and onedir versions | |
| - name: Build standalone binary (macOS onefile) | |
| if: runner.os == 'macOS' | |
| run: make build-bin | |
| - name: Build standalone binary (macOS onedir) | |
| if: runner.os == 'macOS' | |
| run: make build-bin-onedir | |
| # macOS: Sign onefile binary | |
| - name: Sign macOS onefile binary | |
| if: runner.os == 'macOS' | |
| run: | | |
| set -euo pipefail | |
| echo "Signing onefile binary..." | |
| codesign --deep --force --options runtime --timestamp \ | |
| --sign "$APPLE_SIGNING_IDENTITY" \ | |
| --keychain "$APPLE_KEYCHAIN_PATH" \ | |
| dist/onefile/kimi | |
| echo "✅ Onefile binary signed" | |
| codesign -dv --verbose=2 dist/onefile/kimi | |
| # macOS: Sign onedir binaries (all dylibs and executables) | |
| - name: Sign macOS onedir binaries | |
| if: runner.os == 'macOS' | |
| run: | | |
| set -euo pipefail | |
| echo "Signing onedir binaries..." | |
| # 1. Sign all dylibs and so files first (excluding those inside frameworks) | |
| find dist/onedir/kimi -type f \( -name "*.dylib" -o -name "*.so" \) ! -path "*.framework/*" | while read -r lib; do | |
| echo "Signing: $lib" | |
| codesign --force --options runtime --timestamp \ | |
| --sign "$APPLE_SIGNING_IDENTITY" \ | |
| --keychain "$APPLE_KEYCHAIN_PATH" \ | |
| "$lib" | |
| done | |
| # 2. Sign all frameworks with --deep (important for Python.framework) | |
| find dist/onedir/kimi -type d -name "*.framework" | while read -r framework; do | |
| echo "Signing framework: $framework" | |
| codesign --deep --force --options runtime --timestamp \ | |
| --sign "$APPLE_SIGNING_IDENTITY" \ | |
| --keychain "$APPLE_KEYCHAIN_PATH" \ | |
| "$framework" | |
| done | |
| # 3. Sign the main executable last | |
| echo "Signing main executable: dist/onedir/kimi/kimi" | |
| codesign --force --options runtime --timestamp \ | |
| --sign "$APPLE_SIGNING_IDENTITY" \ | |
| --keychain "$APPLE_KEYCHAIN_PATH" \ | |
| dist/onedir/kimi/kimi | |
| echo "✅ Onedir binaries signed" | |
| codesign -dv --verbose=2 dist/onedir/kimi/kimi | |
| codesign --verify --deep --strict dist/onedir/kimi/kimi && echo "✅ Deep verification passed" | |
| # macOS: Notarize onefile binary | |
| - name: Notarize macOS onefile 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 (use --norsrc to avoid ._ AppleDouble files) | |
| zip_path="${RUNNER_TEMP}/kimi-onefile.zip" | |
| ditto -c -k --norsrc --keepParent dist/onefile/kimi "$zip_path" | |
| echo "Submitting onefile for notarization..." | |
| # Submit and capture output for status verification | |
| xcrun notarytool submit "$zip_path" \ | |
| --key "$key_path" \ | |
| --key-id "$APPLE_NOTARIZATION_KEY_ID" \ | |
| --issuer "$APPLE_NOTARIZATION_ISSUER_ID" \ | |
| --wait \ | |
| --timeout 15m \ | |
| 2>&1 | tee /tmp/notarize-onefile.log | |
| # Verify notarization was accepted | |
| if ! grep -q "status: Accepted" /tmp/notarize-onefile.log; then | |
| echo "❌ Onefile notarization failed!" | |
| cat /tmp/notarize-onefile.log | |
| # Get detailed error log from Apple | |
| submission_id=$(grep "id:" /tmp/notarize-onefile.log | head -1 | awk '{print $2}') | |
| if [[ -n "$submission_id" ]]; then | |
| echo "Fetching notarization log for submission: $submission_id" | |
| xcrun notarytool log "$submission_id" \ | |
| --key "$key_path" \ | |
| --key-id "$APPLE_NOTARIZATION_KEY_ID" \ | |
| --issuer "$APPLE_NOTARIZATION_ISSUER_ID" 2>&1 || true | |
| fi | |
| exit 1 | |
| fi | |
| echo "✅ Onefile notarization completed and accepted" | |
| # Verify signature and notarization status | |
| echo "Verifying onefile signature..." | |
| codesign -dv --verbose=2 dist/onefile/kimi | |
| echo "Verifying onefile notarization (online check)..." | |
| spctl -a -vvv -t install dist/onefile/kimi | |
| # Cleanup | |
| rm -f "$zip_path" /tmp/notarize-onefile.log | |
| # macOS: Notarize onedir binaries | |
| - name: Notarize macOS onedir binaries | |
| 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 (might already exist from previous step) | |
| key_path="${RUNNER_TEMP}/AuthKey.p8" | |
| if [[ ! -f "$key_path" ]]; then | |
| echo "$APPLE_NOTARIZATION_KEY_P8" | base64 -d > "$key_path" | |
| fi | |
| # Create zip for notarization (use --norsrc to avoid ._ AppleDouble files) | |
| zip_path="${RUNNER_TEMP}/kimi-onedir.zip" | |
| ditto -c -k --norsrc --keepParent dist/onedir/kimi "$zip_path" | |
| echo "Submitting onedir for notarization..." | |
| # Submit and capture output for status verification | |
| xcrun notarytool submit "$zip_path" \ | |
| --key "$key_path" \ | |
| --key-id "$APPLE_NOTARIZATION_KEY_ID" \ | |
| --issuer "$APPLE_NOTARIZATION_ISSUER_ID" \ | |
| --wait \ | |
| --timeout 15m \ | |
| 2>&1 | tee /tmp/notarize-onedir.log | |
| # Verify notarization was accepted | |
| if ! grep -q "status: Accepted" /tmp/notarize-onedir.log; then | |
| echo "❌ Onedir notarization failed!" | |
| cat /tmp/notarize-onedir.log | |
| # Get detailed error log from Apple | |
| submission_id=$(grep "id:" /tmp/notarize-onedir.log | head -1 | awk '{print $2}') | |
| if [[ -n "$submission_id" ]]; then | |
| echo "Fetching notarization log for submission: $submission_id" | |
| xcrun notarytool log "$submission_id" \ | |
| --key "$key_path" \ | |
| --key-id "$APPLE_NOTARIZATION_KEY_ID" \ | |
| --issuer "$APPLE_NOTARIZATION_ISSUER_ID" 2>&1 || true | |
| fi | |
| exit 1 | |
| fi | |
| echo "✅ Onedir notarization completed and accepted" | |
| # Verify signature and notarization status | |
| echo "Verifying onedir signature..." | |
| codesign -dv --verbose=2 dist/onedir/kimi/kimi | |
| echo "Verifying onedir notarization (online check)..." | |
| spctl -a -vvv -t install dist/onedir/kimi/kimi | |
| # Cleanup | |
| rm -f "$key_path" "$zip_path" /tmp/notarize-onedir.log | |
| # 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 | |
| # Package onefile artifact | |
| - name: Package onefile artifact | |
| shell: python | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| run: | | |
| import os | |
| import pathlib | |
| import tarfile | |
| target = os.environ["TARGET"] | |
| dist_dir = pathlib.Path("dist") | |
| artifacts_dir = pathlib.Path("artifacts") | |
| artifacts_dir.mkdir(parents=True, exist_ok=True) | |
| binary_name = "kimi" | |
| binary_path = dist_dir / "onefile" / binary_name | |
| if not binary_path.exists(): | |
| raise SystemExit(f"Binary not found at {binary_path}") | |
| archive_name = f"kimi-{target}.tar.gz" | |
| archive_path = artifacts_dir / archive_name | |
| with tarfile.open(archive_path, "w:gz") as archive_file: | |
| archive_file.add(binary_path, arcname="kimi") | |
| print(f"Built onefile artifact: {archive_path}") | |
| # Package onedir artifact | |
| - name: Package onedir artifact | |
| shell: python | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| run: | | |
| import os | |
| import pathlib | |
| import tarfile | |
| target = os.environ["TARGET"] | |
| dist_dir = pathlib.Path("dist") | |
| artifacts_dir = pathlib.Path("artifacts") | |
| artifacts_dir.mkdir(parents=True, exist_ok=True) | |
| onedir_path = dist_dir / "onedir" / "kimi" | |
| if not onedir_path.exists() or not onedir_path.is_dir(): | |
| raise SystemExit(f"Onedir directory not found at {onedir_path}") | |
| archive_name = f"kimi-{target}-onedir.tar.gz" | |
| archive_path = artifacts_dir / archive_name | |
| with tarfile.open(archive_path, "w:gz") as archive_file: | |
| # Add the directory contents with kimi/ as the root | |
| for item in onedir_path.iterdir(): | |
| archive_file.add(item, arcname=f"kimi/{item.name}") | |
| print(f"Built onedir artifact: {archive_path}") | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: kimi-${{ matrix.target }} | |
| path: artifacts/* | |
| if-no-files-found: error | |
| retention-days: 7 |