Files
Meshtastic-Android/.github/workflows/reusable-check.yml

493 lines
19 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Reusable Android Check
on:
workflow_call:
inputs:
run_lint:
type: boolean
default: true
run_unit_tests:
type: boolean
default: true
run_coverage:
type: boolean
default: true
run_desktop_builds:
type: boolean
default: true
upload_artifacts:
type: boolean
default: true
secrets:
GRADLE_ENCRYPTION_KEY:
required: false
CODECOV_TOKEN:
required: false
DATADOG_APPLICATION_ID:
required: false
DATADOG_CLIENT_TOKEN:
required: false
GOOGLE_MAPS_API_KEY:
required: false
GRADLE_CACHE_URL:
required: false
GRADLE_CACHE_USERNAME:
required: false
GRADLE_CACHE_PASSWORD:
required: false
env:
DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }}
DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
GITHUB_TOKEN: ${{ github.token }}
GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
# Fallback VERSION_CODE for the lint-check job itself (which computes the real
# value from git). Downstream jobs override this with the git-derived value.
VERSION_CODE: ${{ github.run_number }}
jobs:
# ── Lint & Static Analysis ──────────────────────────────────────────
lint-check:
runs-on: ubuntu-24.04
permissions:
contents: read
timeout-minutes: 30
outputs:
cache_read_only: ${{ steps.cache_config.outputs.cache_read_only }}
version_code: ${{ steps.version_code.outputs.version_code }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
filter: 'blob:none'
submodules: true
- name: Determine cache read-only setting
id: cache_config
shell: bash
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]] || [[ "${{ github.event_name }}" == "merge_group" ]] || [[ "${{ github.ref }}" == gh-readonly-queue/* ]]; then
echo "cache_read_only=false" >> "$GITHUB_OUTPUT"
else
echo "cache_read_only=true" >> "$GITHUB_OUTPUT"
fi
- name: Calculate version code from git commit count
id: version_code
shell: bash
run: |
COMMIT_COUNT=$(git rev-list --count HEAD)
OFFSET=$(grep '^VERSION_CODE_OFFSET=' config.properties | cut -d'=' -f2 || echo 0)
VERSION_CODE=$((COMMIT_COUNT + OFFSET))
echo "version_code=$VERSION_CODE" >> "$GITHUB_OUTPUT"
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache_read_only: ${{ steps.cache_config.outputs.cache_read_only }}
- name: Lint, Analysis & KMP Smoke Compile
if: inputs.run_lint == true
run: ./gradlew spotlessCheck detekt app:lintFdroidDebug app:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug core:api:lintDebug kmpSmokeCompile -Pci=true --continue --scan
- name: KMP Smoke Compile (lint skipped)
if: inputs.run_lint == false
run: ./gradlew kmpSmokeCompile -Pci=true --continue --scan
# ── Reproducible Build Verification ─────────────────────────────────
rb-check:
runs-on: ubuntu-24.04
permissions:
contents: read
timeout-minutes: 30
if: inputs.run_lint == true
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
submodules: true
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache_read_only: 'true'
- name: Verify Reproducible Build (fdroid)
env:
VERSION_CODE: ${{ github.run_number }}
run: |
# Comprehensive RB verification for F-Droid/IzzyOnDroid.
# Based on: https://izzyondroid.org/docs/reproducibleBuilds/DebugFailedRBs/
# Catches regressions that have historically broken reproducibility:
# 1. aboutlibraries.json non-determinism (network fetching)
# 2. Datadog buildId leaking into fdroid APK
# 3. Google/Firebase/GMS/MLKit classes in fdroid APK
# 4. DEPENDENCY_INFO_BLOCK in signing block
# 5. Native library stripping (NDK version mismatch)
# 6. aboutlibraries "generated" timestamp in res/M7.json
# 7. baseline.prof determinism (flaky builds)
# See: https://github.com/meshtastic/Meshtastic-Android/issues/3231
echo "── Step 1: Verify aboutlibraries.json determinism ──"
rm -f app/src/main/resources/aboutlibraries.json
./gradlew :app:exportLibraryDefinitions -Pci=true --no-configuration-cache
cp app/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run1.json
rm -f app/src/main/resources/aboutlibraries.json
./gradlew :app:exportLibraryDefinitions -Pci=true --no-configuration-cache --rerun-tasks
cp app/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run2.json
if ! diff -q /tmp/aboutlibraries-run1.json /tmp/aboutlibraries-run2.json; then
echo "::error::aboutlibraries.json is NOT deterministic across runs!"
diff /tmp/aboutlibraries-run1.json /tmp/aboutlibraries-run2.json | head -20
exit 1
fi
echo "✅ aboutlibraries.json is deterministic"
echo "── Step 2: Build fdroid release APK ──"
./gradlew :app:assembleFdroidRelease -Pci=true -Pmeshtastic.disableAbiSplits=true --no-configuration-cache
APK=$(find app/build/outputs/apk/fdroid/release -name "*.apk" | head -1)
if [ -z "$APK" ]; then
echo "::error::No fdroid release APK found"
exit 1
fi
echo "Checking APK: $APK"
echo "── Step 3: Check for datadog.buildId ──"
if unzip -l "$APK" | grep -q "datadog.buildId"; then
echo "::error::fdroid APK contains assets/datadog.buildId — breaks RB!"
exit 1
fi
echo "✅ No datadog.buildId in fdroid APK"
echo "── Step 4: Check for proprietary libraries (dex scan) ──"
TMPDIR=$(mktemp -d)
unzip -q "$APK" -d "$TMPDIR"
OFFENDERS=""
for pattern in "com/google/firebase" "com/google/android/gms" "com/crashlytics" "com/google/mlkit" "com/google/android/datatransport" "androidx/privacysandbox/ads"; do
for dex in "$TMPDIR"/classes*.dex; do
if [ -f "$dex" ] && strings "$dex" | grep -q "L${pattern}/"; then
OFFENDERS="${OFFENDERS}\n - $pattern"
break
fi
done
done
if [ -n "$OFFENDERS" ]; then
echo -e "::error::fdroid APK contains proprietary libraries:${OFFENDERS}"
rm -rf "$TMPDIR"
exit 1
fi
echo "✅ No proprietary libraries in fdroid APK"
echo "── Step 5: Check for DEPENDENCY_INFO_BLOCK (signing block blob) ──"
# Parse the APK Signing Block structure to find the dependency info pair.
# Naive byte scans produce false positives in large APKs.
if python3 << 'PYEOF'
import struct, sys
with open("$APK", "rb") as f:
data = f.read()
magic = b"APK Sig Block 42"
idx = data.rfind(magic)
if idx < 0:
sys.exit(0)
block_size = struct.unpack_from("<Q", data, idx - 8)[0]
block_start = idx + 16 - 8 - block_size
pos = int(block_start)
end = idx - 8
while pos + 12 <= end:
pair_size = struct.unpack_from("<Q", data, pos)[0]
pair_id = struct.unpack_from("<I", data, pos + 8)[0]
if pair_id == 0x504b4453:
print(f"DEPENDENCY_INFO_BLOCK found (id=0x{pair_id:08x})")
sys.exit(1)
pos += 8 + int(pair_size)
sys.exit(0)
PYEOF
then
echo "::error::fdroid APK contains DEPENDENCY_INFO_BLOCK — remove with dependenciesInfo { includeInApk = false }"
rm -rf "$TMPDIR"
exit 1
fi
echo "✅ No DEPENDENCY_INFO_BLOCK in signing block"
echo "── Step 6: Check native libraries have debug symbols (not stripped) ──"
STRIPPED_LIBS=""
for so in $(find "$TMPDIR" -name "*.so" 2>/dev/null); do
# If .symtab section is missing, the library was stripped
if ! readelf -S "$so" 2>/dev/null | grep -q "\.symtab"; then
# Libraries without symtab are stripped — this is only a problem
# if keepDebugSymbols is not working as expected
LIB_NAME=$(basename "$so")
STRIPPED_LIBS="${STRIPPED_LIBS} ${LIB_NAME}"
fi
done
# Note: Some third-party .so files arrive pre-stripped, which is OK.
# We only warn here; a hard failure would be too aggressive.
if [ -n "$STRIPPED_LIBS" ]; then
echo "::warning::Some native libraries appear stripped (may cause NDK-version-dependent RB failures):${STRIPPED_LIBS}"
else
echo "✅ Native libraries retain debug symbols"
fi
echo "── Step 7: Check aboutlibraries 'generated' timestamp not in APK ──"
# The M7.json (or aboutlibraries.json in Java resources) should NOT contain
# a "generated" field, which introduces a build-time timestamp.
ABOUT_JSON=""
if [ -f "$TMPDIR/aboutlibraries.json" ]; then
ABOUT_JSON="$TMPDIR/aboutlibraries.json"
else
# May be in res/ as M7.json or similar
ABOUT_JSON=$(find "$TMPDIR/res" -name "*.json" -exec grep -l "aboutLibraries" {} \; 2>/dev/null | head -1)
fi
if [ -n "$ABOUT_JSON" ] && grep -q '"generated"' "$ABOUT_JSON"; then
echo "::error::aboutlibraries contains 'generated' timestamp field — add excludeFields = listOf(\"generated\") to build config"
rm -rf "$TMPDIR"
exit 1
fi
echo "✅ No 'generated' timestamp in aboutlibraries data"
echo "── Step 8: Verify build from clean tree (version-control-info) ──"
if [ -f "$TMPDIR/META-INF/version-control-info.textproto" ]; then
if grep -q "modified: true" "$TMPDIR/META-INF/version-control-info.textproto"; then
echo "::warning::APK built from dirty tree (version-control-info shows modified:true). Release builds must use a clean tree."
else
echo "✅ Built from clean tree"
fi
else
echo " No version-control-info.textproto (AGP may not embed it for debug-signed builds)"
fi
rm -rf "$TMPDIR"
echo ""
echo "🎉 All RB checks passed"
# ── Sharded Unit Tests ──────────────────────────────────────────────
# Tests are split into 3 shards that run in parallel:
# shard-core: core:* KMP module tests (allTests)
# shard-feature: feature:* KMP module tests (allTests)
# shard-app: Pure-Android/JVM tests (app, desktop, core:barcode, etc.)
test-shards:
runs-on: ubuntu-24.04
permissions:
contents: read
timeout-minutes: 45
needs: lint-check
if: inputs.run_unit_tests == true
env:
VERSION_CODE: ${{ needs.lint-check.outputs.version_code }}
strategy:
fail-fast: false
matrix:
shard:
- name: shard-core
tasks: >-
:core:ble:allTests
:core:common:allTests
:core:data:allTests
:core:database:allTests
:core:domain:allTests
:core:model:allTests
:core:navigation:allTests
:core:network:allTests
:core:prefs:allTests
:core:repository:allTests
:core:service:allTests
:core:takserver:allTests
:core:testing:allTests
:core:ui:allTests
kover: >-
:core:ble:koverXmlReport
:core:common:koverXmlReport
:core:data:koverXmlReport
:core:database:koverXmlReport
:core:domain:koverXmlReport
:core:model:koverXmlReport
:core:navigation:koverXmlReport
:core:network:koverXmlReport
:core:prefs:koverXmlReport
:core:repository:koverXmlReport
:core:service:koverXmlReport
:core:takserver:koverXmlReport
:core:testing:koverXmlReport
:core:ui:koverXmlReport
- name: shard-feature
tasks: >-
:feature:connections:allTests
:feature:firmware:allTests
:feature:intro:allTests
:feature:map:allTests
:feature:messaging:allTests
:feature:node:allTests
:feature:settings:allTests
kover: >-
:feature:connections:koverXmlReport
:feature:firmware:koverXmlReport
:feature:intro:koverXmlReport
:feature:map:koverXmlReport
:feature:messaging:koverXmlReport
:feature:node:koverXmlReport
:feature:settings:koverXmlReport
- name: shard-app
tasks: >-
:app:testFdroidDebugUnitTest
:app:testGoogleDebugUnitTest
:desktop:test
:core:barcode:testFdroidDebugUnitTest
:core:barcode:testGoogleDebugUnitTest
kover: >-
:app:koverXmlReportFdroidDebug
:app:koverXmlReportGoogleDebug
:core:barcode:koverXmlReportFdroidDebug
:core:barcode:koverXmlReportGoogleDebug
:desktop:koverXmlReport
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
submodules: true
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }}
- name: Run Tests & Coverage (${{ matrix.shard.name }})
run: |
kover_tasks=""
if [[ "${{ inputs.run_coverage }}" == "true" ]]; then
kover_tasks="${{ matrix.shard.kover }}"
fi
./gradlew ${{ matrix.shard.tasks }} $kover_tasks -Pci=true --continue --scan
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: meshtastic/Meshtastic-Android
flags: ${{ matrix.shard.name }}
fail_ci_if_error: false
report_type: test_results
files: "**/build/test-results/**/*.xml"
- name: Upload coverage to Codecov
if: ${{ !cancelled() && inputs.run_coverage }}
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: meshtastic/Meshtastic-Android
flags: ${{ matrix.shard.name }}
fail_ci_if_error: false
files: "**/build/reports/kover/report*.xml"
- name: Upload shard reports
if: ${{ always() && inputs.upload_artifacts }}
uses: actions/upload-artifact@v7
with:
name: reports-${{ matrix.shard.name }}
path: |
**/build/reports
**/build/test-results
retention-days: 7
# ── Android Build ────────────────────────────────────────────────────
android-check:
runs-on: ubuntu-24.04
permissions:
contents: read
timeout-minutes: 60
needs: lint-check
env:
VERSION_CODE: ${{ needs.lint-check.outputs.version_code }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
submodules: true
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }}
- name: Build Android APKs
run: ./gradlew app:assembleFdroidDebug app:assembleGoogleDebug -Pci=true --parallel --configuration-cache --continue --scan
- name: Upload debug artifact
if: ${{ inputs.upload_artifacts }}
uses: actions/upload-artifact@v7
with:
name: app-debug-apks
path: app/build/outputs/apk/*/debug/*.apk
retention-days: 7
- name: Report App Size
if: always()
run: |
echo "### App Size Report" >> $GITHUB_STEP_SUMMARY
echo "| Artifact | Size |" >> $GITHUB_STEP_SUMMARY
echo "| --- | --- |" >> $GITHUB_STEP_SUMMARY
find app/build/outputs/apk -name "*.apk" -exec du -h {} + | awk '{print "| " $2 " | " $1 " |"}' >> $GITHUB_STEP_SUMMARY
# ── Desktop Build ───────────────────────────────────────────────────
build-desktop:
name: Build Desktop Debug (${{ matrix.os }})
if: inputs.run_desktop_builds == true
runs-on: ${{ matrix.os }}
permissions:
contents: read
timeout-minutes: 60
needs: lint-check
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm]
env:
VERSION_CODE: ${{ needs.lint-check.outputs.version_code }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
submodules: true
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }}
- name: Build Desktop
run: ./gradlew :desktop:createDistributable -Pci=true --scan
- name: Upload Desktop artifact
if: ${{ inputs.upload_artifacts }}
uses: actions/upload-artifact@v7
with:
name: desktop-app-${{ runner.os }}-${{ runner.arch }}
path: desktop/build/compose/binaries/main/app/
retention-days: 7