Merge pull request #24 from tomtom215/claude/project-review-modernize… #129
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Continuous integration — enforces all quality gates. | |
| # | |
| # Jobs: test + clippy + fmt + doc (matrix), behavioral-extension tests, | |
| # static checks, security audit, code coverage, Docker build, | |
| # release-build smoke test. | |
| # | |
| # Job dependency graph (all jobs run in parallel): | |
| # | |
| # ci (4-entry matrix) | |
| # behavioral # runs the DuckDB behavioral-extension-backed tests with | |
| # # MALLARD_REQUIRE_BEHAVIORAL=1 so they fail rather than skip. | |
| # static-checks # actionlint + JS syntax + tracker drift + MSRV drift | |
| # security | |
| # coverage | |
| # docker | |
| # release-build # catches target-specific build failures (e.g. musl C++ | |
| # # toolchain) before they slip into the release workflow. | |
| name: CI | |
| on: | |
| push: | |
| branches: [main, "claude/**"] | |
| pull_request: | |
| branches: [main] | |
| # Cancel in-progress runs for the same branch/PR — saves CI minutes when | |
| # multiple commits are pushed in quick succession. Main-branch pushes | |
| # are NOT cancelled so merge builds always complete. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| permissions: | |
| contents: read | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUST_BACKTRACE: 1 | |
| RUSTFLAGS: "-D warnings" | |
| RUSTDOCFLAGS: "-D warnings" | |
| # ── Job 1: Lint, test, and compile (parallel matrix) ────────────────────────── | |
| jobs: | |
| ci: | |
| name: ${{ matrix.name }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: Test | |
| command: cargo test --locked --all-targets && cargo test --locked --doc | |
| - name: Clippy | |
| components: clippy | |
| command: cargo clippy --locked --all-targets --all-features -- -D warnings | |
| - name: Format | |
| components: rustfmt | |
| command: cargo fmt -- --check | |
| - name: Documentation | |
| command: cargo doc --no-deps | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 | |
| with: | |
| toolchain: "1.98.0" | |
| components: ${{ matrix.components || '' }} | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 | |
| - name: ${{ matrix.name }} | |
| run: ${{ matrix.command }} | |
| # ── Job 2: Behavioral-extension integration tests ───────────────────────────── | |
| # | |
| # The DuckDB `behavioral` community extension is downloaded at test time. Unit | |
| # tests skip behavioral assertions when it cannot be loaded (offline dev), so CI | |
| # sets MALLARD_REQUIRE_BEHAVIORAL=1 to turn those skips into hard failures — | |
| # otherwise a regression in funnel/retention/session SQL would pass silently. | |
| behavioral: | |
| name: Behavioral extension tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 | |
| with: | |
| toolchain: "1.98.0" | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 | |
| - name: Run behavioral-backed tests | |
| env: | |
| MALLARD_REQUIRE_BEHAVIORAL: "1" | |
| run: cargo test --locked --all-targets | |
| # ── Job 3: End-to-end smoke test against the real binary ────────────────────── | |
| # | |
| # The other jobs drive the router in-process. This one starts the executable on | |
| # a socket with real configuration, a real DuckDB file and real Parquet on disk. | |
| # Both a 500 on /api/stats/realtime and an unauthenticated /api/keys reached a | |
| # fully green test suite before this existed. | |
| smoke: | |
| name: Smoke test | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 | |
| with: | |
| toolchain: "1.98.0" | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 | |
| - name: Build the binary | |
| run: cargo build --locked | |
| - name: Run the smoke test | |
| run: scripts/smoke-test.sh target/debug/mallard-metrics | |
| # ── Job 4: Static checks (workflows, JS, asset drift) ───────────────────────── | |
| static-checks: | |
| name: Static checks | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Lint GitHub Actions workflows | |
| run: | | |
| docker run --rm -v "$PWD:/repo" --workdir /repo \ | |
| rhysd/actionlint:1.7.7 -color | |
| - name: Check the declared MSRV matches every place it is pinned | |
| run: | | |
| set -euo pipefail | |
| # There is no separate "build at MSRV" job: rust-version and the | |
| # pinned channel are the same version, so it would duplicate the Test | |
| # job exactly. That only holds while they agree, which is what this | |
| # checks — including the literals in the workflows, because a bump | |
| # that misses one silently tests a different compiler than it claims. | |
| CARGO_MSRV=$(grep -m1 '^rust-version' Cargo.toml | sed 's/.*"\(.*\)".*/\1/') | |
| PINNED=$(grep -m1 '^channel' rust-toolchain.toml | sed 's/.*"\(.*\)".*/\1/') | |
| echo "Cargo.toml rust-version: $CARGO_MSRV" | |
| echo "rust-toolchain.toml: $PINNED" | |
| if [ "$CARGO_MSRV" != "$PINNED" ]; then | |
| echo "::error file=Cargo.toml::rust-version ($CARGO_MSRV) and rust-toolchain.toml channel ($PINNED) have drifted apart." | |
| exit 1 | |
| fi | |
| status=0 | |
| for wf in .github/workflows/*.yml; do | |
| while IFS= read -r version; do | |
| if [ "$version" != "$CARGO_MSRV" ]; then | |
| echo "::error file=$wf::pins toolchain $version but the MSRV is $CARGO_MSRV" | |
| status=1 | |
| fi | |
| done < <(grep -oE '^ *toolchain: "[0-9.]+"' "$wf" | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?') | |
| done | |
| exit "$status" | |
| - name: Check JavaScript syntax | |
| run: | | |
| set -euo pipefail | |
| for f in tracking/script.js src/dashboard/assets/app.js; do | |
| echo "Checking $f" | |
| node --check "$f" | |
| done | |
| - name: Check dashboard method references | |
| run: | | |
| set -euo pipefail | |
| # `node --check` validates syntax and says nothing about whether a | |
| # method exists, and the dashboard has no test harness — so a button | |
| # wired to a method that was renamed fails only when a user clicks it. | |
| node scripts/check-dashboard-methods.mjs src/dashboard/assets/app.js | |
| - name: Check the tracking script stays inside its size budget | |
| run: | | |
| set -euo pipefail | |
| # The README and the docs both claim "about 3 KB gzipped". The budget | |
| # is deliberately close to the current size: the script is served to | |
| # every visitor on every page, so growth should be a decision rather | |
| # than an accident. | |
| BUDGET=4096 | |
| SIZE=$(gzip -9 -c tracking/script.js | wc -c) | |
| echo "tracking/script.js: ${SIZE} bytes gzipped (budget ${BUDGET})" | |
| if [ "$SIZE" -gt "$BUDGET" ]; then | |
| echo "::error file=tracking/script.js::Gzipped size ${SIZE} exceeds the ${BUDGET}-byte budget; update the documented size if this is intended." | |
| exit 1 | |
| fi | |
| - name: Ensure the tracking script has a single source | |
| run: | | |
| set -euo pipefail | |
| # tracking/script.js is compiled into the binary with include_str! and | |
| # served from /mallard.js. A second copy under the embedded asset | |
| # directory would silently drift out of date. | |
| if [ -e src/dashboard/assets/mallard.js ]; then | |
| echo "::error file=src/dashboard/assets/mallard.js::Duplicate tracker copy found; tracking/script.js is the only source." | |
| exit 1 | |
| fi | |
| grep -q 'include_str!' src/dashboard/mod.rs | |
| # ── Job 5: Security audit ───────────────────────────────────────────────────── | |
| security: | |
| name: Security audit | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: EmbarkStudios/cargo-deny-action@82eb9f621fbc699dd0918f3ea06864c14cc84246 # v2 | |
| with: | |
| log-level: warn | |
| command: check | |
| arguments: --all-features | |
| # ── Job 6: Code coverage ────────────────────────────────────────────────────── | |
| coverage: | |
| name: Code coverage | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 | |
| with: | |
| toolchain: "1.98.0" | |
| components: llvm-tools-preview | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 | |
| - uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0 | |
| with: | |
| tool: cargo-llvm-cov | |
| - run: cargo llvm-cov --all-features --lcov --output-path lcov.info | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: coverage-report | |
| path: lcov.info | |
| # ── Job 7: Docker build ─────────────────────────────────────────────────────── | |
| docker: | |
| name: Docker build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Build Docker image | |
| run: docker build -t mallard-metrics:test . | |
| # ── Job 8: Release build smoke test ─────────────────────────────────────────── | |
| # | |
| # Exercises the same cross-compilation path the release workflow uses so that | |
| # target-specific build breakage (e.g. a missing musl C++ toolchain for | |
| # `libduckdb-sys`) is caught on every push — not on the first release tag. | |
| # | |
| # Runs both musl targets via `cross`, matching release.yml exactly. The | |
| # aarch64 leg is the slow one; x86_64 is fast and catches the vast majority of | |
| # issues, so we mark the matrix `fail-fast: false` to get full coverage. | |
| release-build: | |
| name: Release build / ${{ matrix.target }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: | |
| - x86_64-unknown-linux-musl | |
| - aarch64-unknown-linux-musl | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 | |
| with: | |
| toolchain: "1.98.0" | |
| targets: ${{ matrix.target }} | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 | |
| with: | |
| key: release-${{ matrix.target }} | |
| - uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0 | |
| with: | |
| tool: cross | |
| - name: Build release (cross) | |
| run: cross build --locked --release --target ${{ matrix.target }} |