Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# actionlint configuration.
#
# The `coverage` job in ci.yml declares the `code-quality: write` token scope
# for GitHub Code Quality (the native PR coverage comment — #133). That scope
# is in public preview (added by GitHub 2026-05) and isn't yet in actionlint's
# built-in scope list, so actionlint v1.7.12 flags it as "unknown permission
# scope". Suppress only that exact message — a genuine scope typo (e.g.
# `contents` → `conten`) still produces a different message and fails. Drop
# this once actionlint ships the scope.
paths:
"**/*.yml":
ignore:
- 'unknown permission scope "code-quality"'
36 changes: 33 additions & 3 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ graph TB
unit -. "coverage-unit (poll)" .-> coverage
integration -. "coverage-integration (poll)" .-> coverage
e2e -. "coverage-e2e (poll)" .-> coverage
coverage --> badge["badge (main) — non-gating"]
title["title (PRs)"] --> ci["CI (aggregator — sole required check)"]
lint["lint"] --> ci
coverage --> ci
Expand Down Expand Up @@ -54,12 +55,14 @@ Break one of these knowingly or not at all.
Go suites) and event-filtered jobs (title on pushes, deploys on PRs)
never orphan the required check, and adding/renaming jobs never
requires a ruleset edit. Consequence: every job that must gate merges
**must be in the aggregator's `needs` list**. Two jobs are deliberately
non-gating and excluded: `timing` (advisory wall-clock table) and
**must be in the aggregator's `needs` list**. Three jobs are deliberately
non-gating and excluded: `timing` (advisory wall-clock table),
`docs-preview` (the convenience Cloudflare preview deploy — `docs-build`
already validates the build and *is* a need, so only the build gates;
the preview deploy reports its own "Docs preview" check but, slow or
failed, never delays or reds `CI`).
failed, never delays or reds `CI`), and `badge` (publishes the README
coverage badge to the `badges` branch on main pushes — a badge push must
never block a merge; it reports its own "Coverage badge" status).

2. **A dedicated `coverage` job applies the consolidated gate, polling —
not `needs`-ing — the suites.** Each suite (`unit`, `integration`,
Expand Down Expand Up @@ -124,6 +127,33 @@ Break one of these knowingly or not at all.
cushion the next run), and concurrent same-key misses produce benign
"already exists" warnings.

## Coverage publishing

The `coverage` job both **gates** (`make cov` against the floors in
`.testcoverage.yml`) and **publishes** — independent concerns, and only the
gate blocks merges ([#133](https://github.com/Wave-RF/WaveHouse/issues/133)):

- **Per-run job summary** — the merged per-package func table on every run's
Summary page.
- **PR comment (GitHub Code Quality)** — on same-repo PRs and main pushes the
job converts the merged Go profile to Cobertura (`go tool gocover-cobertura`,
`-ignore-dirs` mirroring `.testcoverage.yml`'s global excludes so the % tracks
the merged-total gate) and uploads it via `actions/upload-code-coverage`; the
`github-code-quality[bot]` posts the aggregate + per-file diff-vs-main
comment. **Non-gating**: the upload is `continue-on-error`, so this
public-preview feature can never red `CI`. Fork PRs skip (no `code-quality`
token, per GitHub's own guard). Renders only once the repo's *Settings →
Security → Code quality* is enabled.
- **README badge** — on main pushes the job emits a shields.io endpoint JSON
for the merged Go total (`cov badge`, the exact gated number); the separate
non-gating `badge` job publishes it to the orphan `badges` branch, which the
README badge reads over `raw.githubusercontent.com` (public-repo only). The
`badge` job is the sole holder of `contents:write` and runs only on trusted
main, so a push to `badges` can't be influenced by PR code (invariant 5).

SDK (TS) coverage is gated by `make cov` but not yet published — extend with a
`language: javascript` upload step and a second badge JSON when wanted.

## Merge queue

PRs land through a **merge queue**: "Merge when ready" enqueues the PR,
Expand Down
97 changes: 94 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,12 @@ jobs:
permissions:
contents: read
actions: read # poll for + download the suites' coverage fragments from this run
# Upload the Cobertura report to GitHub Code Quality, which posts the
# PR coverage comment (aggregate + per-file diff vs main). Narrow
# scope: it can't read secrets or write contents. Public preview, so
# actionlint doesn't know the scope yet — suppressed in
# .github/actionlint.yaml.
code-quality: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
Expand Down Expand Up @@ -358,8 +364,56 @@ jobs:
path: tmp/coverage
- name: Render consolidated report + gate thresholds
run: make cov
# Coverage in the job-summary panel. See #133 for post-launch
# reporting decisions.
# ── GitHub Code Quality: PR coverage comment (non-gating) ──────────
# Convert the merged Go profile to Cobertura and hand it to GitHub
# Code Quality, whose bot posts the PR coverage comment (aggregate +
# per-file gained/lost vs main). `!cancelled()` so a coverage *drop*
# that fails the gate above still gets a comment; the upload is
# continue-on-error, so this preview feature can never red CI — the
# gate stays `make cov`. The -ignore-dirs mirror .testcoverage.yml's
# global excludes so Code Quality's % tracks the merged-total gate.
- name: Convert Go coverage to Cobertura
if: ${{ !cancelled() }}
run: |
[ -f tmp/coverage/total/coverage.txt ] || exit 0
go tool gocover-cobertura \
-ignore-dirs '/(testutil|tests|scripts)(/|$)' \
< tmp/coverage/total/coverage.txt > tmp/coverage/go-coverage.xml
# Skipped on fork PRs (no code-quality token), per GitHub's own guard:
# baseline on main pushes, comparison on same-repo PRs.
- name: Upload Go coverage to Code Quality
if: >-
${{ !cancelled() && (
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository)
) }}
continue-on-error: true
uses: actions/upload-code-coverage@abb5995db9e0199b0e2bb9dbd136fce4cb1ec4d3 # v1.3.0
with:
file: tmp/coverage/go-coverage.xml
language: go
# ── Go coverage badge data (main only) ─────────────────────────────
# Emit the shields.io endpoint JSON for the merged Go total (the exact
# gated number) and hand it to the `badge` job, the sole holder of
# contents:write, which publishes it to the `badges` branch.
- name: Generate Go coverage badge
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
run: |
mkdir -p tmp/coverage/badge
go run ./scripts/cov badge > tmp/coverage/badge/coverage-go.json
- name: Upload badge data
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: go-coverage-badge
path: tmp/coverage/badge
if-no-files-found: error
retention-days: 3
overwrite: true
# Coverage in the job-summary panel — the at-a-glance per-run number
# (the README badge + Code Quality PR comment are the published
# surfaces; this stays for the full per-package func table).
- name: Coverage summary
if: always()
run: |
Expand All @@ -372,6 +426,42 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"
fi

# ── Coverage badge publish (main only, non-gating) ─────────────────
# Publishes the Go merged-total coverage to the orphan `badges` branch as
# a shields.io endpoint JSON, which the README badge reads over
# raw.githubusercontent.com (#133). The ONLY job holding contents:write,
# and it runs solely on main pushes off a trusted-main checkout — it never
# executes PR code (trust-domain invariant). Non-gating: deliberately NOT
# in the `CI` aggregator's needs (a badge push must never block a merge) —
# it reports its own "Coverage badge" status. The number is produced by
# the coverage job (`cov badge`, the exact gated value) and passed here as
# the go-coverage-badge artifact, so this job needs no Go toolchain.
badge:
name: Coverage badge
needs: [changes, coverage]
if: >-
github.event_name == 'push' && github.ref == 'refs/heads/main' &&
needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write # push the badge JSON to the `badges` branch
actions: read # download the go-coverage-badge artifact from this run
steps:
# persist-credentials stays at its default (true): this job's whole
# purpose is to push, and it runs only on trusted main. publish-badge.sh
# fetches the badges branch itself, so a shallow checkout is enough.
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 1
- name: Download badge data
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: go-coverage-badge
path: tmp/coverage/badge
- name: Publish badge to the badges branch
run: scripts/ci/publish-badge.sh tmp/coverage/badge/coverage-go.json coverage-go.json

# ── Docs deploys ─────────────────────────────────────────────────────
# Both deploy jobs check out the DEFAULT branch (or, on push, the pushed
# main commit itself) and install wrangler from that trusted lockfile —
Expand Down Expand Up @@ -565,7 +655,7 @@ jobs:
# deploy (see its job), non-gating by design, so it neither delays nor
# reds the required check — its own "Docs preview" status reports it. The
# `docs-build` it depends on IS here, so a broken docs BUILD still gates.
# `timing` (non-gating) is the only other job excluded.
# `timing` and `badge` (both non-gating) are the only other jobs excluded.
ci:
name: CI
needs:
Expand Down Expand Up @@ -615,6 +705,7 @@ jobs:
integration,
e2e,
coverage,
badge,
docs-preview,
docs-deploy,
]
Expand Down
6 changes: 4 additions & 2 deletions .testcoverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
# behind `make cov` / `make test-<suite>`) which renders profiles and
# gates each suite against the numbers below. Schema is compatible with
# `vladopajic/go-test-coverage`'s config format, but we don't run that
# action anymore — see issue #133 for the future-state coverage
# reporting decision (post-OSS-launch, likely Codecov or similar).
# action anymore. Publishing (#133) lives in CI's `coverage` job — a
# README badge from the merged Go total (`cov badge`) and PR comments
# via GitHub Code Quality; see .github/workflows/README.md "Coverage
# publishing". This file is the gate; publishing reads from it.

# Merged-suites profile produced by `scripts/cov merge` at the end of
# `make ci`. Contains unit + integration + e2e coverage stitched
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ Verbose: `V=1 make test`. Extra args: `make test ARGS="-run TestFoo"`. Build tag

Tooling notes (the non-obvious bits `make help` won't tell you):

- Dev tools (`gotestsum`, `gofumpt`, `goimports`, `govulncheck`, `go-test-coverage`, `deadcode`, `gsa`, `goda`) are pinned in `go.mod` via `tool` directives — `go tool <name>`, no manual install.
- Dev tools (`gotestsum`, `gofumpt`, `goimports`, `govulncheck`, `go-test-coverage`, `gocover-cobertura`, `deadcode`, `gsa`, `goda`) are pinned in `go.mod` via `tool` directives — `go tool <name>`, no manual install.
- `golangci-lint` is pinned in the Makefile (v2.11.4), auto-installed to `.bin/` on first `make lint` — kept out of `go.mod` (its deps conflict with the main module).
- `pnpm` (≥ 11.1) + `Node 22 LTS` (`.nvmrc`, matches CI) must be on PATH; `make tools` runs one root `pnpm install --frozen-lockfile` across the three workspaces (SDK `clients/ts/`, E2E `tests/e2e/sdk/`, docs `docs/`).
- **GNU Make 4+** required (uses `--output-sync=target`); macOS BSD Make 3.81 won't parse it. Full setup: `docs/src/content/docs/development.md` § Prerequisites.
Expand All @@ -123,7 +123,7 @@ Tooling notes (the non-obvious bits `make help` won't tell you):
- **Policy helpers**: Use `policy.NewMemoryStore(p)` for in-memory policy testing without NATS.
- **Pipes helpers**: Use `pipes.NewMemoryStore(queries...)` for in-memory pipes testing without NATS.
- **Response assertions**: Use `testutil.AssertJSONResponse(t, rec, status, expected)` and `testutil.AssertJSONContains(t, rec, status, substring)`.
- **Coverage target**: 80% project-wide (CI enforces `threshold.total` in `.testcoverage.yml` against the merged unit + integration + e2e profile). Per-suite minima also enforced: unit 80%, integration 20%, e2e 60%, sdk 50%. Aim for 80%+ on new code.
- **Coverage target**: 80% project-wide (CI enforces `threshold.total` in `.testcoverage.yml` against the merged unit + integration + e2e profile). Per-suite minima also enforced: unit 80%, integration 20%, e2e 60%, sdk 50%. Aim for 80%+ on new code. Coverage is published as a README badge (Go merged-total) and as PR comments via GitHub Code Quality — see `.github/workflows/README.md` "Coverage publishing"; the gate is unchanged.
- **Every new function should have corresponding test cases.** Run `make lint` and `make test` before considering work complete.
- **E2E tests via SDK**: The TypeScript SDK is the primary E2E test harness. Tests in `tests/e2e/sdk/` exercise the full pipeline (ingest → ClickHouse → query) and simultaneously validate backend behavior and SDK correctness. Use `make test-e2e` to run. Add new E2E scenarios as `tests/e2e/sdk/*.test.ts` files using helpers from `tests/e2e/sdk/helpers.ts`.
- **Per-suite table isolation**: Each e2e test file owns its own ClickHouse tables — `clicks_<suite>` / `events_<suite>` / `users_<suite>`, generated from `tests/e2e/sdk/tables.ts` and created by `setup.ts`. A new test file must (1) add its suite name to `SUITES` in `tables.ts` and (2) get its names via `const T = suiteTables("<suite>")`, then reference `T.clicks` etc. — never a bare `clicks`. This makes cross-file *data* contamination structurally impossible. Files still run **sequentially** (`vitest.config.ts` `maxWorkers: 1`): running them in parallel is blocked by shared *global policy* state (several files read-modify-write the single policy document; `streaming.test.ts` flips the global `default_role`), so policy-mutating tests snapshot the full policy and restore it. Dropping `maxWorkers: 1` is a deferred follow-up tracked in #214 (per-table policy storage; see `docs/src/content/docs/ingest-pipeline.md` § Deferred).
Expand Down
Loading
Loading