name: CI

# One gate per pull request. Branch pushes no longer trigger CI on their own,
# so a change is built once on its PR (covering feature -> release/* and
# release/* -> main) instead of once per push and again on the merge to main.
# The merge itself is handled by release.yaml, which only does heavy work when
# the merge actually cuts a release.
on:
  pull_request:

# Cancel superseded runs for the same PR.
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # Single job named `ci` so the required "CI" status check is always reported,
  # even for docs-only PRs: those just skip the Android build and the job still
  # succeeds (fast green check) instead of being filtered out and leaving the
  # required check pending forever.
  ci:
    runs-on: docker
    env:
      ANDROID_HOME: /opt/android-sdk
      ANDROID_SDK_ROOT: /opt/android-sdk
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          # Full history so the base..HEAD diff below has a merge-base.
          fetch-depth: 0
          submodules: recursive

      # Cheap, always-on guard: the release build must stay reproducible for the
      # official F-Droid repo (no AGP VCS-info embedding). Runs regardless of
      # change scope so a regression can't slip through on a "docs-only" PR.
      - name: Reproducible-release invariant
        run: bash scripts/check_reproducible_release.sh

      # Decide whether anything that affects the app build changed. Docs,
      # F-Droid metadata and the licence don't, so those PRs skip the SDK +
      # Gradle work below but still report a green `ci`.
      - name: Classify change scope
        id: scope
        run: |
          set -e
          BASE="${{ github.base_ref }}"
          # Full (not --depth=1) base fetch so the merge-base is present even when
          # the PR branch forked several commits back; a shallow tip has no merge
          # base with a divergent branch and `git diff base...HEAD` aborts.
          git fetch --no-tags origin "$BASE"
          MB=$(git merge-base "origin/$BASE" HEAD 2>/dev/null || true)
          if [ -z "$MB" ]; then
            # No common ancestor available — don't risk skipping the build.
            echo "No merge base with origin/$BASE — running the full build to be safe."
            echo "code=true" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          CHANGED=$(git diff --name-only "$MB" HEAD)
          echo "Changed files:"; echo "$CHANGED"
          if echo "$CHANGED" | grep -vE '(\.md$|^docs/|^fdroid-metadata/|^fastlane/|^LICENSE$)' | grep -q .; then
            echo "code=true" >> "$GITHUB_OUTPUT"
          else
            echo "code=false" >> "$GITHUB_OUTPUT"
            echo "Docs/metadata-only change — skipping the Android build."
          fi

      - name: Setup Java
        if: steps.scope.outputs.code == 'true'
        uses: actions/setup-java@v4
        with:
          distribution: 'zulu'
          java-version: '17'

      - name: Setup Android SDK
        if: steps.scope.outputs.code == 'true'
        uses: android-actions/setup-android@v3
        with:
          # Default ("tools platform-tools") drags in the Android Emulator
          # (~300 MB) which the build never uses.
          packages: ''

      - name: Setup Android SDK cache
        if: steps.scope.outputs.code == 'true'
        uses: actions/cache@v4
        with:
          path: /opt/android-sdk
          key: ${{ runner.os }}-android-sdk-37-36.0.0

      - name: Install Android SDK packages
        if: steps.scope.outputs.code == 'true'
        run: |
          yes | sdkmanager --licenses >/dev/null || true
          sdkmanager \
            "platform-tools" \
            "platforms;android-37.0" \
            "build-tools;36.0.0"

      - name: Setup Gradle cache
        if: steps.scope.outputs.code == 'true'
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle/libs.versions.toml') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Grant execute permission for gradlew
        if: steps.scope.outputs.code == 'true'
        run: chmod +x ./gradlew

      # No --no-daemon: the daemon lives only as long as this job container
      # and lets the following steps skip JVM startup + reconfiguration.
      - name: Lint (debug variant only)
        if: steps.scope.outputs.code == 'true'
        run: ./gradlew lintDebug

      - name: Unit tests
        if: steps.scope.outputs.code == 'true'
        run: ./gradlew testDebugUnitTest

      - name: Assemble debug APK
        if: steps.scope.outputs.code == 'true'
        run: ./gradlew assembleDebug

      - name: Trivy filesystem scan
        if: steps.scope.outputs.code == 'true'
        run: |
          set -e
          SUDO=""
          if command -v sudo >/dev/null 2>&1; then
            SUDO="sudo"
          fi
          if command -v apt-get >/dev/null 2>&1; then
            $SUDO apt-get update
            $SUDO apt-get install -y wget apt-transport-https gnupg lsb-release
            wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | $SUDO tee /usr/share/keyrings/trivy.gpg > /dev/null
            echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | $SUDO tee /etc/apt/sources.list.d/trivy.list
            $SUDO apt-get update
            $SUDO apt-get install -y trivy
          fi
          trivy filesystem --severity HIGH,CRITICAL --exit-code 0 .
        continue-on-error: true
