Skip to content

Merge pull request #24 from tomtom215/claude/project-review-modernize… #129

Merge pull request #24 from tomtom215/claude/project-review-modernize…

Merge pull request #24 from tomtom215/claude/project-review-modernize… #129

Workflow file for this run

# 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 }}