diff --git a/.github/runs-on.yml b/.github/runs-on.yml new file mode 100644 index 00000000000..0965a316ff6 --- /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 d64efb605fd..f5858c8506c 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 diff --git a/src/Utilities/Platform/QGCKeychain.cc b/src/Utilities/Platform/QGCKeychain.cc index 343d10cee06..b8eb0841b0a 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