From 02e01bb3318dd254322ef4fc15e0777df1c8b8f8 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:34:46 -0600 Subject: [PATCH] ci: optimize, secure, and modernize CI pipeline (#4711) --- .github/workflows/merge-queue.yml | 1 - .github/workflows/pull-request.yml | 74 +++++++++++++--------------- .github/workflows/release.yml | 8 +-- .github/workflows/reusable-check.yml | 55 ++++++++------------- 4 files changed, 58 insertions(+), 80 deletions(-) diff --git a/.github/workflows/merge-queue.yml b/.github/workflows/merge-queue.yml index 27e532a26..06ecfa2c2 100644 --- a/.github/workflows/merge-queue.yml +++ b/.github/workflows/merge-queue.yml @@ -14,7 +14,6 @@ jobs: uses: ./.github/workflows/reusable-check.yml with: api_levels: '[26, 35]' # Comprehensive testing for Merge Queue - flavors: '["google", "fdroid"]' upload_artifacts: false secrets: inherit diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 4e215d2dd..8ba00d417 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,72 +1,66 @@ +name: Pull Request CI + on: pull_request: - branches: - - main - workflow_dispatch: + branches: [ main, develop ] + paths-ignore: + - '**.md' + - 'docs/**' + - '.gitignore' concurrency: - group: build-pr-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: + # 1. CHANGE DETECTION: Prevents unnecessary builds check-changes: if: github.repository == 'meshtastic/Meshtastic-Android' && !( github.head_ref == 'scheduled-updates' || github.head_ref == 'l10n_main' ) runs-on: ubuntu-latest outputs: - code_changed: ${{ steps.filter.outputs.code }} + android: ${{ steps.filter.outputs.android }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - uses: dorny/paths-filter@v3 id: filter with: filters: | - code: - - '**/*.kt' - - '**/*.java' - - '**/*.xml' - - '**/*.kts' - - '**/*.properties' - - 'gradle/**' - - 'gradlew' - - 'gradlew.bat' - - '**/src/**' - - '.github/workflows/**' + android: + - 'app/**' + - 'core/**' + - 'feature/**' + - 'build-logic/**' + - 'build.gradle.kts' + - 'gradle.properties' - android-check: + # 2. VALIDATION & BUILD: Delegate to reusable-check.yml + # We disable instrumented tests for PRs to keep feedback fast (< 10 mins). + validate-and-build: needs: check-changes - if: needs.check-changes.outputs.code_changed == 'true' + if: needs.check-changes.outputs.android == 'true' uses: ./.github/workflows/reusable-check.yml with: - api_levels: '[35]' # Only test latest API on PRs for speed - flavors: '["google","fdroid"]' + run_lint: true + run_unit_tests: true + run_instrumented_tests: false + api_levels: '[35]' + upload_artifacts: true secrets: inherit - skip-notice: - needs: check-changes - if: needs.check-changes.outputs.code_changed != 'true' - runs-on: ubuntu-latest - steps: - - name: Skip CI for non-code changes - run: echo "Skipping CI - no code changes detected (docs/config only)" - + # 3. WORKFLOW STATUS: Ensures required checks are satisfied check-workflow-status: name: Check Workflow Status runs-on: ubuntu-latest - needs: - - check-changes - - android-check + needs: [check-changes, validate-and-build] if: always() steps: - name: Check Workflow Status run: | - if [[ "${{ needs.check-changes.outputs.code_changed }}" != "true" ]]; then - echo "No code changes - CI jobs skipped as expected" - exit 0 - fi - - if [[ "${{ needs.android-check.result }}" == "failure" || "${{ needs.android-check.result }}" == "cancelled" ]]; then + # If changes were detected but build failed, fail the status check + if [[ "${{ needs.check-changes.outputs.android }}" == "true" && ("${{ needs.validate-and-build.result }}" == "failure" || "${{ needs.validate-and-build.result }}" == "cancelled") ]]; then echo "::error::Android Check failed" exit 1 fi - - echo "All jobs passed successfully" + + # If no changes were detected, this still succeeds to satisfy required status check + echo "Workflow status satisfied." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d67a5f665..3f69ded64 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,6 +44,10 @@ on: required: false GRADLE_CACHE_PASSWORD: required: false + INTERNAL_BUILDS_HOST: + required: false + INTERNAL_BUILDS_HOST_PAT: + required: false concurrency: group: ${{ github.workflow }}-${{ inputs.tag_name }} @@ -62,7 +66,6 @@ jobs: run_lint: true run_unit_tests: false run_instrumented_tests: false - flavors: '["google"]' upload_artifacts: false secrets: inherit @@ -72,7 +75,6 @@ jobs: APP_VERSION_NAME: ${{ steps.get_version_name.outputs.APP_VERSION_NAME }} APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }} env: - GRADLE_OPTS: "-Dorg.gradle.daemon=false" GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }} GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }} @@ -120,7 +122,6 @@ jobs: needs: [prepare-build-info, run-lint] environment: Release env: - GRADLE_OPTS: "-Dorg.gradle.daemon=false" GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }} GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }} @@ -212,7 +213,6 @@ jobs: needs: [prepare-build-info, run-lint] environment: Release env: - GRADLE_OPTS: "-Dorg.gradle.daemon=false" GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }} GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }} diff --git a/.github/workflows/reusable-check.yml b/.github/workflows/reusable-check.yml index e480374fd..e55d58b2f 100644 --- a/.github/workflows/reusable-check.yml +++ b/.github/workflows/reusable-check.yml @@ -12,15 +12,9 @@ on: run_instrumented_tests: type: boolean default: true - flavors: - type: string - default: '["google"]' api_levels: type: string default: '[35]' - num_shards: - type: number - default: 1 upload_artifacts: type: boolean default: true @@ -45,14 +39,14 @@ on: jobs: check: runs-on: ubuntu-latest + permissions: + contents: read timeout-minutes: 60 strategy: fail-fast: true matrix: api_level: ${{ fromJson(inputs.api_levels) }} - flavor: ${{ fromJson(inputs.flavors) }} env: - GRADLE_OPTS: "-Dorg.gradle.daemon=false" DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }} DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }} MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} @@ -67,16 +61,21 @@ jobs: fetch-depth: 0 submodules: 'recursive' + - name: Validate Gradle Wrapper + uses: gradle/actions/wrapper-validation@v4 + - name: Set up JDK 17 uses: actions/setup-java@v5 with: java-version: '17' - distribution: 'jetbrains' + distribution: 'zulu' - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 with: + dependency-graph: generate-and-submit cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + cache-cleanup: true build-scan-publish: true build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service' build-scan-terms-of-use-agree: 'yes' @@ -89,40 +88,26 @@ jobs: - name: Determine Tasks id: tasks run: | - FLAVOR="${{ matrix.flavor }}" - FLAVOR_CAP=$(echo $FLAVOR | awk '{print toupper(substr($0,1,1))substr($0,2)}') IS_FIRST_API=$(echo '${{ inputs.api_levels }}' | jq -r '.[0] == ${{ matrix.api_level }}') - IS_FIRST_FLAVOR=$(echo '${{ inputs.flavors }}' | jq -r '.[0] == "${{ matrix.flavor }}"') # Matrix-specific tasks - TASKS="assemble${FLAVOR_CAP}Debug " - [ "${{ inputs.run_lint }}" = "true" ] && TASKS="$TASKS lint${FLAVOR_CAP}Debug " - [ "${{ inputs.run_unit_tests }}" = "true" ] && TASKS="$TASKS test${FLAVOR_CAP}DebugUnitTest " + TASKS="assembleDebug " + [ "${{ inputs.run_lint }}" = "true" ] && TASKS="$TASKS lintDebug " # Instrumented Test Tasks if [ "${{ inputs.run_instrumented_tests }}" = "true" ]; then - if [ "$FLAVOR" = "google" ]; then - TASKS="$TASKS connectedGoogleDebugAndroidTest " - elif [ "$FLAVOR" = "fdroid" ]; then - TASKS="$TASKS connectedFdroidDebugAndroidTest " - fi - fi - - # Run coverage report for this flavor - if [ "${{ inputs.run_unit_tests }}" = "true" ]; then - TASKS="$TASKS koverXmlReport${FLAVOR_CAP}Debug " + TASKS="$TASKS connectedDebugAndroidTest " fi echo "tasks=$TASKS" >> $GITHUB_OUTPUT echo "is_first_api=$IS_FIRST_API" >> $GITHUB_OUTPUT - echo "is_first_flavor=$IS_FIRST_FLAVOR" >> $GITHUB_OUTPUT - name: Code Style & Static Analysis - if: steps.tasks.outputs.is_first_api == 'true' && steps.tasks.outputs.is_first_flavor == 'true' + if: steps.tasks.outputs.is_first_api == 'true' run: ./gradlew spotlessCheck detekt -Pci=true - name: Shared Unit Tests - if: steps.tasks.outputs.is_first_api == 'true' && steps.tasks.outputs.is_first_flavor == 'true' && inputs.run_unit_tests == true + if: steps.tasks.outputs.is_first_api == 'true' && inputs.run_unit_tests == true run: ./gradlew testDebugUnitTest koverXmlReportDebug -Pci=true --continue - name: Enable KVM group perms @@ -143,13 +128,13 @@ jobs: force-avd-creation: false emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true - script: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true --continue --scan + script: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true --parallel --configuration-cache --continue --scan - name: Run Flavor Check (no Emulator) if: inputs.run_instrumented_tests == false env: VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }} - run: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true --continue --scan + run: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true --parallel --configuration-cache --continue --scan - name: Upload coverage results to Codecov if: ${{ !cancelled() }} @@ -169,10 +154,10 @@ jobs: - name: Upload debug artifact if: ${{ steps.tasks.outputs.is_first_api == 'true' && inputs.upload_artifacts }} - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v4 with: - name: ${{ matrix.flavor }}Debug - path: app/build/outputs/apk/${{ matrix.flavor }}/debug/app-${{ matrix.flavor }}-debug.apk + name: app-debug-apks + path: app/build/outputs/apk/*/debug/*.apk retention-days: 14 - name: Report App Size @@ -185,9 +170,9 @@ jobs: - name: Upload reports if: ${{ always() && inputs.upload_artifacts }} - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v4 with: - name: reports-${{ matrix.flavor }}-api-${{ matrix.api_level }} + name: reports-api-${{ matrix.api_level }} path: | **/build/reports **/build/test-results