name: MobileGL APK

on:
  push:
    branches:
      - dev
      - Feat/Backend-Direct-GLES
      - Feat/Backend-Direct-Vulkan
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      actions: write
      contents: read
    env:
      CCACHE_BASEDIR: ${{ github.workspace }}
      CCACHE_COMPRESS: "true"
      CCACHE_DIR: ${{ github.workspace }}/.ccache
      CCACHE_MAXSIZE: 4G
      CCACHE_NOHASHDIR: "true"
      MOBILEGL_CMAKE_COMPILER_LAUNCHER: ccache

    steps:
      - name: Checkout repo
        uses: actions/checkout@v6
        with:
          submodules: recursive

      - name: Set artifact metadata
        run: |
          echo "date_today=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"

      - name: Set up JDK
        uses: actions/setup-java@v5
        with:
          distribution: zulu
          java-version: '17'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v6
        with:
          gradle-version: 8.10.2

      - name: Restore ccache
        uses: actions/cache/restore@v5
        with:
          path: .ccache
          key: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
          restore-keys: |
            ${{ runner.os }}-apk-${{ github.job }}-ccache-

      - name: Install ccache
        run: |
          sudo apt-get update
          sudo apt-get install -y ccache
          ccache --version

      - name: Setup Android SDK
        uses: android-actions/setup-android@v4
        with:
          accept-android-sdk-licenses: false

      - name: Accept Android SDK licenses
        run: yes | sdkmanager --licenses >/dev/null

      - name: Install Android NDK
        run: |
          sdkmanager "ndk;27.3.13750724"
          echo "ndk.dir=$ANDROID_HOME/ndk/27.3.13750724" >> android-plugin/local.properties

      - name: Update glslang external sources
        working-directory: 3rdparty/glslang
        run: python update_glslang_sources.py

      - name: Build plugin APK
        run: gradle --no-daemon -p android-plugin :app:assemblePluginRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
        env:
          SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
          SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
          SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}

      - name: Download ANGLE x86_64 libraries
        run: |
          angle_dir="android-plugin/app/src/trace/jniLibs/x86_64"
          rm -rf "${angle_dir}"
          mkdir -p "${angle_dir}"

          package_angle_variant() {
            variant="$1"
            commit="$2"
            egl_sha="$3"
            gles_sha="$4"
            source_dir="${RUNNER_TEMP}/mobilegl-angle-${variant}"
            base="https://raw.githubusercontent.com/FCL-Team/FoldCraftLauncher/${commit}/FCLauncher/src/main/jniLibs/x86_64"
            mkdir -p "${source_dir}"
            curl -L --fail --retry 3 -o "${source_dir}/libEGL_angle.so" "${base}/libEGL_angle.so"
            curl -L --fail --retry 3 -o "${source_dir}/libGLESv2_angle.so" "${base}/libGLESv2_angle.so"
            echo "${egl_sha}  ${source_dir}/libEGL_angle.so" | sha256sum -c -
            echo "${gles_sha}  ${source_dir}/libGLESv2_angle.so" | sha256sum -c -
            for library in libEGL_angle libGLESv2_angle; do
              filename="${library}_${variant}.so"
              cp "${source_dir}/${library}.so" "${angle_dir}/${filename}"
            done
          }

          package_angle_variant \
            ec889e6ea831 \
            f2a3d510dffd8f6540a52e1a7d0c5787d151075b \
            c41828768d089899fa058ec0bee711a91be88347f29bdb935223da6be1149c40 \
            e4f820d99f94365c66df868c7740fef142fe5c0cd7c941790a9e30638857ca4d
          package_angle_variant \
            90a62123d794 \
            bdcc96ac11c79001018ae4375eb73cb54a9f682f \
            d0f4298ccc770cc801fc52e21733521646161e8a4adb3bd0052d9a1b57ee0ca8 \
            66fdc867e552192d553d59095ea2e3cef4829de65c356f1fd826027b1905972e

      - name: Build retrace APK
        run: gradle --no-daemon -p android-plugin :app:assembleTraceRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.abis=all -Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
        env:
          SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
          SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
          SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}

      - name: Show ccache stats
        if: always()
        run: ccache --show-stats

      # Rewrite one rolling entry per job on the default branch. The upload stays
      # cumulative - it carries every object restored at the top of this run plus
      # the few TUs that actually changed - but Actions cache keys are immutable,
      # so the superseded blob has to be released before the same key can be
      # re-uploaded. Running after the build means a failed build leaves the
      # existing entry untouched. The other trigger branches restore this entry
      # rather than each writing a ~4 GB one of their own.
      - name: Release superseded ccache entry
        if: github.ref_name == github.event.repository.default_branch
        env:
          GH_TOKEN: ${{ github.token }}
          CACHE_KEY: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
        run: gh cache delete "${CACHE_KEY}" || true

      - name: Save ccache
        if: github.ref_name == github.event.repository.default_branch
        continue-on-error: true
        uses: actions/cache/save@v5
        with:
          path: .ccache
          key: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1

      - name: Verify APK metadata and packaging
        run: |
          AAPT2="$(find "$ANDROID_HOME/build-tools" -name aapt2 -type f | sort -V | tail -n 1)"
          plugin_apk="android-plugin/app/build/outputs/apk/plugin/release/MobileGL-plugin-release-${GITHUB_SHA}.apk"
          trace_apk="android-plugin/app/build/outputs/apk/trace/release/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
          test -f "${plugin_apk}"
          test -f "${trace_apk}"
          bash .github/scripts/validate-plugin-apks.sh "$AAPT2" "$plugin_apk" "$trace_apk"

      - name: Verify signed APKs
        run: |
          APKSIGNER="$(find "$ANDROID_HOME/build-tools" -name apksigner -type f | sort -V | tail -n 1)"
          mapfile -t APKS < <(printf '%s\n' \
            "android-plugin/app/build/outputs/apk/plugin/release/MobileGL-plugin-release-${GITHUB_SHA}.apk" \
            "android-plugin/app/build/outputs/apk/trace/release/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk")
          for APK in "${APKS[@]}"; do
            if [[ ! -f "$APK" ]]; then
              echo "::error::Expected release APK was not produced: $APK"
              exit 1
            fi
          done

          for APK in "${APKS[@]}"; do
            if [[ "$APK" == *-unsigned.apk ]]; then
              echo "::error::Unsigned release APK produced: $APK"
              exit 1
            fi
            "$APKSIGNER" verify --verbose "$APK"
          done

      - name: Upload plugin APK
        uses: actions/upload-artifact@v7
        with:
          name: MobileGL-plugin-${{ env.date_today }}-${{ github.sha }}
          path: android-plugin/app/build/outputs/apk/plugin/release/MobileGL-plugin-release-${{ github.sha }}.apk
          archive: false
          if-no-files-found: error

      - name: Upload retrace APK
        uses: actions/upload-artifact@v7
        with:
          name: MobileGL-retrace-apk-${{ env.date_today }}-${{ github.sha }}
          path: android-plugin/app/build/outputs/apk/trace/release/MobileGL-plugin-trace-release-${{ github.sha }}.apk
          archive: false
          if-no-files-found: error

  trace-cases:
    name: trace case matrix
    runs-on: ubuntu-latest
    needs: build
    outputs:
      android: ${{ steps.trace-cases.outputs.android }}
      names: ${{ steps.trace-cases.outputs.names }}
    steps:
      - name: Checkout repo
        uses: actions/checkout@v6

      - name: Load trace cases
        id: trace-cases
        run: |
          echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk-matrix)" >> "$GITHUB_OUTPUT"
          echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"

  trace-fixtures:
    name: trace fixture (${{ matrix.case }})
    runs-on: ubuntu-latest
    needs: trace-cases
    strategy:
      fail-fast: false
      max-parallel: 4
      matrix:
        case: ${{ fromJSON(needs.trace-cases.outputs.names) }}
    steps:
      - name: Checkout repo
        uses: actions/checkout@v6

      - name: Derive trace fixture cache key
        id: fixture-key
        run: bash .github/scripts/trace-fixture-cache.sh key '${{ matrix.case }}'

      - name: Restore trace fixture cache
        id: fixture-cache
        if: steps.fixture-key.outputs.cacheable == 'true'
        uses: actions/cache/restore@v5
        with:
          path: ${{ steps.fixture-key.outputs.paths }}
          key: ${{ steps.fixture-key.outputs.key }}

      - name: Verify restored trace fixture
        id: fixture-verify
        if: steps.fixture-cache.outputs.cache-hit == 'true'
        run: |
          if bash .github/scripts/trace-fixture-cache.sh verify '${{ matrix.case }}'; then
            echo "ok=true" >> "$GITHUB_OUTPUT"
          else
            echo "ok=false" >> "$GITHUB_OUTPUT"
            echo "::warning::Cached fixture for ${{ matrix.case }} failed verification; falling back to the download path"
            bash .github/scripts/trace-fixture-cache.sh reset '${{ matrix.case }}'
          fi

      - name: Fetch trace fixture
        if: steps.fixture-verify.outputs.ok != 'true'
        run: bash .github/scripts/fetch-trace-fixture-lfs.sh '${{ matrix.case }}'

      - name: Save trace fixture cache
        if: steps.fixture-key.outputs.cacheable == 'true' && steps.fixture-cache.outputs.cache-hit != 'true'
        uses: actions/cache/save@v5
        with:
          path: ${{ steps.fixture-key.outputs.paths }}
          key: ${{ steps.fixture-key.outputs.key }}

      - name: Stage trace fixture
        run: |
          safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
          stage_dir="trace-fixtures/${safe_case}"
          mkdir -p "${stage_dir}"
          python3 tools/trace_replay/trace_cases.py --format fixture-files --case '${{ matrix.case }}' |
            while IFS= read -r file; do
              cp "${file}" "${stage_dir}/"
            done

      - name: Upload trace fixture
        uses: actions/upload-artifact@v7
        with:
          name: MobileGL-trace-fixture-${{ matrix.case }}
          path: trace-fixtures/**
          if-no-files-found: error

  android-avd:
    name: android avd image
    runs-on: ubuntu-latest
    env:
      AVD_NAME: mobilegl-ci
      ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
      ANDROID_HOME: ${{ github.workspace }}/.android/sdk
      ANDROID_SDK_ROOT: ${{ github.workspace }}/.android/sdk
    steps:
      - name: Checkout repo
        uses: actions/checkout@v6

      - name: Setup Android SDK
        uses: android-actions/setup-android@v4
        with:
          accept-android-sdk-licenses: false

      - name: Accept Android SDK licenses
        run: yes | sdkmanager --licenses >/dev/null

      - name: Restore Android AVD cache
        id: android-avd-cache
        uses: actions/cache@v5
        with:
          path: |
            ${{ env.ANDROID_AVD_HOME }}
            ${{ env.ANDROID_SDK_ROOT }}/emulator
            ${{ env.ANDROID_SDK_ROOT }}/platform-tools
            ${{ env.ANDROID_SDK_ROOT }}/platforms/android-35
            ${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64
          key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }}

      - name: Create AVD
        if: steps.android-avd-cache.outputs.cache-hit != 'true'
        run: |
          sh android-plugin/run-avd-ci.sh create \
            --api-level 35 \
            --target google_apis \
            --arch x86_64 \
            --profile pixel_6 \
            --avd-name "${AVD_NAME}"

  retrace:
    name: retrace (${{ matrix.backend.name }}, ${{ matrix.case.name }})
    runs-on: ubuntu-latest
    needs:
      - build
      - android-avd
      - trace-cases
      - trace-fixtures
    if: ${{ always() && needs.build.result == 'success' && needs.android-avd.result == 'success' && needs.trace-cases.result == 'success' }}
    timeout-minutes: 75
    env:
      AVD_NAME: mobilegl-ci
      ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
      ANDROID_HOME: ${{ github.workspace }}/.android/sdk
      ANDROID_SDK_ROOT: ${{ github.workspace }}/.android/sdk
    strategy:
      fail-fast: false
      max-parallel: 4
      matrix: ${{ fromJSON(needs.trace-cases.outputs.android) }}
    steps:
      - name: Set Swap Space
        uses: pierotofy/set-swap-space@v1.0
        with:
          swap-size-gb: 8

      - name: Checkout repo
        uses: actions/checkout@v6

      - name: Download trace fixture
        uses: actions/download-artifact@v8
        with:
          name: MobileGL-trace-fixture-${{ matrix.case.name }}
          path: trace-fixture-download

      - name: Install trace fixture
        run: |
          mkdir -p tools/trace_replay/fixtures
          find trace-fixture-download -type f -exec cp {} tools/trace_replay/fixtures/ \;

      - name: Set artifact metadata
        run: |
          echo "date_today=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
          echo "EMULATOR_LOG=${RUNNER_TEMP}/mobilegl-emulator.log" >> "$GITHUB_ENV"
          echo "EMULATOR_PID_FILE=${RUNNER_TEMP}/mobilegl-emulator.pid" >> "$GITHUB_ENV"

      - name: Setup Android SDK
        uses: android-actions/setup-android@v4
        with:
          accept-android-sdk-licenses: false

      - name: Accept Android SDK licenses
        run: yes | sdkmanager --licenses >/dev/null

      - name: Restore Android AVD cache
        id: android-avd-cache
        uses: actions/cache/restore@v5
        with:
          path: |
            ${{ env.ANDROID_AVD_HOME }}
            ${{ env.ANDROID_SDK_ROOT }}/emulator
            ${{ env.ANDROID_SDK_ROOT }}/platform-tools
            ${{ env.ANDROID_SDK_ROOT }}/platforms/android-35
            ${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64
          key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }}

      - name: Download retrace APK
        uses: actions/download-artifact@v8
        with:
          name: MobileGL-plugin-trace-release-${{ github.sha }}.apk
          path: android-retrace-apks

      - 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: Create AVD
        if: steps.android-avd-cache.outputs.cache-hit != 'true'
        run: |
          sh android-plugin/run-avd-ci.sh create \
            --api-level 35 \
            --target google_apis \
            --arch x86_64 \
            --profile pixel_6 \
            --avd-name "${AVD_NAME}"

      - name: Launch Emulator
        run: |
          sh android-plugin/run-avd-ci.sh start \
            --avd-name "${AVD_NAME}" \
            --gpu "${{ matrix.backend.gpu }}" \
            --emulator-log "${EMULATOR_LOG}" \
            --pid-file "${EMULATOR_PID_FILE}" \
            --boot-timeout 300

      - name: Retrace and validate
        env:
          MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
          MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
          MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
          MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
          MOBILEGL_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
          MOBILEGL_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
        run: |
          apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
          test -f "${apk_file}"
          extra_retrace_args=()
          # Bliss needs the newer signed ANGLE variant plus sampler mipmap
          # min-filter downgrading on ANGLE llvmpipe.
          if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.name }}" = "minecraft-1.21.4-fabric-iris-bliss-in-world" ]; then
            extra_retrace_args+=(--avoid-angle-llvmpipe-sampler-mipmap-min-filter)
          fi
          if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.avoid_angle_llvmpipe_explicit_lod_bias || false }}" = "true" ]; then
            extra_retrace_args+=(--avoid-angle-llvmpipe-explicit-lod-bias)
          fi
          if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
            extra_retrace_args+=(--coherent-as-flush)
          fi

          run_retrace() {
            timeout "$(( ${{ matrix.case.timeout_seconds }} + 300 ))" sh android-plugin/trace-replay-ci.sh \
              --apk-file "${apk_file}" \
              --package top.mobilegl.plugin.trace \
              --backend "${{ matrix.backend.name }}" \
              --result-root android-retrace-result \
              --fixture-root android-retrace-fixture \
              --case "${{ matrix.case.name }}" \
              --trace-archive "${{ matrix.case.trace_archive }}" \
              --trace-file "${{ matrix.case.trace_file }}" \
              --golden "${{ matrix.case.golden }}" \
              --alternate-golden "${{ matrix.case.alternate_golden || '' }}" \
              --target-call "${{ matrix.case.target_call }}" \
              --width "${{ matrix.case.width }}" \
              --height "${{ matrix.case.height }}" \
              --ssim-threshold "${{ matrix.case.ssim_threshold || '0.99' }}" \
              --crop-x "${{ matrix.case.crop_x }}" \
              --crop-y "${{ matrix.case.crop_y }}" \
              --crop-width "${{ matrix.case.crop_width }}" \
              --crop-height "${{ matrix.case.crop_height }}" \
              --timeout-seconds "${{ matrix.case.timeout_seconds }}" \
              "${extra_retrace_args[@]}"
          }

          retrace_status=0
          run_retrace || retrace_status=$?
          if [ "${retrace_status}" -eq 75 ]; then
            echo "::warning::Android emulator infrastructure failed; restarting it and retrying this retrace once."
            # Surface-lost is retried rather than failed, so it would otherwise
            # be invisible. Report it per job - a healthy run prints nothing and
            # a rate spike shows up as a row per affected case.
            reason_file="android-retrace-result/infrastructure-failure-reason.txt"
            surface_lost_retries=0
            if [ -f "${reason_file}" ]; then
              surface_lost_retries="$(grep -c 'angle-surface-lost' "${reason_file}" || true)"
            fi
            if [ "${surface_lost_retries}" -gt 0 ]; then
              echo "surface-lost retries: ${surface_lost_retries} (${{ matrix.backend.name }}, ${{ matrix.case.name }})" \
                >> "${GITHUB_STEP_SUMMARY}"
            fi
            # The restart truncates EMULATOR_LOG, and the attempt that lost the
            # emulator is the one worth reading - the retry usually only shows
            # the wreckage. Keep the first attempt's log before it is clobbered.
            if [ -f "${EMULATOR_LOG}" ]; then
              cp "${EMULATOR_LOG}" "${EMULATOR_LOG}.first-attempt" || true
            fi
            sh android-plugin/run-avd-ci.sh stop \
              --avd-name "${AVD_NAME}" \
              --emulator-log "${EMULATOR_LOG}" \
              --pid-file "${EMULATOR_PID_FILE}"
            adb kill-server || true
            sleep 2
            sh android-plugin/run-avd-ci.sh start \
              --avd-name "${AVD_NAME}" \
              --gpu "${{ matrix.backend.gpu }}" \
              --emulator-log "${EMULATOR_LOG}" \
              --pid-file "${EMULATOR_PID_FILE}" \
              --boot-timeout 300
            run_retrace
          elif [ "${retrace_status}" -ne 0 ]; then
            exit "${retrace_status}"
          fi

      - name: Collect retrace summary inputs
        if: always()
        run: |
          safe_case="$(printf '%s' '${{ matrix.case.name }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
          result_dir="android-retrace-result/${safe_case}-${{ matrix.backend.name }}"
          mkdir -p "${result_dir}"
          if [ -s "${{ matrix.case.golden }}" ]; then
            cp "${{ matrix.case.golden }}" "${result_dir}/${safe_case}-${{ matrix.backend.name }}-golden.png"
          fi
          if [ -n "${{ matrix.case.alternate_golden || '' }}" ] && [ -s "${{ matrix.case.alternate_golden || '' }}" ]; then
            cp "${{ matrix.case.alternate_golden || '' }}" "${result_dir}/${safe_case}-${{ matrix.backend.name }}-alternate-golden.png"
          fi

      - name: Collect emulator diagnostics
        if: always()
        run: |
          mkdir -p android-retrace-result/diagnostics
          adb devices -l > android-retrace-result/diagnostics/adb-devices.txt || true
          timeout 30 adb logcat -d -t 1000 > android-retrace-result/diagnostics/logcat.txt || true
          if [ -f "${EMULATOR_LOG}" ]; then
            cp "${EMULATOR_LOG}" android-retrace-result/diagnostics/emulator.log
          fi
          if [ -f "${EMULATOR_LOG}.first-attempt" ]; then
            cp "${EMULATOR_LOG}.first-attempt" android-retrace-result/diagnostics/emulator-first-attempt.log
          fi
          # A vanished emulator looks identical whether the host OOM killer took
          # qemu or the renderer faulted. These two say which.
          free -h > android-retrace-result/diagnostics/host-memory.txt 2>&1 || true
          sudo dmesg -T 2>/dev/null | tail -300 > android-retrace-result/diagnostics/host-dmesg.txt || true

      - name: Stop Emulator
        if: always()
        run: |
          sh android-plugin/run-avd-ci.sh stop \
            --avd-name "${AVD_NAME}" \
            --emulator-log "${EMULATOR_LOG}" \
            --pid-file "${EMULATOR_PID_FILE}"

      - name: Upload Android retrace result
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: MobileGL-android-retrace-result-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }}
          path: android-retrace-result/**
          if-no-files-found: warn

  retrace-summary:
    name: retrace summary
    runs-on: ubuntu-latest
    needs: retrace
    if: always()

    steps:
      - name: Checkout repo
        uses: actions/checkout@v6

      - name: Set artifact metadata
        run: |
          echo "date_today=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"

      - name: Set up Node.js
        uses: actions/setup-node@v7
        with:
          node-version: '22'

      - name: Download Android retrace results
        uses: actions/download-artifact@v8
        with:
          pattern: MobileGL-android-retrace-result-*
          path: retrace-artifacts

      - name: Render retrace summary
        run: |
          node tools/trace_replay/render_retrace_summary.mjs \
            --input retrace-artifacts \
            --output-dir android-retrace-summary \
            --title "MobileGL Android retrace overview" \
            --group-label "Android Emulator" \
            --html mobilegl-android-retrace-overview.html

      - name: Upload Android retrace summary
        uses: actions/upload-artifact@v7
        with:
          path: android-retrace-summary/mobilegl-android-retrace-overview.html
          archive: false
          if-no-files-found: error

  remove-artifact-clutter:
    name: remove artifact clutter
    runs-on: ubuntu-latest
    needs: retrace-summary
    if: always()
    permissions:
      actions: write
    steps:
      - name: Delete intermediate Android retrace artifacts
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          declare -A failed_cases=()
          while IFS= read -r job_name; do
            case_name="${job_name#retrace (*, }"
            case_name="${case_name%)}"
            failed_cases["${case_name}"]=1
          done < <(
            gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
              --jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name'
          )

          if ((${#failed_cases[@]})); then
            echo "Retaining fixtures and results for failed retrace case(s):"
            printf '  %s\n' "${!failed_cases[@]}"
          else
            echo "All retrace jobs succeeded; nothing needs to be retained."
          fi

          deleted=0
          retained=0
          while IFS=$'\t' read -r artifact_id artifact_name; do
            keep=0
            if [[ "${artifact_name}" == MobileGL-trace-fixture-* ]]; then
              case_name="${artifact_name#MobileGL-trace-fixture-}"
              if [[ -v "failed_cases[${case_name}]" ]]; then
                keep=1
              fi
            elif [[ "${artifact_name}" == MobileGL-android-retrace-result-* ]]; then
              # The result artifact carries mobilegl.log, retrace.log, logcat,
              # the emulator log and the actual/diff images - the only record of
              # why a retrace failed. Its name ends in -<backend>-<case>, so a
              # suffix match on the case name keeps both backends' results for a
              # case that failed on either of them, which is what a comparison
              # needs. The match is anchored at the end, so a case name that is a
              # prefix of a longer one does not retain the longer one's results.
              for case_name in "${!failed_cases[@]}"; do
                if [[ "${artifact_name}" == *-"${case_name}" ]]; then
                  keep=1
                  break
                fi
              done
            fi

            if ((keep)); then
              echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
              ((retained += 1))
              continue
            fi

            echo "Deleting ${artifact_name} (${artifact_id})"
            gh api --method DELETE "repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}"
            ((deleted += 1))
          done < <(
            gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
              --jq '.artifacts[] | select(.name | startswith("MobileGL-trace-fixture-") or startswith("MobileGL-android-retrace-result-") or startswith("trace-fixture-") or startswith("retrace-result-")) | [.id, .name] | @tsv'
          )

          echo "Deleted ${deleted} intermediate Android artifact(s); retained ${retained} failed-retrace fixture(s)."
