From 8f458150593b1442e58827c00210b619a4cd07ad Mon Sep 17 00:00:00 2001 From: sami Date: Wed, 9 Sep 2026 19:21:12 +0400 Subject: [PATCH] pkg: add CI workflow and branch-protection policy .github/workflows/ci.yml: fast lint jobs (clang-format, testserver selftest, bootstrap.sh --check) that need no compiler; a gcc/clcang build matrix and an ASan/UBSan + TSan sanitizer matrix that bootstrap via tools/bootstrap.sh and run `ctest --preset {ci,dev,tsan}`; advisory clang-tidy on changed files; and extension-lint + conformance jobs that short-circuit to a passing "skipped" step until their lane lands, so they can be marked required now. CMakePresets.json gains matching `tsan` and `ci` test presets. .github/BRANCH_PROTECTION.md records the intended required-checks policy (conformance required = the M0 exit gate). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS --- .github/BRANCH_PROTECTION.md | 36 ++++++++ .github/workflows/.gitkeep | 0 .github/workflows/ci.yml | 166 +++++++++++++++++++++++++++++++++++ CMakePresets.json | 12 +++ 4 files changed, 214 insertions(+) create mode 100644 .github/BRANCH_PROTECTION.md delete mode 100644 .github/workflows/.gitkeep create mode 100644 .github/workflows/ci.yml diff --git a/.github/BRANCH_PROTECTION.md b/.github/BRANCH_PROTECTION.md new file mode 100644 index 0000000..c006899 --- /dev/null +++ b/.github/BRANCH_PROTECTION.md @@ -0,0 +1,36 @@ +# Branch protection for `main` + +CI defines the checks; **branch protection is a repo setting** (Settings → Branches → +Add rule) and has to be configured once by an admin. This file records the intended +policy so it can be re-applied or audited. + +## Rule: `main` + +- **Require a pull request before merging.** No direct pushes. +- **Require status checks to pass before merging**, and require branches to be up to date + first. Required checks: + + | Check (job name in `ci.yml`) | Required from | + |---|---| + | `clang-format` | now | + | `testserver` | now | + | `bootstrap-script` | now | + | `build (gcc)` / `build (clang)` | when the first C++ lane merges | + | `sanitizers (dev)` / `sanitizers (tsan)` | when the first C++ lane merges | + | `conformance` | **when `tests/conformance/` lands — this is the M0 exit gate** | + | `extension-lint` | when `extension/` lands | + + `clang-tidy` is intentionally **not** required through M1 (`continue-on-error: true`, + `.clang-tidy` has `WarningsAsErrors: ''`). Make it required at M2. + +- **Require linear history** (matches CLAUDE.md §6: rebase onto `main`, no merge commits). +- **Require conversation resolution before merging.** +- Do **not** allow force pushes or deletions. +- Apply the rule to administrators too, except for the initial scaffolding period. + +## Note on the "skipped" job steps + +Several jobs (`conformance`, `extension-lint`, `clang-tidy`) short-circuit to a "skipped" +echo when their lane hasn't landed. They still report **success**, so they can be marked +required now without blocking — they start doing real work automatically on the commit +that adds the lane. diff --git a/.github/workflows/.gitkeep b/.github/workflows/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5b5c5b4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,166 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # GitHub-hosted runners are Ubuntu 24.04; the project targets 26.04. bootstrap.sh warns + # but proceeds. Revisit when 26.04 runners exist. + DEBIAN_FRONTEND: noninteractive + +jobs: + # --- fast lint jobs: no compiler, no heavy deps ------------------------------------- + clang-format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install clang-format + run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends clang-format + - name: Check formatting + run: | + shopt -s globstar nullglob + files=(core/**/*.{cpp,hpp} daemon/**/*.{cpp,hpp} cli/**/*.{cpp,hpp} nmhost/**/*.{cpp,hpp}) + if [ ${#files[@]} -eq 0 ]; then echo "no C++ sources yet — skipping"; exit 0; fi + printf '%s\n' "${files[@]}" + clang-format --dry-run --Werror "${files[@]}" + + testserver: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: testserver self-test + run: python3 tools/testserver/selftest.py + + bootstrap-script: + # Keeps tools/bootstrap.sh honest: it must run clean and its --check must pass. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: sudo ./tools/bootstrap.sh --with-clang + - run: ./tools/bootstrap.sh --check + + extension-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - id: check + run: | + if [ -f extension/package.json ]; then echo "present=true" >> "$GITHUB_OUTPUT" + else echo "present=false" >> "$GITHUB_OUTPUT"; fi + - uses: actions/setup-node@v4 + if: steps.check.outputs.present == 'true' + with: + node-version: '20' + - name: web-ext lint + if: steps.check.outputs.present == 'true' + working-directory: extension + run: | + npm ci + npx web-ext lint --source-dir . + - name: skipped + if: steps.check.outputs.present == 'false' + run: echo "extension/ has not landed yet — skipping web-ext lint" + + # --- build + test matrix ---------------------------------------------------------- + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + compiler: [gcc, clang] + env: + CC: ${{ matrix.compiler == 'gcc' && 'gcc' || 'clang' }} + CXX: ${{ matrix.compiler == 'gcc' && 'g++' || 'clang++' }} + steps: + - uses: actions/checkout@v4 + - name: Bootstrap toolchain + run: sudo ./tools/bootstrap.sh --with-clang + - name: Configure + run: cmake --preset ci + - name: Build + run: cmake --build --preset ci + - name: Test + run: ctest --preset ci --output-on-failure + + sanitizers: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + preset: [dev, tsan] # dev = ASan + UBSan + steps: + - uses: actions/checkout@v4 + - name: Bootstrap toolchain + run: sudo ./tools/bootstrap.sh --with-clang + - name: Configure + run: cmake --preset ${{ matrix.preset }} + - name: Build + run: cmake --build --preset ${{ matrix.preset }} + - name: Test + run: ctest --preset ${{ matrix.preset }} --output-on-failure + env: + ASAN_OPTIONS: detect_leaks=1:halt_on_error=1 + UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 + TSAN_OPTIONS: halt_on_error=1 + + clang-tidy: + # Advisory through M1 (see .clang-tidy WarningsAsErrors: ''); becomes required at M2. + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - id: check + run: | + if ls core/CMakeLists.txt daemon/CMakeLists.txt >/dev/null 2>&1; then + echo "present=true" >> "$GITHUB_OUTPUT" + else echo "present=false" >> "$GITHUB_OUTPUT"; fi + - name: Bootstrap toolchain + if: steps.check.outputs.present == 'true' + run: sudo ./tools/bootstrap.sh + - name: Configure (for compile_commands.json) + if: steps.check.outputs.present == 'true' + run: cmake --preset dev + - name: Run clang-tidy on changed files + if: steps.check.outputs.present == 'true' + run: | + mapfile -t files < <(git diff --name-only --diff-filter=ACM \ + "${{ github.event.pull_request.base.sha || 'HEAD~1' }}" HEAD \ + -- '*.cpp' '*.hpp' || true) + [ ${#files[@]} -eq 0 ] && { echo "no C++ changes"; exit 0; } + printf '%s\n' "${files[@]}" + clang-tidy -p build/dev "${files[@]}" + - name: skipped + if: steps.check.outputs.present == 'false' + run: echo "no C++ lane has landed a CMakeLists yet — skipping clang-tidy" + + conformance: + # Required check on every PR once tests/conformance/ lands (branch protection is + # configured in the repo settings, not here — see .github/BRANCH_PROTECTION.md). + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - id: check + run: | + if [ -f tests/conformance/CMakeLists.txt ] || [ -f tests/conformance/package.json ]; then + echo "present=true" >> "$GITHUB_OUTPUT" + else echo "present=false" >> "$GITHUB_OUTPUT"; fi + - name: Bootstrap toolchain + if: steps.check.outputs.present == 'true' + run: sudo ./tools/bootstrap.sh + - name: Run conformance suite + if: steps.check.outputs.present == 'true' + run: | + cmake --preset dev + ctest --preset dev --output-on-failure -L conformance + - name: skipped + if: steps.check.outputs.present == 'false' + run: echo "tests/conformance/ has not landed yet — skipping" diff --git a/CMakePresets.json b/CMakePresets.json index ae78247..e7016b6 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -58,6 +58,18 @@ "configurePreset": "dev", "output": { "outputOnFailure": true }, "execution": { "noTestsAction": "error", "stopOnFailure": false } + }, + { + "name": "tsan", + "configurePreset": "tsan", + "output": { "outputOnFailure": true }, + "execution": { "noTestsAction": "error", "stopOnFailure": false } + }, + { + "name": "ci", + "configurePreset": "ci", + "output": { "outputOnFailure": true }, + "execution": { "noTestsAction": "error", "stopOnFailure": false } } ] }