Merge branch 'mavlink:master' into Stable_V1.0 #17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Linux | |
| on: | |
| push: | |
| branches: | |
| - master | |
| - 'Stable*' | |
| tags: | |
| - 'v*' | |
| paths-ignore: | |
| - 'docs/**' | |
| pull_request: | |
| merge_group: | |
| workflow_dispatch: | |
| inputs: | |
| build_type: | |
| description: 'Build type' | |
| required: false | |
| default: 'Release' | |
| type: choice | |
| options: | |
| - Release | |
| - Debug | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| attestations: write | |
| security-events: write | |
| actions: read | |
| checks: write | |
| jobs: | |
| changes: | |
| uses: ./.github/workflows/_detect-changes.yml | |
| with: | |
| platform: linux | |
| build: | |
| name: Build ${{ matrix.arch }} ${{ matrix.build_type }} | |
| needs: changes | |
| if: always() && !cancelled() && (needs.changes.outputs.should_build == 'true' || needs.changes.result == 'skipped') | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 120 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| build_type: ${{ inputs.build_type && fromJSON(format('["{0}"]', inputs.build_type)) || fromJSON('["Debug", "Release"]') }} | |
| os: [ubuntu-24.04-arm, ubuntu-22.04] | |
| include: | |
| - os: ubuntu-24.04-arm | |
| package: QGroundControl-aarch64 | |
| host: linux_arm64 | |
| arch: linux_gcc_arm64 | |
| - os: ubuntu-22.04 | |
| package: QGroundControl-x86_64 | |
| host: linux | |
| arch: linux_gcc_64 | |
| exclude: | |
| - os: ubuntu-24.04-arm | |
| build_type: Debug | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - name: Harden Runner | |
| if: runner.arch != 'ARM64' | |
| uses: step-security/harden-runner@v2 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: ${{ github.event_name == 'pull_request' && 1 || 0 }} | |
| fetch-tags: ${{ github.event_name != 'pull_request' }} | |
| - name: Build Setup | |
| uses: ./.github/actions/build-setup | |
| with: | |
| qt-host: ${{ matrix.host }} | |
| qt-arch: ${{ matrix.arch }} | |
| build-type: ${{ matrix.build_type }} | |
| - name: Install Dependencies | |
| uses: ./.github/actions/install-dependencies | |
| - name: Initialize CodeQL | |
| if: matrix.build_type == 'Debug' && github.event_name != 'pull_request' | |
| uses: github/codeql-action/init@v4 | |
| with: | |
| languages: c-cpp | |
| config-file: ./.github/codeql/codeql-config.yml | |
| - name: Configure | |
| uses: ./.github/actions/cmake-configure | |
| with: | |
| build-dir: ${{ runner.temp }}/build | |
| build-type: ${{ matrix.build_type }} | |
| testing: ${{ matrix.build_type == 'Debug' && 'true' || 'false' }} | |
| stable: ${{ (github.ref_type == 'tag' || contains(github.ref, 'Stable')) && 'true' || 'false' }} | |
| - name: Build | |
| uses: ./.github/actions/cmake-build | |
| with: | |
| build-dir: ${{ runner.temp }}/build | |
| build-type: ${{ matrix.build_type }} | |
| reviewdog: ${{ github.event_name == 'pull_request' && 'true' || 'false' }} | |
| reviewdog-token: ${{ github.token }} | |
| - name: Perform CodeQL Analysis | |
| if: matrix.build_type == 'Debug' && github.event_name != 'pull_request' | |
| continue-on-error: true | |
| uses: github/codeql-action/analyze@v4 | |
| with: | |
| category: "/language:c-cpp" | |
| - name: Run Unit Tests | |
| if: matrix.build_type == 'Debug' | |
| uses: ./.github/actions/run-unit-tests | |
| with: | |
| build-dir: ${{ runner.temp }}/build | |
| junit-output: junit-results-linux-${{ matrix.arch }}.xml | |
| ctest-output: test-output-linux-${{ matrix.arch }}.txt | |
| include-labels: 'Unit|Integration' | |
| exclude-labels: 'Flaky|Network' | |
| - name: Report Test Results | |
| if: always() && !cancelled() && matrix.build_type == 'Debug' | |
| uses: ./.github/actions/test-report | |
| with: | |
| name: Unit Tests (${{ matrix.arch }}) | |
| build-dir: ${{ runner.temp }}/build | |
| junit-file: junit-results-linux-${{ matrix.arch }}.xml | |
| output-file: test-output-linux-${{ matrix.arch }}.txt | |
| artifact-name: test-results-${{ matrix.arch }} | |
| retention-days: 7 | |
| trunk-org-slug: ${{ vars.TRUNK_ORG_SLUG }} | |
| trunk-token: ${{ secrets.TRUNK_TOKEN }} | |
| - name: Verify Executable | |
| if: matrix.build_type == 'Release' | |
| uses: ./.github/actions/verify-executable | |
| with: | |
| binary-path: ${{ runner.temp }}/build/${{ matrix.build_type }}/QGroundControl | |
| - name: Analyze binary size | |
| if: matrix.build_type == 'Release' && matrix.os == 'ubuntu-22.04' | |
| uses: ./.github/actions/size-analysis | |
| with: | |
| binary-path: ${{ runner.temp }}/build/${{ matrix.build_type }}/QGroundControl | |
| output-file: ${{ runner.temp }}/build/metrics.json | |
| - name: Upload metrics artifact | |
| if: matrix.build_type == 'Release' && matrix.os == 'ubuntu-22.04' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: size-metrics | |
| path: ${{ runner.temp }}/build/metrics.json | |
| if-no-files-found: error | |
| retention-days: 90 | |
| - name: Create AppImage | |
| working-directory: ${{ runner.temp }}/build | |
| run: cmake --install . --config ${{ matrix.build_type }} | |
| - name: Verify AppImage | |
| if: matrix.build_type == 'Release' | |
| uses: ./.github/actions/verify-executable | |
| with: | |
| binary-path: ${{ runner.temp }}/build/${{ matrix.package }}.AppImage | |
| type: appimage | |
| - name: Attest and Upload | |
| if: matrix.build_type == 'Release' | |
| uses: ./.github/actions/attest-and-upload | |
| with: | |
| artifact-name: ${{ matrix.package }}.AppImage | |
| package-name: ${{ matrix.package }} | |
| aws-role-arn: ${{ secrets.AWS_ROLE_ARN }} | |
| aws-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| aws-distribution-id: ${{ secrets.AWS_DISTRIBUTION_ID }} | |
| debug-validation: | |
| name: ${{ matrix.job_name }} | |
| needs: changes | |
| if: ${{ !cancelled() && (needs.changes.outputs.should_build == 'true' || needs.changes.result == 'skipped') && (github.event_name != 'workflow_dispatch' || inputs.build_type == 'Debug') }} | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: ${{ matrix.timeout_minutes }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # - job_name: Test + Coverage linux_gcc_64 Debug | |
| # mode: coverage | |
| # timeout_minutes: 60 | |
| # fetch_depth: 0 | |
| # fetch_tags: false | |
| - job_name: Sanitizers linux_gcc_64 Debug (ASan+UBSan) | |
| mode: sanitizers | |
| timeout_minutes: 120 | |
| fetch_depth: 1 | |
| fetch_tags: false | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@v2 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: ${{ matrix.fetch_depth }} | |
| fetch-tags: ${{ matrix.fetch_tags }} | |
| - name: Restore file timestamps | |
| if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' | |
| uses: chetan/git-restore-mtime-action@v2 | |
| - name: Build Setup | |
| uses: ./.github/actions/build-setup | |
| with: | |
| restore-timestamps: 'false' | |
| qt-host: linux | |
| qt-arch: linux_gcc_64 | |
| build-type: Debug | |
| - name: Install Dependencies | |
| uses: ./.github/actions/install-dependencies | |
| - name: Configure (Coverage) | |
| if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' | |
| uses: ./.github/actions/cmake-configure | |
| with: | |
| build-dir: ${{ runner.temp }}/build | |
| build-type: Debug | |
| testing: 'true' | |
| coverage: 'true' | |
| stable: ${{ (github.ref_type == 'tag' || contains(github.ref, 'Stable')) && 'true' || 'false' }} | |
| - name: Build (Coverage) | |
| if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' | |
| uses: ./.github/actions/cmake-build | |
| with: | |
| build-dir: ${{ runner.temp }}/build | |
| build-type: Debug | |
| - name: Configure (ASan+UBSan) | |
| if: matrix.mode == 'sanitizers' | |
| uses: ./.github/actions/cmake-configure | |
| with: | |
| build-dir: ${{ runner.temp }}/build | |
| build-type: Debug | |
| testing: 'true' | |
| stable: ${{ (github.ref_type == 'tag' || contains(github.ref, 'Stable')) && 'true' || 'false' }} | |
| extra-args: -DQGC_ENABLE_ASAN=ON -DQGC_ENABLE_UBSAN=ON | |
| - name: Build (ASan+UBSan) | |
| if: matrix.mode == 'sanitizers' | |
| uses: ./.github/actions/cmake-build | |
| with: | |
| build-dir: ${{ runner.temp }}/build | |
| build-type: Debug | |
| - name: Run Unit Tests | |
| if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' | |
| uses: ./.github/actions/run-unit-tests | |
| with: | |
| build-dir: ${{ runner.temp }}/build | |
| junit-output: junit-results-linux-linux_gcc_64.xml | |
| ctest-output: test-output-linux-linux_gcc_64.txt | |
| include-labels: 'Unit|Integration' | |
| exclude-labels: 'Flaky|Network' | |
| # All tests execute the same instrumented binary; serial execution | |
| # avoids gcov data file races that can yield empty coverage output. | |
| parallel: '1' | |
| - name: Run Unit Tests (ASan+UBSan) | |
| if: matrix.mode == 'sanitizers' | |
| uses: ./.github/actions/run-unit-tests | |
| env: | |
| ASAN_OPTIONS: detect_leaks=1:halt_on_error=1:check_initialization_order=1 | |
| LSAN_OPTIONS: suppressions=${{ runner.temp }}/build/asan_suppressions.txt | |
| UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1:suppressions=${{ runner.temp }}/build/ubsan_suppressions.txt | |
| with: | |
| build-dir: ${{ runner.temp }}/build | |
| junit-output: junit-results-linux-sanitizers.xml | |
| ctest-output: test-output-linux-sanitizers.txt | |
| include-labels: 'Unit|Integration' | |
| exclude-labels: 'Flaky|Network|NoSanitizer' | |
| parallel: '1' | |
| - name: Analyze Unit Test Durations | |
| if: always() && !cancelled() && matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' | |
| uses: ./.github/actions/test-duration-report | |
| with: | |
| junit-path: ${{ runner.temp }}/build/junit-results-linux-linux_gcc_64.xml | |
| baseline-path: ${{ github.workspace }}/.github/test-duration-baseline.json | |
| report-json-path: ${{ runner.temp }}/build/test-duration-linux-linux_gcc_64.json | |
| top-n: '20' | |
| slow-threshold-seconds: '60' | |
| regression-factor: '1.5' | |
| min-delta-seconds: '5' | |
| fail-on-regression: 'false' | |
| - name: Analyze Unit Test Durations (Sanitizers) | |
| if: always() && !cancelled() && matrix.mode == 'sanitizers' | |
| uses: ./.github/actions/test-duration-report | |
| with: | |
| junit-path: ${{ runner.temp }}/build/junit-results-linux-sanitizers.xml | |
| baseline-path: ${{ github.workspace }}/.github/test-duration-baseline.json | |
| report-json-path: ${{ runner.temp }}/build/test-duration-linux-sanitizers.json | |
| top-n: '20' | |
| slow-threshold-seconds: '60' | |
| regression-factor: '1.5' | |
| min-delta-seconds: '5' | |
| fail-on-regression: 'false' | |
| - name: Report Test Results | |
| if: always() && !cancelled() && matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' | |
| uses: ./.github/actions/test-report | |
| with: | |
| name: Unit Tests (linux_gcc_64) | |
| build-dir: ${{ runner.temp }}/build | |
| junit-file: junit-results-linux-linux_gcc_64.xml | |
| output-file: test-output-linux-linux_gcc_64.txt | |
| artifact-name: test-results-linux_gcc_64 | |
| retention-days: 7 | |
| trunk-org-slug: ${{ vars.TRUNK_ORG_SLUG }} | |
| trunk-token: ${{ secrets.TRUNK_TOKEN }} | |
| - name: Upload Duration Report | |
| if: always() && !cancelled() && matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: test-duration-linux_gcc_64 | |
| path: ${{ runner.temp }}/build/test-duration-linux-linux_gcc_64.json | |
| retention-days: 7 | |
| - name: Upload Test Results (Sanitizers) | |
| if: always() && !cancelled() && matrix.mode == 'sanitizers' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: test-results-linux-sanitizers | |
| path: | | |
| ${{ runner.temp }}/build/test-output-linux-sanitizers.txt | |
| ${{ runner.temp }}/build/junit-results-linux-sanitizers.xml | |
| ${{ runner.temp }}/build/test-duration-linux-sanitizers.json | |
| retention-days: 7 | |
| - name: Ensure coverage tools are available | |
| if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' | |
| run: | | |
| ccache_path="$(command -v ccache 2>/dev/null)" || { | |
| echo "Error: ccache not found — expected pinned binary from cache action" >&2 | |
| exit 1 | |
| } | |
| echo "ccache found at: ${ccache_path}" | |
| if [[ "${ccache_path}" != "/usr/local/bin/ccache" ]]; then | |
| echo "Warning: ccache resolved to ${ccache_path}, expected /usr/local/bin/ccache" >&2 | |
| fi | |
| ccache_ver="$(ccache --version | head -1)" | |
| echo "ccache version: ${ccache_ver}" | |
| if ! command -v gcovr >/dev/null 2>&1; then | |
| pipx install gcovr | |
| fi | |
| - name: Verify coverage data files exist | |
| if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' | |
| working-directory: ${{ runner.temp }}/build | |
| run: | | |
| gcda_count=$(find . -name '*.gcda' | wc -l | tr -d '[:space:]') | |
| echo "Detected ${gcda_count} .gcda files" | |
| if [ "${gcda_count}" -eq 0 ]; then | |
| echo "::error::No .gcda files were produced by test execution" | |
| exit 1 | |
| fi | |
| - name: Coverage Report | |
| if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' | |
| uses: ./.github/actions/coverage | |
| with: | |
| build-dir: ${{ runner.temp }}/build | |
| mode: report-only | |
| - name: Verify Coverage Thresholds | |
| if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' | |
| working-directory: ${{ runner.temp }}/build | |
| run: | | |
| line_thresh=$(cmake -L -N . 2>/dev/null | sed -n 's/^QGC_COVERAGE_LINE_THRESHOLD:STRING=//p') | |
| branch_thresh=$(cmake -L -N . 2>/dev/null | sed -n 's/^QGC_COVERAGE_BRANCH_THRESHOLD:STRING=//p') | |
| line_thresh="${line_thresh:-30}" | |
| branch_thresh="${branch_thresh:-20}" | |
| python3 - "$line_thresh" "$branch_thresh" <<'PY' | |
| import os, sys | |
| from pathlib import Path | |
| sys.path.insert(0, os.path.join(os.environ["GITHUB_WORKSPACE"], ".github", "scripts")) | |
| from xml_utils import xml_parse | |
| line_thresh = float(sys.argv[1]) | |
| branch_thresh = float(sys.argv[2]) | |
| if not Path("coverage.xml").exists(): | |
| print("::warning::coverage.xml not found, skipping threshold check") | |
| sys.exit(0) | |
| tree = xml_parse("coverage.xml") | |
| cov = tree.getroot() | |
| lines_valid = int(cov.get("lines-valid", 0)) | |
| line_rate = float(cov.get("line-rate", 0)) * 100 | |
| branch_rate = float(cov.get("branch-rate", 0)) * 100 | |
| if lines_valid == 0: | |
| lines_covered = int(cov.get("lines-covered", 0)) | |
| print( | |
| "::error::Coverage report contains 0 lines — " | |
| f"lines-covered={lines_covered}, line-rate={line_rate:.2f}%, branch-rate={branch_rate:.2f}%" | |
| ) | |
| output = Path("coverage-output.txt") | |
| if output.exists(): | |
| print("::group::coverage-output tail") | |
| for line in output.read_text(encoding="utf-8", errors="replace").splitlines()[-40:]: | |
| print(line) | |
| print("::endgroup::") | |
| sys.exit(1) | |
| print(f"Line coverage: {line_rate:.1f}% (threshold: {line_thresh}%)") | |
| print(f"Branch coverage: {branch_rate:.1f}% (threshold: {branch_thresh}%)") | |
| failed = False | |
| if line_rate < line_thresh: | |
| print(f"::error::Line coverage {line_rate:.1f}% is below {line_thresh}% threshold") | |
| failed = True | |
| if branch_rate < branch_thresh: | |
| print(f"::error::Branch coverage {branch_rate:.1f}% is below {branch_thresh}% threshold") | |
| failed = True | |
| sys.exit(1 if failed else 0) | |
| PY | |