Skip to content

Merge pull request #400 from ahrav/feature/postgres-git-persistence-b… #1700

Merge pull request #400 from ahrav/feature/postgres-git-persistence-b…

Merge pull request #400 from ahrav/feature/postgres-git-persistence-b… #1700

Workflow file for this run

name: CI
on:
push:
branches: [main]
paths-ignore:
- '.beads/**'
- 'docs/**'
- 'diagrams/**'
- '*.md'
- 'LICENSE'
pull_request:
branches: [main]
paths-ignore:
- '.beads/**'
- 'docs/**'
- 'diagrams/**'
- '*.md'
- 'LICENSE'
schedule:
- cron: "0 2 * * *" # nightly at 02:00 UTC
workflow_dispatch:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
permissions:
contents: read
actions: read
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: "0"
CARGO_PROFILE_DEV_DEBUG: "0"
CARGO_PROFILE_TEST_DEBUG: "0"
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
jobs:
# ── Path filtering — skip expensive jobs on docs-only PRs ──────────
changes:
name: Detect Changes
runs-on: ubuntu-24.04
timeout-minutes: 5
if: github.event_name == 'pull_request'
outputs:
rust: ${{ steps.filter.outputs.rust }}
fuzz: ${{ steps.filter.outputs.fuzz }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3.0.3
id: filter
with:
filters: |
rust:
- 'crates/**'
- 'tests/**'
- 'benches/**'
- 'examples/**'
- 'tools/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/**'
fuzz:
- 'crates/scanner-git/fuzz/**'
- 'crates/scanner-git/src/**'
# ── Format ─────────────────────────────────────────────────────────
fmt:
name: Format
runs-on: ubuntu-24.04
timeout-minutes: 5
if: ${{ always() && !cancelled() && (github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true') }}
needs: [changes]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # master
with:
toolchain: stable
components: rustfmt
# No cache needed — fmt does not compile
- run: cargo fmt --all -- --check
# ── Lint ───────────────────────────────────────────────────────────
clippy:
name: Clippy (${{ matrix.label }})
runs-on: ${{ matrix.os }}
timeout-minutes: 20
if: ${{ always() && !cancelled() && (github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true') }}
needs: [changes]
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
shared-key: stable-linux
label: x86_64
- os: macos-15
shared-key: stable-macos
label: aarch64
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-rust
with:
toolchain: stable
components: clippy
shared-key: ${{ matrix.shared-key }}
- run: cargo clippy --all-targets --all-features -- -D warnings
# ── Test ───────────────────────────────────────────────────────────
# PR/push: lightweight feature set — excludes stdx-proptest, sim-harness,
# tiger-harness, kgram-gate, bench, b64-stats, perf-counters.
# Those are exercised nightly by test-full-harness with --all-features.
# Infrastructure-dependent crates (postgres, etcd) run in dedicated jobs
# with service containers.
test:
name: Test (${{ matrix.label }})
runs-on: ${{ matrix.os }}
timeout-minutes: ${{ matrix.timeout }}
if: ${{ always() && !cancelled() && (github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true') }}
needs: [changes]
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
shared-key: stable-linux
timeout: 15
label: x86_64
extra-excludes: ""
- os: macos-15
shared-key: stable-macos
timeout: 30
label: aarch64
# gossip-worker tests need Docker (testcontainers) which is
# unavailable on macOS runners. Those tests run on x86_64/ubuntu
# and in the dedicated postgres/etcd CI jobs.
extra-excludes: "--exclude gossip-worker"
env:
PROPTEST_CASES: "4"
STRESS_ITERATIONS: "20"
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-rust
with:
toolchain: stable
shared-key: ${{ matrix.shared-key }}
save-cache: "false"
- name: Install cargo-nextest
uses: taiki-e/install-action@06203676c62f0d3c765be3f2fcfbebbcb02d09f5 # v2.69.6
with:
tool: cargo-nextest
- name: Build tests
run: >-
cargo nextest run
--workspace
--features test-support,perf-stats
--exclude gossip-findings-postgres
--exclude gossip-done-ledger-postgres
--exclude gossip-git-persistence-postgres
--exclude gossip-coordination-etcd
${{ matrix.extra-excludes }}
--no-run
--profile ci
- name: Run tests
run: >-
cargo nextest run
--workspace
--features test-support,perf-stats
--exclude gossip-findings-postgres
--exclude gossip-done-ledger-postgres
--exclude gossip-git-persistence-postgres
--exclude gossip-coordination-etcd
${{ matrix.extra-excludes }}
--profile ci
# nextest does not run doctests; run them separately.
- name: Run doctests
run: >-
cargo test
--features test-support,perf-stats
--doc
# ── Test (PostgreSQL integration) ─────────────────────────────────
# Runs PostgreSQL-dependent tests against a real service container.
# GOSSIP_POSTGRES_TEST_URL bypasses testcontainers so all PG crates
# share the same service container instead of spinning up their own.
test-postgres:
name: Test (PostgreSQL)
runs-on: ubuntu-24.04
timeout-minutes: 15
if: ${{ always() && !cancelled() && (github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true') }}
needs: [changes]
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
env:
GOSSIP_POSTGRES_TEST_URL: "host=localhost port=5432 user=postgres password=postgres dbname=postgres"
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-rust
with:
toolchain: stable
shared-key: stable-linux
save-cache: "false"
- name: Install cargo-nextest
uses: taiki-e/install-action@06203676c62f0d3c765be3f2fcfbebbcb02d09f5 # v2.69.6
with:
tool: cargo-nextest
- name: Run PostgreSQL integration tests (done-ledger)
run: >-
cargo nextest run
-p gossip-done-ledger-postgres
--profile ci
- name: Run PostgreSQL integration tests (findings)
run: >-
cargo nextest run
-p gossip-findings-postgres
--features test-utils
--profile ci
- name: Run PostgreSQL integration tests (git-persistence)
run: >-
cargo nextest run
-p gossip-git-persistence-postgres
--features test-utils
--profile ci
- name: Run PostgreSQL integration tests (persistence differential oracle)
run: >-
cargo nextest run
-p gossip-persistence-inmemory
--run-ignored ignored-only
--profile ci
-E 'test(done_ledger_pg_matches_in_memory)'
# ── Test (etcd integration) ──────────────────────────────────────
# Runs etcd-dependent tests against a real service container.
# ETCD_ENDPOINTS bypasses testcontainers so all tests share the
# same service container instead of spinning up their own.
#
# The coreos/etcd image is distroless (no /bin/sh), so Docker's
# CMD-SHELL health checks cannot work. We skip the health check
# in `options` and instead poll the HTTP /health endpoint from
# the runner in an explicit step.
test-etcd:
name: Test (etcd)
runs-on: ubuntu-24.04
timeout-minutes: 15
if: ${{ always() && !cancelled() && (github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true') }}
needs: [changes]
services:
etcd:
image: quay.io/coreos/etcd:v3.5.15
env:
ETCD_NAME: node1
ETCD_DATA_DIR: /etcd-data
ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
ETCD_ADVERTISE_CLIENT_URLS: http://etcd:2379
ETCD_LISTEN_PEER_URLS: http://0.0.0.0:2380
ETCD_INITIAL_ADVERTISE_PEER_URLS: http://etcd:2380
ETCD_INITIAL_CLUSTER: node1=http://etcd:2380
ETCD_INITIAL_CLUSTER_TOKEN: gossip-rs-ci-cluster
ETCD_INITIAL_CLUSTER_STATE: new
ports:
- 2379:2379
env:
ETCD_ENDPOINTS: "http://localhost:2379"
steps:
- name: Wait for etcd
run: |
for i in $(seq 1 30); do
if curl -sf http://localhost:2379/health > /dev/null 2>&1; then
echo "etcd is ready"
exit 0
fi
echo "Waiting for etcd... ($i/30)"
sleep 1
done
echo "etcd failed to become ready"
docker logs "$(docker ps -q --filter ancestor=quay.io/coreos/etcd:v3.5.15)" 2>&1 || true
exit 1
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-rust
with:
toolchain: stable
shared-key: stable-linux
save-cache: "false"
- name: Install cargo-nextest
uses: taiki-e/install-action@06203676c62f0d3c765be3f2fcfbebbcb02d09f5 # v2.69.6
with:
tool: cargo-nextest
- name: Run etcd integration tests
run: >-
cargo nextest run
-p gossip-coordination-etcd
--features test-support
--profile ci
- name: Run etcd ignored tests (differential oracle, needs service container)
run: >-
cargo nextest run
-p gossip-coordination-etcd
--features test-support
--run-ignored ignored-only
--profile ci
-E 'binary(~differential_oracle)'
- name: Run etcd integration tests (release)
if: github.event_name == 'schedule'
run: >-
cargo nextest run
-p gossip-coordination-etcd
--features test-support
--release
--profile nightly
- name: Run all etcd ignored tests (release, nightly)
if: github.event_name == 'schedule'
run: >-
cargo nextest run
-p gossip-coordination-etcd
--features test-support
--release
--run-ignored ignored-only
--profile nightly
# ── Benchmark (compile check) ────────────────────────────────────
bench:
name: Bench (compile)
runs-on: ubuntu-24.04
timeout-minutes: 10
if: ${{ always() && !cancelled() && (github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true') }}
needs: [changes]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-rust
with:
toolchain: stable
shared-key: stable-linux-bench
save-cache: "true"
- run: cargo bench --features test-support,bench,perf-stats --no-run
# ── MSRV ──────────────────────────────────────────────────────────
msrv:
name: MSRV (1.93)
runs-on: ubuntu-24.04
timeout-minutes: 15
if: ${{ always() && !cancelled() && (github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true') }}
needs: [changes]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-rust
with:
toolchain: "1.93"
shared-key: msrv-linux
- run: cargo check --all-features
# ── Docs ───────────────────────────────────────────────────────────
doc:
name: Doc
runs-on: ubuntu-24.04
timeout-minutes: 10
if: ${{ always() && !cancelled() && (github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true') }}
needs: [changes]
env:
RUSTDOCFLAGS: -D warnings
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-rust
with:
toolchain: stable
shared-key: stable-linux-doc
save-cache: "true"
# Explicit feature list instead of --all-features: avoids compiling
# librocksdb-sys (C++ from source, ~4 min on CI) which adds no public
# API surface. Only test-support features are needed to resolve
# doc-links to cfg-gated modules.
- run: >-
cargo doc --no-deps
--features gossip-contracts/test-support,gossip-coordination/test-support,gossip-coordination-etcd/test-support,gossip-git-persistence-postgres/test-utils,gossip-pg-common/test-support,gossip-done-ledger-postgres/test-utils,gossip-findings-postgres/test-utils,gossip-persistence-inmemory/test-support,gossip-scanner-runtime/test-support,scanner-engine/test-support
# ── Dependency Lint (licenses, advisories, bans) ───────────────────
deny:
name: cargo-deny
runs-on: ubuntu-24.04
timeout-minutes: 10
if: ${{ always() && !cancelled() && (github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true') }}
needs: [changes]
env:
RUSTC_WRAPPER: ""
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2
with:
command: check
arguments: --all-features
# ── Eval Harness ──────────────────────────────────────────────────
# Separate workspace in tools/eval-harness/ — has its own Cargo.toml
# and Cargo.lock, so fmt/clippy/test run independently.
eval-harness:
name: Eval Harness
runs-on: ubuntu-24.04
timeout-minutes: 15
if: ${{ always() && !cancelled() && (github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true') }}
needs: [changes]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-rust
with:
toolchain: stable
components: rustfmt, clippy
shared-key: eval-harness-linux
cache-workspaces: "tools/eval-harness -> target"
- run: cargo fmt --check
working-directory: tools/eval-harness
- run: cargo clippy --all-targets --all-features -- -D warnings
working-directory: tools/eval-harness
- name: Install cargo-nextest
uses: taiki-e/install-action@06203676c62f0d3c765be3f2fcfbebbcb02d09f5 # v2.69.6
with:
tool: cargo-nextest
- run: cargo nextest run
working-directory: tools/eval-harness
# ── Dependency Review (PR only) ────────────────────────────────────
# Catches newly introduced vulnerable or license-incompatible deps at
# PR time, before they land. Complements cargo-deny (license/ban rules)
# and rustsec/audit-check (post-merge advisory scan).
dependency-review:
name: Dependency Review
runs-on: ubuntu-24.04
timeout-minutes: 5
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
with:
fail-on-severity: high
deny-licenses: AGPL-3.0, AGPL-3.0-only, AGPL-3.0-or-later, GPL-3.0, GPL-3.0-only, GPL-3.0-or-later
# ── Security Audit ─────────────────────────────────────────────────
audit:
name: Security Audit
runs-on: ubuntu-24.04
timeout-minutes: 10
env:
RUSTC_WRAPPER: ""
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: rustsec/audit-check@69366f33c96575abad1ee0dba8212993eecbe998 # v2.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
# ── Fuzz Pack Smoke (PR only) ───────────────────────────────────────
# Quick sanity check on pack-format fuzz targets when fuzz or source
# files change in a PR. Each target runs for 20 seconds max.
# Targets are auto-discovered from fuzz_targets/fuzz_pack_*.rs.
fuzz-pack-smoke:
name: Fuzz Pack Smoke (PR)
runs-on: ubuntu-24.04
timeout-minutes: 10
if: github.event_name == 'pull_request' && needs.changes.outputs.fuzz == 'true'
needs: [changes]
env:
RUSTC_WRAPPER: ""
FUZZ_SECONDS: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-rust
with:
toolchain: nightly
components: rust-src
shared-key: nightly-linux-fuzz-smoke
extra-apt: "clang llvm"
cache-workspaces: "crates/scanner-git/fuzz -> target"
- uses: ./.github/actions/setup-cargo-fuzz
- name: Restore fuzz corpus cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: crates/scanner-git/fuzz/corpus
key: fuzz-corpus-pack-${{ github.sha }}
restore-keys: |
fuzz-corpus-pack-
- name: Run pack fuzz targets
working-directory: crates/scanner-git/fuzz
run: |
set -euo pipefail
ran=0
for f in fuzz_targets/fuzz_pack_*.rs; do
[ -f "$f" ] || continue
target="$(basename "${f%.rs}")"
echo "Running: ${target}"
cargo +nightly fuzz run "${target}" \
--target x86_64-unknown-linux-gnu \
-- -max_total_time="${FUZZ_SECONDS}"
ran=$((ran + 1))
done
if [ "${ran}" -eq 0 ]; then
echo "::error::No fuzz targets discovered — expected fuzz_targets/fuzz_pack_*.rs"
exit 1
fi
echo "Ran ${ran} fuzz target(s)"
# ══════════════════════════════════════════════════════════════════
# Scheduled-only jobs (nightly at 02:00 UTC)
# ══════════════════════════════════════════════════════════════════
# ── Test (full harness, nightly) ─────────────────────────────────
# --all-features enables stdx-proptest, sim-harness, tiger-harness,
# kgram-gate, b64-stats, perf-counters — everything the PR job skips.
# Debug and release run as parallel matrix legs for faster wall-clock time.
test-full-harness:
name: Test (Scheduled, ${{ matrix.profile }})
if: github.event_name == 'schedule'
runs-on: ubuntu-24.04
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- profile: debug
release-flag: ""
- profile: release
release-flag: "--release"
env:
PROPTEST_CASES: "64"
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-rust
with:
toolchain: stable
shared-key: stable-linux
save-cache: "false"
- name: Install cargo-nextest
uses: taiki-e/install-action@06203676c62f0d3c765be3f2fcfbebbcb02d09f5 # v2.69.6
with:
tool: cargo-nextest
- name: Run tests (${{ matrix.profile }})
run: >-
cargo nextest run --workspace --all-features ${{ matrix.release-flag }}
--profile nightly
--exclude gossip-coordination-etcd
- name: Run ignored slow tests (${{ matrix.profile }})
run: >-
cargo nextest run --workspace --all-features ${{ matrix.release-flag }}
--profile nightly
--exclude gossip-coordination-etcd
--run-ignored ignored-only
- name: Run doctests (${{ matrix.profile }})
run: >-
cargo test --all-features ${{ matrix.release-flag }} --doc
# ── Miri (nightly only) ────────────────────────────────────────────
miri:
name: Miri (Scheduled, Nightly)
runs-on: ubuntu-24.04
timeout-minutes: 180
if: >-
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch'
env:
RUSTC_WRAPPER: ""
MIRIFLAGS: >-
-Zmiri-strict-provenance
-Zmiri-symbolic-alignment-check
-Zmiri-preemption-rate=0.1
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-rust
with:
toolchain: nightly
components: miri
shared-key: nightly-linux-miri
# Pure-Rust crates — run full test suites under Miri.
- name: Miri — gossip-stdx
run: cargo +nightly miri test -p gossip-stdx --all-features
- name: Miri — gossip-contracts
run: cargo +nightly miri test -p gossip-contracts --all-features
- name: Miri — gossip-coordination
run: cargo +nightly miri test -p gossip-coordination --all-features
- name: Miri — gossip-frontier
run: cargo +nightly miri test -p gossip-frontier --all-features
# FFI-heavy crates — Miri interprets Rust but cannot execute C/asm.
# Most scanner-engine tests call Vectorscan FFI (via demo_engine /
# cached_demo_engine → VsPrefilterDb::try_new → hs_compile_multi),
# which Miri cannot interpret. Only run modules with pure-Rust logic
# and #[cfg(miri)] scalar fallbacks: simd_classify, offline_validate,
# and set_associative_cache. Use aegis-pure-rust to avoid AES-NI /
# NEON intrinsics that Miri cannot interpret.
- name: Miri — scanner-engine (pure-Rust paths)
run: >-
cargo +nightly miri test -p scanner-engine
--features aegis-pure-rust,test-support
-- simd_classify offline_validate set_associative_cache
# scanner-git: all tests call Vectorscan FFI (via
# demo_engine_with_anchor_mode); no miri-safe test modules exist yet.
# TODO: add #[cfg(not(miri))] guards to engine_adapter_tests and
# introduce miri-safe unit tests for pure-Rust logic.
# ── Kani (nightly only) ──────────────────────────────────────────────
kani:
name: Kani Verification (Scheduled)
runs-on: ubuntu-24.04
timeout-minutes: 180
if: >-
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch'
env:
RUSTC_WRAPPER: ""
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
# Kani verifies all #[kani::proof] harnesses per crate.
# 100 total proofs across 4 crates.
- name: Kani — gossip-stdx (74 proofs)
uses: model-checking/kani-github-action@f838096619a707b0f6b2118cf435eaccfa33e51f # v1.1
with:
args: --package gossip-stdx --features kani
- name: Kani — scanner-engine (15 proofs)
uses: model-checking/kani-github-action@f838096619a707b0f6b2118cf435eaccfa33e51f # v1.1
with:
args: --package scanner-engine --features kani
- name: Kani — scanner-scheduler (9 proofs)
uses: model-checking/kani-github-action@f838096619a707b0f6b2118cf435eaccfa33e51f # v1.1
with:
args: --package scanner-scheduler --features kani
- name: Kani — scanner-git (2 proofs)
uses: model-checking/kani-github-action@f838096619a707b0f6b2118cf435eaccfa33e51f # v1.1
with:
args: --package scanner-git --features kani
# ── Loom (concurrency model checking) ────────────────────────────────
loom:
name: Loom Concurrency (Scheduled)
runs-on: ubuntu-24.04
timeout-minutes: 60
if: >-
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch'
env:
RUSTC_WRAPPER: ""
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-rust
with:
toolchain: stable
shared-key: stable-linux
save-cache: "false"
# Loom exhaustively explores thread interleavings.
- name: Loom — gossip-stdx (11 tests)
run: >-
RUSTFLAGS="--cfg loom" cargo test --lib
-p gossip-stdx --features loom -- loom_tests
- name: Loom — scanner-scheduler (4 tests)
run: >-
RUSTFLAGS="--cfg loom" cargo test --lib
-p scanner-scheduler --features loom -- loom_tests
# ── ASAN (nightly only) ─────────────────────────────────────────────
asan:
name: ASAN (Scheduled, Nightly)
runs-on: ubuntu-24.04
timeout-minutes: 60
if: >-
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch'
env:
RUSTC_WRAPPER: ""
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-rust
with:
toolchain: nightly
components: rust-src
shared-key: nightly-linux-asan
# AddressSanitizer targets SIMD and offline validation modules in
# scanner-engine where unsafe pointer arithmetic is concentrated.
- name: ASAN — scanner-engine (simd_classify + offline_validate)
run: >-
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test
-Zbuild-std --target x86_64-unknown-linux-gnu
-p scanner-engine --lib
-- engine::simd_classify engine::offline_validate
# ── Fuzz (nightly) ──────────────────────────────────────────────────
# Discovers and runs all fuzz targets across every crate with a fuzz/
# directory. Each target gets 10K iterations per platform.
#
# On Linux we install clang+llvm via extra-apt because the system GCC
# lacks sanitizer integration. On macOS, Xcode ships Apple Clang with
# full libFuzzer and AddressSanitizer support, so no extra packages.
fuzz-nightly:
name: Fuzz (Scheduled, ${{ matrix.label }})
runs-on: ${{ matrix.os }}
timeout-minutes: 90
if: github.event_name == 'schedule'
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
target: x86_64-unknown-linux-gnu
label: Linux
shared-key: nightly-linux-fuzz
extra-apt: "clang llvm"
- os: macos-15
target: aarch64-apple-darwin
label: macOS
shared-key: nightly-macos-fuzz
env:
RUSTC_WRAPPER: ""
FUZZ_RUNS: 10000
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-rust
with:
toolchain: nightly
components: rust-src
shared-key: ${{ matrix.shared-key }}
extra-apt: ${{ matrix.extra-apt }}
- uses: ./.github/actions/setup-cargo-fuzz
# Corpus accumulates: restore latest via prefix, save under unique key.
# GitHub's 10 GB LRU cache evicts oldest entries automatically.
- name: Restore fuzz corpus cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
crates/gossip-contracts/fuzz/corpus
crates/gossip-coordination-etcd/fuzz/corpus
crates/gossip-stdx/fuzz/corpus
crates/scanner-engine/fuzz/corpus
crates/scanner-git/fuzz/corpus
key: fuzz-corpus-nightly-${{ matrix.label }}-${{ github.run_number }}
restore-keys: |
fuzz-corpus-nightly-${{ matrix.label }}-
- name: Fuzz all targets
env:
FUZZ_TARGET_TRIPLE: ${{ matrix.target }}
run: |
set -euo pipefail
failed=0
total_ran=0
for fuzz_dir in crates/*/fuzz; do
[ -d "${fuzz_dir}/fuzz_targets" ] || continue
crate_name="$(basename "$(dirname "${fuzz_dir}")")"
targets=()
for f in "${fuzz_dir}"/fuzz_targets/fuzz_*.rs; do
[ -f "$f" ] || continue
targets+=("$(basename "${f%.rs}")")
done
[ "${#targets[@]}" -gt 0 ] || continue
echo "::group::${crate_name} (${#targets[@]} targets)"
cd "${fuzz_dir}"
for target in "${targets[@]}"; do
echo " Running: ${target}"
if ! cargo +nightly fuzz run "${target}" \
--target "${FUZZ_TARGET_TRIPLE}" \
-- -runs="${FUZZ_RUNS}"; then
echo "::error::Fuzz target ${target} in ${crate_name} failed"
failed=1
fi
total_ran=$((total_ran + 1))
done
cd "${GITHUB_WORKSPACE}"
echo "::endgroup::"
done
if [ "${total_ran}" -eq 0 ]; then
echo "::error::No fuzz targets discovered across any crate"
exit 1
fi
echo "Ran ${total_ran} fuzz target(s) total"
[ "${failed}" -eq 0 ] || exit 1
# ── Nightly Summary ───────────────────────────────────────────────
nightly-summary:
name: Nightly Summary
if: always() && github.event_name == 'schedule'
needs: [test-full-harness, test-etcd, test-postgres, miri, kani, loom, asan, fuzz-nightly, audit]
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
issues: write
steps:
- name: Create issue on failure
if: contains(needs.*.result, 'failure')
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const { owner, repo } = context.repo;
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
const today = new Date().toISOString().split('T')[0];
const existing = await github.rest.issues.listForRepo({
owner, repo,
labels: 'nightly-failure',
state: 'open',
per_page: 1,
sort: 'created',
direction: 'desc',
});
if (existing.data.length > 0) {
await github.rest.issues.createComment({
owner, repo,
issue_number: existing.data[0].number,
body: `Another nightly failure on ${today}.\n\n[View run](${runUrl})`,
});
} else {
await github.rest.issues.create({
owner, repo,
title: `Nightly CI failure: ${today}`,
body: `One or more nightly jobs failed.\n\n[View workflow run](${runUrl})`,
labels: ['nightly-failure'],
});
}
- name: Fail on nightly errors
if: contains(needs.*.result, 'failure')
run: |
echo "::error::One or more nightly jobs failed. Check the workflow run for details."
exit 1