Skip to content

Connect v3 CI, fuzz campaigns and guarded release publication #995

Connect v3 CI, fuzz campaigns and guarded release publication

Connect v3 CI, fuzz campaigns and guarded release publication #995

Workflow file for this run

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() }}