Release Build #19
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: | |
| version: | |
| description: 'Release version (e.g., 1.0.1)' | |
| required: true | |
| type: string | |
| release_notes: | |
| description: 'Release notes' | |
| required: false | |
| type: string | |
| default: 'Bug fixes and improvements' | |
| env: | |
| SCHEME: 'aizen' | |
| CONFIGURATION: 'Release' | |
| jobs: | |
| build-and-release: | |
| runs-on: macos-26 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| lfs: true | |
| - name: Checkout LFS objects | |
| run: git lfs pull | |
| - name: Show Xcode version | |
| run: xcodebuild -version | |
| - name: Bump version | |
| run: | | |
| chmod +x scripts/bump-version.sh | |
| scripts/bump-version.sh "${{ github.event.inputs.version }}" | |
| - name: Install dependencies | |
| run: | | |
| # Install create-dmg for DMG creation | |
| brew install create-dmg | |
| # Install AWS CLI for R2 uploads | |
| brew install awscli | |
| # Install Sparkle tools | |
| brew install sparkle | |
| - name: Build and archive app | |
| run: | | |
| xcodebuild clean archive \ | |
| -scheme "${{ env.SCHEME }}" \ | |
| -configuration "${{ env.CONFIGURATION }}" \ | |
| -archivePath build/aizen.xcarchive \ | |
| -arch arm64 \ | |
| ONLY_ACTIVE_ARCH=NO \ | |
| CODE_SIGN_IDENTITY="-" \ | |
| CODE_SIGNING_REQUIRED=NO | |
| - name: Export app bundle | |
| run: | | |
| mkdir -p build/dmg | |
| cp -R build/aizen.xcarchive/Products/Applications/aizen.app build/dmg/ | |
| - name: Ad-hoc sign app (fallback) | |
| run: | | |
| # Deep sign entire bundle to ensure all components have matching Team IDs | |
| codesign --force --deep --sign - build/dmg/aizen.app | |
| # Conditional code signing | |
| - name: Import signing certificate | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| if: ${{ env.APPLE_CERTIFICATE != '' }} | |
| run: | | |
| echo "Code signing certificate found, importing..." | |
| # Create keychain | |
| 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 | |
| # Import certificate | |
| 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 | |
| # Allow codesign to access keychain | |
| 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: | | |
| echo "Signing app bundle..." | |
| codesign --force --deep --sign "Developer ID Application" \ | |
| --options runtime \ | |
| --entitlements aizen/aizen.entitlements \ | |
| build/dmg/aizen.app | |
| - name: Create DMG | |
| run: | | |
| create-dmg \ | |
| --volname "aizen" \ | |
| --window-pos 200 120 \ | |
| --window-size 800 400 \ | |
| --icon-size 100 \ | |
| --icon "aizen.app" 200 190 \ | |
| --hide-extension "aizen.app" \ | |
| --app-drop-link 600 185 \ | |
| "build/Aizen-${{ github.event.inputs.version }}.dmg" \ | |
| "build/dmg/" || true | |
| # create-dmg sometimes fails with exit code 2 even on success | |
| if [ ! -f "build/Aizen-${{ github.event.inputs.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: | | |
| echo "Signing DMG..." | |
| codesign --force --sign "Developer ID Application" \ | |
| "build/Aizen-${{ github.event.inputs.version }}.dmg" | |
| # Conditional notarization | |
| - 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: | | |
| echo "Notarizing DMG..." | |
| xcrun notarytool submit \ | |
| "build/Aizen-${{ github.event.inputs.version }}.dmg" \ | |
| --apple-id "${{ secrets.APPLE_ID }}" \ | |
| --team-id "${{ secrets.APPLE_TEAM_ID }}" \ | |
| --password "${{ secrets.APPLE_APP_PASSWORD }}" \ | |
| --wait | |
| echo "Notarization submitted, waiting for response..." | |
| - 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: | | |
| echo "Stapling notarization to DMG..." | |
| xcrun stapler staple "build/Aizen-${{ github.event.inputs.version }}.dmg" | |
| - name: Generate Sparkle signature | |
| run: | | |
| # Save private key from secrets | |
| echo "${{ secrets.SPARKLE_PRIVATE_KEY }}" > sparkle_priv.pem | |
| # Find sign_update binary (version-independent) | |
| SIGN_UPDATE=$(find /opt/homebrew/Caskroom/sparkle -name sign_update -type f | grep -v old_dsa | grep -v dSYM | head -1) | |
| # Sign the DMG (using -f flag to avoid keychain prompt, -p for signature-only output) | |
| SIGNATURE=$("$SIGN_UPDATE" -f sparkle_priv.pem -p "build/Aizen-${{ github.event.inputs.version }}.dmg") | |
| echo "SPARKLE_SIGNATURE=$SIGNATURE" >> $GITHUB_ENV | |
| # Clean up private key | |
| rm sparkle_priv.pem | |
| - name: Generate appcast entry | |
| run: | | |
| DMG_SIZE=$(stat -f%z "build/Aizen-${{ github.event.inputs.version }}.dmg") | |
| DOWNLOAD_URL="${{ secrets.R2_PUBLIC_URL }}/Aizen-${{ github.event.inputs.version }}.dmg" | |
| # Create appcast entry | |
| cat > build/appcast-entry.xml << EOF | |
| <item> | |
| <title>Version ${{ github.event.inputs.version }}</title> | |
| <description><![CDATA[${{ github.event.inputs.release_notes }}]]></description> | |
| <pubDate>$(date -R)</pubDate> | |
| <sparkle:version>${{ github.event.inputs.version }}</sparkle:version> | |
| <sparkle:shortVersionString>${{ github.event.inputs.version }}</sparkle:shortVersionString> | |
| <enclosure url="$DOWNLOAD_URL" | |
| length="$DMG_SIZE" | |
| type="application/octet-stream" | |
| sparkle:edSignature="${{ env.SPARKLE_SIGNATURE }}" /> | |
| <sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion> | |
| </item> | |
| EOF | |
| cat build/appcast-entry.xml | |
| - 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: | | |
| # Upload DMG | |
| aws s3 cp "build/Aizen-${{ github.event.inputs.version }}.dmg" \ | |
| "s3://${{ secrets.R2_BUCKET_NAME }}/Aizen-${{ github.event.inputs.version }}.dmg" \ | |
| --endpoint-url "${{ secrets.R2_ENDPOINT }}" \ | |
| --region auto | |
| echo "✅ DMG uploaded to R2" | |
| # Download existing appcast if it exists | |
| aws s3 cp "s3://${{ secrets.R2_BUCKET_NAME }}/appcast.xml" appcast.xml \ | |
| --endpoint-url "${{ secrets.R2_ENDPOINT }}" \ | |
| --region auto || echo '<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"><channel><title>Aizen Updates</title><description>Updates for Aizen</description><language>en</language></channel></rss>' > appcast.xml | |
| # Add new entry to appcast | |
| perl -i -pe 's|</channel>|'"$(cat build/appcast-entry.xml)"'\n </channel>|' appcast.xml | |
| # Upload updated appcast | |
| aws s3 cp appcast.xml "s3://${{ secrets.R2_BUCKET_NAME }}/appcast.xml" \ | |
| --endpoint-url "${{ secrets.R2_ENDPOINT }}" \ | |
| --region auto \ | |
| --content-type "application/xml" | |
| echo "✅ Appcast updated on R2" | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: v${{ github.event.inputs.version }} | |
| name: Release ${{ github.event.inputs.version }} | |
| body: ${{ github.event.inputs.release_notes }} | |
| draft: false | |
| prerelease: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| # Delete keychain if it was created | |
| 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.version }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **DMG URL:** ${{ secrets.R2_PUBLIC_URL }}/Aizen-${{ github.event.inputs.version }}.dmg" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Appcast URL:** ${{ secrets.R2_PUBLIC_URL }}/appcast.xml" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ -n "${{ secrets.APPLE_CERTIFICATE }}" ]; then | |
| echo "✅ App was code signed" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "⚠️ App was NOT code signed (certificate not configured)" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ -n "${{ secrets.APPLE_ID }}" ]; then | |
| echo "✅ App was notarized" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "⚠️ App was NOT notarized (Apple ID not configured)" >> $GITHUB_STEP_SUMMARY | |
| fi |