feat: powersync hs256 secret auth #603
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
| # about runners https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories | |
| name: CNPlus CI (Android) App Build | |
| env: | |
| # The name of the main module repository | |
| main_project_module: app | |
| # The name of the Play Store | |
| app_name: "Calendar Notifications Plus" | |
| app_file_name_prefix: "calendar_notifications_plus" | |
| # Explicitly set CI flag to true for our Gradle task | |
| CI: true | |
| # Android Emulator Configuration | |
| ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL: 5 | |
| ANDROID_API_LEVEL: 34 | |
| ANDROID_TARGET: google_apis | |
| ANDROID_ARCH: x86_64 | |
| ANDROID_PROFILE: Nexus 5X # Light profile for CI stability (foldable was too resource-heavy) | |
| ANDROID_BUILD_TOOLS_VERSION: "36.0.0" | |
| ANDROID_EMULATOR_MEMORY: 4096 # Increased from 2048 for CI stability | |
| # Define constant for each job to use | |
| GRADLE_OPTS: "-Dorg.gradle.daemon=true -Dorg.gradle.workers.max=4 -Dorg.gradle.parallel=true -Dorg.gradle.caching=true" | |
| # Setup ccache environment variables | |
| CCACHE_DEBUG: 1 | |
| CCACHE_VERBOSE: 1 | |
| CCACHE_MAXSIZE: 5G | |
| CCACHE_BASEDIR: ${{ github.workspace }} | |
| on: | |
| push: | |
| tags: | |
| - '**' | |
| pull_request: | |
| branches: | |
| - "**" # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#patterns-to-match-branches-and-tags | |
| # Allows you to run this workflow manually from the Actions tab | |
| workflow_dispatch: | |
| jobs: | |
| set_build_datetime: | |
| name: Set Build Datetime | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Set Build Datetime | |
| id: set_date | |
| run: | | |
| echo "build_datetime=$(date +'%Y-%m-%d %H_%M_%S')" >> $GITHUB_OUTPUT | |
| outputs: | |
| build_datetime: ${{ steps.set_date.outputs.build_datetime }} | |
| build: | |
| name: Build Android App | |
| needs: set_build_datetime | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| arch: [arm64-v8a, x86_64] | |
| fail-fast: false | |
| env: | |
| ENTRY_FILE: "index.tsx" | |
| GRADLE_ABI: ${{ matrix.arch }} | |
| BUILD_ARCH: ${{ matrix.arch }} | |
| timeout-minutes: 40 # Increased for cache rebuilds after dependency updates | |
| outputs: | |
| repository_name: ${{ steps.set_repo.outputs.repository_name }} | |
| steps: | |
| - uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 1 | |
| - name: ccache | |
| uses: hendrikmuhs/ccache-action@v1.2 | |
| with: | |
| create-symlink: true | |
| # Set repository name as env variable | |
| - name: Set repository name as env variable | |
| id: set_repo | |
| run: | | |
| echo "repository_name=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_ENV | |
| echo "repository_name=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_OUTPUT | |
| - name: Common Setup | |
| id: common-setup | |
| uses: ./.github/actions/common-setup | |
| with: | |
| gradle_max_workers: "4" | |
| node_version: "22.x" | |
| arch: ${{ matrix.arch }} | |
| # Debug Main Build Task Graph | |
| - name: Debug Main Build Task Graph | |
| run: | | |
| cd android && \ | |
| echo "=== Main Build Task Graph ===" && \ | |
| ./gradlew -PBUILD_ARCH="${BUILD_ARCH}" \ | |
| -PreactNativeArchitectures="${BUILD_ARCH}" \ | |
| ":${{ env.main_project_module }}:assemble${{ matrix.arch == 'arm64-v8a' && 'Arm64V8a' || 'X8664' }}Debug" \ | |
| ":${{ env.main_project_module }}:assemble${{ matrix.arch == 'arm64-v8a' && 'Arm64V8a' || 'X8664' }}Release" \ | |
| ":${{ env.main_project_module }}:bundle${{ matrix.arch == 'arm64-v8a' && 'Arm64V8a' || 'X8664' }}Release" \ | |
| ":${{ env.main_project_module }}:assemble${{ matrix.arch == 'arm64-v8a' && 'Arm64V8a' || 'X8664' }}DebugAndroidTest" \ | |
| --dry-run \ | |
| --info \ | |
| --console=verbose \ | |
| --parallel --max-workers=$MAX_WORKERS --build-cache | |
| env: | |
| BUILD_ARCH: ${{ matrix.arch }} | |
| - name: Ccachify the Native Modules | |
| shell: bash | |
| run: | | |
| chmod +x scripts/ccachify_native_modules.sh | |
| ./scripts/ccachify_native_modules.sh | |
| - name: Build Android APKs (Debug and Release) with Gradle | |
| run: | | |
| echo "Building for architecture: $BUILD_ARCH" | |
| cd android && \ | |
| # Pass BUILD_ARCH directly to Gradle process to ensure it's available during configuration | |
| ./gradlew -PBUILD_ARCH="${BUILD_ARCH}" \ | |
| -PreactNativeArchitectures="${BUILD_ARCH}" \ | |
| ":${{ env.main_project_module }}:assemble${{ matrix.arch == 'arm64-v8a' && 'Arm64V8a' || 'X8664' }}Debug" \ | |
| ":${{ env.main_project_module }}:assemble${{ matrix.arch == 'arm64-v8a' && 'Arm64V8a' || 'X8664' }}Release" \ | |
| ":${{ env.main_project_module }}:bundle${{ matrix.arch == 'arm64-v8a' && 'Arm64V8a' || 'X8664' }}Release" \ | |
| ":${{ env.main_project_module }}:assemble${{ matrix.arch == 'arm64-v8a' && 'Arm64V8a' || 'X8664' }}DebugAndroidTest" \ | |
| --parallel --max-workers=$MAX_WORKERS --build-cache | |
| env: | |
| # Explicitly set BUILD_ARCH for Gradle process | |
| BUILD_ARCH: ${{ matrix.arch }} | |
| - name: Setup tmate session | |
| if: ${{ failure() }} | |
| uses: mxschmitt/action-tmate@v3 | |
| with: | |
| limit-access-to-actor: true | |
| # Save the build artifacts to be used in other jobs | |
| - name: List APKs before upload | |
| run: | | |
| echo "Available APKs in build directory:" | |
| find android/${{ env.main_project_module }}/build/outputs/apk/ -type f -name "*.apk" | sort | |
| - name: Upload APK artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-apk-artifacts-${{ matrix.arch }} | |
| path: | | |
| android/${{ env.main_project_module }}/build/outputs/apk/${{ matrix.arch == 'arm64-v8a' && 'arm64v8a' || 'x8664' }}/debug/*-debug.apk | |
| android/${{ env.main_project_module }}/build/outputs/apk/${{ matrix.arch == 'arm64-v8a' && 'arm64v8a' || 'x8664' }}/release/*-release-unsigned.apk | |
| retention-days: 1 | |
| if-no-files-found: error | |
| # Save the test APKs separately | |
| - name: Upload Test APK artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-test-apk-artifacts-${{ matrix.arch }} | |
| path: android/${{ env.main_project_module }}/build/outputs/apk/androidTest/${{ matrix.arch == 'arm64-v8a' && 'arm64v8a' || 'x8664' }}/debug/ | |
| retention-days: 1 | |
| if-no-files-found: error | |
| # Save the build artifacts to be used in other jobs | |
| - name: Upload Artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-coverage-artifacts-${{ matrix.arch }} | |
| path: | | |
| android/${{ env.main_project_module }}/build/intermediates/classes | |
| android/${{ env.main_project_module }}/build/intermediates/javac | |
| android/${{ env.main_project_module }}/build/tmp/kotlin-classes | |
| retention-days: 1 | |
| if-no-files-found: error | |
| - name: Upload Bundle artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-bundle-artifacts-${{ matrix.arch }} | |
| path: android/${{ env.main_project_module }}/build/outputs/bundle/ | |
| retention-days: 1 | |
| if-no-files-found: error | |
| # Update all caches | |
| - name: Update Caches | |
| if: always() | |
| uses: ./.github/actions/cache-update | |
| with: | |
| arch: ${{ matrix.arch }} | |
| sign: | |
| name: Sign Android APKs and Bundles (${{ matrix.arch }}) | |
| needs: [set_build_datetime, build] | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| arch: [arm64-v8a, x86_64] | |
| env: | |
| ENTRY_FILE: "index.tsx" | |
| GRADLE_ABI: ${{ matrix.arch }} | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 1 | |
| # Import build variables from build job | |
| - name: Set build variables | |
| run: | | |
| echo "repository_name=${{ needs.build.outputs.repository_name }}" >> $GITHUB_ENV | |
| # Download build artifacts for current architecture | |
| - name: Download APK artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: android-apk-artifacts-${{ matrix.arch }} | |
| path: artifacts/apk/ | |
| - name: Download Bundle artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: android-bundle-artifacts-${{ matrix.arch }} | |
| path: artifacts/bundle/ | |
| # Prepare directory structure for signing | |
| - name: Prepare directories for signing | |
| run: | | |
| mkdir -p signing/apk/debug | |
| mkdir -p signing/apk/release | |
| mkdir -p signing/bundle/release | |
| # Organize APKs by type | |
| find artifacts/apk/ -path "*/debug/*" -name "*.apk" -exec cp {} signing/apk/debug/ \; | |
| find artifacts/apk/ -path "*/release/*" -name "*.apk" -exec cp {} signing/apk/release/ \; | |
| find artifacts/bundle/ -name "*.aab" -exec cp {} signing/bundle/release/ \; | |
| # Verify contents | |
| echo "Files to sign:" | |
| find signing -type f | sort | |
| # Sign the APKs and AAB | |
| - uses: r0adkll/sign-android-release@v1 | |
| name: Sign debug app APKs | |
| id: sign_debug_app | |
| with: | |
| releaseDirectory: signing/apk/debug/ | |
| signingKeyBase64: ${{ secrets.DEBUG_SIGNING_KEYSTORE }} | |
| alias: ${{ secrets.DEBUG_KEYSTORE_ALIAS }} | |
| keyStorePassword: ${{ secrets.DEBUG_KEYSTORE_PASSWORD }} | |
| keyPassword: ${{ secrets.DEBUG_KEYSTORE_ALIAS_PASS }} | |
| env: | |
| BUILD_TOOLS_VERSION: ${{ env.ANDROID_BUILD_TOOLS_VERSION }} | |
| - uses: r0adkll/sign-android-release@v1 | |
| name: Sign RELEASE app APKs | |
| id: sign_release_app | |
| with: | |
| releaseDirectory: signing/apk/release/ | |
| signingKeyBase64: ${{ secrets.RELEASE_SIGNING_KEYSTORE }} | |
| alias: ${{ secrets.RELEASE_KEYSTORE_ALIAS }} | |
| keyStorePassword: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }} | |
| keyPassword: ${{ secrets.RELEASE_KEYSTORE_ALIAS_PASS }} | |
| env: | |
| BUILD_TOOLS_VERSION: ${{ env.ANDROID_BUILD_TOOLS_VERSION }} | |
| - uses: r0adkll/sign-android-release@v1 | |
| name: Sign RELEASE app AABs | |
| id: sign_release_aab | |
| with: | |
| releaseDirectory: signing/bundle/release/ | |
| signingKeyBase64: ${{ secrets.RELEASE_SIGNING_KEYSTORE }} | |
| alias: ${{ secrets.RELEASE_KEYSTORE_ALIAS }} | |
| keyStorePassword: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }} | |
| keyPassword: ${{ secrets.RELEASE_KEYSTORE_ALIAS_PASS }} | |
| env: | |
| BUILD_TOOLS_VERSION: ${{ env.ANDROID_BUILD_TOOLS_VERSION }} | |
| # Rename APKs | |
| - name: Rename and organize signed artifacts | |
| run: | | |
| mkdir -p renamed_apks | |
| sanitize_branch_name() { | |
| echo "$1" | sed 's/[^a-zA-Z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//' | cut -c1-20 | |
| } | |
| if [[ $GITHUB_REF == refs/tags/* ]]; then | |
| # For tags | |
| VERSION=${GITHUB_REF#refs/tags/v} | |
| SUFFIX="-${VERSION}" | |
| elif [[ $GITHUB_REF == refs/pull/* ]]; then | |
| # For pull requests | |
| PR_NUMBER=$(echo $GITHUB_REF | awk 'BEGIN { FS = "/" } ; { print $3 }') | |
| BRANCH_NAME=$(sanitize_branch_name "$GITHUB_HEAD_REF") | |
| SHORT_SHA=$(echo $GITHUB_SHA | cut -c1-7) | |
| SUFFIX="-pr-${PR_NUMBER}-${BRANCH_NAME}-${SHORT_SHA}" | |
| else | |
| # For pushes to branches other than main | |
| BRANCH_NAME=$(sanitize_branch_name "${GITHUB_REF#refs/heads/}") | |
| SHORT_SHA=$(echo $GITHUB_SHA | cut -c1-7) | |
| SUFFIX="-${BRANCH_NAME}-${SHORT_SHA}" | |
| fi | |
| # Find and rename signed debug APKs | |
| find signing/apk/debug/ -name "*-signed.apk" | while read apk; do | |
| cp "$apk" renamed_apks/${{env.app_file_name_prefix}}${SUFFIX}-${{ matrix.arch }}-debug.apk | |
| done | |
| # Find and rename signed release APKs | |
| find signing/apk/release/ -name "*-signed.apk" | while read apk; do | |
| cp "$apk" renamed_apks/${{env.app_file_name_prefix}}${SUFFIX}-${{ matrix.arch }}-release.apk | |
| done | |
| # Find and rename signed AABs | |
| find signing/bundle/release/ -name "*-signed.aab" | while read aab; do | |
| cp "$aab" renamed_apks/${{env.app_file_name_prefix}}${SUFFIX}-${{ matrix.arch }}.aab | |
| done | |
| # List all renamed files | |
| echo "Renamed files:" | |
| ls -la renamed_apks/ | |
| # Upload signed artifacts | |
| - name: Upload SIGNED APK Debug | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: "signed-${{ needs.set_build_datetime.outputs.build_datetime }}-${{ env.app_name }}-APK-debug-${{ matrix.arch }}" | |
| path: renamed_apks/${{env.app_file_name_prefix}}-*-debug.apk | |
| - name: Upload SIGNED APK RELEASE | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: "signed-${{ needs.set_build_datetime.outputs.build_datetime }}-${{ env.app_name }}-APK-release-${{ matrix.arch }}" | |
| path: renamed_apks/${{env.app_file_name_prefix}}-*-release.apk | |
| - name: Upload SIGNED AAB (App Bundle) Release | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: "signed-${{ needs.set_build_datetime.outputs.build_datetime }}-${{ env.app_name }}-AAB-release-${{ matrix.arch }}" | |
| path: renamed_apks/${{env.app_file_name_prefix}}-*.aab | |
| comment-pr: | |
| name: Comment on PR with build links | |
| if: github.event_name == 'pull_request' | |
| needs: sign | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Comment PR | |
| uses: actions/github-script@v6 | |
| with: | |
| github-token: ${{secrets.GITHUB_TOKEN}} | |
| script: | | |
| const workflowUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: `Build artifacts for PR #${context.issue.number} (commit ${context.sha}) are available: | |
| - [Debug APKs (arm64-v8a, x86_64)](${workflowUrl}#artifacts) | |
| - [Release APKs (arm64-v8a, x86_64)](${workflowUrl}#artifacts) | |
| - [AAB](${workflowUrl}#artifacts) | |
| You can download these artifacts from the "Artifacts" section of the workflow run.` | |
| }); | |
| unit-tests: | |
| name: Run Unit Tests | |
| runs-on: ubuntu-latest | |
| needs: set_build_datetime | |
| timeout-minutes: 25 # Increased for cache rebuilds after dependency updates | |
| strategy: | |
| matrix: | |
| arch: [x86_64] # Only test on x86_64 to save time and resources | |
| fail-fast: false | |
| steps: | |
| - uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 1 | |
| - name: ccache | |
| uses: hendrikmuhs/ccache-action@v1.2 | |
| with: | |
| create-symlink: true | |
| # Execute the common setup | |
| - name: Common Setup | |
| uses: ./.github/actions/common-setup | |
| with: | |
| optional_cache_key: "unit-tests" | |
| gradle_max_workers: "4" | |
| node_version: "22.x" | |
| arch: ${{ matrix.arch }} | |
| # Run Jest tests (TypeScript/JavaScript unit tests) with coverage | |
| - name: Run Jest Tests | |
| run: yarn test --ci --coverage --reporters=default --reporters=jest-junit | |
| env: | |
| JEST_JUNIT_OUTPUT_DIR: ./jest-results | |
| JEST_JUNIT_OUTPUT_NAME: jest-results.xml | |
| # Publish Jest test results | |
| - name: Publish Jest Test Results | |
| uses: dorny/test-reporter@v2 | |
| if: always() | |
| with: | |
| name: 'Jest Tests' | |
| path: 'jest-results/jest-results.xml' | |
| reporter: jest-junit | |
| fail-on-error: true | |
| # Upload Jest coverage report | |
| - name: Upload Jest Coverage Report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: jest-coverage-report-${{ needs.set_build_datetime.outputs.build_datetime }} | |
| path: coverage/ | |
| retention-days: 90 | |
| # Debug unit test task graph | |
| - name: Debug Unit Test Task Graph | |
| run: | | |
| cd android && \ | |
| echo "=== Unit Test Task Graph ===" && \ | |
| ./gradlew -PBUILD_ARCH="${{ matrix.arch }}" \ | |
| -PreactNativeArchitectures="${{ matrix.arch }}" \ | |
| :${{ env.main_project_module }}:test${{ matrix.arch == 'arm64-v8a' && 'Arm64V8a' || 'X8664' }}DebugUnitTest \ | |
| :${{ env.main_project_module }}:create${{ matrix.arch == 'arm64-v8a' && 'Arm64V8a' || 'X8664' }}DebugUnitTestCoverageReport \ | |
| --dry-run \ | |
| --info \ | |
| --console=verbose \ | |
| --continue | |
| env: | |
| BUILD_ARCH: ${{ matrix.arch }} | |
| # Run the unit tests | |
| - name: Run Unit Tests | |
| run: | | |
| cd android && \ | |
| ./gradlew -PBUILD_ARCH="${{ matrix.arch }}" \ | |
| -PreactNativeArchitectures="${{ matrix.arch }}" \ | |
| :${{ env.main_project_module }}:generatePackageList \ | |
| :${{ env.main_project_module }}:test${{ matrix.arch == 'arm64-v8a' && 'Arm64V8a' || 'X8664' }}DebugUnitTest \ | |
| :${{ env.main_project_module }}:create${{ matrix.arch == 'arm64-v8a' && 'Arm64V8a' || 'X8664' }}DebugUnitTestCoverageReport \ | |
| --continue \ | |
| -Pandroid.externalNativeBuild.skip=true \ | |
| -x preBuild -x preDebugBuild -x preReleaseBuild | |
| env: | |
| BUILD_ARCH: ${{ matrix.arch }} | |
| # Generate Jacoco coverage report | |
| # always() is used to ensure the coverage report is generated even if the tests fail | |
| - name: Generate Jacoco Coverage Report | |
| if: always() | |
| run: | | |
| cd android && \ | |
| ./gradlew jacocoAndroidTestReport --parallel --max-workers=$MAX_WORKERS --build-cache --info --stacktrace \ | |
| -Pandroid.externalNativeBuild.skip=true | |
| # Debug: List coverage report files | |
| - name: List Coverage Report Files | |
| if: always() | |
| run: | | |
| echo "=== Unit Test Coverage Reports ===" | |
| find android/app/build/reports -name "*.xml" -o -name "*.csv" 2>/dev/null | head -20 || echo "No XML/CSV files found" | |
| echo "" | |
| echo "=== Coverage directory structure ===" | |
| find android/app/build/reports/coverage -type f 2>/dev/null | head -20 || echo "No coverage directory" | |
| # Publish test results using dorny/test-reporter | |
| - name: Publish Unit Test Results | |
| uses: dorny/test-reporter@v2 | |
| if: always() | |
| with: | |
| name: 'Unit Tests' | |
| path: 'android/${{ env.main_project_module }}/build/test-results/test*DebugUnitTest/**/*.xml' | |
| reporter: java-junit | |
| use-actions-summary: 'true' | |
| fail-on-error: true | |
| # Upload test results and coverage | |
| - name: Upload Unit Test Results and Coverage | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: unit-test-results-and-coverage-${{ needs.set_build_datetime.outputs.build_datetime }} | |
| path: | | |
| android/app/build/reports/ | |
| android/app/build/test-results/ | |
| android/app/build/outputs/logs/ | |
| retention-days: 90 | |
| # NOTE: this single cAT integration test is just to make sure cAT isn't broken | |
| # if we revert back to cAT as the main integration test, we can remove this job | |
| verify-connected-android-test-working-single-integration-test: | |
| name: "Verify Connected Android Test Still Works (Working Single Integration Test)" | |
| needs: [set_build_datetime] | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| arch: [x86_64] # Only test on x86_64 to save time and resources | |
| fail-fast: false | |
| env: | |
| ENTRY_FILE: "index.tsx" | |
| GRADLE_ABI: ${{ matrix.arch }} | |
| BUILD_ARCH: ${{ matrix.arch }} | |
| timeout-minutes: 40 # Increased for cache rebuilds after dependency updates | |
| steps: | |
| - uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 1 | |
| # Execute the common setup with emulator configuration | |
| - name: Common Setup | |
| id: common-setup | |
| uses: ./.github/actions/common-setup | |
| with: | |
| optional_cache_key: "integration-test" | |
| gradle_max_workers: "4" | |
| node_version: "22.x" | |
| arch: ${{ matrix.arch }} | |
| android_api_level: ${{ env.ANDROID_API_LEVEL }} | |
| android_target: ${{ env.ANDROID_TARGET }} | |
| android_profile: ${{ env.ANDROID_PROFILE }} | |
| run_emulator_setup: "true" | |
| - name: Make scripts executable | |
| run: | | |
| chmod +x scripts/wait_for_emulator.sh | |
| chmod +x scripts/run_verify_connected_android_test.sh | |
| - name: Debug directory structure | |
| run: | | |
| echo "Directory structure after download:" | |
| if [ -d "android/${{ env.main_project_module }}/build/" ]; then | |
| find android/${{ env.main_project_module }}/build/ -type d | |
| else | |
| echo "Directory android/${{ env.main_project_module }}/build/ doesn't exist" | |
| fi | |
| - name: Run Android Tests | |
| id: run_tests | |
| uses: reactivecircus/android-emulator-runner@v2 | |
| with: | |
| api-level: ${{ env.ANDROID_API_LEVEL }} | |
| target: ${{ env.ANDROID_TARGET }} | |
| arch: ${{ matrix.arch }} | |
| profile: ${{ env.ANDROID_PROFILE }} | |
| force-avd-creation: false | |
| emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot -memory ${{ env.ANDROID_EMULATOR_MEMORY }} | |
| disable-animations: true | |
| # TODO: make a PR so this thing can properly take a multiline script call | |
| # too easy to make mistakes with line this long | |
| # super annoying to have to make yet another script to forget to chmod +x on accident | |
| script: ./scripts/wait_for_emulator.sh && ./scripts/run_verify_connected_android_test.sh ${{ matrix.arch }} ${{ env.main_project_module }} ${{ env.ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL }} | |
| # Publish integration test results | |
| - name: Publish Integration Test Results | |
| uses: dorny/test-reporter@v2 | |
| if: always() | |
| with: | |
| name: 'Integration Tests' | |
| path: android/app/build/outputs/*.xml,android/app/build/outputs/**/*.xml | |
| reporter: java-junit | |
| fail-on-error: true | |
| - name: Upload Emulator Log | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: verify-connected-android-test-emulator-log | |
| path: verify-connected-android-test-emulator.log | |
| retention-days: 90 | |
| - name: Setup tmate session | |
| if: ${{ failure() }} | |
| uses: mxschmitt/action-tmate@v3 | |
| with: | |
| limit-access-to-actor: true | |
| # connected-android-integration-test: | |
| # name: Test Android App | |
| # needs: [build] | |
| # runs-on: ubuntu-latest | |
| # strategy: | |
| # matrix: | |
| # arch: [x86_64] # Only test on x86_64 to save time and resources | |
| # fail-fast: false | |
| # env: | |
| # ENTRY_FILE: "index.tsx" | |
| # GRADLE_ABI: ${{ matrix.arch }} | |
| # BUILD_ARCH: ${{ matrix.arch }} | |
| # timeout-minutes: 30 | |
| # steps: | |
| # - uses: actions/checkout@v3 | |
| # with: | |
| # fetch-depth: 1 | |
| # # Execute the common setup with emulator configuration | |
| # - name: Common Setup | |
| # id: common-setup | |
| # uses: ./.github/actions/common-setup | |
| # with: | |
| # gradle_max_workers: "4" | |
| # node_version: "22.x" | |
| # arch: ${{ matrix.arch }} | |
| # android_api_level: ${{ env.ANDROID_API_LEVEL }} | |
| # android_target: ${{ env.ANDROID_TARGET }} | |
| # android_profile: ${{ env.ANDROID_PROFILE }} | |
| # run_emulator_setup: "true" | |
| # optional_cache_key: "connected-android-integration-test" | |
| # - name: Make scripts executable | |
| # run: | | |
| # chmod +x scripts/wait_for_emulator.sh | |
| # chmod +x scripts/run_android_tests.sh | |
| # - name: Download APK artifacts | |
| # uses: actions/download-artifact@v4 | |
| # with: | |
| # name: android-apk-artifacts-${{ matrix.arch }} | |
| # path: android/app/build/outputs/apk | |
| # - name: Run Android Tests | |
| # id: run_tests | |
| # uses: reactivecircus/android-emulator-runner@v2 | |
| # with: | |
| # api-level: ${{ env.ANDROID_API_LEVEL }} | |
| # target: ${{ env.ANDROID_TARGET }} | |
| # arch: ${{ matrix.arch }} | |
| # profile: ${{ env.ANDROID_PROFILE }} | |
| # avd-name: 'connected-android-integration-test' | |
| # force-avd-creation: false | |
| # emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot -memory ${{ env.ANDROID_EMULATOR_MEMORY }} | |
| # disable-animations: true | |
| # script: ./scripts/wait_for_emulator.sh && ./scripts/deprecated_run_android_tests.sh ${{ matrix.arch }} ${{ env.main_project_module }} ${{ env.ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL }} | |
| # # Step 3: Generate JaCoCo coverage report | |
| # - name: Generate Jacoco Coverage Report | |
| # if: always() | |
| # run: | | |
| # cd android && \ | |
| # ./gradlew jacocoAndroidTestReport --parallel --max-workers=$MAX_WORKERS --build-cache --info --stacktrace | |
| # # Publish integration test results | |
| # - name: Publish Integration Test Results | |
| # uses: dorny/test-reporter@v2 | |
| # if: always() | |
| # with: | |
| # name: 'Integration Tests' | |
| # path: | | |
| # android/${{ env.main_project_module }}/build/outputs/androidTest-results/connected/**/*.xml | |
| # android/${{ env.main_project_module }}/build/outputs/connected/**/*.xml | |
| # reporter: java-junit | |
| # fail-on-error: true | |
| # - name: Upload Coverage Report | |
| # if: always() | |
| # uses: actions/upload-artifact@v4 | |
| # with: | |
| # name: android-test-coverage-report | |
| # path: | | |
| # android/app/build/reports/ | |
| # android/app/build/reports/jacoco/jacocoAndroidTestReport/ | |
| # android/app/build/outputs/code_coverage/ | |
| # android/app/build/outputs/logs/ | |
| # android/app/build/test-results/ | |
| # retention-days: 90 | |
| # - name: Upload Emulator Log | |
| # if: always() | |
| # uses: actions/upload-artifact@v4 | |
| # with: | |
| # name: emulator-log | |
| # path: emulator.log | |
| # retention-days: 90 | |
| # # Update all caches | |
| # - name: Update Caches | |
| # if: always() | |
| # uses: ./.github/actions/cache-update | |
| # with: | |
| # arch: ${{ matrix.arch }} | |
| # run_emulator_setup: "true" | |
| # - name: Setup tmate session | |
| # if: ${{ failure() }} | |
| # uses: mxschmitt/action-tmate@v3 | |
| # with: | |
| # limit-access-to-actor: true | |
| # TODO: before instrument-test can take over from integration-test (and drop rebuilding the app for no reason) | |
| # 1. we have to figure out how mark tests failing as a failure but still get the coverage files exported ( just occured to me we can use same xml file as below!) | |
| # 2. we need to get the xml file that does the test results for the dorny thing | |
| integration-test: | |
| name: Test Android App (Shard ${{ matrix.shard }}) | |
| needs: [set_build_datetime, build] | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| arch: [x86_64] | |
| shard: [0, 1, 2, 3] # 4 parallel shards (2 UI + 2 non-UI) - 8 was too flaky | |
| fail-fast: false | |
| env: | |
| ENTRY_FILE: "index.tsx" | |
| GRADLE_ABI: ${{ matrix.arch }} | |
| BUILD_ARCH: ${{ matrix.arch }} | |
| NUM_SHARDS: 4 | |
| timeout-minutes: 35 # Increased for cache rebuilds after dependency updates | |
| steps: | |
| - uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 1 | |
| # Execute the common setup with emulator configuration | |
| - name: Common Setup | |
| id: common-setup | |
| uses: ./.github/actions/common-setup | |
| with: | |
| optional_cache_key: "integration-test" | |
| gradle_max_workers: "4" | |
| node_version: "22.x" | |
| arch: ${{ matrix.arch }} | |
| android_api_level: ${{ env.ANDROID_API_LEVEL }} | |
| android_target: ${{ env.ANDROID_TARGET }} | |
| android_profile: ${{ env.ANDROID_PROFILE }} | |
| run_emulator_setup: "true" | |
| - name: Make scripts executable | |
| run: | | |
| chmod +x scripts/wait_for_emulator.sh | |
| chmod +x scripts/matrix_run_android_tests.sh | |
| chmod +x scripts/generate_android_coverage.sh | |
| - name: Download APK artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: android-apk-artifacts-${{ matrix.arch }} | |
| path: android/app/build/outputs/apk | |
| - name: Download Test APK artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: android-test-apk-artifacts-${{ matrix.arch }} | |
| path: android/app/build/outputs/apk/androidTest | |
| - name: Download Coverage artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: android-coverage-artifacts-${{ matrix.arch }} | |
| path: android/${{ env.main_project_module }}/build/ | |
| - name: Debug directory structure | |
| run: | | |
| echo "Directory structure after download:" | |
| if [ -d "android/${{ env.main_project_module }}/build/" ]; then | |
| find android/${{ env.main_project_module }}/build/ -type d | |
| else | |
| echo "Directory android/${{ env.main_project_module }}/build/ doesn't exist" | |
| fi | |
| - name: Run Android Tests (Shard ${{ matrix.shard }}) | |
| id: run_tests | |
| uses: reactivecircus/android-emulator-runner@v2 | |
| with: | |
| api-level: ${{ env.ANDROID_API_LEVEL }} | |
| target: ${{ env.ANDROID_TARGET }} | |
| arch: ${{ matrix.arch }} | |
| profile: ${{ env.ANDROID_PROFILE }} | |
| force-avd-creation: false | |
| emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot -memory ${{ env.ANDROID_EMULATOR_MEMORY }} | |
| disable-animations: true | |
| script: ./scripts/wait_for_emulator.sh && ./scripts/matrix_run_android_tests.sh --shard-index ${{ matrix.shard }} --num-shards ${{ env.NUM_SHARDS }} --arch ${{ matrix.arch }} --module ${{ env.main_project_module }} --timeout 25m && ./scripts/generate_android_coverage.sh ${{ matrix.arch }} ${{ env.main_project_module }} ${{ matrix.shard }} | |
| # Note: JaCoCo report generation moved to merge-integration-coverage job | |
| # Verify test results were generated (catches am instrument crashes) | |
| - name: Verify test results exist (Shard ${{ matrix.shard }}) | |
| id: verify_results | |
| if: always() | |
| run: | | |
| echo "=== Looking for XML test results ===" | |
| XML_COUNT=$(find android/app/build/outputs -name "*.xml" -type f 2>/dev/null | wc -l) | |
| echo "Found $XML_COUNT XML files" | |
| find android/app/build/outputs -name "*.xml" -type f 2>/dev/null || true | |
| if [ "$XML_COUNT" -eq 0 ]; then | |
| echo "::error::No test result XML files found! Tests may have crashed." | |
| echo "This usually means am instrument failed mid-execution." | |
| echo "has_results=false" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| echo "has_results=true" >> $GITHUB_OUTPUT | |
| # Publish integration test results (unique name per shard to avoid conflicts) | |
| # Only runs if verify step found XML files (prevents cryptic dorny error) | |
| - name: Publish Integration Test Results (Shard ${{ matrix.shard }}) | |
| uses: dorny/test-reporter@v2 | |
| if: always() && steps.verify_results.outputs.has_results == 'true' | |
| with: | |
| name: 'Integration Tests (Shard ${{ matrix.shard }})' | |
| # Exclude allure-results which contain non-JUnit XML files (use comma-separated paths) | |
| path: 'android/app/build/outputs/*.xml,android/app/build/outputs/androidTest-results/**/*.xml,android/app/build/outputs/connected/**/*.xml' | |
| reporter: java-junit | |
| fail-on-error: true | |
| - name: Upload Shard Coverage Data | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: integration-coverage-shard-${{ matrix.shard }} | |
| path: android/app/build/outputs/code_coverage/ | |
| retention-days: 1 | |
| - name: Upload Allure Results (Shard ${{ matrix.shard }}) | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: allure-results-shard-${{ matrix.shard }} | |
| # Note: pull_allure_results creates nested allure-results/allure-results structure | |
| path: android/app/build/outputs/allure-results/allure-results/ | |
| retention-days: 1 | |
| if-no-files-found: ignore | |
| - name: Upload Emulator Log (Shard ${{ matrix.shard }}) | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: emulator-log-shard-${{ matrix.shard }} | |
| path: emulator.log | |
| retention-days: 90 | |
| # Update all caches | |
| - name: Update Caches | |
| if: always() | |
| uses: ./.github/actions/cache-update | |
| with: | |
| arch: ${{ matrix.arch }} | |
| run_emulator_setup: "true" | |
| android_api_level: ${{ env.ANDROID_API_LEVEL }} | |
| android_target: ${{ env.ANDROID_TARGET }} | |
| - name: Setup tmate session | |
| if: ${{ failure() }} | |
| uses: mxschmitt/action-tmate@v3 | |
| with: | |
| limit-access-to-actor: true | |
| merge-integration-coverage: | |
| name: Merge Integration Test Coverage | |
| needs: [set_build_datetime, build, integration-test] | |
| runs-on: ubuntu-latest | |
| if: always() && needs.integration-test.result != 'cancelled' | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 1 | |
| # Fail early if any integration test shard failed (prevents release with broken tests) | |
| - name: Check integration test results | |
| run: | | |
| if [ "${{ needs.integration-test.result }}" != "success" ]; then | |
| echo "::error::Integration tests failed or were skipped. Result: ${{ needs.integration-test.result }}" | |
| exit 1 | |
| fi | |
| echo "All integration test shards passed" | |
| - name: Download all shard coverage artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: integration-coverage-shard-* | |
| path: android/app/build/outputs/code_coverage/ | |
| merge-multiple: true | |
| - name: Download all shard Allure results | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: allure-results-shard-* | |
| path: android/app/build/outputs/allure-results/ | |
| merge-multiple: true | |
| continue-on-error: true | |
| - name: Debug Allure directory structure | |
| if: always() | |
| run: | | |
| echo "=== Allure results structure ===" | |
| find android/app/build/outputs/allure-results/ -type f 2>/dev/null | head -20 || echo "No allure files found" | |
| - name: Download Coverage artifacts (classes for JaCoCo) | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: android-coverage-artifacts-x86_64 | |
| path: android/app/build/ | |
| - name: Debug merged coverage files | |
| run: | | |
| echo "=== Merged coverage files ===" | |
| find android/app/build/outputs/code_coverage/ -name "*.ec" -ls 2>/dev/null || echo "No .ec files found" | |
| - name: Common Setup | |
| uses: ./.github/actions/common-setup | |
| with: | |
| optional_cache_key: "merge-coverage" | |
| gradle_max_workers: "4" | |
| node_version: "22.x" | |
| arch: x86_64 | |
| - name: Generate Merged JaCoCo Report | |
| run: | | |
| cd android && ./gradlew jacocoAndroidTestReport --parallel --max-workers=$MAX_WORKERS --build-cache --info --stacktrace | |
| - name: Upload Merged Coverage Report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-test-coverage-report-${{ needs.set_build_datetime.outputs.build_datetime }} | |
| path: | | |
| android/app/build/reports/ | |
| android/app/build/reports/jacoco/jacocoAndroidTestReport/ | |
| android/app/build/outputs/code_coverage/ | |
| android/app/build/outputs/logs/ | |
| android/app/build/test-results/ | |
| retention-days: 90 | |
| - name: Generate Allure HTML Report | |
| uses: simple-elf/allure-report-action@v1.13 | |
| if: always() | |
| with: | |
| allure_results: android/app/build/outputs/allure-results | |
| allure_report: android/app/build/outputs/allure-report | |
| keep_reports: 1 | |
| - name: Upload Allure HTML Report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: allure-html-report-${{ needs.set_build_datetime.outputs.build_datetime }} | |
| path: android/app/build/outputs/allure-report/ | |
| retention-days: 90 | |
| if-no-files-found: ignore | |
| coverage-report: | |
| name: Process Code Coverage | |
| needs: [set_build_datetime, unit-tests, build, merge-integration-coverage] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 1 | |
| # Download coverage results from both unit and integration tests | |
| - name: Download unit test coverage | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: unit-test-results-and-coverage-${{ needs.set_build_datetime.outputs.build_datetime }} | |
| path: coverage-data/unit-tests/android/app/build/ | |
| continue-on-error: true | |
| - name: Download integration test coverage (merged from all shards) | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: android-test-coverage-report-${{ needs.set_build_datetime.outputs.build_datetime }} | |
| path: coverage-data/integration-tests/android/app/build/ | |
| continue-on-error: true | |
| # Process and generate coverage report from Jacoco XML files | |
| - name: Generate JaCoCo Coverage Report | |
| id: jacoco | |
| uses: madrapps/jacoco-report@v1.6.1 | |
| with: | |
| paths: | | |
| ${{ github.workspace }}/coverage-data/unit-tests/android/app/build/reports/coverage/**/*.xml | |
| ${{ github.workspace }}/coverage-data/integration-tests/android/app/build/reports/jacoco/**/*.xml | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| min-coverage-overall: 30 | |
| min-coverage-changed-files: 60 | |
| title: 'Code Coverage Report' | |
| update-comment: true | |
| # Upload combined coverage reports (HTML, XML, CSV) | |
| - name: Upload Combined Coverage Reports | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: full-code-coverage-reports-${{ needs.set_build_datetime.outputs.build_datetime }} | |
| path: | | |
| coverage-data/unit-tests/android/app/build/reports/jacoco/**/html | |
| coverage-data/unit-tests/android/app/build/reports/jacoco/**/xml | |
| coverage-data/unit-tests/android/app/build/reports/jacoco/**/*.csv | |
| coverage-data/unit-tests/android/app/build/reports/coverage/**/*.xml | |
| coverage-data/unit-tests/android/app/build/reports/coverage/**/*.csv | |
| coverage-data/integration-tests/android/app/build/reports/jacoco/**/html | |
| coverage-data/integration-tests/android/app/build/reports/jacoco/**/xml | |
| coverage-data/integration-tests/android/app/build/reports/jacoco/**/*.csv | |
| retention-days: 90 | |
| # Create a PR comment with coverage report link if this is a PR | |
| - name: PR Comment with Coverage Report | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v6 | |
| with: | |
| github-token: ${{secrets.GITHUB_TOKEN}} | |
| script: | | |
| const workflowUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const coverageSummary = ` | |
| | Coverage Type | Coverage | | |
| | ---- | -------- | | |
| | Overall | ${{ steps.jacoco.outputs.coverage-overall }} | | |
| | Changed Files | ${{ steps.jacoco.outputs.coverage-changed-files }} | | |
| [View detailed coverage report](${workflowUrl}#artifacts) | |
| `; | |
| github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: `## 📊 Code Coverage Summary\n${coverageSummary}` | |
| }); | |
| create-release: | |
| name: Create GitHub Release | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| needs: [sign, merge-integration-coverage, unit-tests] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: signed-* | |
| path: artifacts | |
| - name: Generate Changelog | |
| run: | | |
| echo "# Release ${GITHUB_REF#refs/tags/}" > ${{ github.workspace }}-CHANGELOG.txt | |
| echo "" >> ${{ github.workspace }}-CHANGELOG.txt | |
| echo "## What's Changed" >> ${{ github.workspace }}-CHANGELOG.txt | |
| git log $(git describe --tags --abbrev=0 HEAD^)..HEAD --pretty=format:"* %s" >> ${{ github.workspace }}-CHANGELOG.txt | |
| - name: Organize release files | |
| run: | | |
| mkdir -p release_files | |
| find artifacts/ -name "*.apk" -o -name "*.aab" | xargs -I{} cp {} release_files/ | |
| - name: Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| body_path: ${{ github.workspace }}-CHANGELOG.txt | |
| files: release_files/* |