ci: fix broken pipe error in onedir analysis #6
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 | |
| echo "========================================" | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting onedir notarization" | |
| echo "========================================" | |
| # 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 | |
| # Analyze onedir contents before zipping | |
| echo "" | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Analyzing onedir contents..." | |
| echo "--- File count by type ---" | |
| find dist/onedir/kimi -type f | sed 's/.*\.//' | sort | uniq -c | sort -rn | head -20 | |
| echo "" | |
| echo "--- Total file count ---" | |
| find dist/onedir/kimi -type f | wc -l | |
| echo "" | |
| echo "--- Total directory size ---" | |
| du -sh dist/onedir/kimi | |
| echo "" | |
| echo "--- Largest files (top 20) ---" | |
| find dist/onedir/kimi -type f -exec du -h {} + 2>/dev/null | sort -rh 2>/dev/null | head -20 || true | |
| echo "" | |
| echo "--- Signed binaries count ---" | |
| find dist/onedir/kimi -type f \( -name "*.dylib" -o -name "*.so" -o -name "kimi" \) | wc -l | |
| # Create zip for notarization (use --norsrc to avoid ._ AppleDouble files) | |
| echo "" | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Creating zip archive..." | |
| zip_path="${RUNNER_TEMP}/kimi-onedir.zip" | |
| time ditto -c -k --norsrc --keepParent dist/onedir/kimi "$zip_path" | |
| echo "" | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Zip created" | |
| echo "--- Zip file size ---" | |
| ls -lh "$zip_path" | |
| du -h "$zip_path" | |
| echo "" | |
| echo "========================================" | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Submitting onedir for notarization..." | |
| echo "========================================" | |
| # Submit and capture output for status verification | |
| # Use timeout command to get more info if it hangs | |
| start_time=$(date +%s) | |
| 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 | |
| exit_code=${PIPESTATUS[0]} | |
| end_time=$(date +%s) | |
| duration=$((end_time - start_time)) | |
| echo "" | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Notarization command completed" | |
| echo "--- Duration: ${duration} seconds ---" | |
| echo "--- Exit code: ${exit_code} ---" | |
| # Check for timeout | |
| if [[ $duration -ge 900 ]]; then | |
| echo "⚠️ WARNING: Notarization took ${duration}s (>= 900s timeout threshold)" | |
| fi | |
| # Verify notarization was accepted | |
| if ! grep -q "status: Accepted" /tmp/notarize-onedir.log; then | |
| echo "" | |
| echo "========================================" | |
| echo "❌ Onedir notarization failed!" | |
| echo "========================================" | |
| echo "" | |
| echo "--- Full notarization log ---" | |
| 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 "" | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Fetching detailed 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 | |
| echo "" | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Fetching submission info..." | |
| xcrun notarytool info "$submission_id" \ | |
| --key "$key_path" \ | |
| --key-id "$APPLE_NOTARIZATION_KEY_ID" \ | |
| --issuer "$APPLE_NOTARIZATION_ISSUER_ID" 2>&1 || true | |
| else | |
| echo "⚠️ Could not extract submission ID from log" | |
| fi | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "========================================" | |
| echo "✅ Onedir notarization completed and accepted" | |
| echo " Total time: ${duration} seconds" | |
| echo "========================================" | |
| # Verify signature and notarization status | |
| echo "" | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Verifying onedir signature..." | |
| codesign -dv --verbose=2 dist/onedir/kimi/kimi | |
| echo "" | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] 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 | |
| echo "" | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Onedir notarization step completed" | |
| # 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 |