From 5d3af65c39b51c72cd2d0213317ee53b6269c0c0 Mon Sep 17 00:00:00 2001 From: Ramon Roche Date: Wed, 13 May 2026 11:44:39 -0700 Subject: [PATCH 1/2] fix(QGCKeychain): fall back to QSettings on headless D-Bus failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isMissingSecretService() routes "no Secret Service reachable" errors through the QSettings fallback so reads and writes still succeed. It only matched libsecret's org.freedesktop.secrets / ServiceUnknown patterns, which covers a Secret Service daemon not being registered on the bus, but not the case where there is no session bus at all. On a headless host (CI without dbus-launch / gnome-keyring, Docker, embedded test rigs) libsecret instead reports: "Cannot autolaunch D-Bus without X11 \$DISPLAY" That's a QKeychain::OtherError, did not match the existing patterns, and dropped through to the terminal error branch — QGCKeychain::write returned false and QGCKeychainTest / SigningTest failed. Recognize the autolaunch and missing-session-bus messages as the same "no backend" condition so the fallback kicks in and callers see the behavior they already see on macOS/Windows when no keychain is configured. Signed-off-by: Ramon Roche --- src/Utilities/Platform/QGCKeychain.cc | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Utilities/Platform/QGCKeychain.cc b/src/Utilities/Platform/QGCKeychain.cc index 343d10cee06a..b8eb0841b0ac 100644 --- a/src/Utilities/Platform/QGCKeychain.cc +++ b/src/Utilities/Platform/QGCKeychain.cc @@ -90,14 +90,22 @@ bool indicatesMissingBackend(QKeychain::Error err) return err == QKeychain::NoBackendAvailable || err == QKeychain::AccessDenied; } -// Linux libsecret backend reports DBus ServiceUnknown as OtherError — recognize it as "no backend". +// Linux libsecret backend reports a few "no Secret Service reachable" conditions +// as QKeychain::OtherError. Recognize the known patterns so we transparently fall +// back to QSettings instead of failing the call. bool isMissingSecretService(QKeychain::Error err, const QString& errorString) { if (err != QKeychain::OtherError) { return false; } - return errorString.contains(QLatin1String("org.freedesktop.secrets")) || - errorString.contains(QLatin1String("ServiceUnknown")); + // libsecret can't reach the Secret Service over D-Bus. + if (errorString.contains(QLatin1String("org.freedesktop.secrets")) || + errorString.contains(QLatin1String("ServiceUnknown"))) { + return true; + } + // No session bus to talk to (headless CI without dbus-launch / gnome-keyring). + return errorString.contains(QLatin1String("autolaunch D-Bus")) || + errorString.contains(QLatin1String("DBUS_SESSION_BUS_ADDRESS")); } } // namespace From c5de6f32df7cbe4de4f8b9fdadf25632660be42b Mon Sep 17 00:00:00 2001 From: Ramon Roche Date: Wed, 13 May 2026 11:44:56 -0700 Subject: [PATCH 2/2] ci: migrate Linux workflow to RunsOn self-hosted runners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the Linux build and debug-validation jobs from GitHub-hosted runners to RunsOn ephemeral EC2 runners (qgc-ci-runs-on stack in us-west-2, RunsOn v3.0.6). x86_64 builds now run on c8i.2xlarge, ARM64 on c8g.2xlarge, the Debug+coverage test job on m8i.2xlarge — all 8 vCPU, On-Demand, ubuntu24-full-* images. Runner labels are inline in the workflow so this PR is self-contained (named runner profiles in .github/runs-on.yml require the config on the default branch first); the named profiles are kept in the tree for the next workflow migration to reuse. Caching: extras=s3-cache + runs-on/action@v2 transparently redirect all existing actions/cache@v5 calls (ccache, Qt SDK, GStreamer, pipx, apt, CPM) to the S3 bucket provisioned by the stack. runs-on/action is a no-op on GitHub-hosted runners so the workflow stays portable. Matrix cleanup: dropped the dual-purpose `matrix.os` field on the build job (it was both a runner selector and a discriminator for two size-analysis steps). matrix.arch is now the single discriminator; the previous `matrix.os == 'ubuntu-22.04'` conditions on lines 128 and 135 now correctly read `matrix.arch == 'linux_gcc_64'`. Both architectures build on Ubuntu 24.04 (was 22.04 for x64, 24.04 for ARM). This bumps the AppImage glibc baseline from 2.35 to 2.39; older distros (RHEL 8, Ubuntu 20.04, Debian 11) won't run binaries produced here. Test execution restructured by label. cmake/QGCTest.cmake:164 auto-attaches RESOURCE_LOCK "MockLink" to every Integration test because MockLink shares a LinkManager singleton and static _nextVehicleSystemId counter; a single CTest invocation over both labels with --parallel auto silently serialized everything on that lock. Split into two passes: - Run Unit Tests (parallel): -L Unit, --parallel auto. 151 Unit tests with no shared state. - Run Integration Tests (serial): -L Integration, --parallel 1. 37 Integration tests serialize on shared MockLink state. Each pass writes its own junit + ctest output; downstream Analyze / Report / Upload steps run once per pass. Coverage path picks up .gcda from both passes via the existing find . -name '*.gcda'. Tester runner uses volume=60gb (40GB default left ~1-2GB headroom at peak with the Debug build + Qt SDK + caches + .gcda + scratch, which silently killed the agent before any diagnostic could run). Signed-off-by: Ramon Roche --- .github/runs-on.yml | 17 ++++++ .github/workflows/linux.yml | 109 ++++++++++++++++++++++++++---------- 2 files changed, 97 insertions(+), 29 deletions(-) create mode 100644 .github/runs-on.yml diff --git a/.github/runs-on.yml b/.github/runs-on.yml new file mode 100644 index 000000000000..0965a316ff62 --- /dev/null +++ b/.github/runs-on.yml @@ -0,0 +1,17 @@ +runners: + linux-x64-builder: + family: ["c8i.2xlarge"] + spot: false + image: ubuntu24-full-x64 + extras: s3-cache + linux-arm64-builder: + family: ["c8g.2xlarge"] + spot: false + image: ubuntu24-full-arm64 + extras: s3-cache + linux-x64-tester: + family: ["m8i.2xlarge"] + spot: false + image: ubuntu24-full-x64 + extras: s3-cache + volume: 60gb diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index d64efb605fd3..f5858c8506cd 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -39,29 +39,31 @@ jobs: name: Release ${{ matrix.arch }} needs: changes if: always() && !cancelled() && (needs.changes.outputs.should_build == 'true' || needs.changes.result == 'skipped') - runs-on: ${{ matrix.os }} + runs-on: ${{ matrix.runs_on }} timeout-minutes: 120 strategy: fail-fast: false matrix: - os: [ubuntu-24.04-arm, ubuntu-22.04] include: - - os: ubuntu-24.04-arm + - arch: linux_gcc_arm64 + runs_on: runs-on=${{ github.run_id }}/family=c8g.2xlarge/spot=false/image=ubuntu24-full-arm64/extras=s3-cache package: QGroundControl-aarch64 host: linux_arm64 - arch: linux_gcc_arm64 - - os: ubuntu-22.04 + - arch: linux_gcc_64 + runs_on: runs-on=${{ github.run_id }}/family=c8i.2xlarge/spot=false/image=ubuntu24-full-x64/extras=s3-cache package: QGroundControl-x86_64 host: linux - arch: linux_gcc_64 defaults: run: shell: bash steps: + - name: Enable RunsOn magic cache + uses: runs-on/action@v2 + - name: Harden Runner if: runner.arch != 'ARM64' uses: step-security/harden-runner@v2 @@ -123,14 +125,14 @@ jobs: binary-path: ${{ runner.temp }}/build/Release/QGroundControl - name: Analyze binary size - if: matrix.os == 'ubuntu-22.04' + if: matrix.arch == 'linux_gcc_64' uses: ./.github/actions/size-analysis with: binary-path: ${{ runner.temp }}/build/Release/QGroundControl output-file: ${{ runner.temp }}/build/metrics.json - name: Upload metrics artifact - if: matrix.os == 'ubuntu-22.04' + if: matrix.arch == 'linux_gcc_64' uses: actions/upload-artifact@v7 with: name: size-metrics @@ -185,7 +187,11 @@ jobs: name: ${{ matrix.job_name }} needs: [changes, debug-matrix] if: ${{ !cancelled() && (needs.changes.outputs.should_build == 'true' || needs.changes.result == 'skipped') }} - runs-on: ubuntu-22.04 + # 60GB volume so the Debug build + Qt SDK + caches + Integration test + # artifacts fit comfortably; the 40GB default left only ~1-2GB headroom + # at peak and killed the runner agent before any diagnostic step could + # run. + runs-on: runs-on=${{ github.run_id }}/family=m8i.2xlarge/spot=false/image=ubuntu24-full-x64/extras=s3-cache/volume=60gb timeout-minutes: ${{ matrix.timeout_minutes }} strategy: @@ -198,6 +204,9 @@ jobs: shell: bash steps: + - name: Enable RunsOn magic cache + uses: runs-on/action@v2 + - name: Harden Runner uses: step-security/harden-runner@v2 with: @@ -240,8 +249,29 @@ jobs: build-dir: ${{ runner.temp }}/build build-type: Debug - - name: Run Unit Tests - id: tests + # Split test execution by label: Unit tests parallelize cleanly (no + # MockLink/Vehicle shared state), Integration tests share LinkManager + # singletons and per-test RESOURCE_LOCK so they serialize naturally on + # one CTest invocation. Run them in two passes so the Unit pass can use + # all cores while the Integration pass stays correct. + - name: Run Unit Tests (parallel) + id: unit_tests + uses: ./.github/actions/run-unit-tests + env: + ASAN_OPTIONS: ${{ matrix.mode == 'sanitizers' && 'detect_leaks=1:halt_on_error=1:check_initialization_order=1' || '' }} + LSAN_OPTIONS: ${{ matrix.mode == 'sanitizers' && format('suppressions={0}/build/asan_suppressions.txt', runner.temp) || '' }} + UBSAN_OPTIONS: ${{ matrix.mode == 'sanitizers' && format('print_stacktrace=1:halt_on_error=1:suppressions={0}/build/ubsan_suppressions.txt', runner.temp) || '' }} + with: + build-dir: ${{ runner.temp }}/build + junit-output: junit-results-linux-${{ matrix.mode }}-unit.xml + ctest-output: test-output-linux-${{ matrix.mode }}-unit.txt + include-labels: 'Unit' + exclude-labels: ${{ matrix.exclude_labels }} + parallel: auto + + - name: Run Integration Tests (serial) + id: integration_tests + if: always() && !cancelled() && steps.unit_tests.conclusion != 'cancelled' uses: ./.github/actions/run-unit-tests env: ASAN_OPTIONS: ${{ matrix.mode == 'sanitizers' && 'detect_leaks=1:halt_on_error=1:check_initialization_order=1' || '' }} @@ -249,44 +279,65 @@ jobs: UBSAN_OPTIONS: ${{ matrix.mode == 'sanitizers' && format('print_stacktrace=1:halt_on_error=1:suppressions={0}/build/ubsan_suppressions.txt', runner.temp) || '' }} with: build-dir: ${{ runner.temp }}/build - junit-output: junit-results-linux-${{ matrix.mode }}.xml - ctest-output: test-output-linux-${{ matrix.mode }}.txt - include-labels: 'Unit|Integration' + junit-output: junit-results-linux-${{ matrix.mode }}-integration.xml + ctest-output: test-output-linux-${{ matrix.mode }}-integration.txt + include-labels: 'Integration' exclude-labels: ${{ matrix.exclude_labels }} - # Coverage requires serial execution (gcov writes race); sanitizers don't. - parallel: ${{ matrix.mode == 'coverage' && '1' || 'auto' }} + parallel: '1' - - name: Analyze Unit Test Durations - if: always() && !cancelled() && steps.tests.conclusion != 'skipped' + - name: Analyze Unit Test Durations (unit) + if: always() && !cancelled() && steps.unit_tests.conclusion != 'skipped' uses: ./.github/actions/test-duration-report with: - junit-path: ${{ runner.temp }}/build/junit-results-linux-${{ matrix.mode }}.xml - report-json-path: ${{ runner.temp }}/build/test-duration-linux-${{ matrix.mode }}.json + junit-path: ${{ runner.temp }}/build/junit-results-linux-${{ matrix.mode }}-unit.xml + report-json-path: ${{ runner.temp }}/build/test-duration-linux-${{ matrix.mode }}-unit.json top-n: '20' slow-threshold-seconds: '60' - - name: Report Test Results - if: always() && !cancelled() && steps.tests.conclusion != 'skipped' + - name: Analyze Unit Test Durations (integration) + if: always() && !cancelled() && steps.integration_tests.conclusion != 'skipped' + uses: ./.github/actions/test-duration-report + with: + junit-path: ${{ runner.temp }}/build/junit-results-linux-${{ matrix.mode }}-integration.xml + report-json-path: ${{ runner.temp }}/build/test-duration-linux-${{ matrix.mode }}-integration.json + top-n: '20' + slow-threshold-seconds: '60' + + - name: Report Test Results (unit) + if: always() && !cancelled() && steps.unit_tests.conclusion != 'skipped' uses: ./.github/actions/test-report with: name: Unit Tests (${{ matrix.mode }}) build-dir: ${{ runner.temp }}/build - junit-file: junit-results-linux-${{ matrix.mode }}.xml - output-file: test-output-linux-${{ matrix.mode }}.txt - artifact-name: test-results-linux-${{ matrix.mode }} + junit-file: junit-results-linux-${{ matrix.mode }}-unit.xml + output-file: test-output-linux-${{ matrix.mode }}-unit.txt + artifact-name: test-results-linux-${{ matrix.mode }}-unit + retention-days: 7 + trunk-org-slug: ${{ vars.TRUNK_ORG_SLUG }} + trunk-token: ${{ secrets.TRUNK_TOKEN }} + + - name: Report Test Results (integration) + if: always() && !cancelled() && steps.integration_tests.conclusion != 'skipped' + uses: ./.github/actions/test-report + with: + name: Integration Tests (${{ matrix.mode }}) + build-dir: ${{ runner.temp }}/build + junit-file: junit-results-linux-${{ matrix.mode }}-integration.xml + output-file: test-output-linux-${{ matrix.mode }}-integration.txt + artifact-name: test-results-linux-${{ matrix.mode }}-integration retention-days: 7 trunk-org-slug: ${{ vars.TRUNK_ORG_SLUG }} trunk-token: ${{ secrets.TRUNK_TOKEN }} - name: Upload Test Artifacts - if: always() && !cancelled() && steps.tests.conclusion != 'skipped' + if: always() && !cancelled() && (steps.unit_tests.conclusion != 'skipped' || steps.integration_tests.conclusion != 'skipped') uses: actions/upload-artifact@v7 with: name: test-artifacts-linux-${{ matrix.mode }} path: | - ${{ runner.temp }}/build/test-output-linux-${{ matrix.mode }}.txt - ${{ runner.temp }}/build/junit-results-linux-${{ matrix.mode }}.xml - ${{ runner.temp }}/build/test-duration-linux-${{ matrix.mode }}.json + ${{ runner.temp }}/build/test-output-linux-${{ matrix.mode }}-*.txt + ${{ runner.temp }}/build/junit-results-linux-${{ matrix.mode }}-*.xml + ${{ runner.temp }}/build/test-duration-linux-${{ matrix.mode }}-*.json retention-days: 7 - name: Verify coverage data files exist