diff --git a/.github/workflows/merge-queue.yml b/.github/workflows/merge-queue.yml index f06e7e1af..96e3350f6 100644 --- a/.github/workflows/merge-queue.yml +++ b/.github/workflows/merge-queue.yml @@ -9,7 +9,14 @@ concurrency: cancel-in-progress: true jobs: - build_and_detekt: + lint: + if: github.repository == 'meshtastic/Meshtastic-Android' + uses: ./.github/workflows/reusable-lint.yml + secrets: + GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + + build: + needs: lint if: github.repository == 'meshtastic/Meshtastic-Android' uses: ./.github/workflows/reusable-android-build.yml with: @@ -21,10 +28,12 @@ jobs: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} androidTest: + needs: lint if: github.repository == 'meshtastic/Meshtastic-Android' uses: ./.github/workflows/reusable-android-test.yml with: api_levels: '[26, 35]' # Run on both API 26 and 35 for merge queue + test_flavors: 'both' # Run both flavors for merge queue (comprehensive) upload_artifacts: false secrets: GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} @@ -34,10 +43,9 @@ jobs: name: Check Workflow Status # Matches another in pull-request, and is required for merge to main. runs-on: ubuntu-latest needs: - [ - build_and_detekt, - androidTest - ] + - lint + - build + - androidTest if: always() steps: - name: Check Workflow Status @@ -48,5 +56,6 @@ jobs: exit 1 fi } - exit_on_result "build_and_detekt" "${{ needs.build_and_detekt.result }}" + exit_on_result "lint" "${{ needs.lint.result }}" + exit_on_result "build" "${{ needs.build.result }}" exit_on_result "androidTest" "${{ needs.androidTest.result }}" diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 3fe68a807..fc3c83579 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -11,40 +11,95 @@ concurrency: cancel-in-progress: true jobs: - - build_and_detekt: + 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 }} + steps: + - uses: actions/checkout@v6 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + code: + - '**/*.kt' + - '**/*.java' + - '**/*.xml' + - '**/*.kts' + - '**/*.properties' + - 'gradle/**' + - 'gradlew' + - 'gradlew.bat' + - '**/src/**' + - '.github/workflows/**' + + lint: + needs: check-changes + if: needs.check-changes.outputs.code_changed == 'true' + uses: ./.github/workflows/reusable-lint.yml + secrets: + GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + + build: + needs: lint + if: ${{ !cancelled() && !failure() }} uses: ./.github/workflows/reusable-android-build.yml secrets: inherit androidTest: - # Assuming androidTest should also only run for the main repository - if: github.repository == 'meshtastic/Meshtastic-Android' && !( github.head_ref == 'scheduled-updates' || github.head_ref == 'l10n_main' ) + needs: lint + if: ${{ !cancelled() && !failure() }} uses: ./.github/workflows/reusable-android-test.yml with: api_levels: '[35]' # Run only on API 35 for PRs - # upload_artifacts defaults to true, so no need to explicitly set + test_flavors: 'google' # Run only Google flavor for PRs (faster) secrets: GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - + + # This job handles the case when no code changes are detected (docs-only PRs) + 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)" + check-workflow-status: name: Check Workflow Status runs-on: ubuntu-latest needs: - [ - build_and_detekt, - androidTest - ] + - check-changes + - lint + - build + - androidTest if: always() steps: - name: Check Workflow Status run: | - exit_on_result() { - if [[ "$2" == "failure" || "$2" == "cancelled" ]]; then - echo "Job '$1' failed or was cancelled." + # If no code changed, all jobs are expected to be skipped - that's success + if [[ "${{ needs.check-changes.outputs.code_changed }}" != "true" ]]; then + echo "No code changes - CI jobs skipped as expected" + exit 0 + fi + + # Code changed - check that all jobs succeeded + check_result() { + local job_name=$1 + local result=$2 + if [[ "$result" == "failure" ]]; then + echo "::error::Job '$job_name' failed" + exit 1 + elif [[ "$result" == "cancelled" ]]; then + echo "::error::Job '$job_name' was cancelled" exit 1 fi } - exit_on_result "build_and_detekt" "${{ needs.build_and_detekt.result }}" - exit_on_result "androidTest" "${{ needs.androidTest.result }}" + + check_result "lint" "${{ needs.lint.result }}" + check_result "build" "${{ needs.build.result }}" + check_result "androidTest" "${{ needs.androidTest.result }}" + + echo "All jobs passed successfully" diff --git a/.github/workflows/reusable-android-build.yml b/.github/workflows/reusable-android-build.yml index 058366124..dbb3cc978 100644 --- a/.github/workflows/reusable-android-build.yml +++ b/.github/workflows/reusable-android-build.yml @@ -1,4 +1,4 @@ -name: Reusable Android Build and Detekt +name: Reusable Android Build and Test on: workflow_call: @@ -19,7 +19,7 @@ on: default: true jobs: - build_and_detekt: + build: runs-on: ubuntu-latest permissions: id-token: write @@ -68,8 +68,8 @@ jobs: run: | echo "datadogApplicationId=$DATADOG_APPLICATION_ID" >> ./secrets.properties echo "datadogClientToken=$DATADOG_CLIENT_TOKEN" >> ./secrets.properties - - name: Run Spotless, Detekt, Build, Lint, and Local Tests - run: ./gradlew spotlessCheck detekt assembleDebug runAllDebugTests koverXmlReport --configuration-cache --scan + - name: Build and Run Unit Tests + run: ./gradlew assembleDebug testGoogleDebugUnitTest testFdroidDebugUnitTest koverXmlReport --continue --scan env: VERSION_CODE: ${{ env.VERSION_CODE }} diff --git a/.github/workflows/reusable-android-test.yml b/.github/workflows/reusable-android-test.yml index b9a2cec69..c906ab946 100644 --- a/.github/workflows/reusable-android-test.yml +++ b/.github/workflows/reusable-android-test.yml @@ -13,6 +13,11 @@ on: required: false type: string default: '[26, 35]' # Default to running both if not specified by caller + test_flavors: + description: 'Which flavors to test: "google", "fdroid", or "both"' + required: false + type: string + default: 'both' secrets: GRADLE_ENCRYPTION_KEY: required: false @@ -31,7 +36,7 @@ jobs: uses: actions/checkout@v6 with: submodules: 'recursive' - fetch-depth: 0 + fetch-depth: 1 # Shallow clone - no version code calculation needed - name: Set up JDK 21 uses: actions/setup-java@v5 @@ -76,6 +81,17 @@ jobs: disable-animations: false script: echo "Generated AVD snapshot for caching." + - name: Determine test tasks + id: test-tasks + run: | + if [ "${{ inputs.test_flavors }}" = "google" ]; then + echo "tasks=connectedGoogleDebugAndroidTest" >> $GITHUB_OUTPUT + elif [ "${{ inputs.test_flavors }}" = "fdroid" ]; then + echo "tasks=connectedFdroidDebugAndroidTest" >> $GITHUB_OUTPUT + else + echo "tasks=connectedFdroidDebugAndroidTest connectedGoogleDebugAndroidTest" >> $GITHUB_OUTPUT + fi + - name: Run Android Instrumented Tests and Generate Coverage uses: reactivecircus/android-emulator-runner@v2 env: @@ -86,7 +102,7 @@ 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 runAllConnectedDebugTests koverXmlReport --configuration-cache --scan && ( killall -INT crashpad_handler || true ) + script: ./gradlew ${{ steps.test-tasks.outputs.tasks }} koverXmlReport --continue --scan && ( killall -INT crashpad_handler || true ) - name: Upload coverage reports to Codecov if: ${{ !cancelled() }} diff --git a/.github/workflows/reusable-lint.yml b/.github/workflows/reusable-lint.yml new file mode 100644 index 000000000..541e0eea2 --- /dev/null +++ b/.github/workflows/reusable-lint.yml @@ -0,0 +1,37 @@ +name: Reusable Lint and Format Check + +on: + workflow_call: + secrets: + GRADLE_ENCRYPTION_KEY: + required: false + +jobs: + lint: + runs-on: ubuntu-latest # Lint is fast, doesn't need large runner + timeout-minutes: 10 + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'jetbrains' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + build-scan-publish: true + build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service' + build-scan-terms-of-use-agree: 'yes' + add-job-summary: always + + - name: Run Spotless and Detekt + run: ./gradlew spotlessCheck detekt --scan diff --git a/build.gradle.kts b/build.gradle.kts index 5d8008d04..f63de7392 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,36 +47,4 @@ plugins { dependencies { dokkaPlugin(libs.dokka.android.documentation.plugin) -} - -val debugTests = listOf( - "testDebugUnitTest", - "testFdroidDebugUnitTest", - "testGoogleDebugUnitTest" -) - -tasks.register("runAllDebugTests") { - group = "verification" - description = "Runs all unit tests for debug variants and flavors" - dependsOn(subprojects.map { subproject -> - subproject.tasks.matching { task -> - task.name in debugTests - } - }) -} - -val connectedTests = listOf( - "connectedDebugAndroidTest", - "connectedFdroidDebugAndroidTest", - "connectedGoogleDebugAndroidTest" -) - -tasks.register("runAllConnectedDebugTests") { - group = "verification" - description = "Runs all connected tests for debug variants and flavors" - dependsOn(subprojects.map { subproject -> - subproject.tasks.matching { task -> - task.name in connectedTests - } - }) } \ No newline at end of file