Release Build #135
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: Release Build | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| marketing_version: | |
| description: 'Marketing version (e.g., 1.0.6)' | |
| required: true | |
| type: string | |
| build_version: | |
| description: 'Build version (e.g., 10006)' | |
| required: true | |
| type: string | |
| release_notes: | |
| description: 'Release notes' | |
| required: false | |
| type: string | |
| default: 'Bug fixes and improvements' | |
| env: | |
| SCHEME: 'aizen' | |
| CONFIGURATION: 'Release' | |
| APP_BUNDLE_NAME: 'Aizen.app' | |
| APP_EXECUTABLE_NAME: 'Aizen' | |
| APP_DISPLAY_NAME: 'Aizen' | |
| DERIVED_DATA_PATH: 'build/DerivedData' | |
| VVDEVKIT_SWIFTPM_SCRATCH_PATH: 'build/DerivedData/VVDevKitSwiftPM' | |
| jobs: | |
| build-and-release: | |
| runs-on: macos-26 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Verify vendored GhosttyKit | |
| run: | | |
| for required_path in \ | |
| Vendor/libghostty/VERSION \ | |
| Vendor/libghostty/GhosttyKit.xcframework \ | |
| Vendor/libghostty/GhosttyKit.xcframework/macos-arm64/libghostty-fat.a | |
| do | |
| if [ ! -e "$required_path" ]; then | |
| echo "Error: Missing vendored GhosttyKit payload file: $required_path" | |
| exit 1 | |
| fi | |
| done | |
| echo "Using vendored libghostty version $(cat Vendor/libghostty/VERSION)" | |
| ls -lh Vendor/libghostty/GhosttyKit.xcframework/macos-arm64/libghostty-fat.a | |
| - name: Show Xcode version | |
| id: xcode-version | |
| run: | | |
| xcodebuild -version | |
| echo "cache_key=$(xcodebuild -version | shasum -a 256 | awk '{print $1}')" >> "$GITHUB_OUTPUT" | |
| - name: Bump version | |
| run: | | |
| chmod +x scripts/bump-version.sh | |
| scripts/bump-version.sh "${{ github.event.inputs.marketing_version }}" "${{ github.event.inputs.build_version }}" | |
| - name: Install dependencies | |
| run: | | |
| brew install create-dmg awscli sparkle | |
| - name: Resolve Swift packages | |
| run: | | |
| xcodebuild -resolvePackageDependencies \ | |
| -scheme "${{ env.SCHEME }}" \ | |
| -configuration "${{ env.CONFIGURATION }}" \ | |
| -derivedDataPath "${{ env.DERIVED_DATA_PATH }}" \ | |
| -skipPackagePluginValidation \ | |
| -skipMacroValidation | |
| - name: Patch mlx-swift Metal warnings | |
| run: | | |
| MLX_DIR=$(find "${{ env.DERIVED_DATA_PATH }}/SourcePackages/checkouts" -maxdepth 1 -type d -name 'mlx-swift' | head -1) | |
| if [ -z "$MLX_DIR" ]; then | |
| echo "Error: Could not locate mlx-swift checkout in DerivedData" | |
| exit 1 | |
| fi | |
| TARGET="$MLX_DIR/Source/Cmlx/mlx-generated/metal/steel/attn/kernels/steel_attention.metal" | |
| if [ ! -f "$TARGET" ]; then | |
| echo "Error: Could not locate steel_attention.metal in mlx-swift checkout" | |
| exit 1 | |
| fi | |
| if ! grep -q 'diagnostic ignored "-Wc++17-extensions"' "$TARGET"; then | |
| chmod u+w "$TARGET" | |
| perl -0pi -e 's@^@#pragma clang diagnostic push\n#pragma clang diagnostic ignored "-Wc++17-extensions"\n@' "$TARGET" | |
| printf '\n#pragma clang diagnostic pop\n' >> "$TARGET" | |
| fi | |
| - name: Cache VVDevKit grammar build | |
| uses: actions/cache@v4 | |
| with: | |
| path: ${{ env.VVDEVKIT_SWIFTPM_SCRATCH_PATH }} | |
| key: vvdevkit-grammars-${{ runner.os }}-${{ steps.xcode-version.outputs.cache_key }}-${{ hashFiles('aizen.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved', '.github/workflows/release.yml') }} | |
| restore-keys: | | |
| vvdevkit-grammars-${{ runner.os }}-${{ steps.xcode-version.outputs.cache_key }}- | |
| vvdevkit-grammars-${{ runner.os }}- | |
| - name: Build VVHighlighting grammar dylibs | |
| run: | | |
| CACHED_COUNT=$(find "${{ env.VVDEVKIT_SWIFTPM_SCRATCH_PATH }}" -type f -name 'libTreeSitter*.dylib' -not -path '*.dSYM/*' 2>/dev/null | wc -l | tr -d ' ') | |
| if [ "$CACHED_COUNT" -gt 0 ]; then | |
| echo "Skipping build — $CACHED_COUNT cached VVHighlighting grammar dylibs found" | |
| exit 0 | |
| fi | |
| VVDEVKIT_DIR=$(find "${{ env.DERIVED_DATA_PATH }}/SourcePackages/checkouts" -maxdepth 1 -type d \( -name 'VVDevKit' -o -name 'vvdevkit' \) | head -1) | |
| if [ -z "$VVDEVKIT_DIR" ]; then | |
| echo "Error: Could not locate VVDevKit checkout in DerivedData" | |
| exit 1 | |
| fi | |
| swift build -c release \ | |
| --package-path "$VVDEVKIT_DIR" \ | |
| --scratch-path "${{ env.VVDEVKIT_SWIFTPM_SCRATCH_PATH }}" | |
| GRAMMAR_COUNT=$(find "${{ env.VVDEVKIT_SWIFTPM_SCRATCH_PATH }}" -type f -name 'libTreeSitter*.dylib' -not -path '*.dSYM/*' | wc -l | tr -d ' ') | |
| if [ "$GRAMMAR_COUNT" -eq 0 ]; then | |
| echo "Error: VVDevKit build did not produce any grammar dylibs" | |
| exit 1 | |
| fi | |
| echo "Built $GRAMMAR_COUNT VVHighlighting grammar dylibs" | |
| - name: Build Apple Silicon archive | |
| run: | | |
| xcodebuild clean archive \ | |
| -scheme "${{ env.SCHEME }}" \ | |
| -configuration "${{ env.CONFIGURATION }}" \ | |
| -derivedDataPath "${{ env.DERIVED_DATA_PATH }}" \ | |
| -archivePath build/aizen.xcarchive \ | |
| -resultBundlePath build/aizen-archive.xcresult \ | |
| -arch arm64 \ | |
| -jobs 1 \ | |
| -skipPackagePluginValidation \ | |
| -skipMacroValidation \ | |
| COMPILER_INDEX_STORE_ENABLE=NO \ | |
| OTHER_METAL_FLAGS='-Wno-c++17-extensions' \ | |
| CODE_SIGN_IDENTITY="-" \ | |
| CODE_SIGNING_REQUIRED=NO | |
| - name: Dump archive diagnostics | |
| if: ${{ failure() }} | |
| run: | | |
| if [ -d build/aizen-archive.xcresult ]; then | |
| xcrun xcresulttool get --legacy --path build/aizen-archive.xcresult --format json > build/aizen-archive.json || true | |
| tail -n 200 build/aizen-archive.json || true | |
| fi | |
| - name: Export app bundle | |
| run: | | |
| mkdir -p build/dmg | |
| cp -R "build/aizen.xcarchive/Products/Applications/${{ env.APP_BUNDLE_NAME }}" build/dmg/ | |
| - name: Ensure VVHighlighting dylibs are bundled | |
| run: | | |
| APP_RESOURCES="build/dmg/${{ env.APP_BUNDLE_NAME }}/Contents/Resources" | |
| GRAMMAR_COUNT=$(find "$APP_RESOURCES" -maxdepth 1 -type f -name 'libTreeSitter*.dylib' | wc -l | tr -d ' ') | |
| if [ "$GRAMMAR_COUNT" -gt 0 ]; then | |
| echo "Found $GRAMMAR_COUNT bundled VVHighlighting dylibs" | |
| exit 0 | |
| fi | |
| echo "No VVHighlighting dylibs found in exported app bundle, copying from DerivedData fallback" | |
| FALLBACK_DYLIBS_FILE="$(mktemp)" | |
| find "${{ env.DERIVED_DATA_PATH }}" -type f -name 'libTreeSitter*.dylib' -not -path '*.dSYM/*' | sort -u > "$FALLBACK_DYLIBS_FILE" | |
| if [ ! -s "$FALLBACK_DYLIBS_FILE" ]; then | |
| rm -f "$FALLBACK_DYLIBS_FILE" | |
| echo "Error: Could not find VVHighlighting dylibs anywhere in DerivedData" | |
| exit 1 | |
| fi | |
| while IFS= read -r dylib_path; do | |
| cp -f "$dylib_path" "$APP_RESOURCES"/ | |
| done < "$FALLBACK_DYLIBS_FILE" | |
| rm -f "$FALLBACK_DYLIBS_FILE" | |
| GRAMMAR_COUNT=$(find "$APP_RESOURCES" -maxdepth 1 -type f -name 'libTreeSitter*.dylib' | wc -l | tr -d ' ') | |
| if [ "$GRAMMAR_COUNT" -eq 0 ]; then | |
| echo "Error: VVHighlighting dylibs are still missing after fallback copy" | |
| exit 1 | |
| fi | |
| echo "Copied $GRAMMAR_COUNT VVHighlighting dylibs from DerivedData fallback" | |
| - name: Read minimum macOS version | |
| run: | | |
| BUNDLE_NAME=$(/usr/libexec/PlistBuddy -c "Print :CFBundleName" "build/dmg/${{ env.APP_BUNDLE_NAME }}/Contents/Info.plist") | |
| SPARKLE_BUNDLE_NAME=$(/usr/libexec/PlistBuddy -c "Print :SUBundleName" "build/dmg/${{ env.APP_BUNDLE_NAME }}/Contents/Info.plist") | |
| MIN_SYSTEM_VERSION=$(/usr/libexec/PlistBuddy -c "Print :LSMinimumSystemVersion" "build/dmg/${{ env.APP_BUNDLE_NAME }}/Contents/Info.plist") | |
| if [ "$BUNDLE_NAME" != "${{ env.APP_DISPLAY_NAME }}" ]; then | |
| echo "Error: CFBundleName is '$BUNDLE_NAME' but expected '${{ env.APP_DISPLAY_NAME }}'" | |
| exit 1 | |
| fi | |
| if [ "$SPARKLE_BUNDLE_NAME" != "${{ env.APP_DISPLAY_NAME }}" ]; then | |
| echo "Error: SUBundleName is '$SPARKLE_BUNDLE_NAME' but expected '${{ env.APP_DISPLAY_NAME }}'" | |
| exit 1 | |
| fi | |
| if [ -z "$MIN_SYSTEM_VERSION" ]; then | |
| echo "Error: Failed to read LSMinimumSystemVersion from built app" | |
| exit 1 | |
| fi | |
| echo "MIN_SYSTEM_VERSION=$MIN_SYSTEM_VERSION" >> "$GITHUB_ENV" | |
| - name: Import signing certificate | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| if: ${{ env.APPLE_CERTIFICATE != '' }} | |
| run: | | |
| echo "Code signing certificate found, importing..." | |
| security create-keychain -p actions build.keychain | |
| security default-keychain -s build.keychain | |
| security unlock-keychain -p actions build.keychain | |
| security set-keychain-settings -t 3600 -u build.keychain | |
| echo "${{ secrets.APPLE_CERTIFICATE }}" | base64 --decode > certificate.p12 | |
| security import certificate.p12 \ | |
| -k build.keychain \ | |
| -P "${{ secrets.APPLE_CERT_PASSWORD }}" \ | |
| -T /usr/bin/codesign \ | |
| -T /usr/bin/productsign | |
| security set-key-partition-list -S apple-tool:,apple: -s -k actions build.keychain | |
| rm certificate.p12 | |
| - name: Sign app bundle | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| if: ${{ env.APPLE_CERTIFICATE != '' }} | |
| run: | | |
| CERT_IDENTITY=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"') | |
| echo "Using certificate: $CERT_IDENTITY" | |
| APP_PATH="build/dmg/${{ env.APP_BUNDLE_NAME }}" | |
| RESOURCE_ROOT="$APP_PATH/Contents/Resources" | |
| CLI_PATH="build/dmg/${{ env.APP_BUNDLE_NAME }}/Contents/Resources/cli/aizen-cli" | |
| if [ -f "$CLI_PATH" ]; then | |
| codesign --force --sign "$CERT_IDENTITY" \ | |
| --options runtime \ | |
| --timestamp \ | |
| "$CLI_PATH" | |
| else | |
| echo "Error: CLI binary not found at $CLI_PATH" | |
| exit 1 | |
| fi | |
| while IFS= read -r resource_path; do | |
| if file -b "$resource_path" | grep -q '^Mach-O'; then | |
| echo "Signing nested resource binary: $resource_path" | |
| codesign --force --sign "$CERT_IDENTITY" \ | |
| --timestamp \ | |
| "$resource_path" | |
| fi | |
| done < <(find "$RESOURCE_ROOT" -type f ! -path "$CLI_PATH") | |
| codesign --force --deep --sign "$CERT_IDENTITY" \ | |
| --options runtime \ | |
| --timestamp \ | |
| --entitlements aizen/aizen.entitlements \ | |
| "$APP_PATH" | |
| codesign --verify --deep --strict --verbose=2 "$APP_PATH" | |
| - name: Create DMG | |
| run: | | |
| create-dmg \ | |
| --volname "${{ env.APP_DISPLAY_NAME }}" \ | |
| --window-pos 200 120 \ | |
| --window-size 800 400 \ | |
| --icon-size 100 \ | |
| --icon "${{ env.APP_BUNDLE_NAME }}" 200 190 \ | |
| --hide-extension "${{ env.APP_BUNDLE_NAME }}" \ | |
| --app-drop-link 600 185 \ | |
| "build/${{ env.APP_DISPLAY_NAME }}-${{ github.event.inputs.marketing_version }}.dmg" \ | |
| "build/dmg/" || true | |
| if [ ! -f "build/${{ env.APP_DISPLAY_NAME }}-${{ github.event.inputs.marketing_version }}.dmg" ]; then | |
| echo "Error: DMG creation failed" | |
| exit 1 | |
| fi | |
| - name: Sign DMG | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| if: ${{ env.APPLE_CERTIFICATE != '' }} | |
| run: | | |
| CERT_IDENTITY=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"') | |
| codesign --force --sign "$CERT_IDENTITY" \ | |
| --timestamp \ | |
| "build/${{ env.APP_DISPLAY_NAME }}-${{ github.event.inputs.marketing_version }}.dmg" | |
| - name: Notarize DMG | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} | |
| if: ${{ env.APPLE_CERTIFICATE != '' && env.APPLE_ID != '' && env.APPLE_APP_PASSWORD != '' }} | |
| run: | | |
| NOTARY_PLIST="$(mktemp)" | |
| xcrun notarytool submit \ | |
| "build/${{ env.APP_DISPLAY_NAME }}-${{ github.event.inputs.marketing_version }}.dmg" \ | |
| --apple-id "${{ secrets.APPLE_ID }}" \ | |
| --team-id "${{ secrets.APPLE_TEAM_ID }}" \ | |
| --password "${{ secrets.APPLE_APP_PASSWORD }}" \ | |
| --wait \ | |
| --output-format plist > "$NOTARY_PLIST" | |
| cat "$NOTARY_PLIST" | |
| NOTARY_ID=$(/usr/libexec/PlistBuddy -c "Print :id" "$NOTARY_PLIST" 2>/dev/null || true) | |
| NOTARY_STATUS=$(/usr/libexec/PlistBuddy -c "Print :status" "$NOTARY_PLIST" 2>/dev/null || true) | |
| rm -f "$NOTARY_PLIST" | |
| if [ -z "$NOTARY_ID" ]; then | |
| echo "Error: Notary submission did not return an id" | |
| exit 1 | |
| fi | |
| echo "NOTARY_REQUEST_ID=$NOTARY_ID" >> "$GITHUB_ENV" | |
| echo "Notary status: $NOTARY_STATUS" | |
| if [ "$NOTARY_STATUS" != "Accepted" ]; then | |
| LOG_PATH="build/notary-log.json" | |
| xcrun notarytool log "$NOTARY_ID" \ | |
| --apple-id "${{ secrets.APPLE_ID }}" \ | |
| --team-id "${{ secrets.APPLE_TEAM_ID }}" \ | |
| --password "${{ secrets.APPLE_APP_PASSWORD }}" > "$LOG_PATH" || true | |
| if [ -s "$LOG_PATH" ]; then | |
| cat "$LOG_PATH" | |
| fi | |
| echo "Error: Notary status is not Accepted" | |
| exit 1 | |
| fi | |
| - name: Staple notarization | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} | |
| if: ${{ env.APPLE_CERTIFICATE != '' && env.APPLE_ID != '' && env.APPLE_APP_PASSWORD != '' }} | |
| run: | | |
| DMG_PATH="build/${{ env.APP_DISPLAY_NAME }}-${{ github.event.inputs.marketing_version }}.dmg" | |
| xcrun stapler staple -v "$DMG_PATH" | |
| xcrun stapler validate -v "$DMG_PATH" | |
| - name: Generate Sparkle signature | |
| run: | | |
| echo "${{ secrets.SPARKLE_PRIVATE_KEY }}" > sparkle_priv.pem | |
| if [ ! -s sparkle_priv.pem ]; then | |
| echo "Error: Private key is empty" | |
| exit 1 | |
| fi | |
| SIGN_UPDATE=$(find /opt/homebrew/Caskroom/sparkle -name sign_update -type f 2>/dev/null | grep -v old_dsa | grep -v dSYM | head -1) | |
| if [ -z "$SIGN_UPDATE" ]; then | |
| echo "Error: sign_update binary not found" | |
| exit 1 | |
| fi | |
| SIGNATURE=$("$SIGN_UPDATE" --ed-key-file sparkle_priv.pem -p "build/${{ env.APP_DISPLAY_NAME }}-${{ github.event.inputs.marketing_version }}.dmg") | |
| if [ -z "$SIGNATURE" ]; then | |
| echo "Error: sign_update returned empty signature" | |
| exit 1 | |
| fi | |
| echo "SPARKLE_SIGNATURE=$SIGNATURE" >> $GITHUB_ENV | |
| - name: Generate appcast | |
| env: | |
| R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }} | |
| SPARKLE_PRIVATE_KEY_FILE: sparkle_priv.pem | |
| run: | | |
| chmod +x scripts/generate-appcast.sh | |
| scripts/generate-appcast.sh \ | |
| "build/${{ env.APP_DISPLAY_NAME }}-${{ github.event.inputs.marketing_version }}.dmg" \ | |
| "${{ github.event.inputs.build_version }}" \ | |
| "${{ github.event.inputs.marketing_version }}" \ | |
| "${{ github.event.inputs.release_notes }}" \ | |
| "${{ env.MIN_SYSTEM_VERSION }}" | |
| - name: Upload to Cloudflare R2 | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} | |
| R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }} | |
| R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }} | |
| run: | | |
| aws s3 cp "build/Aizen-${{ github.event.inputs.marketing_version }}.dmg" \ | |
| "s3://${{ secrets.R2_BUCKET_NAME }}/Aizen-${{ github.event.inputs.marketing_version }}.dmg" \ | |
| --endpoint-url "${{ secrets.R2_ENDPOINT }}" \ | |
| --region auto | |
| aws s3 cp appcast.xml "s3://${{ secrets.R2_BUCKET_NAME }}/appcast.xml" \ | |
| --endpoint-url "${{ secrets.R2_ENDPOINT }}" \ | |
| --region auto \ | |
| --content-type "application/xml" | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: v${{ github.event.inputs.marketing_version }} | |
| name: Release ${{ github.event.inputs.marketing_version }} | |
| body: ${{ github.event.inputs.release_notes }} | |
| draft: false | |
| prerelease: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| rm -f sparkle_priv.pem | |
| if security list-keychains | grep -q build.keychain; then | |
| security delete-keychain build.keychain | |
| fi | |
| - name: Summary | |
| run: | | |
| echo "## Release Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Version:** ${{ github.event.inputs.marketing_version }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Build:** ${{ github.event.inputs.build_version }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **DMG:** ${{ secrets.R2_PUBLIC_URL }}/Aizen-${{ github.event.inputs.marketing_version }}.dmg" >> $GITHUB_STEP_SUMMARY |