Skip to content

Merge pull request #1128 from torlando-tech/fix/migration-export-oom-v2 #2990

Merge pull request #1128 from torlando-tech/fix/migration-export-oom-v2

Merge pull request #1128 from torlando-tech/fix/migration-export-oom-v2 #2990

Workflow file for this run

name: CI
on:
push:
branches: [ main, 'v[0-9].[0-9]+.x', 'release/**' ]
pull_request:
branches: [ main, 'v[0-9].[0-9]+.x', 'release/**' ]
permissions:
contents: read
pull-requests: write
issues: write
jobs:
validate-wrapper:
name: Validate Gradle Wrapper
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@v6.3.0
lint:
name: Code Quality (ktlint + detekt + CPD + Android Lint)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6.3.0
with:
cache-read-only: false
- name: Set up Python 3.11
# Android `lint` covers :rns-backend-py + the pythonBackend variants, which
# trigger Chaquopy's installPythonRequirements — and Chaquopy 17 only accepts
# buildPython 3.11 (same reason the kotlin-tests + release jobs set this up).
uses: actions/setup-python@v7
with:
python-version: '3.11'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run all lint checks (ktlint, detekt, CPD, Android Lint)
# Android `lint` enforces only the checks in `checkOnly` (round 1: NewApi);
# see the Android Lint block in the root build.gradle.kts. No baseline.
# `lint` covers each module's DEFAULT variant; the two extra variant tasks
# cover the flavor source the default misses (app/src/noSentry,
# rns-host/src/pythonBackend) so NewApi is enforced across all variants.
run: >-
./gradlew ktlintCheck detekt cpdCheck
lint :app:lintNoSentryKotlinBackendDebug :rns-host:lintPythonBackendDebug
--continue
- name: Check for forbidden test suppressions
run: |
echo "Checking for forbidden @Suppress annotations in test files..."
# Count files with NoRelaxedMocks suppression
RELAXED_COUNT=$(grep -r "Suppress.*NoRelaxedMocks" app/src/test reticulum/src/test --include="*.kt" -l 2>/dev/null | wc -l || echo "0")
# Count files with NoVerifyOnlyTests suppression
VERIFY_COUNT=$(grep -r "Suppress.*NoVerifyOnlyTests" app/src/test reticulum/src/test --include="*.kt" -l 2>/dev/null | wc -l || echo "0")
echo "Files with @Suppress(\"NoRelaxedMocks\"): $RELAXED_COUNT"
echo "Files with @Suppress(\"NoVerifyOnlyTests\"): $VERIFY_COUNT"
# These rules exist to prevent useless tests that don't test production code.
# Suppressing them defeats the purpose. Fix the tests instead.
#
# BASELINE: Ratchet down over time. Don't increase these numbers.
# Current state reflects Android framework mocks (Context, BluetoothManager, etc.)
# that are impractical to stub explicitly. Fix when possible.
RELAXED_BASELINE=37
VERIFY_BASELINE=3
FAIL=false
if [ "$RELAXED_COUNT" -gt "$RELAXED_BASELINE" ]; then
echo "::error::NoRelaxedMocks suppressions increased from $RELAXED_BASELINE to $RELAXED_COUNT"
echo "Do not add @Suppress(\"NoRelaxedMocks\"). Use explicit stubs instead of relaxed mocks."
FAIL=true
fi
if [ "$VERIFY_COUNT" -gt "$VERIFY_BASELINE" ]; then
echo "::error::NoVerifyOnlyTests suppressions increased from $VERIFY_BASELINE to $VERIFY_COUNT"
echo "Do not add @Suppress(\"NoVerifyOnlyTests\"). Add assertions to your tests."
FAIL=true
fi
if [ "$FAIL" = true ]; then
exit 1
fi
echo "✓ No new forbidden suppressions found"
- name: Upload lint reports
if: always()
uses: actions/upload-artifact@v7
with:
name: lint-reports
path: |
**/build/reports/ktlint/
**/build/reports/detekt/
**/build/reports/lint-results-*.*
build/reports/cpd/
retention-days: 7
threading-audit:
name: Threading Architecture Audit
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
- name: Make audit script executable
run: chmod +x audit-dispatchers.sh
- name: Run dispatcher audit
id: audit
run: |
echo "Running threading architecture audit..."
./audit-dispatchers.sh
exit_code=$?
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
exit $exit_code
- name: Upload audit report
if: always()
uses: actions/upload-artifact@v7
with:
name: threading-audit-report
path: dispatcher-audit-report.txt
retention-days: 30
- name: Comment audit results on PR
if: failure() && github.event_name == 'pull_request'
uses: actions/github-script@v9
with:
script: |
const fs = require('fs');
let report = 'Dispatcher audit report not found';
try {
report = fs.readFileSync('dispatcher-audit-report.txt', 'utf8');
} catch (e) {
console.log('Could not read audit report');
}
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## ❌ Threading Architecture Audit Failed\n\n<details><summary>View Audit Report</summary>\n\n\`\`\`\n${report}\n\`\`\`\n\n</details>\n\nPlease fix the dispatcher violations before merging.`
});
kotlin-tests:
name: Kotlin Tests (Shard ${{ matrix.shard }}/4)
needs: [lint, validate-wrapper]
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3]
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Set up JDK 25
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 25
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6.3.0
with:
cache-read-only: false
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Clean AIDL generated code
run: |
# Clean AIDL generated code to avoid stale cache issues
# This MUST happen before Gradle cache restore to prevent cached stale AIDL files
rm -rf app/build/generated/aidl_source_output_dir
rm -rf */build/generated/aidl_source_output_dir
- name: Clean JaCoCo execution data
run: |
# Clean stale JaCoCo execution data to avoid "exceeds max age" errors in Codecov
rm -rf build/jacoco
rm -rf */build/jacoco
rm -rf build/reports/jacoco
rm -rf */build/reports/jacoco
- name: Cache Robolectric Android SDKs
uses: actions/cache@v6
with:
path: ~/.m2/repository/org/robolectric
key: robolectric-4.16-${{ runner.os }}
restore-keys: |
robolectric-4.16-
robolectric-
- name: Resolve dependencies (with retry)
# Pre-resolve all dependencies before running tests. This isolates transient
# Maven repository errors (e.g. 502 from dl.google.com) from actual test failures,
# and avoids retrying the entire test suite for a network blip.
run: |
for attempt in 1 2 3; do
echo "Dependency resolution attempt $attempt/3..."
if ./gradlew :app:dependencies --configuration noSentryKotlinBackendDebugUnitTestRuntimeClasspath -q 2>/dev/null; then
echo "Dependencies resolved successfully."
break
fi
if [ "$attempt" -eq 3 ]; then
echo "::error::Failed to resolve dependencies after 3 attempts"
exit 1
fi
echo "::warning::Dependency resolution failed (attempt $attempt), retrying in 15s..."
sleep 15
done
- name: Run app unit tests (shard ${{ matrix.shard }})
# Build cache enabled for faster incremental builds
# AIDL and JaCoCo caching issues are handled by clean steps above
# NOTE: For app module, only run noSentry variant (sentry/noSentry share the same test code,
# differing only in SENTRY_DSN buildConfigField which tests don't exercise).
run: ./gradlew :app:testNoSentryKotlinBackendDebugUnitTest -PtestShard=${{ matrix.shard }} -PtestShardTotal=4 --stacktrace --max-workers=4
- name: Upload unit test results
if: always()
uses: actions/upload-artifact@v7
with:
name: kotlin-test-results-shard-${{ matrix.shard }}
path: |
**/build/test-results/test*/*.xml
**/build/test-results/test*/*.html
retention-days: 7
- name: Upload unit test reports
if: failure()
uses: actions/upload-artifact@v7
with:
name: kotlin-test-reports-shard-${{ matrix.shard }}
path: |
**/build/reports/tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./**/build/reports/jacoco/**/*.xml
flags: unittests
name: codecov-shard-${{ matrix.shard }}
fail_ci_if_error: false
verbose: true
- name: Upload Kotlin coverage reports
if: always()
uses: actions/upload-artifact@v7
with:
name: kotlin-coverage-reports-shard-${{ matrix.shard }}
path: |
**/build/reports/jacoco/
retention-days: 7
module-tests:
name: Module Tests (reticulum + data + micron)
needs: [lint, validate-wrapper]
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Set up JDK 25
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 25
- name: Set up Python 3.11
# Required by Chaquopy's `installDebugPythonRequirements` task on
# :rns-backend-py — Chaquopy 17 only accepts buildPython 3.11.
uses: actions/setup-python@v7
with:
python-version: '3.11'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6.3.0
with:
cache-read-only: false
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Clean AIDL generated code
run: |
# Clean AIDL generated code to avoid stale cache issues
# This MUST happen before Gradle cache restore to prevent cached stale AIDL files
rm -rf app/build/generated/aidl_source_output_dir
rm -rf */build/generated/aidl_source_output_dir
- name: Clean JaCoCo execution data
run: |
# Clean stale JaCoCo execution data to avoid "exceeds max age" errors in Codecov
rm -rf build/jacoco
rm -rf */build/jacoco
rm -rf build/reports/jacoco
rm -rf */build/reports/jacoco
- name: Cache Robolectric Android SDKs
uses: actions/cache@v6
with:
path: ~/.m2/repository/org/robolectric
key: robolectric-4.16-${{ runner.os }}
restore-keys: |
robolectric-4.16-
robolectric-
- name: Run RNS, stats, data, and micron module tests
run: ./gradlew :rns-api:testDebugUnitTest :rns-ipc:testDebugUnitTest :rns-host:testKotlinBackendDebugUnitTest :rns-host:testPythonBackendDebugUnitTest :rns-backend-kt:testDebugUnitTest :rns-backend-py:testDebugUnitTest :rns-stats:testDebugUnitTest :data:testDebugUnitTest :micron:test --stacktrace --max-workers=4
- name: Upload unit test results
if: always()
uses: actions/upload-artifact@v7
with:
name: module-test-results
path: |
**/build/test-results/test*/*.xml
**/build/test-results/test*/*.html
retention-days: 7
- name: Upload unit test reports
if: failure()
uses: actions/upload-artifact@v7
with:
name: module-test-reports
path: |
**/build/reports/tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./**/build/reports/jacoco/**/*.xml
flags: unittests
name: codecov-modules
fail_ci_if_error: false
verbose: true
proguard-verification:
name: Verify Python-Kotlin Bridge (R8)
# Builds the minified pythonBackend RELEASE APK and asserts, against R8's own
# mapping.txt, that the @Keep'd Python<->Kotlin bridge classes/methods survive
# un-renamed. Chaquopy resolves them by name, so obfuscation would silently
# break the python flavor at runtime. No emulator needed — checking the rename
# table is the canonical, deterministic way to verify keep rules.
needs: [lint, validate-wrapper]
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Set up JDK 25
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 25
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6.3.0
with:
cache-read-only: false
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: '3.11'
- name: Install Python build dependencies
run: pip install build
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build minified pythonBackend release (generates R8 mapping)
# noSentry + no SENTRY_AUTH_TOKEN keeps the Sentry mapping-upload path out;
# unsigned is fine — minification (and mapping.txt) happens regardless.
timeout-minutes: 40
run: ./gradlew :app:assembleNoSentryPythonBackendRelease --stacktrace --max-workers=2
- name: Assert bridge classes/methods survived R8
run: |
python scripts/assert_bridges_kept.py \
app/build/outputs/mapping/noSentryPythonBackendRelease/mapping.txt
- name: Upload R8 mapping file
# Only on failure — the mapping (~130 MB for the python flavor) is only
# useful for debugging a gate failure, and uploading it every run would
# dominate the Actions storage quota.
if: failure()
uses: actions/upload-artifact@v7
with:
name: r8-mapping-pythonBackend
path: app/build/outputs/mapping/noSentryPythonBackendRelease/mapping.txt
retention-days: 14
- name: Cleanup emulator
if: always()
run: |
adb devices | grep emulator | cut -f1 | xargs -I {} adb -s {} emu kill || true
pkill -9 qemu-system || true
adb kill-server || true
instrumented-tests:
name: Instrumented Tests (Kotlin + Identity Recovery)
# Run in parallel with other jobs
needs: [lint, validate-wrapper]
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Set up JDK 25
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 25
- name: Set up Python 3.11
# Required by Chaquopy when building the Python-backend recovery tests.
uses: actions/setup-python@v7
with:
python-version: '3.11'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6.3.0
with:
cache-read-only: false
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Clean AIDL generated code
run: |
# Clean AIDL generated code to avoid stale cache issues
rm -rf app/build/generated/aidl_source_output_dir
rm -rf */build/generated/aidl_source_output_dir
- 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
- name: Pre-build test APKs
run: |
echo "Building APKs before starting emulator..."
./gradlew :app:assembleNoSentryKotlinBackendDebugAndroidTest --stacktrace --max-workers=2
./gradlew :app:assembleNoSentryKotlinBackendDebug --stacktrace --max-workers=2
./gradlew :app:assembleNoSentryPythonBackendDebugAndroidTest :app:assembleNoSentryPythonBackendDebug --stacktrace --max-workers=2
./gradlew :rns-backend-py:assembleDebugAndroidTest :rns-backend-py:assembleDebug --stacktrace --max-workers=2
echo "APKs built successfully"
- name: AVD cache
uses: actions/cache@v6
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-34-x86_64-v6
- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2.38.0
with:
api-level: 34
arch: x86_64
target: google_apis
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 2048
disable-animations: true
emulator-boot-timeout: 600
script: |
echo "Waiting for emulator to be ready..."
adb wait-for-device
adb shell getprop sys.boot_completed | grep -q 1 || sleep 10
echo "Emulator ready, generating snapshot for caching."
- name: Run instrumented tests
uses: reactivecircus/android-emulator-runner@v2.38.0
with:
api-level: 34
arch: x86_64
target: google_apis
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 2048
disable-animations: true
emulator-boot-timeout: 600
# The emulator action executes each literal-script line as a separate shell.
# Fold this into one command so status is retained across all test groups.
script: >-
echo "Running emulator health check...";
adb shell getprop ro.build.version.sdk;
adb shell pm list packages | head -5;
echo "Emulator is responsive, starting tests...";
status=0;
./gradlew :app:connectedNoSentryKotlinBackendDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=network.columba.app.smoke.SmokeTest,network.columba.app.integration.MessageDataFlowTest,network.columba.app.integration.ConversationCreationFlowTest,network.columba.app.ui.components.MicronTrueColorRenderingTest --stacktrace || status=1;
./gradlew :app:connectedNoSentryPythonBackendDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=network.columba.app.integration.IdentityRecoveryIpcInstrumentedTest,network.columba.app.integration.IdentityRecoveryPersistenceInstrumentedTest --stacktrace || status=1;
./gradlew :rns-backend-py:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=network.columba.app.rns.backend.py.PythonRnsCoreIdentityRecoveryInstrumentedTest --stacktrace || status=1;
exit "$status"
- name: Upload instrumented test results
if: always()
uses: actions/upload-artifact@v7
with:
name: instrumented-test-results
path: |
**/build/outputs/androidTest-results/**/*.xml
**/build/outputs/androidTest-results/**/*.html
retention-days: 7
- name: Upload instrumented test reports
if: failure()
uses: actions/upload-artifact@v7
with:
name: instrumented-test-reports
path: |
**/build/reports/androidTests/
retention-days: 7
- name: Capture logcat on failure
if: failure()
timeout-minutes: 1
continue-on-error: true
run: |
mkdir -p artifacts
timeout 30 adb logcat -d > artifacts/logcat.log || true
- name: Upload logcat output
if: failure()
uses: actions/upload-artifact@v7
with:
name: logcat
path: artifacts/logcat.log
retention-days: 7
if-no-files-found: ignore
- name: Cleanup emulator
if: always()
run: |
adb devices | grep emulator | cut -f1 | xargs -I {} adb -s {} emu kill || true
pkill -9 qemu-system || true
adb kill-server || true
transfer-progress-e2e:
name: Real LXMF Resource Progress E2E
needs: [lint, validate-wrapper]
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Set up JDK 25
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 25
- name: Set up Python 3.11
uses: actions/setup-python@v7
with:
python-version: '3.11'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6.3.0
with:
cache-read-only: false
- name: Install E2E host dependencies
run: |
python -m pip install --disable-pip-version-check -r tests/transfer_progress_e2e/requirements.txt
python -m pytest -q tests/transfer_progress_e2e/test_ui_driver.py
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build Python-backend E2E APK
run: ./gradlew :app:assembleNoSentryPythonBackendDebug --stacktrace --max-workers=2
- 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
- name: Run real two-peer Resource progress E2E
uses: reactivecircus/android-emulator-runner@v2.38.0
with:
api-level: 34
arch: x86_64
target: google_apis
force-avd-creation: true
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 3072
disable-animations: true
emulator-boot-timeout: 600
script: >-
COLUMBA_EMULATOR_SERIAL=emulator-5554
COLUMBA_E2E_APK=app/build/outputs/apk/noSentryPythonBackend/debug/app-noSentry-pythonBackend-x86_64-debug.apk
COLUMBA_E2E_ARTIFACT_DIR=artifacts/transfer-progress-e2e
python -m pytest -v tests/transfer_progress_e2e/test_transfer_progress_e2e.py
--junit-xml=artifacts/transfer-progress-e2e/results.xml;
- name: Upload Resource progress E2E evidence
if: always()
uses: actions/upload-artifact@v7
with:
name: transfer-progress-e2e-evidence
path: artifacts/transfer-progress-e2e/
retention-days: 14
if-no-files-found: error
- name: Cleanup emulator
if: always()
run: |
adb devices | grep emulator | cut -f1 | xargs -I {} adb -s {} emu kill || true
pkill -9 qemu-system || true
adb kill-server || true