Repository navigation
Connect v3 CI, fuzz campaigns and guarded release publication #995
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: "Verify Build" | |
| on: | |
| push: | |
| branches: [master, v3] | |
| pull_request: | |
| # The branches below must be a subset of the branches above | |
| branches: [master, v3] | |
| schedule: | |
| - cron: '0 0 * * 0' | |
| # Restrict every job's GITHUB_TOKEN to read-only by default. | |
| # Jobs that need broader access should override at the job level. | |
| permissions: | |
| contents: read | |
| jobs: | |
| source: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| native: ${{ steps.source.outputs.native }} | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 | |
| - id: source | |
| run: | | |
| if grep -q 'libhttpserver_v3core_la_SOURCES' src/Makefile.am; then | |
| echo 'native=true' >> "$GITHUB_OUTPUT" | |
| else | |
| echo 'native=false' >> "$GITHUB_OUTPUT" | |
| fi | |
| verify: | |
| needs: source | |
| if: needs.source.outputs.native == 'false' | |
| name: Verify | |
| runs-on: ${{ matrix.os }} | |
| env: | |
| DEBUG: ${{ matrix.debug }} | |
| COVERAGE: ${{ matrix.coverage }} | |
| LINKING: ${{ matrix.linking }} | |
| BUILD_TYPE: ${{ matrix.build-type }} | |
| CC: ${{ matrix.c-compiler }} | |
| CXX: ${{ matrix.cc-compiler }} | |
| defaults: | |
| run: | |
| shell: ${{ matrix.shell }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| os-type: [ubuntu, mac] | |
| test-group: [basic] | |
| compiler-family: [none] | |
| c-compiler: [gcc, clang] | |
| cc-compiler: [g++, clang++] | |
| debug: [debug, nodebug] | |
| coverage: [coverage, nocoverage] | |
| linking: [dynamic, static] | |
| build-type: [classic] | |
| shell: [bash] | |
| exclude: | |
| - os: ubuntu-latest | |
| os-type: mac | |
| - os: macos-latest | |
| os-type: ubuntu | |
| - c-compiler: gcc | |
| cc-compiler: clang++ | |
| - c-compiler: clang | |
| cc-compiler: g++ | |
| - c-compiler: clang | |
| debug: debug | |
| - debug: debug | |
| coverage: nocoverage | |
| - debug: nodebug | |
| coverage: coverage | |
| - linking: static | |
| debug: debug | |
| - linking: static | |
| coverage: coverage | |
| include: | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: asan | |
| compiler-family: clang | |
| c-compiler: clang-18 | |
| cc-compiler: clang++-18 | |
| debug: debug | |
| coverage: nocoverage | |
| shell: bash | |
| # TASK-087: restored msan lane. The previous entry (retired | |
| # Ubuntu / Clang toolchain) was dropped when GitHub removed that | |
| # runner image and never replaced, so MemorySanitizer went | |
| # silently absent. This lane runs clang-18 against an | |
| # instrumented libc++ with | |
| # curl kept out of the picture. See the "instrumented libc++" | |
| # and "Run tests" steps below for how the false-positive-free | |
| # dependency set is assembled and why coverage is scoped to the | |
| # backend-free unit tests. | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: msan | |
| compiler-family: clang | |
| c-compiler: clang-18 | |
| cc-compiler: clang++-18 | |
| debug: debug | |
| coverage: nocoverage | |
| shell: bash | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: lsan | |
| compiler-family: clang | |
| c-compiler: clang-18 | |
| cc-compiler: clang++-18 | |
| debug: debug | |
| coverage: nocoverage | |
| shell: bash | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: tsan | |
| compiler-family: clang | |
| c-compiler: clang-18 | |
| cc-compiler: clang++-18 | |
| debug: debug | |
| coverage: nocoverage | |
| shell: bash | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: ubsan | |
| compiler-family: clang | |
| c-compiler: clang-18 | |
| cc-compiler: clang++-18 | |
| debug: debug | |
| coverage: nocoverage | |
| shell: bash | |
| # gcc-9 and gcc-10 dropped (were here); gcc-11 is now the minimum GCC entry in the extra matrix. | |
| # Both lack full C++20 library support (no concepts, no std::span, no <bit>). | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: none | |
| compiler-family: gcc | |
| c-compiler: gcc-11 | |
| cc-compiler: g++-11 | |
| debug: nodebug | |
| coverage: nocoverage | |
| shell: bash | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: none | |
| compiler-family: gcc | |
| c-compiler: gcc-12 | |
| cc-compiler: g++-12 | |
| debug: nodebug | |
| coverage: nocoverage | |
| shell: bash | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: none | |
| compiler-family: gcc | |
| c-compiler: gcc-13 | |
| cc-compiler: g++-13 | |
| debug: nodebug | |
| coverage: nocoverage | |
| shell: bash | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: none | |
| compiler-family: gcc | |
| c-compiler: gcc-14 | |
| cc-compiler: g++-14 | |
| debug: nodebug | |
| coverage: nocoverage | |
| linking: dynamic | |
| shell: bash | |
| # clang-11, clang-12, clang-14, and clang-15 dropped (were here); clang-13 is now the minimum Clang entry. | |
| # All four lack full C++20 support (concepts/<bit>/<span> gaps); clang-13 passes the autoconf C++20 check on ubuntu-22.04. | |
| - test-group: extra | |
| os: ubuntu-22.04 | |
| os-type: ubuntu | |
| build-type: none | |
| compiler-family: clang | |
| c-compiler: clang-13 | |
| cc-compiler: clang++-13 | |
| debug: nodebug | |
| coverage: nocoverage | |
| shell: bash | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: none | |
| compiler-family: clang | |
| c-compiler: clang-16 | |
| cc-compiler: clang++-16 | |
| debug: nodebug | |
| coverage: nocoverage | |
| shell: bash | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: none | |
| compiler-family: clang | |
| c-compiler: clang-17 | |
| cc-compiler: clang++-17 | |
| debug: nodebug | |
| coverage: nocoverage | |
| shell: bash | |
| # TASK-088: split valgrind into three parallel matrix entries — one | |
| # per tool (memcheck / helgrind / drd). GitHub Actions runs them | |
| # concurrently, capping wall-clock cost at the slowest single tool | |
| # instead of the sum of all three serially. | |
| # | |
| # The full check_PROGRAMS suite runs under each tool. helgrind/drd | |
| # scope is intentionally NOT narrowed to unit/ tests only: the | |
| # concurrency tests (threadsafety_stress, route_table_concurrency) | |
| # are the primary reason for running race detectors (DR-008) and must | |
| # not be subsetted out. A per-step timeout-minutes guards against a | |
| # hung race-detector run (helgrind/drd can serialize busy-waits into | |
| # apparent livelocks). | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: valgrind-memcheck | |
| compiler-family: gcc | |
| c-compiler: gcc-14 | |
| cc-compiler: g++-14 | |
| debug: nodebug # configure step passes --enable-debug unconditionally for valgrind-* lanes | |
| coverage: nocoverage | |
| shell: bash | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: valgrind-helgrind | |
| compiler-family: gcc | |
| c-compiler: gcc-14 | |
| cc-compiler: g++-14 | |
| debug: nodebug | |
| coverage: nocoverage | |
| shell: bash | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: valgrind-drd | |
| compiler-family: gcc | |
| c-compiler: gcc-14 | |
| cc-compiler: g++-14 | |
| debug: nodebug | |
| coverage: nocoverage | |
| shell: bash | |
| # Test build without digest auth support (issue #232) | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: iwyu | |
| compiler-family: clang | |
| c-compiler: clang-18 | |
| cc-compiler: clang++-18 | |
| debug: nodebug | |
| coverage: nocoverage | |
| shell: bash | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: lint | |
| compiler-family: gcc | |
| c-compiler: gcc-14 | |
| cc-compiler: g++-14 | |
| debug: debug | |
| coverage: nocoverage | |
| shell: bash | |
| # TASK-007: dedicated header-hygiene gate. Runs `make check-hygiene` | |
| # (preprocesses <httpserver.hpp> against the staged install and greps | |
| # for forbidden backend headers). Surfaces this gate as its own named | |
| # GitHub Actions check so reviewers see header-hygiene status | |
| # independently of the broader `make check` log. HEADER_HYGIENE_STRICT | |
| # defaults to "yes" (flipped from "no" at TASK-020 close); any leak | |
| # of forbidden backend headers through the umbrella fails CI loudly. | |
| - test-group: extra | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: header-hygiene | |
| compiler-family: gcc | |
| c-compiler: gcc-14 | |
| cc-compiler: g++-14 | |
| debug: nodebug | |
| coverage: nocoverage | |
| linking: dynamic | |
| shell: bash | |
| # TASK-037 / PRD-FLG-REQ-001: flag-invariance-on gate. | |
| - test-group: basic | |
| os: windows-latest | |
| os-type: windows | |
| msys-env: MINGW64 | |
| shell: 'msys2 {0}' | |
| build-type: classic | |
| compiler-family: none | |
| c-compiler: gcc | |
| cc-compiler: g++ | |
| debug: nodebug | |
| coverage: nocoverage | |
| linking: dynamic | |
| - test-group: basic | |
| os: windows-latest | |
| os-type: windows | |
| msys-env: MSYS | |
| shell: 'msys2 {0}' | |
| build-type: classic | |
| compiler-family: none | |
| c-compiler: gcc | |
| cc-compiler: g++ | |
| debug: nodebug | |
| coverage: nocoverage | |
| linking: dynamic | |
| # ARM 32-bit cross-compilation (ARMv7 hard-float) | |
| - test-group: cross-compile | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: arm32 | |
| compiler-family: arm-cross | |
| c-compiler: arm-linux-gnueabihf-gcc | |
| cc-compiler: arm-linux-gnueabihf-g++ | |
| debug: nodebug | |
| coverage: nocoverage | |
| linking: dynamic | |
| shell: bash | |
| # ARM 64-bit cross-compilation (AArch64) | |
| - test-group: cross-compile | |
| os: ubuntu-latest | |
| os-type: ubuntu | |
| build-type: arm64 | |
| compiler-family: arm-cross | |
| c-compiler: aarch64-linux-gnu-gcc | |
| cc-compiler: aarch64-linux-gnu-g++ | |
| debug: nodebug | |
| coverage: nocoverage | |
| linking: dynamic | |
| shell: bash | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| # We must fetch at least the immediate parents so that if this is | |
| # a pull request then we can checkout the head. | |
| fetch-depth: 2 | |
| # If this run was triggered by a pull request event, then checkout | |
| # the head of the pull request instead of the merge commit. | |
| - run: git checkout HEAD^2 | |
| shell: bash | |
| if: ${{ github.event_name == 'pull_request' }} | |
| - name: Setup MSYS2 | |
| if: ${{ matrix.os-type == 'windows' }} | |
| uses: msys2/setup-msys2@e9898307ac31d1a803454791be09ab9973336e1c # v2.31.1 | |
| with: | |
| msystem: ${{ matrix.msys-env }} | |
| update: true | |
| install: >- | |
| autotools | |
| base-devel | |
| - name: Install MinGW64 packages | |
| if: ${{ matrix.os-type == 'windows' && matrix.msys-env == 'MINGW64' }} | |
| run: | | |
| # Invariance exclusion (doxygen) — TASK-090, documenting the gap the | |
| # v2 audit flagged. | |
| # WHAT: the doxygen zero-warning invariant (scripts/check-doxygen.sh, | |
| # TASK-043) runs via `make check-local` on EVERY lane, but that | |
| # script auto-SKIPs (exit 0) when doxygen is not on $PATH — see its | |
| # `command -v doxygen` guard (scripts/check-doxygen.sh, the | |
| # "SKIP — doxygen not installed" branch). doxygen + graphviz are | |
| # intentionally NOT installed here, so the invariant silently no-ops | |
| # on Windows. | |
| # WHY excluded: `check-doxygen.sh` spawns `make doxygen-run`, and the | |
| # autoconf-archive-generated MINGW recipe hangs indefinitely waiting | |
| # on a cmd.exe batch prompt (the job timed out at the 6h CI ceiling). | |
| # WHY it is safe: the invariant is fully enforced on the Linux lanes, | |
| # where doxygen + graphviz ARE installed, so this is a deliberate | |
| # per-platform exclusion, not a coverage gap. The Windows lane just | |
| # builds and tests the library. | |
| pacman --noconfirm -S --needed mingw-w64-x86_64-{toolchain,libtool,make,pkg-config,libsystre,curl} | |
| - name: Install MSYS packages | |
| if: ${{ matrix.os-type == 'windows' && matrix.msys-env == 'MSYS' }} | |
| run: | | |
| pacman --noconfirm -S --needed msys2-devel gcc make libcurl-devel | |
| - name: Install Ubuntu test sources | |
| # ppa:ubuntu-toolchain-r/test was historically used to backport newer | |
| # gcc onto older Ubuntu LTS. With the C++20 floor (TASK-001), our matrix | |
| # only retains compilers that ship in stock ubuntu-22.04 / 24.04 repos | |
| # (gcc-11..14, clang-13/16/17/18), so the PPA is no longer needed -- and | |
| # add-apt-repository talks to launchpad, which is a flaky dependency. | |
| run: | | |
| sudo apt-get update ; | |
| if: ${{ matrix.os-type == 'ubuntu' }} | |
| - name: Install apache benchmark if needed | |
| run: sudo apt-get install apache2-utils ; | |
| if: ${{ matrix.test-group == 'performance' && matrix.os-type == 'ubuntu' }} | |
| - name: Install optional clang if needed | |
| run: sudo apt-get install ${{ matrix.c-compiler }} | |
| if: ${{ matrix.compiler-family == 'clang' && matrix.os-type == 'ubuntu' }} | |
| - name: Install optional gcc if needed | |
| run: sudo apt-get install ${{ matrix.cc-compiler }} | |
| if: ${{ matrix.compiler-family == 'gcc' && matrix.os-type == 'ubuntu' }} | |
| - name: Install ARM cross-compilation toolchain | |
| run: | | |
| sudo apt-get update | |
| if [ "${{ matrix.build-type }}" = "arm32" ]; then | |
| sudo apt-get install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf | |
| elif [ "${{ matrix.build-type }}" = "arm64" ]; then | |
| sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu | |
| fi | |
| if: ${{ matrix.compiler-family == 'arm-cross' }} | |
| - name: Install valgrind if needed | |
| run: sudo apt-get install valgrind | |
| if: ${{ startsWith(matrix.build-type, 'valgrind-') && matrix.os-type == 'ubuntu' }} | |
| - name: Install cpplint if needed | |
| run: sudo pip3 install cpplint ; | |
| if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} | |
| - name: Install lizard if needed | |
| run: sudo pip3 install lizard ; | |
| if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} | |
| - name: Install PMD (CPD) if needed | |
| # CPD is shipped as part of the PMD distribution. The apt package | |
| # is pinned to PMD 6.x; we want the modern PMD 7 CLI (`pmd cpd ...`) | |
| # to match the script. Java is pre-installed on ubuntu-latest. | |
| # | |
| # Supply-chain: pmd.zip is sha256-pinned. To rotate PMD, update | |
| # BOTH PMD_VERSION and PMD_SHA256 in the same PR. Obtain the new | |
| # digest from the GitHub release page's pmd-dist-<ver>-bin.zip | |
| # asset (or `sha256sum` the freshly downloaded zip and record the | |
| # resulting value in PMD_SHA256 in this file). | |
| # | |
| # For defence-in-depth, cross-check the digest against the | |
| # pmd-dist-<ver>-bin.zip.sha256 sidecar on the same release page. | |
| # Alternatively, verify the Maven Central / OSSRH attestation for | |
| # the same version if the GitHub release page itself may be | |
| # compromised at rotation time. | |
| run: | | |
| set -euo pipefail | |
| PMD_VERSION=7.24.0 | |
| PMD_SHA256=110934b36d39c19094d1b77386931978093f238f2c2f1851748822b69c7367ac | |
| curl -fsSL -o /tmp/pmd.zip \ | |
| "https://github.com/pmd/pmd/releases/download/pmd_releases%2F${PMD_VERSION}/pmd-dist-${PMD_VERSION}-bin.zip" | |
| echo "${PMD_SHA256} /tmp/pmd.zip" | sha256sum -c | |
| sudo unzip -q /tmp/pmd.zip -d /opt | |
| sudo ln -sf "/opt/pmd-bin-${PMD_VERSION}/bin/pmd" /usr/local/bin/pmd | |
| pmd --version | |
| if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} | |
| - name: Install IWYU dependencies if needed | |
| run: | | |
| sudo apt-get install llvm-18-dev libclang-18-dev clang-18 ; | |
| if: ${{ matrix.build-type == 'iwyu' && matrix.os-type == 'ubuntu' }} | |
| - name: IWYU from cache (for testing) | |
| id: cache-IWYU | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: include-what-you-use | |
| key: ${{ matrix.os }}-${{ matrix.c-compiler }}-include-what-you-use-pre-built | |
| if: ${{ matrix.build-type == 'iwyu' && matrix.os-type == 'ubuntu' }} | |
| # Installing iwyu manually because clang and iwyu paths won't match on Ubuntu otherwise. | |
| # Supply-chain: pinned to an immutable commit SHA on the upstream clang_18 | |
| # branch (latest commit as of 2024-02-10, "[ci] Pin to LLVM 18"). To rotate, | |
| # obtain the new SHA from https://github.com/include-what-you-use/include-what-you-use/commits/clang_18 | |
| # and update IWYU_SHA below; the assertion will fail-fast on any mismatch. | |
| - name: Build IWYU if requested | |
| run: | | |
| set -euo pipefail | |
| IWYU_SHA=377eaef70cdda47368939f4d9beabfabe3f628f0 | |
| CLANG_ROOT_PATH=`llvm-config-18 --prefix` | |
| CLANG_BIN_PATH=`llvm-config-18 --bindir` | |
| git clone https://github.com/include-what-you-use/include-what-you-use.git | |
| cd include-what-you-use | |
| git checkout "${IWYU_SHA}" | |
| HEAD_SHA="$(git rev-parse --verify HEAD)" | |
| if [ "${HEAD_SHA}" != "${IWYU_SHA}" ]; then | |
| echo "IWYU SHA mismatch: expected ${IWYU_SHA}, got ${HEAD_SHA}" >&2 | |
| exit 1 | |
| fi | |
| echo "IWYU pinned at SHA: ${HEAD_SHA}" | |
| mkdir build_iwyu | |
| cd build_iwyu | |
| cmake -G "Unix Makefiles" -DCMAKE_PREFIX_PATH=$CLANG_ROOT_PATH -DCMAKE_C_COMPILER=$CLANG_BIN_PATH/clang -DCMAKE_CXX_COMPILER=$CLANG_BIN_PATH/clang++ ../ | |
| make | |
| sudo make install | |
| if: ${{ matrix.build-type == 'iwyu' && matrix.os-type == 'ubuntu' && steps.cache-IWYU.outputs.cache-hit != 'true' }} | |
| - name: Install IWYU if requested | |
| run: | | |
| cd include-what-you-use/build_iwyu ; | |
| sudo make install ; | |
| if: ${{ matrix.build-type == 'iwyu' && matrix.os-type == 'ubuntu' }} | |
| - name: CURL from cache (for testing) | |
| id: cache-CURL | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: curl-7.75.0 | |
| key: ${{ matrix.os }}-CURL-pre-built-v3 | |
| if: ${{ matrix.os == 'macos-latest' }} | |
| - name: Build CURL (for testing) | |
| # Supply-chain: curl-7.75.0.tar.gz is sha256-pinned. The expected digest | |
| # is the official upstream value also published at https://curl.se/download/ | |
| # (cross-checked against the libhttpserver S3 mirror). To rotate the curl | |
| # version, update both CURL_VERSION and CURL_SHA256 in the same PR. | |
| run: | | |
| set -euo pipefail | |
| CURL_VERSION=7.75.0 | |
| CURL_SHA256=4d51346fe621624c3e4b9f86a8fd6f122a143820e17889f59c18f245d2d8e7a6 | |
| curl -fsSL -o "curl-${CURL_VERSION}.tar.gz" \ | |
| "https://libhttpserver.s3.amazonaws.com/travis_stuff/curl-${CURL_VERSION}.tar.gz" | |
| echo "${CURL_SHA256} curl-${CURL_VERSION}.tar.gz" | shasum -a 256 -c | |
| tar -xzf "curl-${CURL_VERSION}.tar.gz" | |
| cd "curl-${CURL_VERSION}" | |
| ./configure --with-darwinssl --without-ssl --without-nghttp2 --without-libidn2 --without-libssh2 --without-brotli --without-zstd --without-librtmp --without-libpsl --disable-ldap --disable-ldaps | |
| make | |
| if: ${{ matrix.os == 'macos-latest' && steps.cache-CURL.outputs.cache-hit != 'true' }} | |
| - name: Install CURL (for testing on mac only) | |
| run: cd curl-7.75.0 ; sudo make install ; | |
| if: ${{ matrix.os == 'macos-latest' }} | |
| - name: Install CURL (for testing on linux) | |
| run: sudo apt-get install libcurl4-openssl-dev | |
| if: ${{ matrix.os-type == 'ubuntu' }} | |
| - name: Install autotools on mac | |
| run: brew install autoconf automake libtool | |
| if: ${{ matrix.os == 'macos-latest' }} | |
| - name: Setup dependencies (only on linux) | |
| run: | | |
| sudo apt-get install info install-info ; | |
| sudo apt-get install cppcheck ; | |
| if: ${{ matrix.os-type == 'ubuntu' }} | |
| - name: Setup coverage dependencies (only on linux) | |
| run: | | |
| sudo pip install gcovr ; | |
| if: ${{ matrix.os-type == 'ubuntu' && matrix.c-compiler == 'gcc' && matrix.debug == 'debug' && matrix.coverage == 'coverage' }} | |
| - name: Fetch instrumented libc++ from cache (msan lane) | |
| id: cache-libcxx-msan | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ${{ github.workspace }}/libcxx-msan | |
| key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libcxx-msan-llvmorg-18.1.8-origins-v1 | |
| if: ${{ matrix.build-type == 'msan' }} | |
| - name: Build instrumented libc++ (msan lane, if not cached) | |
| # Supply-chain: llvmorg-18.1.8 is a mutable git tag, not an immutable | |
| # commit SHA, so a post-clone SHA assertion guards against the tag | |
| # being force-moved upstream (CWE-829). LLVM_SHA was obtained via | |
| # `git ls-remote https://github.com/llvm/llvm-project refs/tags/llvmorg-18.1.8` | |
| # at pin time; to rotate the tag, update both the --branch value below | |
| # and LLVM_SHA in the same PR. Mirrors the IWYU SHA-pin pattern above. | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get install -y ninja-build cmake ; | |
| LLVM_SHA=3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff ; | |
| git clone --depth 1 --branch llvmorg-18.1.8 https://github.com/llvm/llvm-project.git llvm-project ; | |
| HEAD_SHA="$(git -C llvm-project rev-parse --verify HEAD)" ; | |
| if [ "${HEAD_SHA}" != "${LLVM_SHA}" ]; then | |
| echo "LLVM SHA mismatch: expected ${LLVM_SHA}, got ${HEAD_SHA}" >&2 ; | |
| exit 1 ; | |
| fi | |
| echo "llvm-project pinned at SHA: ${HEAD_SHA}" ; | |
| cmake -G Ninja -S llvm-project/runtimes -B build-libcxx-msan \ | |
| -DCMAKE_C_COMPILER=clang-18 \ | |
| -DCMAKE_CXX_COMPILER=clang++-18 \ | |
| -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi" \ | |
| -DLIBCXXABI_USE_LLVM_UNWINDER=OFF \ | |
| -DLLVM_USE_SANITIZER=MemoryWithOrigins \ | |
| -DCMAKE_BUILD_TYPE=RelWithDebInfo \ | |
| -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/libcxx-msan" ; | |
| ninja -C build-libcxx-msan cxx cxxabi ; | |
| ninja -C build-libcxx-msan install-cxx install-cxxabi ; | |
| if: ${{ matrix.build-type == 'msan' && steps.cache-libcxx-msan.outputs.cache-hit != 'true' }} | |
| - name: Refresh links to shared libs | |
| run: sudo ldconfig ; | |
| if: ${{ matrix.os-type == 'ubuntu' }} | |
| - name: Run cpplint on code | |
| run: cpplint --extensions=cpp,hpp --headers=hpp --recursive . ; | |
| if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} | |
| - name: Run per-function complexity gate (lizard) | |
| run: ./scripts/check-complexity.sh | |
| if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} | |
| - name: Run copy/paste detection gate (PMD CPD) | |
| run: ./scripts/check-duplication.sh | |
| if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} | |
| - name: Run per-file line-count gate | |
| run: ./scripts/check-file-size.sh | |
| if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} | |
| - name: Run warning-suppression gate | |
| # TASK-060: forbid file-scoped `#pragma GCC diagnostic ignored | |
| # "-Warray-bounds"` (and any future broadened list) on the watched | |
| # library TUs. A regression here would silently turn the warning | |
| # off TU-wide. | |
| run: ./scripts/check-warning-suppressions.sh | |
| if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} | |
| - name: Run skip-rationale gate | |
| # TASK-077: forbid `#ifndef _WINDOWS`, `#ifndef DARWIN`, or | |
| # `#if !defined(_WINDOWS)` blocks in `test/integ/` that lack a | |
| # `// reason:` comment pointing at `test/PORTABILITY.md` (or a | |
| # follow-up task). Without this gate, whole-suite skips silently | |
| # drift on the Windows / Darwin CI lanes and the library's | |
| # portability claim quietly decays. | |
| run: ./scripts/check-skip-rationales.sh | |
| if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} | |
| - name: Run msan-lane structural gate | |
| # TASK-087: asserts the msan CI lane is wired correctly (valid YAML, | |
| # matrix entry present with mirrored keys, stale retired-OS commented | |
| # block gone, MSAN_OPTIONS + scoped TESTS= wiring present). Mirrors the | |
| # check-*.sh gate idiom used by complexity, duplication, file-size, | |
| # warning-suppressions, and skip-rationales gates. | |
| run: ./scripts/check-msan-lane.sh | |
| if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} | |
| - name: Run valgrind-lane structural gate | |
| # TASK-088: asserts the Valgrind CI lane is wired correctly (valid YAML, | |
| # valgrind matrix entry present, no --disable-valgrind-{helgrind,drd, | |
| # sgcheck} flags remain, `make "check-valgrind-${TOOL}"` (per-tool | |
| # AX_VALGRIND_CHECK target) invoked, and the results-print step | |
| # surfaces helgrind + drd logs). Valgrind is | |
| # Linux-only, so the actual race-detector run + finding-triage happen on | |
| # the PR's Linux lane; this gate only guards the wiring. Mirrors the | |
| # check-msan-lane.sh gate idiom. | |
| run: ./scripts/check-valgrind-lane.sh | |
| if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} | |
| - name: Run CodeQL-workflow structural gate | |
| # TASK-090: asserts codeql-analysis.yml stays hardened — every action | |
| # SHA-pinned (no floating @vN), the dead Autobuild scaffolding gone, an | |
| # explicit ./configure + make build present so CodeQL's C/C++ extractor | |
| # traces the real compile, and codeql-action/init + analyze pinned to the | |
| # SAME v3 SHA. To rotate the CodeQL SHA see the header comment in | |
| # codeql-analysis.yml. Mirrors the check-valgrind-lane.sh gate idiom. | |
| run: ./scripts/check-codeql-workflow.sh | |
| if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} | |
| - name: Run workflow-pinning / Codecov structural gate | |
| # TASK-090: cross-workflow invariants — BOTH codeql-analysis.yml and | |
| # verify-build.yml carry only SHA-pinned actions, the Codecov upload | |
| # hard-fails CI (fail_ci_if_error: true, an UPLOAD failure — distinct | |
| # from a coverage-percentage drop, which codecov.yml gates), and the | |
| # Windows doxygen invariance exclusion stays documented. Mirrors the | |
| # check-msan-lane.sh gate idiom. | |
| run: ./scripts/check-workflow-pinning.sh | |
| if: ${{ matrix.build-type == 'lint' && matrix.os-type == 'ubuntu' }} | |
| - name: Run libhttpserver configure | |
| # TASK-038: fixed CXXLAGS -> CXXFLAGS typo so C++ TUs are instrumented by the sanitizer matrix. | |
| run: | | |
| # Set memory check flags. They need to stay in step as env variables don't propagate across steps. | |
| if [ "$BUILD_TYPE" = "asan" ]; then export CFLAGS='-fsanitize=address'; export CXXFLAGS='-fsanitize=address'; export LDFLAGS='-fsanitize=address'; fi | |
| # TASK-087: msan needs the instrumented libc++ built above, plus | |
| # origins tracking and frame pointers for readable reports. Point | |
| # the C++ TUs at the instrumented libc++ headers/libs and drop the | |
| # default (uninstrumented) libstdc++ via -nostdinc++/-stdlib=libc++. | |
| if [ "$BUILD_TYPE" = "msan" ]; then | |
| LIBCXX_MSAN="${{ github.workspace }}/libcxx-msan" ; | |
| export CFLAGS="-fsanitize=memory -fsanitize-memory-track-origins -fno-omit-frame-pointer -g -O1" ; | |
| export CXXFLAGS="-stdlib=libc++ -nostdinc++ -isystem ${LIBCXX_MSAN}/include/c++/v1 -fsanitize=memory -fsanitize-memory-track-origins -fno-omit-frame-pointer -g -O1 -DLHS_SANITIZER_OWNS_OPERATOR_NEW" ; | |
| export LDFLAGS="-stdlib=libc++ -fsanitize=memory -L${LIBCXX_MSAN}/lib -Wl,-rpath,${LIBCXX_MSAN}/lib -lc++abi" ; | |
| fi | |
| # tsan/msan/lsan runtimes define their own global operator new/delete; | |
| # -DLHS_SANITIZER_OWNS_OPERATOR_NEW makes the arena alloc-pin test drop | |
| # its overrides so it links (see test/unit/http_request_unescape_arena_test.cpp). | |
| if [ "$BUILD_TYPE" = "lsan" ]; then export CFLAGS='-fsanitize=leak'; export CXXFLAGS='-fsanitize=leak -DLHS_SANITIZER_OWNS_OPERATOR_NEW'; export LDFLAGS='-fsanitize=leak'; fi | |
| if [ "$BUILD_TYPE" = "tsan" ]; then export CFLAGS='-fsanitize=thread'; export CXXFLAGS='-fsanitize=thread -DLHS_SANITIZER_OWNS_OPERATOR_NEW'; export LDFLAGS='-fsanitize=thread'; fi | |
| if [ "$BUILD_TYPE" = "ubsan" ]; then export CFLAGS='-fsanitize=undefined'; export CXXFLAGS='-fsanitize=undefined'; export LDFLAGS='-fsanitize=undefined'; fi | |
| # Additional flags on mac. They need to stay in step as env variables don't propagate across steps. | |
| if [ "${{ matrix.os }}" = "macos-latest" ]; then | |
| export CFLAGS='-mtune=generic' ; | |
| export IPV6_TESTS_ENABLED="true" ; | |
| fi | |
| ./bootstrap ; | |
| mkdir build ; | |
| cd build ; | |
| if [ "${{ matrix.compiler-family }}" = "arm-cross" ]; then | |
| export CPPFLAGS="-I${{ github.workspace }}/arm-sysroot/include" | |
| export LDFLAGS="-L${{ github.workspace }}/arm-sysroot/lib" | |
| export PKG_CONFIG_PATH="${{ github.workspace }}/arm-sysroot/lib/pkgconfig" | |
| if [ "${{ matrix.build-type }}" = "arm32" ]; then | |
| ../configure --host=arm-linux-gnueabihf --disable-fastopen; | |
| else | |
| ../configure --host=aarch64-linux-gnu --disable-fastopen; | |
| fi | |
| elif [ "$LINKING" = "static" ]; then | |
| ../configure --enable-static --disable-fastopen; | |
| elif [ "$DEBUG" = "debug" ] && [ "$COVERAGE" = "coverage" ]; then | |
| ../configure --enable-debug --enable-coverage --disable-shared --disable-fastopen; | |
| elif [ "$DEBUG" = "debug" ]; then | |
| ../configure --enable-debug --disable-shared --disable-fastopen; | |
| elif case "$BUILD_TYPE" in valgrind-*) true;; *) false;; esac; then | |
| # TASK-088: Valgrind lanes (valgrind-memcheck / valgrind-helgrind / | |
| # valgrind-drd). All three share the same configure flags. The race | |
| # detectors (helgrind and drd) were previously turned off by per-tool | |
| # disable flags; removing those flags re-enables them. Each lane | |
| # invokes a single make check-<tool> target derived from BUILD_TYPE, | |
| # so GitHub Actions runs memcheck, helgrind, and drd in parallel | |
| # rather than serially. sgcheck (exp-sgcheck) stays off: removed | |
| # upstream in Valgrind 3.20; ubuntu-latest ships 3.22; disabled in | |
| # configure.ac via AX_VALGRIND_DFLT([sgcheck],[off]). | |
| ../configure --enable-debug --disable-fastopen; | |
| else | |
| ../configure --disable-fastopen; | |
| fi | |
| - name: Print config.log | |
| shell: bash | |
| run: | | |
| cd build ; | |
| cat config.log ; | |
| if: ${{ failure() }} | |
| # Make or run iwyu. If running iwyu, check for the result code to be 2 (IWYU always returns an error code, if it is 2, no corrections are necessary). | |
| - name: Make or run IWYU | |
| run: | | |
| # IWYU always return an error code. If it returns "2" it indicates a success so we manage this within the function below. | |
| function safe_make_iwyu() { | |
| { | |
| make -k CXX='/usr/local/bin/include-what-you-use -Xiwyu --mapping_file=${top_builddir}/../custom_iwyu.imp' CXXFLAGS="-std=c++20 -DHTTPSERVER_COMPILATION -D_REENTRANT $CXXFLAGS" ; | |
| } || { | |
| if [ $? -ne 2 ]; then | |
| return 1; | |
| else | |
| return 0; | |
| fi | |
| } | |
| return 0; | |
| } | |
| cd build ; | |
| if [ "$BUILD_TYPE" = "iwyu" ]; then | |
| safe_make_iwyu ; | |
| else | |
| make ; | |
| fi | |
| - name: Run tests | |
| run: | | |
| cd build ; | |
| # tsan: suppress libstdc++'s benign std::ctype<char> narrow-cache race | |
| # (see test/tsan.supp). All libhttpserver-internal races stay fatal. | |
| if [ "$BUILD_TYPE" = "tsan" ]; then | |
| export TSAN_OPTIONS="suppressions=$(pwd)/../test/tsan.supp" ; | |
| make check ; | |
| elif [ "$BUILD_TYPE" = "msan" ]; then | |
| # TASK-087: msan coverage is deliberately scoped to the | |
| # backend-free UNIT tests. curl (linked into every integ | |
| # round-trip via test/integ/curl_helpers.hpp) cannot be | |
| # practically instrumented, so its uninstrumented output would | |
| # surface as false use-of-uninitialized-value reports. libc++ and | |
| # type inspection — runs clean without wildcard suppressions. | |
| export LD_LIBRARY_PATH="${{ github.workspace }}/libcxx-msan/lib:${LD_LIBRARY_PATH:-}" ; | |
| export MSAN_OPTIONS="abort_on_error=1" ; | |
| # Auto-wire narrow suppressions only if triage produced the file. | |
| # test/msan.supp is intentionally ABSENT until a real finding | |
| # forces a narrow use-of-uninitialized-value:<symbol> entry with | |
| # an upstream-issue comment (never a wildcard over libhttpserver). | |
| if [ -f ../test/msan.supp ]; then | |
| export MSAN_OPTIONS="${MSAN_OPTIONS}:suppressions=$(pwd)/../test/msan.supp" ; | |
| fi | |
| # Derive the unit/ subset from the test Makefile so newly-added | |
| # unit tests are covered automatically, then further exclude any | |
| # unit/-sourced target whose test file transitively links curl | |
| # (e.g. via curl_easy_perform() round-trips) -- curl cannot | |
| # practically be msan-instrumented, so its uninstrumented output | |
| # would surface as false use-of-uninitialized-value reports. | |
| # NOTE: this only matches a target's _SOURCES entry when it is | |
| # written on a single line (`<target>_SOURCES = unit/<file>.cpp`); | |
| # a future multi-line _SOURCES assignment, or a unit/ target whose | |
| # first source is not itself under unit/, would be silently | |
| # dropped without failing CI (the empty-set guard below only | |
| # catches total loss, not partial narrowing). | |
| MSAN_TESTS="" ; | |
| while IFS= read -r src_line; do | |
| target="$(printf '%s' "$src_line" | sed -E 's/_SOURCES.*//')" ; | |
| source_rel="$(printf '%s' "$src_line" | sed -E 's/^[A-Za-z0-9_]+_SOURCES[[:space:]]*=[[:space:]]*//')" ; | |
| if grep -q 'curl/curl.h' "../test/${source_rel}" 2>/dev/null; then | |
| echo "msan: excluding ${target} (${source_rel} includes curl/curl.h)" ; | |
| continue ; | |
| fi | |
| MSAN_TESTS="${MSAN_TESTS}${target} " ; | |
| done <<< "$(grep -E '^[A-Za-z0-9_]+_SOURCES[[:space:]]*=[[:space:]]*unit/' ../test/Makefile.am)" ; | |
| if [ -z "$MSAN_TESTS" ]; then echo "ERROR: msan: derived empty test set — grep pattern found no unit/ targets in test/Makefile.am" >&2; exit 1; fi ; | |
| make check TESTS="$MSAN_TESTS" ; | |
| else | |
| make check ; | |
| fi | |
| # TASK-037: flag-invariance-{on,off} are compile/link-only gates, so | |
| # skip the full `make check` (which spins up real servers and would | |
| # crash on the OFF lane for the BAUTH/DAUTH/WS unit tests anyway). | |
| if: ${{ matrix.build-type != 'iwyu' && matrix.compiler-family != 'arm-cross' && matrix.build-type != 'header-hygiene' }} | |
| - name: Run focused native HTTP/1 and WebSocket gates | |
| timeout-minutes: 10 | |
| run: | | |
| make -C build/test check-v3-http1-websocket | |
| if: ${{ matrix.build-type == 'asan' || matrix.build-type == 'ubsan' || matrix.build-type == 'tsan' }} | |
| - name: Rebuild native combined sanitizer gate with pinned independent client and fuzzing | |
| timeout-minutes: 20 | |
| run: | | |
| python3 -m venv build/protocol-client | |
| build/protocol-client/bin/pip install websockets==15.0.1 | |
| python3 scripts/run-v3-protocol-sanitizers.py --build-dir build/protocol-asan-ubsan --compiler clang++ --sanitizer address,undefined --python "$PWD/build/protocol-client/bin/python" --independent | |
| scripts/run-v3-protocol-fuzz.sh --build-dir build/protocol-fuzz --compiler clang++ --runs 2000 --seconds 30 | |
| if: ${{ matrix.build-type == 'asan' && runner.os == 'Linux' }} | |
| - name: Run header-hygiene check | |
| # TASK-007: dedicated public-header hygiene gate. Runs the | |
| # preprocessor-grep target (Layer 2) against a staged install and | |
| # reports any forbidden backend headers reaching <httpserver.hpp>. | |
| # HEADER_HYGIENE_STRICT defaults to "yes" (set at TASK-020 close); | |
| # any leak of forbidden backend headers through the umbrella fails CI. | |
| run: | | |
| cd build | |
| make check-hygiene | |
| if: ${{ matrix.build-type == 'header-hygiene' }} | |
| - name: Print tests results | |
| shell: bash | |
| run: | | |
| cd build ; | |
| cat test/test-suite.log ; | |
| if: ${{ failure() && matrix.build-type != 'iwyu' && matrix.compiler-family != 'arm-cross' && matrix.build-type != 'header-hygiene' }} | |
| - name: Verify native Windows IOCP oracle and lifetime traces (TASK-146) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cd build | |
| make -C test -j2 io_completion_storage io_iocp_backend io_backend_contract | |
| make -C test -j1 recheck check_PROGRAMS='io_completion_storage io_iocp_backend io_backend_contract' TESTS='io_completion_storage io_iocp_backend io_backend_contract' | |
| log=test/io_iocp_backend.log | |
| for scenario in managed_windows_selects_iocp read_delivers_bytes_exactly_once \ | |
| write_completes_with_transferred timer_fires_at_deadline_not_before \ | |
| two_timers_earliest_first_sequence_tie wake_completes_all_wakes_once \ | |
| cancel_pending_target cancel_terminal_target_reports_invalid_state \ | |
| close_sweeps_every_pending_once submit_after_close_connection_closed \ | |
| late_request_cancel_reports_invalid_state n_awaiter_resume_exactly_once \ | |
| cancel_vs_stimulus_race accept_round_trip http1_round_trip partial_read \ | |
| synchronous_candidate_abort_retries_same_logical_accept \ | |
| failed_candidate_packet_retries_same_logical_accept \ | |
| fatal_accept_initiation_error_retires_listener fatal_accept_packet_error_retires_listener \ | |
| success_claim_before_cancel_delivers_exactly_once cancel_claim_before_packet_delivers_exactly_once \ | |
| read_hangup write_hangup idle_iterations_stay_bounded slow_reader_no_busy_loop \ | |
| queued_success_cancel_not_found_retains_storage_until_packet \ | |
| failed_packet_cancellation_preserves_socket_and_next_read \ | |
| stale_packet_after_release_and_readoption_cannot_touch_new_buffer \ | |
| late_successful_accept_cancellation_closes_candidate_once \ | |
| pending_accept_cancel_and_concurrent_close_drain_every_native_request \ | |
| out_of_order_native_packets_across_owners_preserve_each_result \ | |
| empty_spans_complete_ok_without_eof write_failure_preserves_unread_inbound_bytes \ | |
| managed_http_loopback_uses_completion_backend \ | |
| cancel_unposted_operation_never_borrows_destroyed_storage \ | |
| driver_delivery_can_close_reentrantly_without_self_join \ | |
| opposite_direction_completes_before_earlier_pending_receive \ | |
| receive_eof_retires_registration_and_sweeps_connection_controls; do | |
| grep -F "Running test" "$log" | grep -F ": $scenario" >/dev/null | |
| done | |
| if grep -E 'SKIP|FAILURE|[1-9][0-9]* failures' "$log"; then exit 1; fi | |
| grep -F '0 failures' "$log" | |
| if: ${{ matrix.os-type == 'windows' && matrix.msys-env == 'MINGW64' }} | |
| - name: Run Valgrind checks | |
| timeout-minutes: 90 | |
| run: | | |
| cd build ; | |
| TOOL="${BUILD_TYPE#valgrind-}" ; | |
| make "check-valgrind-${TOOL}" ; | |
| if: ${{ startsWith(matrix.build-type, 'valgrind-') }} | |
| - name: Print Valgrind results | |
| shell: bash | |
| run: | | |
| cd build ; | |
| # TASK-088: surface memcheck + helgrind + drd logs. Without the | |
| # helgrind/drd logs, race-detector failures would be invisible in the | |
| # CI output, making finding-triage on the Linux lane blind. | |
| for log in test-suite-memcheck.log test-suite-helgrind.log test-suite-drd.log; do | |
| if [ -f "test/$log" ]; then | |
| echo "===== test/$log =====" ; | |
| cat "test/$log" ; | |
| else | |
| echo "test/$log not produced by this lane" ; | |
| fi | |
| done | |
| if: ${{ always() && startsWith(matrix.build-type, 'valgrind-') }} | |
| - name: Run cppcheck | |
| run: | | |
| cd src/ ; | |
| cppcheck --error-exitcode=1 . ; | |
| if: ${{ matrix.os-type == 'ubuntu' && matrix.compiler-family != 'arm-cross' }} | |
| - name: Generate coverage report | |
| run: | | |
| cd build | |
| gcovr --root .. --filter '../src/' --xml coverage.xml --xml-pretty --print-summary --gcov-ignore-parse-errors=negative_hits.warn_once_per_file | |
| if: ${{ matrix.os-type == 'ubuntu' && matrix.c-compiler == 'gcc' && matrix.debug == 'debug' && matrix.coverage == 'coverage' && success() }} | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5.5.5 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: build/coverage.xml | |
| # TASK-090: hard-fail CI on a Codecov UPLOAD error (network / auth / | |
| # tokenization / malformed report). Coverage integrity is a | |
| # supply-chain + observability signal; a silently-dropped upload used | |
| # to mask that the coverage number was stale or missing entirely. | |
| # This is distinct from a coverage-PERCENTAGE drop, which is enforced | |
| # separately by Codecov's own project/patch status checks driven by | |
| # codecov.yml — not by this upload step. | |
| fail_ci_if_error: true | |
| if: ${{ matrix.os-type == 'ubuntu' && matrix.c-compiler == 'gcc' && matrix.debug == 'debug' && matrix.coverage == 'coverage' && success() }} |