Nightly build and test #802
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: Nightly build and test | |
| env: | |
| # The name of the main module repository | |
| main_project_module: app | |
| # The name in the Play Store | |
| playstore_name: Soundscape | |
| on: | |
| # Allows you to run this workflow manually from the Actions tab | |
| workflow_dispatch: | |
| # Schedule the build for 05.30 UTC | |
| schedule: | |
| - cron: '30 5 * * *' | |
| jobs: | |
| check_date: | |
| name: Check latest commit | |
| runs-on: ubuntu-latest | |
| environment: development | |
| permissions: | |
| contents: write | |
| outputs: | |
| should_run: ${{ steps.should_run.outputs.should_run }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check latest commit is less than a day | |
| id: should_run | |
| continue-on-error: true | |
| #if: ${{ github.event_name == 'schedule' }} | |
| run: test -z $(git rev-list --after="24 hours" HEAD) && echo "should_run=false" >> $GITHUB_OUTPUT | |
| build: | |
| name: Build package | |
| runs-on: ubuntu-latest | |
| environment: development | |
| needs: check_date | |
| if: ${{ needs.check_date.outputs.should_run != 'false' }} | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup offline maps for tests | |
| uses: ./.github/actions/setup-offline-maps | |
| with: | |
| output_dir: ${{ env.main_project_module }}/src/test/res/org/scottishtecharmy/soundscape | |
| - name: Setup tile and search providers | |
| uses: ./.github/actions/write-provider-config | |
| with: | |
| platform: android | |
| tile_provider_url: ${{ secrets.TILE_PROVIDER_URL }} | |
| tile_provider_api_key: ${{ secrets.TILE_PROVIDER_API_KEY }} | |
| search_provider_url: ${{ secrets.SEARCH_PROVIDER_URL }} | |
| search_provider_api_key: ${{ secrets.SEARCH_PROVIDER_API_KEY }} | |
| extract_provider_url: ${{ secrets.EXTRACT_PROVIDER_URL }} | |
| - name: Decode Google services | |
| uses: ./.github/actions/decode-base64-secret | |
| with: | |
| encoded: ${{ secrets.GOOGLE_SERVICES }} | |
| output_path: ${{ env.main_project_module }}/${{ secrets.GOOGLE_SERVICES_PATH }} | |
| - name: Setup JDK + Gradle | |
| uses: ./.github/actions/setup-jdk-gradle | |
| - name: Run lint over the repo | |
| run: ./gradlew lint | |
| # Run Tests Build | |
| - name: Run gradle tests | |
| run: ./gradlew test | |
| # Slow JVM unit tests (tagged @Category(NightlyOnlyTest::class)) that are excluded from | |
| # the regular `test` task run in run-tests.yaml, e.g. because they just replay every GPX | |
| # fixture through the full grid/callout engine to check nothing crashes. | |
| - name: Run nightly-only unit tests | |
| run: ./gradlew :app:nightlyUnitTest | |
| # Setup release build keys | |
| - name: Decode Keystore | |
| uses: ./.github/actions/decode-base64-secret | |
| with: | |
| encoded: ${{ secrets.SIGNING_KEY_STORE_BASE64 }} | |
| output_path: ${{ env.main_project_module }}/${{ secrets.SIGNING_KEY_STORE_PATH }} | |
| # Create APK Release | |
| - name: Build apk project (APK) | |
| env: | |
| SIGNING_KEY_STORE_PATH: ${{ secrets.SIGNING_KEY_STORE_PATH }} | |
| SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} | |
| SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} | |
| SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }} | |
| run: | | |
| ./gradlew assemble | |
| # Upload Artifact Build | |
| # Noted For Output [main_project_module]/build/outputs/apk/release/ | |
| - name: Upload APK Release | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: release-apk | |
| path: ${{ env.main_project_module }}/build/outputs/apk/releaseTest/ | |
| instrumentation-tests-nightly: | |
| name: Run full instrumentation test suite | |
| runs-on: ubuntu-latest | |
| environment: development | |
| needs: check_date | |
| if: ${{ needs.check_date.outputs.should_run != 'false' }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup offline maps for tests | |
| uses: ./.github/actions/setup-offline-maps | |
| with: | |
| output_dir: ${{ env.main_project_module }}/src/test/res/org/scottishtecharmy/soundscape | |
| - name: Setup JDK + Gradle | |
| uses: ./.github/actions/setup-jdk-gradle | |
| - name: Setup tile and search providers | |
| uses: ./.github/actions/write-provider-config | |
| with: | |
| platform: android | |
| tile_provider_url: ${{ secrets.TILE_PROVIDER_URL }} | |
| tile_provider_api_key: ${{ secrets.TILE_PROVIDER_API_KEY }} | |
| search_provider_url: ${{ secrets.SEARCH_PROVIDER_URL }} | |
| search_provider_api_key: ${{ secrets.SEARCH_PROVIDER_API_KEY }} | |
| extract_provider_url: ${{ secrets.EXTRACT_PROVIDER_URL }} | |
| - name: Enable KVM | |
| run: | | |
| echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules | |
| sudo udevadm control --reload-rules | |
| sudo udevadm trigger --name-match=kvm | |
| # Runs the full instrumented suite unfiltered - both the tests that also run on every PR | |
| # (run-tests.yaml excludes @NightlyOnly there via notAnnotation) and the @NightlyOnly ones | |
| # (real-time audio soak tests, live-network map fetches) that are too slow for PRs. | |
| - name: Full instrumentation test suite | |
| id: instrumentation-tests-nightly | |
| uses: reactivecircus/android-emulator-runner@v2 | |
| continue-on-error: true # IMPORTANT: allow pipeline to continue to log upload step | |
| with: | |
| api-level: 35 | |
| target: google_apis | |
| arch: x86_64 | |
| script: | | |
| adb root | |
| adb shell mkdir -p /storage/emulated/0/Android/data/org.scottishtecharmy.soundscape/files/Download | |
| adb push ${{ env.main_project_module }}/src/test/res/org/scottishtecharmy/soundscape/glasgow-gb.pmtiles /storage/emulated/0/Android/data/org.scottishtecharmy.soundscape/files/Download/ | |
| adb logcat -c # clear logs | |
| touch app/emulator.log # create log file | |
| chmod 777 app/emulator.log # allow writing to log file | |
| adb logcat >> app/emulator.log & # pipe all logcat messages into log file as a background process | |
| ./gradlew connectedCheck | |
| - name: Upload Failing Test Report Log | |
| if: steps.instrumentation-tests-nightly.outcome != 'success' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: nightly-instrumentation-logs | |
| path: app/emulator.log | |
| - name: Raise error on test fail | |
| if: steps.instrumentation-tests-nightly.outcome != 'success' | |
| run: exit 1 | |
| firebase: | |
| name: Run UI tests with Firebase Test Lab | |
| needs: build | |
| environment: development | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download app APK | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: release-apk | |
| - name: Auth | |
| uses: google-github-actions/auth@v3 | |
| with: | |
| credentials_json: ${{ secrets.GCLOUD_CREDENTIALS_JSON }} | |
| - name: Set up Cloud SDK | |
| uses: google-github-actions/setup-gcloud@v3 | |
| - name: Set current project | |
| run: gcloud config set project ${{ secrets.FIREBASE_PROJECT_ID }} | |
| - name: Run Robo test in Firebase Test Lab | |
| run: gcloud firebase test android run --type robo --app=app-releaseTest.apk --robo-script firebase/robo_script.json --timeout=1800s --device model=shiba,version=34,locale=en,orientation=portrait | |
| ios-build: | |
| name: Build iOS app | |
| runs-on: [self-hosted, macOS, ARM64] | |
| environment: development | |
| needs: check_date | |
| if: ${{ needs.check_date.outputs.should_run != 'false' }} | |
| # Konan's devirtualization pass during linkReleaseFrameworkIosArm64 runs | |
| # in-process in the Gradle daemon and OOMs at gradle.properties' 4 GB | |
| # default with Realm + MapLibre + shared/. GRADLE_OPTS overrides the file | |
| # setting for this job only; the Linux runners keep the 4 GB default so | |
| # the Android emulator has room in its 16 GB budget. | |
| env: | |
| GRADLE_OPTS: -Xmx8192m | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| # Konan's devirtualization pass during linkReleaseFrameworkIosArm64 | |
| # runs in-process in the Gradle daemon and OOMs at gradle.properties' | |
| # 4 GB default with Realm + MapLibre + shared/. GRADLE_OPTS only sets | |
| # the client JVM, so patch the file for this checkout instead. Linux | |
| # runners never run this step and keep the 4 GB default so the Android | |
| # emulator has room in its 16 GB budget. | |
| - name: Bump Gradle daemon heap for Kotlin/Native release link | |
| run: | | |
| sed -i.bak 's/^org\.gradle\.jvmargs=.*/org.gradle.jvmargs=-Xmx8192m -Dfile.encoding=UTF-8/' gradle.properties | |
| grep '^org\.gradle\.jvmargs' gradle.properties | |
| - name: Setup iOS toolchain | |
| uses: ./.github/actions/setup-ios-toolchain | |
| - name: Setup tile and search providers | |
| uses: ./.github/actions/write-provider-config | |
| with: | |
| platform: ios | |
| tile_provider_url: ${{ secrets.TILE_PROVIDER_URL }} | |
| search_provider_url: ${{ secrets.SEARCH_PROVIDER_URL }} | |
| extract_provider_url: ${{ secrets.EXTRACT_PROVIDER_URL }} | |
| # Overwrite the committed placeholder GoogleService-Info.plist with the | |
| # real one for the archive step. Firebase itself is only initialised on | |
| # Release + non-XCTest, gated in FirebaseAnalyticsBridge.swift. | |
| - name: Decode Google service info | |
| uses: ./.github/actions/decode-base64-secret | |
| with: | |
| encoded: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST }} | |
| output_path: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST_PATH }} | |
| - name: Generate Xcode project | |
| working-directory: iosApp | |
| run: xcodegen generate | |
| # See run-tests.yaml: linking iosSimulatorArm64Test fails because of | |
| # maplibre-compose's embedded linker flags. xcodebuild archive below | |
| # builds via the Xcode/SPM toolchain that resolves MapLibre. | |
| - name: Compile shared Kotlin/Native tests for iOS simulator | |
| run: ./gradlew :shared:compileTestKotlinIosSimulatorArm64 | |
| - name: Setup iOS signing | |
| uses: ./.github/actions/setup-ios-signing | |
| with: | |
| cert_base64: ${{ secrets.IOS_DIST_CERT_BASE64 }} | |
| cert_password: ${{ secrets.IOS_DIST_CERT_PASSWORD }} | |
| provisioning_profile_base64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }} | |
| shareext_provisioning_profile_base64: ${{ secrets.IOS_SHAREEXT_PROVISIONING_PROFILE_BASE64 }} | |
| keychain_password: ${{ secrets.IOS_KEYCHAIN_PASSWORD }} | |
| # CI runners have no Apple ID logged in, so xcodebuild authenticates | |
| # via the App Store Connect API key for any -allowProvisioningUpdates | |
| # operation; otherwise it fails with "No Accounts: Add a new account | |
| # in Accounts settings." | |
| - name: Decode App Store Connect API key | |
| uses: ./.github/actions/decode-base64-secret | |
| with: | |
| encoded: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }} | |
| output_path: ${{ runner.temp }}/private_keys/AuthKey_${{ secrets.APPSTORE_CONNECT_API_KEY_ID }}.p8 | |
| # Manual signing: project.yml pins each Release target to the | |
| # distribution cert and references PROFILE_NAME_APP / | |
| # PROFILE_NAME_SHAREEXT as user-defined build settings. Passing them | |
| # here lets each target pick up its own profile name (xcodebuild | |
| # SETTING=VALUE overrides apply to all targets, but each target's | |
| # PROVISIONING_PROFILE_SPECIFIER references a different var). No | |
| # -allowProvisioningUpdates: the profiles were already imported by | |
| # setup-ios-signing, so Apple doesn't need to mint fresh ones. | |
| - name: Archive iOS app | |
| run: | | |
| xcodebuild archive \ | |
| -project iosApp/iosApp.xcodeproj \ | |
| -scheme iosApp \ | |
| -configuration Release \ | |
| -sdk iphoneos \ | |
| -destination 'generic/platform=iOS' \ | |
| -archivePath "$RUNNER_TEMP/Soundscape.xcarchive" \ | |
| PROFILE_NAME_APP="$IOS_PROFILE_NAME" \ | |
| PROFILE_NAME_SHAREEXT="$IOS_SHAREEXT_PROFILE_NAME" | |
| # Manual signing during export so xcodebuild reuses the profile we | |
| # imported in setup-ios-signing instead of asking Apple to mint a fresh | |
| # managed profile (which may lack capabilities like Associated Domains | |
| # that exist on the App ID but can't be granted under the API key role). | |
| - name: Write export options | |
| run: | | |
| cat > "$RUNNER_TEMP/ExportOptions.plist" <<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>app-store</string> | |
| <key>teamID</key><string>48N3J4V5KM</string> | |
| <key>signingStyle</key><string>manual</string> | |
| <key>provisioningProfiles</key> | |
| <dict> | |
| <key>org.scottishtecharmy.soundscape</key> | |
| <string>${IOS_PROFILE_NAME}</string> | |
| <key>org.scottishtecharmy.soundscape.share</key> | |
| <string>${IOS_SHAREEXT_PROFILE_NAME}</string> | |
| </dict> | |
| <key>uploadBitcode</key><false/> | |
| <key>uploadSymbols</key><true/> | |
| </dict> | |
| </plist> | |
| PLIST | |
| - name: Export .ipa | |
| env: | |
| KEY_ID: ${{ secrets.APPSTORE_CONNECT_API_KEY_ID }} | |
| ISSUER_ID: ${{ secrets.APPSTORE_CONNECT_API_ISSUER_ID }} | |
| run: | | |
| xcodebuild -exportArchive \ | |
| -archivePath "$RUNNER_TEMP/Soundscape.xcarchive" \ | |
| -exportPath "$RUNNER_TEMP/export" \ | |
| -exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \ | |
| -authenticationKeyPath "$RUNNER_TEMP/private_keys/AuthKey_$KEY_ID.p8" \ | |
| -authenticationKeyID "$KEY_ID" \ | |
| -authenticationKeyIssuerID "$ISSUER_ID" \ | |
| -allowProvisioningUpdates | |
| - name: Upload .ipa | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: release-ipa | |
| path: ${{ runner.temp }}/export/*.ipa |