Build & Release #48
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 & Release | |
| on: | |
| workflow_dispatch: | |
| push: | |
| tags: ['v*.*.*'] | |
| permissions: | |
| contents: write | |
| jobs: | |
| build: | |
| runs-on: macos-26 | |
| outputs: | |
| marketing_version: ${{ steps.version.outputs.marketing_version }} | |
| tag: ${{ steps.version.outputs.tag }} | |
| is_release: ${{ steps.version.outputs.is_release }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install signing certificate | |
| env: | |
| BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} | |
| P12_PASSWORD: ${{ secrets.P12_PASSWORD }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} | |
| run: | | |
| CERTIFICATE_PATH="$RUNNER_TEMP/build_certificate.p12" | |
| KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" | |
| CERTIFICATE_BASE64_PATH="$RUNNER_TEMP/build_certificate.base64" | |
| printf '%s' "$BUILD_CERTIFICATE_BASE64" > "$CERTIFICATE_BASE64_PATH" | |
| base64 -D -i "$CERTIFICATE_BASE64_PATH" -o "$CERTIFICATE_PATH" | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security import "$CERTIFICATE_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"') | |
| - name: Set version | |
| id: version | |
| run: | | |
| if [[ "$GITHUB_REF" == refs/tags/* ]]; then | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| echo "tag=$TAG" >> $GITHUB_OUTPUT | |
| echo "marketing_version=${TAG#v}" >> $GITHUB_OUTPUT | |
| echo "is_release=true" >> $GITHUB_OUTPUT | |
| else | |
| SHORT_SHA="${GITHUB_SHA:0:7}" | |
| echo "tag=dev-${SHORT_SHA}" >> $GITHUB_OUTPUT | |
| echo "marketing_version=$SHORT_SHA" >> $GITHUB_OUTPUT | |
| echo "is_release=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Archive | |
| env: | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| xcodebuild \ | |
| -project Wave.xcodeproj \ | |
| -scheme Wave \ | |
| -configuration Release \ | |
| -archivePath build/Wave.xcarchive \ | |
| -derivedDataPath build \ | |
| archive \ | |
| MARKETING_VERSION="${{ steps.version.outputs.marketing_version }}" \ | |
| CURRENT_PROJECT_VERSION="${{ steps.version.outputs.marketing_version }}" \ | |
| CODE_SIGN_STYLE=Manual \ | |
| CODE_SIGN_IDENTITY="Developer ID Application" \ | |
| DEVELOPMENT_TEAM="${APPLE_TEAM_ID}" \ | |
| PROVISIONING_PROFILE_SPECIFIER="" \ | |
| PROVISIONING_PROFILE="" \ | |
| STRIP_INSTALLED_PRODUCT=YES \ | |
| DEAD_CODE_STRIPPING=YES \ | |
| DEBUG_INFORMATION_FORMAT=dwarf-with-dsym \ | |
| SWIFT_OPTIMIZATION_LEVEL=-Osize \ | |
| SWIFT_COMPILATION_MODE=wholemodule \ | |
| ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES=NO | |
| - name: Export signed app | |
| env: | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| cat <<EOF > "$RUNNER_TEMP/ExportOptions.plist" | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>method</key> | |
| <string>developer-id</string> | |
| <key>signingStyle</key> | |
| <string>manual</string> | |
| <key>signingCertificate</key> | |
| <string>Developer ID Application</string> | |
| <key>teamID</key> | |
| <string>${APPLE_TEAM_ID}</string> | |
| </dict> | |
| </plist> | |
| EOF | |
| xcodebuild \ | |
| -exportArchive \ | |
| -archivePath build/Wave.xcarchive \ | |
| -exportPath build/export \ | |
| -exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \ | |
| CODE_SIGN_STYLE=Manual \ | |
| CODE_SIGN_IDENTITY="Developer ID Application" \ | |
| DEVELOPMENT_TEAM="${APPLE_TEAM_ID}" \ | |
| PROVISIONING_PROFILE_SPECIFIER="" \ | |
| PROVISIONING_PROFILE="" | |
| - name: Debug app size | |
| run: | | |
| echo "=== App bundle total ===" | |
| du -sh build/export/Wave.app | |
| echo "=== Contents breakdown ===" | |
| du -sh build/export/Wave.app/Contents/* | |
| echo "=== Frameworks ===" | |
| du -sh build/export/Wave.app/Contents/Frameworks/* 2>/dev/null || echo "No frameworks" | |
| echo "=== Binary ===" | |
| du -sh build/export/Wave.app/Contents/MacOS/* | |
| echo "=== Plugins/XPC ===" | |
| du -sh build/export/Wave.app/Contents/PlugIns/* 2>/dev/null || echo "No plugins" | |
| echo "=== Resources (top level) ===" | |
| du -sh build/export/Wave.app/Contents/Resources/* 2>/dev/null | sort -rh | head -20 | |
| - name: Verify signed app | |
| run: | | |
| codesign --verify --deep --strict --verbose=2 build/export/Wave.app | |
| - name: Upload app | |
| run: ditto -c -k --sequesterRsrc --keepParent build/export/Wave.app Wave.app.zip | |
| - name: Upload app artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: Wave.app.zip | |
| path: Wave.app.zip | |
| package: | |
| runs-on: macos-26 | |
| needs: build | |
| outputs: | |
| dmg_name: ${{ steps.dmg.outputs.dmg_name }} | |
| steps: | |
| - name: Download app | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: Wave.app.zip | |
| - name: Extract app | |
| run: ditto -x -k Wave.app.zip . | |
| - name: Install signing certificate | |
| env: | |
| BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} | |
| P12_PASSWORD: ${{ secrets.P12_PASSWORD }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} | |
| run: | | |
| CERTIFICATE_PATH="$RUNNER_TEMP/build_certificate.p12" | |
| KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" | |
| CERTIFICATE_BASE64_PATH="$RUNNER_TEMP/build_certificate.base64" | |
| printf '%s' "$BUILD_CERTIFICATE_BASE64" > "$CERTIFICATE_BASE64_PATH" | |
| base64 -D -i "$CERTIFICATE_BASE64_PATH" -o "$CERTIFICATE_PATH" | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security import "$CERTIFICATE_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"') | |
| - name: Install create-dmg | |
| run: brew install create-dmg | |
| - name: Create DMG | |
| id: dmg | |
| run: | | |
| TAG="${{ needs.build.outputs.tag }}" | |
| DMG_NAME="Wave-Installer-${TAG}.dmg" | |
| echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT | |
| create-dmg \ | |
| --volname "Wave" \ | |
| --window-pos 200 120 \ | |
| --window-size 640 360 \ | |
| --icon-size 96 \ | |
| --icon "Wave.app" 180 170 \ | |
| --hide-extension "Wave.app" \ | |
| --app-drop-link 460 170 \ | |
| "$DMG_NAME" \ | |
| Wave.app/ | |
| - name: Sign DMG | |
| run: | | |
| DMG_NAME="${{ steps.dmg.outputs.dmg_name }}" | |
| SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$RUNNER_TEMP/app-signing.keychain-db" | awk -F '"' '/Developer ID Application/ { print $2; exit }') | |
| test -n "$SIGNING_IDENTITY" | |
| codesign --force --sign "$SIGNING_IDENTITY" --keychain "$RUNNER_TEMP/app-signing.keychain-db" "$DMG_NAME" | |
| codesign --verify --verbose=2 "$DMG_NAME" | |
| - name: Notarize and staple DMG | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| DMG_NAME="${{ steps.dmg.outputs.dmg_name }}" | |
| KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" | |
| xcrun notarytool store-credentials wave-notary \ | |
| --apple-id "$APPLE_ID" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --password "$APPLE_APP_SPECIFIC_PASSWORD" \ | |
| --keychain "$KEYCHAIN_PATH" | |
| xcrun notarytool submit "$DMG_NAME" \ | |
| --keychain-profile wave-notary \ | |
| --keychain "$KEYCHAIN_PATH" \ | |
| --wait | |
| xcrun stapler staple "$DMG_NAME" | |
| spctl -a -t open --context context:primary-signature -vv "$DMG_NAME" | |
| - name: Upload DMG | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ steps.dmg.outputs.dmg_name }} | |
| path: ${{ steps.dmg.outputs.dmg_name }} | |
| release: | |
| runs-on: macos-26 | |
| needs: [build, package] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Download DMG | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ${{ needs.package.outputs.dmg_name }} | |
| path: release-dir | |
| - name: Install Sparkle tools | |
| if: needs.build.outputs.is_release == 'true' | |
| run: | | |
| curl -L https://github.com/sparkle-project/Sparkle/releases/download/2.9.0/Sparkle-2.9.0.tar.xz -o sparkle.tar.xz | |
| mkdir sparkle-tools | |
| tar -xf sparkle.tar.xz -C sparkle-tools | |
| SPARKLE_BIN=$(find sparkle-tools -name generate_appcast -type f | head -1 | xargs dirname) | |
| echo "$SPARKLE_BIN" >> $GITHUB_PATH | |
| - name: Generate appcast | |
| if: needs.build.outputs.is_release == 'true' | |
| env: | |
| SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} | |
| run: | | |
| TAG="${{ needs.build.outputs.tag }}" | |
| DMG="${{ needs.package.outputs.dmg_name }}" | |
| echo "$SPARKLE_PRIVATE_KEY" > /tmp/sparkle.key | |
| generate_appcast \ | |
| --ed-key-file /tmp/sparkle.key \ | |
| --download-url-prefix "https://github.com/mxvsh/wave/releases/download/${TAG}/" \ | |
| --link "https://github.com/mxvsh/wave" \ | |
| release-dir/ | |
| rm /tmp/sparkle.key | |
| - name: Create GitHub Release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| TAG="${{ needs.build.outputs.tag }}" | |
| DMG="${{ needs.package.outputs.dmg_name }}" | |
| IS_RELEASE="${{ needs.build.outputs.is_release }}" | |
| if [[ "$IS_RELEASE" == "true" ]]; then | |
| VERSION="${{ needs.build.outputs.marketing_version }}" | |
| awk "/^## \[${VERSION}\]/{found=1; next} found && /^## /{exit} found{print}" CHANGELOG.md > release-notes.md | |
| gh release create "$TAG" \ | |
| --title "$TAG" \ | |
| --notes-file release-notes.md \ | |
| "release-dir/$DMG" | |
| else | |
| gh release create "$TAG" \ | |
| --title "$TAG" \ | |
| --prerelease \ | |
| --notes "Development build from commit ${GITHUB_SHA}" \ | |
| "release-dir/$DMG" | |
| fi | |
| - name: Push appcast to gh-pages | |
| if: needs.build.outputs.is_release == 'true' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git fetch origin gh-pages | |
| git checkout gh-pages | |
| cp release-dir/appcast.xml appcast.xml | |
| git add appcast.xml | |
| git commit -m "Update appcast for ${{ needs.build.outputs.tag }}" | |
| git push origin gh-pages |