diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8004983 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/scripts/check-c.sh b/.github/scripts/check-c.sh new file mode 100644 index 0000000..fdd06b1 --- /dev/null +++ b/.github/scripts/check-c.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Compile every production translation unit; warnings are CI failures. +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" +compiler=${CC:-gcc} +flags=(-std=c23 -O1 -Wall -Wextra -Werror -Iinclude -Isrc -Ilib/include -Ilib/src) +case "${1:-warnings}" in + warnings) ;; + analyze) flags+=(-fanalyzer -fno-analyzer-state-merge) ;; + *) echo "Usage: bash .github/scripts/check-c.sh [warnings|analyze]" >&2; exit 2 ;; +esac +mkdir -p build/ci +object_dir=$(mktemp -d "$PWD/build/ci/check-c.XXXXXX") +trap 'rm -rf "$object_dir"' EXIT +mapfile -d '' sources < <(git ls-files -z -- 'src/*.c' 'lib/src/*.c') +(( ${#sources[@]} > 0 )) +for source in "${sources[@]}"; do + echo "Checking $source" + "$compiler" "${flags[@]}" -c "$source" -o "$object_dir/${source//\//_}.o" +done diff --git a/.github/scripts/configure.sh b/.github/scripts/configure.sh new file mode 100644 index 0000000..569796e --- /dev/null +++ b/.github/scripts/configure.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Configure the local build entry point for CI or an equivalent local run. +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" +compiler_path=$(command -v "${CC:-gcc}") +case "$(basename "$compiler_path")" in + gcc*) backend=GCC ;; + clang*) backend=CLANG ;; + *) echo "CC must select GCC or Clang" >&2; exit 2 ;; +esac +sed -i '/^compiler=/d; /^compiler_path=/d; /^sanitize=/d' build.conf +printf '\ncompiler=%s\ncompiler_path=%s\nsanitize=1\n' "$backend" "$compiler_path" >> build.conf +"$compiler_path" --version diff --git a/.github/scripts/install-tools.sh b/.github/scripts/install-tools.sh new file mode 100644 index 0000000..2ddb4ce --- /dev/null +++ b/.github/scripts/install-tools.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Pinned CLI releases, verified before executing; no GitHub secrets required. +set -euo pipefail +destination=${1:?Pass an installation directory} +mkdir -p "$destination" +destination=$(cd "$destination" && pwd) +download_dir=$(mktemp -d) +trap 'rm -rf "$download_dir"' EXIT +install_release() { + local url=$1 checksum=$2 executable=$3 + curl --fail --location --silent --show-error --retry 3 "$url" -o "$download_dir/archive.tar.gz" + printf '%s %s\n' "$checksum" "$download_dir/archive.tar.gz" | sha256sum --check + tar -xzf "$download_dir/archive.tar.gz" -C "$destination" "$executable" +} +install_release \ + https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_linux_amd64.tar.gz \ + 023070a287cd8cccd71515fedc843f1985bf96c436b7effaecce67290e7e0757 actionlint +install_release \ + https://github.com/gitleaks/gitleaks/releases/download/v8.24.2/gitleaks_8.24.2_linux_x64.tar.gz \ + fa0500f6b7e41d28791ebc680f5dd9899cd42b58629218a5f041efa899151a8e gitleaks diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3044ac2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,184 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +defaults: + run: + shell: bash + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + ASAN_OPTIONS: detect_leaks=1:halt_on_error=1 + UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1 + +jobs: + build-test: + name: Build and test (${{ matrix.cc }}) + runs-on: ubuntu-24.04 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + cc: [gcc-14, clang-19] + env: + CC: ${{ matrix.cc }} + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + - name: Install both supported toolchains + timeout-minutes: 5 + run: | + # The hosted image's Azure mirror can stall on LLVM package downloads. + if test -f /etc/apt/apt-mirrors.txt; then + printf '%s\n' 'https://archive.ubuntu.com/ubuntu' | sudo tee /etc/apt/apt-mirrors.txt > /dev/null + fi + sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 update + sudo env DEBIAN_FRONTEND=noninteractive apt-get \ + -o DPkg::Lock::Timeout=120 -o Acquire::Retries=3 \ + -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 \ + install --no-install-recommends -y gcc-14 clang-19 llvm-19 libclang-rt-19-dev binutils pkg-config libpcre2-dev + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 100 + sudo update-alternatives --set gcc /usr/bin/gcc-14 + sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-19 190 + sudo update-alternatives --set clang /usr/bin/clang-19 + - name: Verify compiler versions used by generated builds + run: | + test "$(gcc -dumpversion | cut -d. -f1)" = 14 + test "$(clang -dumpversion | cut -d. -f1)" = 19 + - name: Configure the selected compiler + run: bash .github/scripts/configure.sh + - name: Reject compiler warnings + run: bash .github/scripts/check-c.sh + - name: Check out pinned Forge build dependency + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + repository: minelogy-dev/forge + ref: b6826ba762a1e9e337895e01ee604b943c682d23 + path: .ci-deps/forge + persist-credentials: false + - name: Bootstrap Forge without installing system files + env: + CC: gcc + run: | + sh .ci-deps/forge/bootstrap.sh --no-test + echo "$GITHUB_WORKSPACE/.ci-deps/forge/build/output" >> "$GITHUB_PATH" + echo "CPATH=$GITHUB_WORKSPACE/.ci-deps/forge/build/output/include" >> "$GITHUB_ENV" + - name: Build the shared library with the real build script + run: | + forge . + ./make + - name: Run the complete sanitizer suite + run: ./make test -j 2 2>&1 | tee build/ci-tests.log + - name: Validate installation, pkg-config and shared-library exports + run: | + ./make install "$RUNNER_TEMP/stringx-install" + export PKG_CONFIG_PATH="$RUNNER_TEMP/stringx-install/lib/pkgconfig" + # Link the existing regex tests to the installed shared library. + read -r -a pkg_flags <<< "$(pkg-config --cflags --libs libstringx)" + "$CC" -std=c23 -fsanitize=address,undefined -fno-sanitize-recover=all \ + -fno-omit-frame-pointer tests/reg.c \ + "${pkg_flags[@]}" \ + -Wl,-rpath,"$RUNNER_TEMP/stringx-install/lib" -o build/linked-reg + ./build/linked-reg + ./make gen_deb + dpkg-deb --info build/deb/libstringx_*.deb + dpkg-deb --contents build/deb/libstringx_*.deb + - name: Keep diagnostics after a failure + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: diagnostics-${{ matrix.cc }} + path: | + build/ci-tests.log + build/**/*.d + build/**/*.meta + test/edge_*/build.conf + test/edge_*/build.c + if-no-files-found: ignore + retention-days: 7 + + static-analysis: + name: GCC static analysis + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + - name: Install analyzer dependencies + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y gcc-14 libpcre2-dev + - name: Check production code for unsafe memory and resource use + env: + CC: gcc-14 + run: | + mkdir -p build + bash .github/scripts/check-c.sh analyze 2>&1 | tee build/ci-analyzer.log + - name: Keep analyzer diagnostics after a failure + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: analyzer-diagnostics + path: build/ci-analyzer.log + if-no-files-found: ignore + retention-days: 7 + + quality: + name: Workflow and secret checks + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + fetch-depth: 0 + persist-credentials: false + - name: Install ShellCheck and pinned security tools + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y shellcheck + bash .github/scripts/install-tools.sh "$RUNNER_TEMP/ci-tools" + - name: Lint workflows and helper scripts + run: | + "$RUNNER_TEMP/ci-tools/actionlint" -color + shellcheck .github/scripts/*.sh + - name: Scan the complete Git history for credentials + run: '"$RUNNER_TEMP/ci-tools/gitleaks" git --redact --verbose' + - name: Reject whitespace errors in the proposed changes + env: + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + HEAD_SHA: ${{ github.sha }} + run: | + if test -n "$BASE_SHA" && git cat-file -e "$BASE_SHA^{commit}" 2>/dev/null; then + git diff --check "$BASE_SHA" "$HEAD_SHA" + else + git show --format= --check "$HEAD_SHA" + fi + + ci: + name: CI + if: always() + needs: [build-test, static-analysis, quality] + runs-on: ubuntu-24.04 + timeout-minutes: 2 + steps: + - name: Require every check to pass + env: + BUILD_TEST: ${{ needs.build-test.result }} + STATIC_ANALYSIS: ${{ needs.static-analysis.result }} + QUALITY: ${{ needs.quality.result }} + run: | + test "$BUILD_TEST" = success + test "$STATIC_ANALYSIS" = success + test "$QUALITY" = success diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..8887cb2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,67 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + schedule: + - cron: '17 3 * * 1' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: CodeQL (C/C++) + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + security-events: write + actions: read + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y gcc-14 binutils pkg-config libpcre2-dev + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 100 + - name: Check out pinned Forge build dependency + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + repository: minelogy-dev/forge + ref: b6826ba762a1e9e337895e01ee604b943c682d23 + path: .ci-deps/forge + persist-credentials: false + - name: Bootstrap Forge without installing system files + env: + CC: gcc + run: | + sh .ci-deps/forge/bootstrap.sh --no-test + echo "$GITHUB_WORKSPACE/.ci-deps/forge/build/output" >> "$GITHUB_PATH" + echo "CPATH=$GITHUB_WORKSPACE/.ci-deps/forge/build/output/include" >> "$GITHUB_ENV" + - name: Configure the build compiler + env: + CC: gcc + run: bash .github/scripts/configure.sh + - name: Initialize security and maintainability queries + uses: github/codeql-action/init@793b778da9c9275a0f276522b27cb5fc14bba279 # v4 + with: + languages: c-cpp + build-mode: manual + queries: security-and-quality + - name: Build production code for analysis + run: | + forge . + ./make + - name: Publish findings to code scanning and the pull request + uses: github/codeql-action/analyze@793b778da9c9275a0f276522b27cb5fc14bba279 # v4 + with: + category: /language:c-cpp diff --git a/.gitignore b/.gitignore index 1704a6f..ac4485d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /make /build/ /test/ +/.ci-deps/ diff --git a/README.md b/README.md index a67020a..ad5567e 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,9 @@ Contract highlights (full detail in `sx.h`): ## Tests +Pull requests run compiler, sanitizer, static-analysis and secret checks. +See [Continuous integration](docs/CI.md) for coverage and merge-protection setup. + ```sh ./make test ``` @@ -131,4 +134,4 @@ the exact behavior boundaries it locks in. MIT — see [LICENSE](LICENSE) and the SPDX headers in the source files. The regex layer depends on **PCRE2** (BSD-3-Clause); its attribution and license -are in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). \ No newline at end of file +are in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). diff --git a/README.zh-CN.md b/README.zh-CN.md index c3abe53..8331344 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -112,6 +112,9 @@ sx_reg_free(re); ## 测试 +PR 会自动执行编译、内存检查、静态分析和密钥扫描。 +检查范围与分支保护设置见[持续集成说明](docs/CI.md)。 + ```sh ./make test ``` @@ -124,4 +127,4 @@ sx_reg_free(re); MIT —— 见 [LICENSE](LICENSE) 及源码文件中的 SPDX 标注。正则层依赖 **PCRE2**(BSD-3-Clause),其归属与许可全文见 -[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。 \ No newline at end of file +[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。 diff --git a/build.c b/build.c index 6274b84..b96da22 100644 --- a/build.c +++ b/build.c @@ -3,13 +3,19 @@ pcre2-8 is attached to every test target here — via forge's reserved per-test DSL overrides (LANGUAGE / TOOLCHAIN expand inside each test target block; see build.h "test DSL default macros"). - Must be #defined before #include (the guards). */ + Must be #defined before #include (the guards). + Tests disable LTO because the pinned Forge emits GCC-only -flto=N + for parallel Clang links. Production library targets keep LTO. */ #define LANGUAGE \ - { \ + do { \ set_language(C); \ + set(TARGET_LTO, 1, DISABLE); \ add_link_lib("pcre2-8"); \ - } -#define TOOLCHAIN set_toolchain(GCC) + } while (0) +#ifndef CONF_compiler +#define CONF_compiler GCC +#endif +#define TOOLCHAIN set_toolchain(CONF_compiler) #include #define STR_IMPL(tok) #tok @@ -23,6 +29,7 @@ function(check) { parse_args(builtin_args); target("stringx") { + set_toolchain(CONF_compiler); add_sources_r("src"); set_type(FORGE_SHARED_LIB); add_include_path("include"); @@ -31,7 +38,9 @@ function(check) { set_optimization(OPT_AGGRESSIVE); set_standard("c23"); set_output_path("build"); - add(COMPILER_OPTIONS, 5, "-Wall", "-Wextra", "-Wpedantic", "-Werror", "-fanalyzer"); + add(COMPILER_OPTIONS, 4, "-Wall", "-Wextra", "-Wpedantic", "-Werror"); + if (CONF_compiler == GCC) + add(COMPILER_OPTIONS, 1, "-fanalyzer"); set_output_intermediate_path("build/exe-obj"); return compile(); } @@ -48,8 +57,8 @@ function(build) { for (int i = 0; i < n; i++) { if (!strstr(names[i], ".h")) continue; - char *src = os_cstr(os_path_join("include", names[i])); - char *dst = os_cstr(os_path_join("build/output/include", names[i])); + char *src = os_path_join("include", names[i]); + char *dst = os_path_join("build/output/include", names[i]); int r = (!src || !dst) ? -1 : os_copy_file(src, dst); free(src); free(dst); @@ -65,6 +74,7 @@ function(build) { free(names); } target("stringx") { + set_toolchain(CONF_compiler); add_sources_r("src"); set_type(FORGE_SHARED_LIB); add_include_path("include"); @@ -103,8 +113,8 @@ static int copy_dir_r(const char *src_dir, const char *dst_dir) { return -1; int ret = 0; for (int i = 0; i < n && ret == 0; i++) { - char *s = os_cstr(os_path_join(src_dir, names[i])); - char *d = os_cstr(os_path_join(dst_dir, names[i])); + char *s = os_path_join(src_dir, names[i]); + char *d = os_path_join(dst_dir, names[i]); if (!s || !d) { ret = -1; } else if (os_path_is_dir(s)) { @@ -133,9 +143,9 @@ static int copy_dir_r(const char *src_dir, const char *dst_dir) { into the .pc). Returns 0 on success. */ static int install_to(const char *prefix, const char *pc_prefix) { int ret = 0; - char *lib = os_cstr(os_path_join(prefix, "lib")); - char *inc = os_cstr(os_path_join(prefix, "include")); - char *doc = os_cstr(os_path_join(prefix, "share/doc/libstringx")); + char *lib = os_path_join(prefix, "lib"); + char *inc = os_path_join(prefix, "include"); + char *doc = os_path_join(prefix, "share/doc/libstringx"); if (!lib || !inc || !doc || os_mkdir_r(lib) != 0 || os_mkdir_r(inc) != 0 || os_mkdir_r(doc) != 0) { free(lib); @@ -144,7 +154,7 @@ static int install_to(const char *prefix, const char *pc_prefix) { return -1; } - char *dst = os_cstr(os_path_join(lib, "libstringx.so")); + char *dst = os_path_join(lib, "libstringx.so"); if (os_copy_file("build/output/libstringx.so", dst) != 0) ret = -1; free(dst); @@ -152,15 +162,15 @@ static int install_to(const char *prefix, const char *pc_prefix) { if (copy_dir_r("build/output/include", inc) != 0) ret = -1; - char *f = os_cstr(os_path_join(doc, "LICENSE")); + char *f = os_path_join(doc, "LICENSE"); if (os_copy_file("LICENSE", f) != 0) ret = -1; free(f); - f = os_cstr(os_path_join(doc, "THIRD_PARTY_NOTICES.md")); + f = os_path_join(doc, "THIRD_PARTY_NOTICES.md"); if (os_copy_file("THIRD_PARTY_NOTICES.md", f) != 0) ret = -1; free(f); - f = os_cstr(os_path_join(doc, "copyright")); /* Debian's conventional name */ + f = os_path_join(doc, "copyright"); /* Debian's conventional name */ if (os_copy_file("LICENSE", f) != 0) ret = -1; free(f); @@ -172,8 +182,8 @@ static int install_to(const char *prefix, const char *pc_prefix) { const char *v = SX_VERSION; if (*v == 'v') v++; - char *pcdir = os_cstr(os_path_join(lib, "pkgconfig")); - char *pc = os_cstr(os_path_join(pcdir, "libstringx.pc")); + char *pcdir = os_path_join(lib, "pkgconfig"); + char *pc = os_path_join(pcdir, "libstringx.pc"); if (!pcdir || !pc || os_mkdir_r(pcdir) != 0) { ret = -1; } else { @@ -211,10 +221,10 @@ static int install_to(const char *prefix, const char *pc_prefix) { aarch64->arm64; anything else is kept as-is */ static const char *deb_arch(void) { static char buf[64]; - sx_t *out = os_shell((char *[]){"uname", "-m", NULL}); + char *out = os_shell((char *[]){"uname", "-m", NULL}); if (!out) return "amd64"; - char *s = sx_buf_mut(out); + char *s = out; size_t len = strlen(s); while (len > 0 && (s[len - 1] == '\n' || s[len - 1] == '\r')) s[--len] = '\0'; @@ -224,7 +234,7 @@ static const char *deb_arch(void) { else if (strcmp(m, "aarch64") == 0) m = "arm64"; snprintf(buf, sizeof(buf), "%s", m); - sx_free(out); + free(out); return buf; } @@ -232,7 +242,7 @@ static const char *deb_arch(void) { returns -1 on failure */ static int run_quiet(char **argv) { int st = -1; - sx_free(os_execute_capture_all_status(argv[0], argv, "", NULL, &st)); + free(os_execute_capture_all_status(argv[0], argv, "", NULL, &st)); return st == 0 ? 0 : -1; } #endif /* LINUX */ @@ -281,9 +291,9 @@ function(gen_deb) { build/deb/ debian-binary data/ control-dir/control control.tar.gz data.tar.gz libstringx__.deb */ - char *work = os_cstr(os_path_join("build", "deb")); - char *data = os_cstr(os_path_join(work, "data")); - char *cdir = os_cstr(os_path_join(work, "control-dir")); + char *work = os_path_join("build", "deb"); + char *data = os_path_join(work, "data"); + char *cdir = os_path_join(work, "control-dir"); if (!work || !data || !cdir) { free(work); free(data); @@ -302,10 +312,12 @@ function(gen_deb) { mirrors the filesystem root: packages install under /usr (FHS), so the copy root is /usr — lib/libstringx.so, the headers, share/doc/libstringx (LICENSE, notices, copyright). */ - char *data_usr = os_cstr(os_path_join(data, "usr")); + char *data_usr = os_path_join(data, "usr"); if (!data_usr) { - ret = -1; - goto cleanup; + free(work); + free(data); + free(cdir); + return -1; } if (install_to(data_usr, "/usr") != 0) { free(data_usr); @@ -316,10 +328,10 @@ function(gen_deb) { } free(data_usr); - char *deb_bin = os_cstr(os_path_join(work, "debian-binary")); - char *ctl = os_cstr(os_path_join(cdir, "control")); - char *cgz = os_cstr(os_path_join(work, "control.tar.gz")); - char *dgz = os_cstr(os_path_join(work, "data.tar.gz")); + char *deb_bin = os_path_join(work, "debian-binary"); + char *ctl = os_path_join(cdir, "control"); + char *cgz = os_path_join(work, "control.tar.gz"); + char *dgz = os_path_join(work, "data.tar.gz"); size_t deblen = strlen(work) + strlen(ver) + strlen(arch) + 28; char *deb = malloc(deblen); if (!deb_bin || !ctl || !cgz || !dgz || !deb) { diff --git a/docs/CI.md b/docs/CI.md new file mode 100644 index 0000000..83e8606 --- /dev/null +++ b/docs/CI.md @@ -0,0 +1,101 @@ +# Continuous integration + +Changes to `main`, pull requests, and manual runs use GitHub Actions. +The workflows have read-only repository access, except for the CodeQL job's +permission to publish code-scanning results. Pull-request code never runs via +`pull_request_target`, and checkout credentials are not persisted. + +## Checks + +| Check | What it verifies | +| --- | --- | +| Build and test (gcc-14 / clang-19) | A real build on Ubuntu 24.04 with GCC 14 and Clang 19, the complete test suite, ASan, UBSan and leak detection | +| Compiler warnings | Every production C translation unit compiles with `-Wall -Wextra -Werror` | +| GCC static analysis | `-fanalyzer` checks production translation units; diagnostics fail the job | +| Workflow and secret checks | Actionlint, ShellCheck, whitespace errors and Gitleaks over the complete Git history, with redacted output | +| CodeQL (C/C++) | Manual build tracing with `security-and-quality` queries, findings in the Security tab and on pull requests | +| CI | A stable required check that fails if any build, analyzer or quality job fails or is cancelled | + +CodeQL also runs every Monday at 11:17 Asia/Shanghai (03:17 UTC). +Its successful job status means the analysis completed; it does **not** mean +there are no alerts. Use a code-scanning merge rule to enforce alert severity. + +GCC analysis uses `-O1 -fno-analyzer-state-merge`: GCC 14's default state merging +produces spurious allocation/cleanup diagnostics in this code. No diagnostic +categories are suppressed. An analyzer failure should be investigated, not +worked around with `continue-on-error`. + +All external Actions use immutable commit SHAs. Dependabot proposes weekly +GitHub Actions updates. The Actionlint and Gitleaks CLIs have pinned versions +and SHA-256 checksums in `.github/scripts/install-tools.sh`; update both values +together when upgrading them. + +## Enable merge protection + +After the first successful run, create a branch ruleset for `main`: + +1. Require pull requests and the `CI` and `CodeQL (C/C++)` status checks. +2. Require branches to be up to date before merging. +3. Add “Require code scanning results”, select CodeQL, and block high/critical + security alerts and error-level quality alerts. Tighten these thresholds + after triaging existing findings. +4. Use the checked-in advanced CodeQL workflow rather than enabling a duplicate + default-setup scan. + +Workflow files alone do not enable branch protection. These repository settings +require a one-time maintainer configuration. Changes to CI permissions, checks +and dependency pins still deserve human review. + +## Reproduce locally + +Install Linux build dependencies and a C23 compiler (GCC 14+ or Clang 19+). +Save local `build.conf` changes before running `configure.sh`: it updates the +compiler selection and enables Forge test instrumentation in that file. + +```sh +CC=gcc bash .github/scripts/configure.sh # or CC=clang +CC=gcc bash .github/scripts/check-c.sh +CC=gcc bash .github/scripts/check-c.sh analyze +``` + +Use GCC for `analyze`. Runtime checks set: + +```sh +export ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 +export UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 +``` + +The build dependency is Forge commit +`b6826ba762a1e9e337895e01ee604b943c682d23`, checked out under +`.ci-deps/forge` in CI and bootstrapped without installing system files. +Update this reviewed pin in **both** workflow files when changing Forge. +It is intentionally independent of Forge's moving `main` branch and is not +updated by Dependabot. PCRE2 comes from Ubuntu's `libpcre2-dev` package. + +With a bootstrapped Forge checkout available locally: + +```sh +export PATH="/path/to/forge/build/output:$PATH" +export CPATH="/path/to/forge/build/output/include" +forge . +./make +./make test -j 2 +``` + +Tests compile the library sources with sanitizers, including the exact-reserve +stress test. The build jobs also install into a temporary prefix, compile and +run the existing regex suite against the **installed shared library** using +`pkg-config`, and inspect the generated Debian package. This checks public +headers, exported symbols, package metadata and PCRE2 linkage in addition to +source-level behavior. + +## Review and failures + +Open the failed check for compiler diagnostics, sanitizer stack traces or +secret-scan locations. Build failures retain dependency and command metadata +for seven days; the Actions log contains the full build/test output. +CodeQL alerts include the affected lines and data flow where available. + +Automation covers mechanical regressions and many common memory-safety bugs. +Human review should concentrate on requirements, API contracts, ownership, +architecture and the findings reported by these tools.