From 19b4269c5c011b3ef14a054768195e700c5acbfd Mon Sep 17 00:00:00 2001 From: Albert Bausili Date: Sat, 26 Sep 2026 15:39:49 +0200 Subject: [PATCH 1/3] ci: set up CodeRabbit, Codecov and CodSpeed (celeris#690) Configuration as code for the three GitHub Apps installed on the org. All three are advisory: none can fail or block a pull request. - .coderabbit.yaml: reviews tuned to this codebase (path instructions for engine, adaptive, internal, protocol, middleware, driver, tests, workflows, magefiles), never requests changes or approves, drafts and Dependabot not auto-reviewed, the summary kept out of the PR body, golangci-lint with .golangci.yml, actionlint, zizmor, shellcheck and gitleaks. - test-coverage.yml + codecov.yml: the ci.yml unit package set, -race, covermode atomic, one Codecov flag per Go module, uploaded over OIDC (tokenless for fork PRs), every status informational. - codspeed.yml: the hot-path benchmarks (root, protocol, internal, core middleware chains, logger) in walltime mode on the CodSpeed arm64 macro runner, the one runner the free plan includes; triggered only by changes to the code those benchmarks execute, never on fork PR code. - .github/actionlint.yaml: declare the macro-runner label. No existing workflow is modified. Refs #690 --- .coderabbit.yaml | 242 ++++++++++++++++++++++++++++ .github/actionlint.yaml | 6 + .github/workflows/codspeed.yml | 185 +++++++++++++++++++++ .github/workflows/test-coverage.yml | 144 +++++++++++++++++ codecov.yml | 122 ++++++++++++++ 5 files changed, 699 insertions(+) create mode 100644 .coderabbit.yaml create mode 100644 .github/actionlint.yaml create mode 100644 .github/workflows/codspeed.yml create mode 100644 .github/workflows/test-coverage.yml create mode 100644 codecov.yml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..09d289a4 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,242 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +# +# CodeRabbit review configuration for goceleris/celeris (celeris#690). +# Reference: https://docs.coderabbit.ai/reference/configuration +# `@coderabbitai configuration` on any pull request prints the resolved config. +# +# CodeRabbit is ADVISORY. It never requests changes or approves, it is not a +# required check, and its comments are input to the human review, not a +# verdict. The merge rule stays GOVERNANCE.md's: green required checks and a +# code-owner approval. + +language: en-US + +tone_instructions: >- + Be terse and specific: file, line, the input or interleaving that breaks it, + and what happens. No praise, no emoji, no restating the diff. If unsure, say + which test or measurement would settle it. + +early_access: false + +reviews: + # Balanced feedback; the path instructions below point it at what matters. + profile: chill + # Never "Request changes", never auto-approve: CodeRabbit cannot block or + # satisfy the merge rule. + request_changes_workflow: false + # Keep generated text out of the author's PR description (it becomes the + # squash commit message); the summary goes in the walkthrough comment. + high_level_summary: false + high_level_summary_in_walkthrough: true + # Say when and why a review was skipped (draft, ignored title, paused). + review_status: true + review_details: false + collapse_walkthrough: true + changed_files_summary: true + # A generated sequence diagram draws one happy path; the hazards in this + # codebase are interleavings, which it cannot show. + sequence_diagrams: false + estimate_code_review_effort: true + assess_linked_issues: true + related_issues: true + related_prs: true + # Release notes are generated from these labels (.github/release.yml), so + # suggest them, but leave applying them to a human. + suggested_labels: true + auto_apply_labels: false + labeling_instructions: + - label: bug + instructions: A fix for incorrect behaviour (lost request, leak, race, wrong result, crash). + - label: enhancement + instructions: A new feature or a new public API. + - label: performance + instructions: A change whose purpose is speed or allocations; it should carry a measurement. + - label: security + instructions: A fix or hardening for a security weakness (parsing of untrusted input, auth middleware, secrets). + - label: breaking + instructions: Removes or changes an exported identifier, a default, or documented behaviour. + # One maintainer today (MAINTAINERS.md); suggestions would only be noise. + suggested_reviewers: false + auto_assign_reviewers: false + in_progress_fortune: false + poem: false + enable_prompt_for_ai_agents: true + + auto_review: + enabled: true + auto_incremental_review: true + # Drafts are reviewed once marked ready, or on `@coderabbitai review`. + drafts: false + ignore_title_keywords: + - "[WIP]" + - "WIP:" + - "DO NOT REVIEW" + # Version bumps: the diff is a hash or a version string. + ignore_usernames: + - "dependabot[bot]" + + # Lock files and generated or vendored code are not reviewed. + path_filters: + - "!**/go.sum" + - "!**/vendor/**" + - "!**/*.pb.go" + - "!**/testdata/fuzz/**" + - "!**/*.png" + + path_instructions: + - path: "engine/**" + instructions: | + The epoll, io_uring and std engines. A bug here loses or corrupts requests for every user. Check: + - io_uring SQE/CQE lifetimes: every buffer, iovec, sockaddr and connState an SQE points at must stay valid and + unrecycled until its final CQE (a multishot op ends only on a CQE without IORING_CQE_F_MORE; an IOSQE_IO_LINK + chain ends with its last op). A CQE whose user_data carries an older connection generation must be ignored. + An ASYNC_CANCEL must match exactly the op it means to (keyed by user_data, not by fd, unless every op on the + socket is meant). + - fd lifetime: a descriptor must not be closed while an op, a cancel, or the other engine (a transplant or + hand-off between epoll and io_uring) can still use it. Once closed, the number can come back from the next + accept, and anything still holding it acts on a stranger's connection. Check every close path (error, + shutdown, hijack/detach, hand-off, worker init failure) for a double close and for a leaked listen socket, + ring or eventfd. + - Locks: state the order two locks are taken in and flag any path that takes them the other way. Flag a + callback, a user handler, a blocking syscall or a channel send made while holding a lock, and an event loop + that parks waiting for work only it can finish. + - Hot path (accept, recv, parse, dispatch, send): a new allocation, `defer` around Lock/Unlock, a new lock or + atomic, `fmt`, or an interface conversion needs a measurement in the PR (a benchmark with -benchmem, or a + goceleris/probatorium result). Ask for it when it is missing. + - Goroutines and timers: every goroutine needs an exit tied to shutdown or a context, Shutdown must wait for + it, and every timer must be stopped. + - Build tags: a `_linux.go` or `//go:build linux` change needs its non-Linux stub to keep compiling. + - Exported metrics and counters are read by goceleris/probatorium; renaming or removing one breaks it. + - path: "adaptive/**" + instructions: | + The adaptive engine runs epoll and io_uring side by side and switches between them (promote / revert) under + load. Check: + - Every connection is owned by exactly one engine at every instant of a switch: none dropped, none served by + both, including the ones still in an accept queue or idle on keep-alive when the switch happens. + - Listener hand-over: no window in which neither engine accepts, and no connection reset by closing a listener + whose queue still holds connections. + - Every wait (bind, drain, settle) must also select on ctx.Done() and on shutdown. + - Controller decisions need hysteresis; flag anything that can oscillate. + - Metrics aggregated across sub-engines: sums for counters, max for maxima, never a sum of maxima. + - path: "internal/**" + instructions: | + Shared connection handling (internal/conn: H1 and H2 state used by every engine), socket options and the + wake-fd. The engine rules apply: hot-path allocations and locks need a measurement, lock order must be + consistent, and a descriptor must not be used after close. + - path: "protocol/**" + instructions: | + The HTTP/1.1 and HTTP/2 parsers read untrusted bytes from the network. Check: + - Request smuggling (RFC 9112): Content-Length together with Transfer-Encoding, duplicate or differing + Content-Length, obs-fold, bare CR or LF, whitespace before the colon. + - Every slice index bounds-checked; chunk sizes and lengths checked for overflow; header size and count limits + enforced before allocating. + - HTTP/2 (RFC 9113): stream state transitions, flow-control windows, SETTINGS limits, HPACK table size, and + CONTINUATION floods. + - A zero-copy view into a read buffer must not outlive the buffer's reuse. + - The assembly header scan (findheader_*.s) must agree byte for byte with the generic Go version; a change to + either needs the fuzz or equivalence test to cover it. + - path: "middleware/**" + instructions: | + Follow CONTRIBUTING.md's middleware pattern: config.go with defaults and validation that panics at init, + `New(config ...Config) celeris.HandlerFunc`, doc.go with security notes, bench_test.go with b.ReportAllocs, + and celeris.SkipHelper for skip logic. Check: + - A middleware must not keep a *celeris.Context, or a slice of its buffers, after the handler returns: + contexts are pooled and reused. + - Auth and security middleware (jwt, csrf, basicauth, keyauth, cors, secure, ratelimit, proxy, session): secret + comparisons in constant time, fail closed on error, X-Forwarded-For trusted only from configured proxies, no + secret or token in a log line or error message. + - compress, metrics, otel and protobuf are separate Go modules; their go.mod pin on the root module moves only + through `mage PrepRelease`. + - path: "driver/**" + instructions: | + The Postgres, Redis and Memcached drivers. Server replies are parsed with bounds checks like untrusted input. A + connection goes back to the pool only in a clean protocol state; one cancelled mid-request is discarded, not + reused. Every call honours its context. A connection registered on an engine's event loop is unregistered + before its descriptor is closed. + - path: "**/*_test.go" + instructions: | + This project's test rules: + - A SKIP is never a PASS. A test that can skip in CI (memlock, io_uring availability, kernel feature) needs a + CELERIS_REQUIRE_* switch or a CI tally that fails when it does not run. Flag a new skip that CI would not + notice. + - A regression test must fail on the unfixed code. Ask for that negative control if the PR does not show it. + - No timing knife-edges: no sleep used as synchronisation, no assertion on a wall-clock duration with a tight + margin, no dependence on goroutine scheduling order. Wait on a channel, a condition, or a condition polled + until a generous deadline. + - Tests run under -race: state shared with a goroutine must be synchronised. + - No test that only restates a constant or duplicates existing coverage (CONTRIBUTING.md). + - .github/workflows/ci.yml runs some tests by exact name and counts their PASS lines. Renaming or removing one + of those tests without updating that list turns CI red. + - path: ".github/workflows/**" + instructions: | + - Every `uses:` pinned to a full 40-character commit SHA with a `# vX.Y.Z` comment that matches it. + - Top-level `permissions` read-only; a job raises only what it needs, with a comment saying why. + - actions/checkout with `persist-credentials: false`. + - No `${{ }}` expression inside `run:`; pass values through `env:`. + - No `pull_request_target`. Code from a fork pull request never runs on a self-hosted or CodSpeed macro runner. + - The lint job runs actionlint and zizmor over this directory; both must stay clean. + - Steps in ci.yml that count PASS lines of named tests must change together with those tests. + - path: "mage*.go" + instructions: | + Magefiles carry `//go:build mage`; only the lint job's `mage -compile` builds them. The release stamps + (celeris.Version in server.go, the four middleware go.mod pins, the README heading) must move together + through `mage PrepRelease` and pass `mage CheckRelease`. + + pre_merge_checks: + # Exported identifiers already need doc comments (revive); a percentage + # gate over every function would contradict CONTRIBUTING.md's "no + # unnecessary comments". + docstrings: + mode: "off" + title: + mode: warning + requirements: >- + Conventional-commit form `type(scope): summary` or `type: summary`, with type one of feat, fix, perf, + security, test, docs, ci, chore, refactor or deps; the summary says what changes in plain words; a PR + that fixes an issue ends with the reference, e.g. `(celeris#657)`. + description: + mode: warning + issue_assessment: + mode: warning + + finishing_touches: + # Generated tests cannot show that they fail on the unfixed code, which + # this project requires of every regression test. + unit_tests: + enabled: false + docstrings: + enabled: false + + tools: + golangci-lint: + enabled: true + config_file: .golangci.yml + actionlint: + enabled: true + zizmor: + enabled: true + shellcheck: + enabled: true + gitleaks: + enabled: true + # Grammar suggestions on long, deliberate prose comments are noise. + languagetool: + enabled: false + +chat: + auto_reply: true + art: false + +knowledge_base: + opt_out: false + code_guidelines: + enabled: true + filePatterns: + - "CONTRIBUTING.md" + - "GOVERNANCE.md" + learnings: + scope: local + issues: + scope: local + pull_requests: + scope: local diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 00000000..b44bc563 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,6 @@ +self-hosted-runner: + # CodSpeed Macro Runner label used by .github/workflows/codspeed.yml + # (https://codspeed.io/docs/features/macro-runners). actionlint knows only + # GitHub-hosted labels and rejects any other label as unknown. + labels: + - codspeed-macro-arm64-graviton-ubuntu-22-04 diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 00000000..12ecb73e --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,185 @@ +# CodSpeed: continuous benchmarking of the request hot path (celeris#690). +# +# What it measures: the standard `testing.B` benchmarks of the code every +# request runs through -- the router, Context and handler chain (root +# bench_test.go), the HTTP/1 parser, protocol detection and the H2 stream +# pool (./protocol/...), the wake-fd primitive (./internal/...), the core +# middleware chains (./middleware) and the logger. Nothing in the benchmarks +# changed: CodSpeed's Go runner builds them with an overlay of `testing` and +# measures each one (https://codspeed.io/docs/benchmarks/go). +# +# Not measured, on purpose: test/drivercmp/* and test/benchcmp_* (separate +# modules that need Redis, Postgres or Memcached), driver/* (same), the +# middleware sub-modules (own go.mod), middleware/websocket (real sockets), +# and the engines. Engine throughput is judged on real hardware by +# goceleris/probatorium's cluster matrix, not by micro-benchmarks, and the +# engines change so often that watching them here would spend the whole +# monthly budget (see below). +# +# Where it runs: a CodSpeed Macro Runner, a dedicated bare-metal machine. +# Go supports only the walltime instrument, and walltime on a shared +# GitHub-hosted VM is too noisy to catch a regression. A report is only +# compared with a baseline measured on the same machine type. +# +# Only the arm64 Graviton runner is used: it is the one runner the CodSpeed +# Free plan (the open-source plan) includes, 600 minutes a month. The Ryzen +# x64 runner is Pro-only. arm64 == x86 parity for a release stays with +# probatorium's cluster. +# +# PREREQUISITE (an org setting, not code): Organization settings -> Actions +# -> Runner groups -> Default -> allow public repositories. Until that is +# set, the job below waits in the queue for a runner that never comes. That +# is expected and is not a failure of this workflow. +# +# Security: a macro runner is a self-hosted runner outside GitHub's +# sandbox, so code from a FORK pull request never runs on it (the job's +# `if:` below). Same-repository branches can only be pushed by people with +# write access. No pull_request_target, no secrets: the upload +# authenticates with the job's OIDC token. +# +# Budget (600 macro-runner minutes a month). The trigger paths below are the +# non-test code of every package the benchmark set executes (measured: the +# set run once with -coverpkg over the whole module), the benchmark files +# themselves, the root go.mod/go.sum and this file. Replaying the 30 days to +# 2026-09-26 -- the busiest month on record, 113 pushes to main and 268 +# pushes to pull requests -- through those paths gives the run count; +# SETUP.md in the evidence for celeris#690 has the arithmetic and the +# per-run minutes. -benchtime=1s instead of the runner's 3s default is what +# keeps that month inside the budget; raise it if the minutes allow. +name: CodSpeed + +on: + push: + branches: [main] + paths: + - "*.go" + - "celeristest/**" + - "internal/**" + - "observe/**" + - "protocol/**" + - "middleware/*.go" + - "middleware/adapters/**" + - "middleware/circuitbreaker/**" + - "middleware/cors/**" + - "middleware/etag/**" + - "middleware/internal/**" + - "middleware/logger/**" + - "middleware/methodoverride/**" + - "middleware/pprof/**" + - "middleware/proxy/**" + - "middleware/ratelimit/**" + - "middleware/recovery/**" + - "middleware/redirect/**" + - "middleware/requestid/**" + - "middleware/rewrite/**" + - "middleware/secure/**" + - "middleware/singleflight/**" + - "middleware/static/**" + - "middleware/swagger/**" + - "middleware/timeout/**" + - "!mage*.go" + - "!**/*_test.go" + - "*bench*_test.go" + - "internal/**/*bench*_test.go" + - "protocol/**/*bench*_test.go" + - "protocol/detect/detect_test.go" + - "protocol/h2/stream/intern_test.go" + - "protocol/h2/stream/stream_test.go" + - "middleware/*bench*_test.go" + - "middleware/logger/*bench*_test.go" + - "go.mod" + - "go.sum" + - ".github/workflows/codspeed.yml" + pull_request: + branches: [main] + types: [opened, synchronize, reopened, ready_for_review] + paths: + - "*.go" + - "celeristest/**" + - "internal/**" + - "observe/**" + - "protocol/**" + - "middleware/*.go" + - "middleware/adapters/**" + - "middleware/circuitbreaker/**" + - "middleware/cors/**" + - "middleware/etag/**" + - "middleware/internal/**" + - "middleware/logger/**" + - "middleware/methodoverride/**" + - "middleware/pprof/**" + - "middleware/proxy/**" + - "middleware/ratelimit/**" + - "middleware/recovery/**" + - "middleware/redirect/**" + - "middleware/requestid/**" + - "middleware/rewrite/**" + - "middleware/secure/**" + - "middleware/singleflight/**" + - "middleware/static/**" + - "middleware/swagger/**" + - "middleware/timeout/**" + - "!mage*.go" + - "!**/*_test.go" + - "*bench*_test.go" + - "internal/**/*bench*_test.go" + - "protocol/**/*bench*_test.go" + - "protocol/detect/detect_test.go" + - "protocol/h2/stream/intern_test.go" + - "protocol/h2/stream/stream_test.go" + - "middleware/*bench*_test.go" + - "middleware/logger/*bench*_test.go" + - "go.mod" + - "go.sum" + - ".github/workflows/codspeed.yml" + # Backtests: CodSpeed dispatches this to measure older commits, and it is + # the manual way to take a fresh baseline. + workflow_dispatch: + +permissions: + contents: read + +# One run per pull request: a new push cancels the one in flight, so a +# burst of pushes costs one run. Pushes to main are never cancelled -- each +# is a baseline -- but a burst still collapses to the newest pending one. +concurrency: + group: codspeed-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + benchmarks: + name: Benchmarks (arm64 macro runner) + # Fork pull requests never reach a macro runner; drafts wait until they + # are marked ready (ready_for_review above), to save minutes. + if: >- + github.event_name != 'pull_request' || + (github.event.pull_request.head.repo.full_name == github.repository && + !github.event.pull_request.draft) + runs-on: codspeed-macro-arm64-graviton-ubuntu-22-04 + # A hang must not burn the month's minutes. A full run is a few minutes. + timeout-minutes: 20 + permissions: + contents: read # checkout + id-token: write # CodSpeed upload over OIDC, so no upload token is stored + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: "1.27.0" + # The CodSpeed Go runner builds with its own fresh GOCACHE and + # GOMODCACHE, so a restored setup-go cache would never be read. + cache: false + - name: Run the benchmarks + uses: CodSpeedHQ/action@373d6868929f444bc08d901fd0eb0ad52a8875ea # v5.2.1 + env: + # Frame-pointer unwinding: DWARF unwinding leaves Go flamegraphs + # incomplete on arm64 (https://codspeed.io/docs/benchmarks/go). + CODSPEED_PERF_UNWINDING_MODE: fp + with: + mode: walltime + # Pinned: a runner change can move every number, and 1.3.0 is the + # first release that supports Go 1.27. Bump it deliberately. + go-runner-version: "1.3.0" + run: go test -bench=. -benchtime=1s . ./protocol/... ./internal/... ./middleware ./middleware/logger diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml new file mode 100644 index 00000000..b34995a4 --- /dev/null +++ b/.github/workflows/test-coverage.yml @@ -0,0 +1,144 @@ +# Coverage: runs the unit suites of ci.yml's `unit` job -- the same packages, +# the same exclusions, -race, and the runner's own memlock -- with a coverage +# profile per module, and uploads each module to Codecov as its own flag +# (celeris#690). It is informational: codecov.yml marks every status +# informational, and this workflow is not a required check. +# +# It is a separate workflow rather than new steps in ci.yml on purpose: +# ci.yml's unit steps are held by PASS-count interlocks and edited by +# in-flight pull requests. The cost is that the unit suites run twice per +# push; GitHub-hosted minutes are free for public repositories. +# +# What is NOT in the profile, and why (so a low number is read correctly): +# ./test/... (integration/spec harness, not library code), ./adaptive/... +# (needs raised memlock; ci.yml's `adaptive` job), and all of +# ./middleware/websocket except its backpressure tests (ci.yml runs only +# those on GitHub-hosted runners). ./engine/iouring runs at the runner's +# 8 MiB memlock, where io_uring gets one worker, so its multi-worker paths +# read as uncovered here although the goceleris/probatorium cluster runs +# them. Coverage is per package (no -coverpkg), as `go test` reports it. +# +# (The file is test-coverage.yml, not coverage.yml: .gitignore ignores +# `coverage.*` for local coverage output.) +name: Coverage + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: coverage-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + coverage: + name: Coverage (root + middleware sub-modules) + runs-on: ubuntu-latest + timeout-minutes: 30 + # A fork pull request gets no OIDC token; its uploads go tokenless instead + # (the use_oidc expression below), which Codecov accepts for public repos. + permissions: + contents: read # checkout + id-token: write # Codecov upload over OIDC, so no CODECOV_TOKEN secret is stored + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: "1.27.0" + + - name: Root module (ci.yml unit package set) + run: | + echo "memlock (KiB): $(ulimit -l)" + pkgs=$(go list ./... | grep -vE '/test/|/adaptive($|/)|/middleware/websocket($|/)') + # shellcheck disable=SC2086 # word-splitting is intentional -- `go test` wants one package per arg + go test -race -count=1 -timeout=300s -covermode=atomic \ + -coverprofile="$RUNNER_TEMP/cover-root.out" $pkgs + - name: middleware/websocket (backpressure tests, as ci.yml runs them) + env: + WS484_CONNS: "16" + WS484_BURSTS: "2" + WS484_BURST_FRAMES: "1000" + run: | + go test -race -count=1 -timeout=120s -covermode=atomic \ + -coverprofile="$RUNNER_TEMP/cover-websocket.out" \ + -run '^TestBackpressure' ./middleware/websocket/... + - name: middleware/compress + working-directory: middleware/compress + run: go test -race -count=1 -timeout=120s -covermode=atomic -coverprofile="$RUNNER_TEMP/cover-compress.out" ./... + - name: middleware/metrics + working-directory: middleware/metrics + run: go test -race -count=1 -timeout=120s -covermode=atomic -coverprofile="$RUNNER_TEMP/cover-metrics.out" ./... + - name: middleware/otel + working-directory: middleware/otel + run: go test -race -count=1 -timeout=120s -covermode=atomic -coverprofile="$RUNNER_TEMP/cover-otel.out" ./... + - name: middleware/protobuf + working-directory: middleware/protobuf + run: go test -race -count=1 -timeout=120s -covermode=atomic -coverprofile="$RUNNER_TEMP/cover-protobuf.out" ./... + + # An empty profile is absent, never a pass: every file must hold the + # mode line plus at least one counted block, or nothing is uploaded. + - name: Every profile is non-empty + run: | + fail=0 + for m in root websocket compress metrics otel protobuf; do + f="$RUNNER_TEMP/cover-$m.out" + blocks=0 + [ -f "$f" ] && blocks=$(grep -vc '^mode: ' "$f" || true) + echo "cover-$m.out: $blocks blocks" + [ "$blocks" -gt 0 ] || fail=1 + done + exit "$fail" + + - name: Upload root + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 + with: + files: ${{ runner.temp }}/cover-root.out,${{ runner.temp }}/cover-websocket.out + flags: root + name: root + disable_search: true + fail_ci_if_error: true + use_oidc: ${{ !github.event.pull_request.head.repo.fork }} + - name: Upload middleware/compress + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 + with: + files: ${{ runner.temp }}/cover-compress.out + flags: compress + name: compress + disable_search: true + fail_ci_if_error: true + use_oidc: ${{ !github.event.pull_request.head.repo.fork }} + - name: Upload middleware/metrics + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 + with: + files: ${{ runner.temp }}/cover-metrics.out + flags: metrics + name: metrics + disable_search: true + fail_ci_if_error: true + use_oidc: ${{ !github.event.pull_request.head.repo.fork }} + - name: Upload middleware/otel + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 + with: + files: ${{ runner.temp }}/cover-otel.out + flags: otel + name: otel + disable_search: true + fail_ci_if_error: true + use_oidc: ${{ !github.event.pull_request.head.repo.fork }} + - name: Upload middleware/protobuf + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 + with: + files: ${{ runner.temp }}/cover-protobuf.out + flags: protobuf + name: protobuf + disable_search: true + fail_ci_if_error: true + use_oidc: ${{ !github.event.pull_request.head.repo.fork }} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..cb450482 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,122 @@ +# Codecov configuration (celeris#690). Reference: +# https://docs.codecov.com/docs/codecovyml-reference +# Validate a change before pushing it: +# curl --data-binary @codecov.yml https://codecov.io/validate +# +# Coverage is REPORTED, not enforced: every status below is informational, so +# Codecov can never turn a pull request red. The profiles come from +# .github/workflows/test-coverage.yml, which also says what they leave out. + +codecov: + # Report as soon as this commit's uploads are in. Do not wait for, or depend + # on, unrelated CI (the driver matrix, the CodSpeed job that queues for a + # macro runner): coverage is informational and must not stall behind them. + require_ci_to_pass: false + notify: + wait_for_ci: false + # One upload per module: root, compress, metrics, otel, protobuf. Waiting + # for all five keeps Codecov from posting a partial, misleading number. + after_n_builds: 5 + +coverage: + precision: 2 + round: down + status: + project: + default: + target: auto + threshold: 1% + informational: true + patch: + default: + target: auto + informational: true + changes: false + +# Go writes module-qualified paths (github.com/goceleris/celeris/router.go, +# github.com/goceleris/celeris/middleware/compress/compress.go); strip the +# module prefix so every path is relative to the repository root. The +# middleware sub-modules live under the root module's path, so one rule +# covers all five. +fixes: + - "github.com/goceleris/celeris/::" + +ignore: + # Separate harness modules and integration/spec suites, not library code. + - "test/**" + # Fuzz seed corpora and fixtures. + - "**/testdata/**" + # Generated protobuf code, should any be committed. + - "**/*.pb.go" + +# One flag per Go module, so a sub-module's number is not diluted by the +# root module's and each can be made blocking on its own later. +flag_management: + default_rules: + # Every module is uploaded on every run (test-coverage.yml has no paths + # filter), so there is nothing to carry forward, and a stale number + # would hide a module whose tests stopped running. + carryforward: false + # `root` needs no paths: `go list ./...` in the root module never reaches + # the four sub-modules, so its uploads hold root-module files only. + individual_flags: + - name: root + - name: compress + paths: + - "middleware/compress/" + - name: metrics + paths: + - "middleware/metrics/" + - name: otel + paths: + - "middleware/otel/" + - name: protobuf + paths: + - "middleware/protobuf/" + +# Coverage by area, whichever module's upload it came from. +component_management: + individual_components: + - component_id: core + name: core (router, context, internal) + paths: + - "^[^/]+\\.go$" + - "internal/**" + - "celeristest/**" + - "observe/**" + - "resource/**" + - "validation/**" + - "probe/**" + - component_id: engines + name: engines (epoll, io_uring, std) + paths: + - "engine/**" + - component_id: adaptive + name: adaptive + paths: + - "adaptive/**" + - component_id: protocol + name: protocol (h1, h2, detect) + paths: + - "protocol/**" + - component_id: middleware + name: middleware + paths: + - "middleware/**" + - component_id: drivers + name: drivers + paths: + - "driver/**" + +comment: + layout: "condensed_header, diff, flags, components, condensed_files, condensed_footer" + # One comment per pull request, edited in place on every push. + behavior: default + require_changes: false + after_n_builds: 5 + +github_checks: + # No per-line "not covered" annotations in the Files tab. Many io_uring and + # adaptive paths only run on the cluster or at raised memlock, so here they + # would be flagged as untested when they are not. + annotations: false From 16b999157e4926231294e10ad0361cd15a5d2a7d Mon Sep 17 00:00:00 2001 From: Albert Bausili Date: Sat, 26 Sep 2026 15:46:41 +0200 Subject: [PATCH 2/3] ci: keep the websocket timing tests out of coverage, and address review - test-coverage.yml: drop the middleware/websocket backpressure step. Under coverage instrumentation TestBackpressurePauseDoesNotCancelInflightSend failed on both engines (4 close-timeouts each, 29 and 1 non-ECANCELED write errors) on 19b4269, whose uninstrumented CI `unit` job passed the same test. The step stays in ci.yml, where it is proven. - test-coverage.yml: set -o pipefail so a `go list` failure stops the job instead of handing grep a partial package list. - codspeed.yml: give every workflow_dispatch its own concurrency group, so backtests and manual baselines are never cancelled by a push to main. - .coderabbit.yaml: the SHA-pin rule applies to other repositories' actions; a local ./path action takes no ref. Refs #690 --- .coderabbit.yaml | 3 ++- .github/workflows/codspeed.yml | 4 +++- .github/workflows/test-coverage.yml | 31 +++++++++++++---------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 09d289a4..0933dab7 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -169,7 +169,8 @@ reviews: of those tests without updating that list turns CI red. - path: ".github/workflows/**" instructions: | - - Every `uses:` pinned to a full 40-character commit SHA with a `# vX.Y.Z` comment that matches it. + - Every `uses:` of another repository's action or workflow (`owner/repo@...`) pinned to a full 40-character + commit SHA with a `# vX.Y.Z` comment that matches it. A local `./path` action takes no ref. - Top-level `permissions` read-only; a job raises only what it needs, with a comment saying why. - actions/checkout with `persist-credentials: false`. - No `${{ }}` expression inside `run:`; pass values through `env:`. diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 12ecb73e..eec8c0f2 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -142,8 +142,10 @@ permissions: # One run per pull request: a new push cancels the one in flight, so a # burst of pushes costs one run. Pushes to main are never cancelled -- each # is a baseline -- but a burst still collapses to the newest pending one. +# Every dispatch gets a group of its own, so the backtests CodSpeed +# dispatches together never cancel one another. concurrency: - group: codspeed-${{ github.event.pull_request.number || github.ref }} + group: codspeed-${{ github.event.pull_request.number || (github.event_name == 'workflow_dispatch' && github.run_id) || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index b34995a4..8c78645b 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -11,12 +11,16 @@ # # What is NOT in the profile, and why (so a low number is read correctly): # ./test/... (integration/spec harness, not library code), ./adaptive/... -# (needs raised memlock; ci.yml's `adaptive` job), and all of -# ./middleware/websocket except its backpressure tests (ci.yml runs only -# those on GitHub-hosted runners). ./engine/iouring runs at the runner's -# 8 MiB memlock, where io_uring gets one worker, so its multi-worker paths -# read as uncovered here although the goceleris/probatorium cluster runs -# them. Coverage is per package (no -coverpkg), as `go test` reports it. +# (needs raised memlock; ci.yml's `adaptive` job), and ./middleware/websocket. +# ci.yml runs only websocket's backpressure tests on GitHub-hosted runners, +# and those are timing-bound: under coverage instrumentation +# TestBackpressurePauseDoesNotCancelInflightSend failed on both engines +# (close-timeouts, write errors) on the same commit whose uninstrumented +# `unit` job passed (celeris#699), so they are left to ci.yml. ./engine/iouring +# runs at the runner's 8 MiB memlock, where io_uring gets one worker, so its +# multi-worker paths read as uncovered here although the +# goceleris/probatorium cluster runs them. Coverage is per package (no +# -coverpkg), as `go test` reports it. # # (The file is test-coverage.yml, not coverage.yml: .gitignore ignores # `coverage.*` for local coverage output.) @@ -56,20 +60,13 @@ jobs: - name: Root module (ci.yml unit package set) run: | + # A `go list` failure must stop the job, not hand grep a partial list. + set -o pipefail echo "memlock (KiB): $(ulimit -l)" pkgs=$(go list ./... | grep -vE '/test/|/adaptive($|/)|/middleware/websocket($|/)') # shellcheck disable=SC2086 # word-splitting is intentional -- `go test` wants one package per arg go test -race -count=1 -timeout=300s -covermode=atomic \ -coverprofile="$RUNNER_TEMP/cover-root.out" $pkgs - - name: middleware/websocket (backpressure tests, as ci.yml runs them) - env: - WS484_CONNS: "16" - WS484_BURSTS: "2" - WS484_BURST_FRAMES: "1000" - run: | - go test -race -count=1 -timeout=120s -covermode=atomic \ - -coverprofile="$RUNNER_TEMP/cover-websocket.out" \ - -run '^TestBackpressure' ./middleware/websocket/... - name: middleware/compress working-directory: middleware/compress run: go test -race -count=1 -timeout=120s -covermode=atomic -coverprofile="$RUNNER_TEMP/cover-compress.out" ./... @@ -88,7 +85,7 @@ jobs: - name: Every profile is non-empty run: | fail=0 - for m in root websocket compress metrics otel protobuf; do + for m in root compress metrics otel protobuf; do f="$RUNNER_TEMP/cover-$m.out" blocks=0 [ -f "$f" ] && blocks=$(grep -vc '^mode: ' "$f" || true) @@ -100,7 +97,7 @@ jobs: - name: Upload root uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 with: - files: ${{ runner.temp }}/cover-root.out,${{ runner.temp }}/cover-websocket.out + files: ${{ runner.temp }}/cover-root.out flags: root name: root disable_search: true From 97b3b00bd6d5dfd87054594c0b632f73e61c2f0f Mon Sep 17 00:00:00 2001 From: FumingPower Date: Sat, 26 Sep 2026 16:26:15 +0200 Subject: [PATCH 3/3] ci: re-trigger CodSpeed now that its action is allowed (celeris#690)