Publish #152
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: Publish | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| platform: | |
| description: 'Which platform(s) to publish' | |
| required: true | |
| default: 'all' | |
| type: choice | |
| options: | |
| - all | |
| - android | |
| - ios | |
| - mac | |
| jobs: | |
| publish-android-app: | |
| if: ${{ inputs.platform == 'all' || inputs.platform == 'android' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: 'write' | |
| id-token: 'write' | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v5 | |
| - name: Setup java | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: 'zulu' | |
| java-version: 21 | |
| - name: Setup Gradle | |
| uses: gradle/actions/setup-gradle@v4 | |
| - name: Create Google services file | |
| shell: bash | |
| env: | |
| # Map the secret to a shell variable here | |
| GOOGLE_SERVICES_ENCODED: ${{ secrets.GOOGLE_SERVICES_BASE_64 }} | |
| run: | | |
| # Create the directory structure | |
| mkdir -p "apps/androidApp/src/release/" | |
| # Decode the env variable into the file | |
| echo "$GOOGLE_SERVICES_ENCODED" | openssl base64 -d -A \ | |
| -out "apps/androidApp/src/release/google-services.json" | |
| - name: Build Unsigned AAB | |
| env: | |
| HERON_ENDPOINT: ${{ secrets.HERON_ENDPOINT }} | |
| run: | | |
| ./gradlew spotlessCheck \ | |
| bundleRelease \ | |
| -Pheron.versionCode=${{ github.run_number }} \ | |
| -Pheron.isRelease=true \ | |
| -Pheron.endpoint="$HERON_ENDPOINT" | |
| - name: Sign AAB | |
| uses: r0adkll/sign-android-release@v1 | |
| id: sign_app | |
| with: | |
| releaseDirectory: apps/androidApp/build/outputs/bundle/release | |
| signingKeyBase64: ${{ secrets.SIGNING_KEY_BASE_64 }} | |
| alias: ${{ secrets.ALIAS }} | |
| keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} | |
| keyPassword: ${{ secrets.KEY_PASSWORD }} | |
| - name: Get play store upload credentials | |
| uses: google-github-actions/auth@v2 | |
| id: auth | |
| with: | |
| project_id: 'heron-d0ff3' | |
| workload_identity_provider: projects/313045152492/locations/global/workloadIdentityPools/github/providers/github-actions-provider | |
| service_account: heron-play-store-publish@heron-d0ff3.iam.gserviceaccount.com | |
| - name: Upload to play store | |
| uses: r0adkll/upload-google-play@v1 | |
| with: | |
| serviceAccountJson: ${{ steps.auth.outputs.credentials_file_path }} | |
| packageName: com.tunjid.heron | |
| releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}} | |
| track: internal | |
| mappingFile: apps/androidApp/build/outputs/mapping/release/mapping.txt | |
| debugSymbols: apps/androidApp/build/intermediates/merged_native_libs/release/mergeReleaseNativeLibs/out/lib | |
| - name: Build and sign per-ABI sideload APKs | |
| env: | |
| HERON_ENDPOINT: ${{ secrets.HERON_ENDPOINT }} | |
| SIGNING_KEY_BASE_64: ${{ secrets.SIGNING_KEY_BASE_64 }} | |
| ALIAS: ${{ secrets.ALIAS }} | |
| KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }} | |
| KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} | |
| run: | | |
| # Per-ABI split APKs (plus a universal fallback) for GitHub sideloads. litertlm's native | |
| # runtime is packaged per architecture, so the arm64-v8a APK (~37 MB) is far smaller than | |
| # the fused universal (~68 MB). This is a separate build from the Play AAB above; with | |
| # heron.isRelease=true the APKs come out UNSIGNED, so zipalign + apksigner each with the | |
| # same release keystore the AAB uses. | |
| ./gradlew assembleRelease \ | |
| -Pheron.versionCode=${{ github.run_number }} \ | |
| -Pheron.isRelease=true \ | |
| -Pheron.endpoint="$HERON_ENDPOINT" | |
| echo "$SIGNING_KEY_BASE_64" | base64 -d > "${RUNNER_TEMP}/release.keystore" | |
| BUILD_TOOLS="$(ls -d "${ANDROID_HOME}"/build-tools/* | sort -V | tail -1)" | |
| OUT=apps/androidApp/build/outputs/apk/release/signed | |
| mkdir -p "$OUT" | |
| for apk in apps/androidApp/build/outputs/apk/release/*-release-unsigned.apk; do | |
| name="$(basename "$apk")" | |
| base="${name%-unsigned.apk}" | |
| "${BUILD_TOOLS}/zipalign" -f -p 4 "$apk" "${RUNNER_TEMP}/${base}-aligned.apk" | |
| "${BUILD_TOOLS}/apksigner" sign \ | |
| --ks "${RUNNER_TEMP}/release.keystore" \ | |
| --ks-pass "pass:${KEY_STORE_PASSWORD}" \ | |
| --ks-key-alias "${ALIAS}" \ | |
| --key-pass "pass:${KEY_PASSWORD}" \ | |
| --out "${OUT}/${base}.apk" \ | |
| "${RUNNER_TEMP}/${base}-aligned.apk" | |
| done | |
| ls -lh "$OUT" | |
| - name: Upload to GitHub releases | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ github.run_number }} | |
| name: Heron v${{ github.run_number }} | |
| files: apps/androidApp/build/outputs/apk/release/signed/*.apk | |
| draft: true | |
| allow_updates: true | |
| publish-ios-app: | |
| if: ${{ inputs.platform == 'all' || inputs.platform == 'ios' }} | |
| runs-on: macos-latest | |
| permissions: | |
| contents: 'write' | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v5 | |
| - name: Setup Xcode | |
| uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: latest-stable | |
| - name: Setup java | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: 'zulu' | |
| java-version: 21 | |
| - name: Setup Gradle | |
| uses: gradle/actions/setup-gradle@v4 | |
| - name: Cache KMP tooling | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.konan | |
| key: ${{ runner.os }}-v1-${{ hashFiles('gradle/libs.versions.toml') }} | |
| - name: Create Firebase Plist | |
| env: | |
| FIREBASE_IOS_PLIST: ${{ secrets.FIREBASE_IOS_PLIST }} | |
| run: | | |
| echo "$FIREBASE_IOS_PLIST" | base64 -d \ | |
| > apps/iosApp/iosApp/GoogleService-Info.plist | |
| - name: Import signing certificate | |
| uses: apple-actions/import-codesign-certs@v2 | |
| with: | |
| p12-file-base64: ${{ secrets.IOS_CERTIFICATES_P12 }} | |
| p12-password: ${{ secrets.IOS_CERTIFICATES_PASSWORD }} | |
| - name: Download provisioning profiles | |
| uses: apple-actions/download-provisioning-profiles@v2 | |
| with: | |
| bundle-id: com.tunjid.heron | |
| issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} | |
| api-key-id: ${{ secrets.APPSTORE_KEY_ID }} | |
| api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }} | |
| - name: Set version and build number | |
| run: | | |
| # Axion's currentVersion task prints "Project version: X.Y.Z", not | |
| # just the version — grab the version number with a regex to match | |
| # what Android uses via scmVersion.version, stripping any suffix | |
| # like -SNAPSHOT. | |
| RAW=$(./gradlew -q currentVersion) | |
| VERSION=$(printf '%s\n' "$RAW" | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -n 1) | |
| if [ -z "$VERSION" ]; then | |
| echo "Failed to parse version from: $RAW" >&2 | |
| exit 1 | |
| fi | |
| echo "Extracted version: $VERSION" | |
| /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" \ | |
| apps/iosApp/iosApp/Info.plist | |
| /usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${{ github.run_number }}" \ | |
| apps/iosApp/iosApp/Info.plist | |
| - name: Patch project for manual signing | |
| env: | |
| PROFILE_NAME: ${{ secrets.IOS_DIST_PROVISIONING_PROFILE_NAME }} | |
| run: | | |
| PBXPROJ=apps/iosApp/iosApp.xcodeproj/project.pbxproj | |
| # Scope manual signing to the iosApp target only (SPM deps keep their | |
| # automatic/no-profile defaults). Escape the profile name for sed. | |
| ESCAPED_PROFILE=$(printf '%s\n' "$PROFILE_NAME" | sed 's|[&/\]|\\&|g') | |
| sed -i '' \ | |
| -e 's|CODE_SIGN_STYLE = Automatic;|CODE_SIGN_STYLE = Manual;|g' \ | |
| -e 's|CODE_SIGN_IDENTITY = "Apple Development";|CODE_SIGN_IDENTITY = "Apple Distribution";|g' \ | |
| -e "s|PROVISIONING_PROFILE_SPECIFIER = \"\";|PROVISIONING_PROFILE_SPECIFIER = \"$ESCAPED_PROFILE\";|g" \ | |
| "$PBXPROJ" | |
| - name: Patch entitlements for production APNs | |
| run: | | |
| # Local dev keeps aps-environment=development so running from Xcode | |
| # hits the sandbox APNs gateway. TestFlight / App Store builds need | |
| # production APNs, so we flip the entitlement at CI time only. | |
| /usr/libexec/PlistBuddy -c "Set :aps-environment production" \ | |
| apps/iosApp/iosApp/iosApp.entitlements | |
| - name: Build archive | |
| env: | |
| HERON_ENDPOINT: ${{ secrets.HERON_ENDPOINT }} | |
| ORG_GRADLE_PROJECT_heron.isRelease: "true" | |
| ORG_GRADLE_PROJECT_heron.versionCode: ${{ github.run_number }} | |
| run: | | |
| cd apps/iosApp | |
| xcrun xcodebuild \ | |
| -scheme "iosApp" \ | |
| -configuration "Release" \ | |
| -sdk "iphoneos" \ | |
| -parallelizeTargets \ | |
| -showBuildTimingSummary \ | |
| -derivedDataPath "${RUNNER_TEMP}/Build/DerivedData" \ | |
| -archivePath "${RUNNER_TEMP}/Build/Archives/heron.xcarchive" \ | |
| -resultBundlePath "${RUNNER_TEMP}/Build/Artifacts/heron.xcresult" \ | |
| -destination "generic/platform=iOS" \ | |
| archive | |
| - name: Generate ExportOptions.plist | |
| run: | | |
| cat <<EOF > ${RUNNER_TEMP}/Build/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>destination</key> | |
| <string>export</string> | |
| <key>method</key> | |
| <string>app-store</string> | |
| <key>signingStyle</key> | |
| <string>manual</string> | |
| <key>generateAppStoreInformation</key> | |
| <true/> | |
| <key>stripSwiftSymbols</key> | |
| <true/> | |
| <key>teamID</key> | |
| <string>${{ secrets.APPSTORE_TEAM_ID }}</string> | |
| <key>uploadSymbols</key> | |
| <true/> | |
| <key>provisioningProfiles</key> | |
| <dict> | |
| <key>com.tunjid.heron</key> | |
| <string>${{ secrets.IOS_DIST_PROVISIONING_PROFILE_NAME }}</string> | |
| </dict> | |
| </dict> | |
| </plist> | |
| EOF | |
| - name: Export IPA | |
| run: | | |
| xcrun xcodebuild \ | |
| -exportArchive \ | |
| -exportOptionsPlist "${RUNNER_TEMP}/Build/ExportOptions.plist" \ | |
| -archivePath "${RUNNER_TEMP}/Build/Archives/heron.xcarchive" \ | |
| -exportPath "${RUNNER_TEMP}/Build/Archives/heron.xcarchive" \ | |
| PRODUCT_BUNDLE_IDENTIFIER="com.tunjid.heron" | |
| echo "IPA_PATH=${RUNNER_TEMP}/Build/Archives/heron.xcarchive/heron.ipa" >> $GITHUB_ENV | |
| - name: Upload to TestFlight | |
| uses: Apple-Actions/upload-testflight-build@v1 | |
| with: | |
| app-path: ${{ env.IPA_PATH }} | |
| issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} | |
| api-key-id: ${{ secrets.APPSTORE_KEY_ID }} | |
| api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }} | |
| publish-mac-app: | |
| if: ${{ inputs.platform == 'all' || inputs.platform == 'mac' }} | |
| runs-on: macos-latest | |
| permissions: | |
| contents: 'write' | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v5 | |
| - name: Setup java | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: 'zulu' | |
| java-version: 21 | |
| - name: Setup Gradle | |
| uses: gradle/actions/setup-gradle@v4 | |
| - name: Cache KMP tooling | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.konan | |
| key: ${{ runner.os }}-v1-${{ hashFiles('gradle/libs.versions.toml') }} | |
| - name: Import signing certificate | |
| uses: apple-actions/import-codesign-certs@v2 | |
| with: | |
| p12-file-base64: ${{ secrets.MACOS_SIGNING_CERTIFICATE_P12_DATA }} | |
| p12-password: ${{ secrets.MACOS_SIGNING_CERTIFICATE_PASSWORD }} | |
| - name: Build and sign release DMG | |
| env: | |
| HERON_ENDPOINT: ${{ secrets.HERON_ENDPOINT }} | |
| run: | | |
| ./gradlew :desktopApp:packageReleaseDmg \ | |
| -Pheron.versionCode=${{ github.run_number }} \ | |
| -Pheron.endpoint="$HERON_ENDPOINT" \ | |
| -Pheron.macOS.signing.identity="${{ secrets.MACOS_SIGNING_IDENTITY }}" | |
| - name: Notarize and staple DMG | |
| env: | |
| MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }} | |
| MACOS_NOTARIZATION_PASSWORD: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }} | |
| MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }} | |
| run: | | |
| set -euo pipefail | |
| DMG_PATH=$(find apps/desktopApp/build/release/main-release/dmg -name "*.dmg" | head -1) | |
| if [ -z "$DMG_PATH" ]; then echo "::error::No DMG found to notarize"; exit 1; fi | |
| # `notarytool submit --wait` exits 0 even when the result is Invalid, so gate on the | |
| # status explicitly and dump the notarization log (the per-file reasons) on failure. | |
| SUBMIT_JSON=$(xcrun notarytool submit "$DMG_PATH" \ | |
| --apple-id "$MACOS_NOTARIZATION_APPLE_ID" \ | |
| --password "$MACOS_NOTARIZATION_PASSWORD" \ | |
| --team-id "$MACOS_NOTARIZATION_TEAM_ID" \ | |
| --output-format json --wait) | |
| echo "$SUBMIT_JSON" | |
| SUBMISSION_ID=$(echo "$SUBMIT_JSON" | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])') | |
| STATUS=$(echo "$SUBMIT_JSON" | python3 -c 'import sys,json;print(json.load(sys.stdin)["status"])') | |
| if [ "$STATUS" != "Accepted" ]; then | |
| echo "::error::Notarization returned status '$STATUS'. Fetching log for $SUBMISSION_ID:" | |
| xcrun notarytool log "$SUBMISSION_ID" \ | |
| --apple-id "$MACOS_NOTARIZATION_APPLE_ID" \ | |
| --password "$MACOS_NOTARIZATION_PASSWORD" \ | |
| --team-id "$MACOS_NOTARIZATION_TEAM_ID" || true | |
| exit 1 | |
| fi | |
| xcrun stapler staple "$DMG_PATH" | |
| - name: Upload to GitHub releases | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ github.run_number }} | |
| name: Heron v${{ github.run_number }} | |
| files: apps/desktopApp/build/release/main-release/dmg/*.dmg | |
| draft: true | |
| allow_updates: true |