name: Native apps release # Builds the signed store builds of the iOS and Android apps and uploads them: # the Android App Bundle to a Google Play testing track, the iOS archive to # App Store Connect (TestFlight). Promotion to production stays a manual step # in both consoles. # # Runs only in liketrek/TREK, only by hand or on a version tag, and only after a # required reviewer approves the `store` environment, which holds every secret # (see wiki/Native-Apps.md for the list). Pull requests never reach it. # # Both stores get the same numbers: the version from client/package.json # without a prerelease suffix, and a build number that only grows. A prerelease # reuses its X.Y.Z with a higher build number and belongs on the internal track # and in TestFlight only. on: workflow_dispatch: inputs: track: description: Google Play track type: choice options: [internal, alpha, beta] default: internal push: tags: ['v*'] permissions: contents: read concurrency: group: native-release cancel-in-progress: false jobs: android: # Gated on a repository variable so the release can ship iOS alone while the # Google Play account and its signing secrets are not yet in place. Set the # repository variable NATIVE_RELEASE_ANDROID to 1 to turn the Android half on. if: github.repository == 'liketrek/TREK' && vars.NATIVE_RELEASE_ANDROID == '1' runs-on: ubuntu-24.04 timeout-minutes: 40 environment: store steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: npm - uses: actions/setup-java@v5 with: distribution: temurin java-version: 21 cache: gradle - name: Version and build number id: version run: | echo "version=$(node -p "require('./client/package.json').version.split('-')[0]")" >> "$GITHUB_OUTPUT" echo "build=$((GITHUB_RUN_NUMBER + 1000))" >> "$GITHUB_OUTPUT" - name: Install run: npm ci --workspace=shared --workspace=client - name: Build shared run: npm run build --workspace=shared - name: Build the address screen and sync it into the shell working-directory: client run: npm run cap:sync -- android - name: Upload key env: KEYSTORE_B64: ${{ secrets.ANDROID_UPLOAD_KEYSTORE_B64 }} run: printf '%s' "$KEYSTORE_B64" | base64 -d > "$RUNNER_TEMP/upload.jks" - name: Build the signed App Bundle working-directory: client/android env: ORG_GRADLE_PROJECT_trekKeystorePassword: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ORG_GRADLE_PROJECT_trekKeyAlias: ${{ secrets.ANDROID_KEY_ALIAS }} ORG_GRADLE_PROJECT_trekKeyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} run: >- ./gradlew bundleRelease --no-daemon -PtrekVersionName=${{ steps.version.outputs.version }} -PtrekVersionCode=${{ steps.version.outputs.build }} -PtrekKeystore="$RUNNER_TEMP/upload.jks" - uses: actions/upload-artifact@v6 with: name: trek-android-${{ steps.version.outputs.version }}-${{ steps.version.outputs.build }} path: client/android/app/build/outputs/bundle/release/app-release.aab retention-days: 30 # Play only accepts API uploads once one bundle was uploaded by hand # (wiki/Native-Apps.md). `draft` until the app has left draft in the console. - name: Upload to Google Play uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5 with: serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} packageName: com.liketrek.trek releaseFiles: client/android/app/build/outputs/bundle/release/app-release.aab track: ${{ inputs.track || 'internal' }} status: draft - name: Remove the upload key if: always() run: rm -f "$RUNNER_TEMP/upload.jks" ios: if: github.repository == 'liketrek/TREK' runs-on: macos-15 timeout-minutes: 60 environment: store steps: - uses: actions/checkout@v6 # App Store uploads must be built with Xcode 26; the image's default is older. - name: Select Xcode 26 run: | XCODE=$(ls -d /Applications/Xcode_26*.app | sort -V | tail -1) test -n "$XCODE" sudo xcode-select -s "$XCODE" xcodebuild -version - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: npm - name: Version and build number id: version run: | echo "version=$(node -p "require('./client/package.json').version.split('-')[0]")" >> "$GITHUB_OUTPUT" echo "build=$((GITHUB_RUN_NUMBER + 1000))" >> "$GITHUB_OUTPUT" - name: Install run: npm ci --workspace=shared --workspace=client - name: Build shared run: npm run build --workspace=shared - name: Build the address screen and sync it into the shell working-directory: client run: npm run cap:sync -- ios - name: App Store Connect API key env: ASC_KEY_P8_B64: ${{ secrets.ASC_KEY_P8_B64 }} run: printf '%s' "$ASC_KEY_P8_B64" | base64 -d > "$RUNNER_TEMP/AuthKey.p8" - name: Archive working-directory: client/ios/App env: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} run: >- xcodebuild archive -project App.xcodeproj -scheme App -configuration Release -destination 'generic/platform=iOS' -archivePath "$RUNNER_TEMP/TREK.xcarchive" MARKETING_VERSION=${{ steps.version.outputs.version }} CURRENT_PROJECT_VERSION=${{ steps.version.outputs.build }} DEVELOPMENT_TEAM="$APPLE_TEAM_ID" -allowProvisioningUpdates -authenticationKeyPath "$RUNNER_TEMP/AuthKey.p8" -authenticationKeyID "$ASC_KEY_ID" -authenticationKeyIssuerID "$ASC_ISSUER_ID" # ExportOptions.plist says destination=upload, so this step sends the build # to App Store Connect, where it shows up in TestFlight after processing. - name: Export and upload to App Store Connect working-directory: client/ios/App env: ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} run: >- xcodebuild -exportArchive -archivePath "$RUNNER_TEMP/TREK.xcarchive" -exportOptionsPlist ../ExportOptions.plist -exportPath "$RUNNER_TEMP/export" -allowProvisioningUpdates -authenticationKeyPath "$RUNNER_TEMP/AuthKey.p8" -authenticationKeyID "$ASC_KEY_ID" -authenticationKeyIssuerID "$ASC_ISSUER_ID" - name: Remove the API key if: always() run: rm -f "$RUNNER_TEMP/AuthKey.p8"