From 2f899ca4245c5a5e2396ffeb82b8486b97c53b6e Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:05:51 -0500 Subject: [PATCH] ci: merge-queue contention fixes, desktop builds on main, and 22.04-runner exit for desktop releases (#6174) Co-authored-by: Claude Fable 5 --- .github/workflows/main-check.yml | 15 +++++- .github/workflows/merge-queue.yml | 70 +++++++++++++++++++++++++++- .github/workflows/release.yml | 35 ++++++++++++-- .github/workflows/reusable-check.yml | 14 +++--- .github/workflows/verify-flatpak.yml | 5 ++ .skills/testing-ci/SKILL.md | 1 + 6 files changed, 126 insertions(+), 14 deletions(-) diff --git a/.github/workflows/main-check.yml b/.github/workflows/main-check.yml index 6fc20148e..5c4346c11 100644 --- a/.github/workflows/main-check.yml +++ b/.github/workflows/main-check.yml @@ -15,12 +15,20 @@ concurrency: cancel-in-progress: true jobs: + # Every commit on main arrives via the merge queue, which already ran lint, tests, + # screenshot validation, and rb-check on this exact merge commit. Re-running them here + # would be pure duplication — this workflow builds the debug APKs for the snapshot + # release below (run_lint: false also skips screenshot-check and rb-check) plus the + # desktop distributables. Desktop packaging runs here post-merge rather than in the + # merge queue: :desktopApp:test in the queue's shard-app already covers compilation, + # and the 4-OS matrix (macos/windows queue times) would slow every merge. validate-and-build: if: github.repository == 'meshtastic/Meshtastic-Android' uses: ./.github/workflows/reusable-check.yml with: - run_lint: true + run_lint: false run_unit_tests: false + run_desktop_builds: true upload_artifacts: true secrets: inherit @@ -30,7 +38,10 @@ jobs: # expire after 7 days). publish-snapshot: needs: validate-and-build - if: github.repository == 'meshtastic/Meshtastic-Android' + # !cancelled(): a desktop-matrix failure fails validate-and-build as a whole, but the + # Android APKs may still have built fine — attempt the snapshot regardless. If the + # APK build itself failed, the artifact download below fails and this job goes red. + if: github.repository == 'meshtastic/Meshtastic-Android' && !cancelled() runs-on: ubuntu-24.04-arm permissions: contents: write diff --git a/.github/workflows/merge-queue.yml b/.github/workflows/merge-queue.yml index f78e30116..35bf1c331 100644 --- a/.github/workflows/merge-queue.yml +++ b/.github/workflows/merge-queue.yml @@ -7,13 +7,74 @@ on: permissions: contents: read +# Note: github.ref is unique per merge-group entry (gh-readonly-queue/main/pr-N-), +# so this group never dedupes across re-queues of the same PR — the cancel-superseded +# job below handles that explicitly. concurrency: group: build-mq-${{ github.ref }} cancel-in-progress: true jobs: - android-check: + # When a PR is re-queued (an entry ahead of it failed or was removed), GitHub creates a + # new merge group but does NOT cancel the workflow runs of the destroyed one. Those stale + # runs sit queued/running and starve the runner pool. Cancel any older merge-queue run + # for the same PR — only the newest merge group per PR is ever valid. + cancel-superseded: + name: Cancel Superseded Queue Runs if: github.repository == 'meshtastic/Meshtastic-Android' + runs-on: ubuntu-24.04-arm + timeout-minutes: 5 + permissions: + actions: write + steps: + - name: Cancel older merge-queue runs for the same PR + env: + GH_TOKEN: ${{ github.token }} + run: | + # github.ref_name: gh-readonly-queue/main/pr-- + PR_PREFIX=$(echo "${{ github.ref_name }}" | grep -oE 'gh-readonly-queue/.+/pr-[0-9]+-') + if [ -z "$PR_PREFIX" ]; then + echo "Could not parse PR from ref '${{ github.ref_name }}'; skipping." + exit 0 + fi + for status in queued in_progress; do + gh api "repos/${{ github.repository }}/actions/runs?event=merge_group&status=${status}&per_page=100" \ + --jq ".workflow_runs[] | select(.head_branch | startswith(\"$PR_PREFIX\")) | select(.id < ${{ github.run_id }}) | .id" + done | sort -u | while read -r run_id; do + echo "Cancelling superseded run $run_id" + gh run cancel "$run_id" --repo "${{ github.repository }}" || true + done + + # Docs-only queue entries (changelog updates, markdown fixes) cannot affect the build; + # skip the heavy pipeline for them. Anything outside docs/ and *.md runs full CI. + # Mirrors the paths-ignore list in main-check.yml. + check-changes: + name: Check Changes + if: github.repository == 'meshtastic/Meshtastic-Android' + runs-on: ubuntu-24.04-arm + timeout-minutes: 5 + outputs: + android: ${{ steps.filter.outputs.android }} + steps: + - uses: actions/checkout@v7.0.0 + with: + fetch-depth: 1 + - name: Diff merge group against its base + id: filter + run: | + git fetch --depth=1 origin "${{ github.event.merge_group.base_sha }}" + changed=$(git diff --name-only "${{ github.event.merge_group.base_sha }}" "${{ github.event.merge_group.head_sha }}") + echo "Changed files:" + echo "$changed" + if echo "$changed" | grep -qvE '^docs/|\.md$|^$'; then + echo "android=true" >> "$GITHUB_OUTPUT" + else + echo "android=false" >> "$GITHUB_OUTPUT" + fi + + android-check: + needs: check-changes + if: github.repository == 'meshtastic/Meshtastic-Android' && needs.check-changes.outputs.android == 'true' uses: ./.github/workflows/reusable-check.yml with: run_lint: true @@ -27,12 +88,17 @@ jobs: timeout-minutes: 5 permissions: {} needs: + - check-changes - android-check if: always() steps: - name: Check Workflow Status run: | - if [[ "${{ needs.android-check.result }}" == "failure" || "${{ needs.android-check.result }}" == "cancelled" ]]; then + if [[ "${{ needs.check-changes.result }}" != "success" ]]; then + echo "::error::Change detection failed" + exit 1 + fi + if [[ "${{ needs.check-changes.outputs.android }}" == "true" && ("${{ needs.android-check.result }}" == "failure" || "${{ needs.android-check.result }}" == "cancelled") ]]; then echo "::error::Android Check failed" exit 1 fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4c0ef201..743eb4e5f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -264,7 +264,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, windows-latest, ubuntu-22.04, ubuntu-22.04-arm] + os: [macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm] env: GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }} @@ -286,7 +286,7 @@ jobs: - name: Install dependencies for AppImage if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install -y libfuse2 + run: sudo apt-get update && sudo apt-get install -y libfuse2t64 - name: Package Native Distributions env: @@ -308,6 +308,33 @@ jobs: ${{ contains(runner.os, 'macOS') && ':desktopApp:packageReleaseUberJarForCurrentOS' || '' }} '-PaboutLibraries.release=true' --no-daemon + # jpackage computes the .deb Depends: line from the build host's package names. + # Ubuntu 24.04 records time_t64 transition names (libasound2t64, libpng16-16t64) + # that don't exist on Debian 12 Bookworm / Raspberry Pi OS, making the .deb + # uninstallable there (#5233 pinned ubuntu-22.04 for this; those runners retire + # 2027-04, brownouts from 2026-09). Strip the t64 suffix instead: Bookworm ships + # the plain names, and on Trixie / Ubuntu 24.04+ the t64 packages Provide them. + # Rewriting the control file is the only host-independent fix — jpackage's + # --linux-package-deps is additive-only, and the Compose plugin clears its + # --resource-dir during the task, so a custom control template can't be injected. + - name: Rewrite t64 dependency names in .deb (Debian Bookworm / Pi OS compatibility) + if: runner.os == 'Linux' + run: | + shopt -s nullglob + for deb in desktopApp/build/compose/binaries/main-release/deb/*.deb; do + work=$(mktemp -d) + dpkg-deb -R "$deb" "$work" + echo "$(basename "$deb") before: $(grep '^Depends:' "$work/DEBIAN/control")" + sed -i -E '/^(Depends|Pre-Depends|Recommends):/ s/(lib[a-z0-9.+-]*)t64([, (]|$)/\1\2/g' "$work/DEBIAN/control" + echo "$(basename "$deb") after: $(grep '^Depends:' "$work/DEBIAN/control")" + if grep -nE '^[A-Za-z-]+:.*\blib[a-z0-9.+-]*t64\b' "$work/DEBIAN/control"; then + echo "::error::t64 dependency name survived rewrite in $(basename "$deb") — extend the sed above" + exit 1 + fi + dpkg-deb -b --root-owner-group "$work" "$deb" + rm -rf "$work" + done + - name: List Desktop Binaries if: runner.os == 'Linux' || runner.os == 'macOS' run: ls -R desktopApp/build/compose/binaries/main-release @@ -348,7 +375,9 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-22.04, ubuntu-22.04-arm] + # No .deb host-coupling here (pure-Java uber jar + URL manifest) — free to track + # the current LTS image. + os: [ubuntu-24.04, ubuntu-24.04-arm] env: GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }} diff --git a/.github/workflows/reusable-check.yml b/.github/workflows/reusable-check.yml index c1310534a..296bf74a8 100644 --- a/.github/workflows/reusable-check.yml +++ b/.github/workflows/reusable-check.yml @@ -12,6 +12,9 @@ on: run_coverage: type: boolean default: true + run_desktop_builds: + type: boolean + default: false run_desktop_flatpak_src: type: boolean default: false @@ -93,6 +96,7 @@ jobs: contents: read timeout-minutes: 30 needs: setup + if: inputs.run_lint == true env: VERSION_CODE: ${{ needs.setup.outputs.version_code }} @@ -112,13 +116,8 @@ jobs: install_jetbrains_jdk: 'true' - name: Lint, Analysis & KMP Smoke Compile - if: inputs.run_lint == true run: ./gradlew spotlessCheck detekt androidApp:lintFdroidDebug androidApp:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug kmpSmokeCompile -Pci=true --continue - - name: KMP Smoke Compile (lint skipped) - if: inputs.run_lint == false - run: ./gradlew kmpSmokeCompile -Pci=true --continue - # ── Screenshot Test Validation ────────────────────────────────────── screenshot-check: runs-on: ubuntu-24.04 @@ -156,13 +155,14 @@ jobs: if-no-files-found: warn # ── Reproducible Build Verification ───────────────────────────────── - # Only runs on main push and merge-queue (not PRs) to keep PR feedback fast. + # Only runs in the merge queue (not PRs, to keep PR feedback fast; not main pushes, + # which are the same merge commit the queue just verified). rb-check: runs-on: ubuntu-24.04 permissions: contents: read timeout-minutes: 30 - if: inputs.run_lint == true && (github.event_name == 'push' || github.event_name == 'merge_group') + if: inputs.run_lint == true && github.event_name == 'merge_group' steps: - name: Checkout code diff --git a/.github/workflows/verify-flatpak.yml b/.github/workflows/verify-flatpak.yml index 6a1688fc5..75e59dd11 100644 --- a/.github/workflows/verify-flatpak.yml +++ b/.github/workflows/verify-flatpak.yml @@ -8,6 +8,11 @@ on: - 'build.gradle.kts' - 'settings.gradle.kts' - '.github/workflows/verify-flatpak.yml' + # The desktop module's build config shapes the uber jar the flatpak wraps. + - 'desktopApp/**' + # The offline manifest pins the Gradle distribution independently of the wrapper — + # a wrapper bump without a manifest update breaks the offline build silently. + - 'gradle/wrapper/**' workflow_dispatch: permissions: diff --git a/.skills/testing-ci/SKILL.md b/.skills/testing-ci/SKILL.md index 3ea9e51be..53b421267 100644 --- a/.skills/testing-ci/SKILL.md +++ b/.skills/testing-ci/SKILL.md @@ -131,6 +131,7 @@ CI is defined in `.github/workflows/reusable-check.yml` and structured as parall - **`fail-fast: false`:** Test sharding does not cancel other shards on failure. - **Explicit Gradle task paths:** Prefer `app:lintFdroidDebug` over shorthand `lintDebug` in CI. - **Pull request CI:** Main-only (`.github/workflows/pull-request.yml` targets `main`). +- **Merge queue hygiene:** `merge-queue.yml` cancels superseded runs for the same PR (GitHub does not auto-cancel destroyed merge-group runs) and skips the heavy pipeline for docs-only entries (`docs/**`, `*.md`). `rb-check` runs ONLY in the merge queue. `main-check.yml` passes `run_lint: false` — every main commit is a merge-queue-verified merge commit, so main pushes only rebuild the debug APKs for the snapshot release. - **Cache writes:** Trusted on `main` and merge queue runs; other refs use read-only cache. - **Path filtering:** `check-changes` in `pull-request.yml` must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.). - **AboutLibraries:** Runs in `offlineMode` by default (no GitHub/SPDX API calls). Release builds pass `-PaboutLibraries.release=true` via Fastlane/Gradle CLI to enable remote license fetching. Do NOT re-gate on `CI` or `GITHUB_TOKEN` alone.