Skip to content

feat: powersync hs256 secret auth #603

feat: powersync hs256 secret auth

feat: powersync hs256 secret auth #603

Workflow file for this run

# 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/*