Skip to content
Draft
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
165 changes: 165 additions & 0 deletions .github/scripts/report_fuzz_findings.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#!/usr/bin/env bash
#
# report_fuzz_findings.sh — triage the nightly simulation-soak logs and, if any
# property failures were found, file (or update) a single GitHub issue with a
# reproducible repro. Invoked by .github/workflows/nightly-fuzz.yml.
#
# Anti-spam policy: at most ONE open `fuzz-finding` issue at a time. If one is
# already open, this appends the new run's findings as a comment instead of
# opening a duplicate — a persistent bug accumulates recurrences on one issue,
# and a fresh issue only opens once a human has triaged and closed the last one.
#
# Usage: report_fuzz_findings.sh <log-dir>
# Reads every `soak-*.log` under <log-dir>. Requires `gh` authenticated with
# `issues: write` (the workflow's GITHUB_TOKEN).
#
# Env (provided by the workflow, all optional — used only to enrich the body):
# GITHUB_RUN_ID, GITHUB_SHA, GITHUB_SERVER_URL, GITHUB_REPOSITORY

set -euo pipefail

LOG_DIR=${1:?"usage: report_fuzz_findings.sh <log-dir>"}

shopt -s nullglob globstar
logs=("$LOG_DIR"/**/soak-*.log "$LOG_DIR"/soak-*.log)
if [ ${#logs[@]} -eq 0 ]; then
echo "No soak-*.log files found under $LOG_DIR; nothing to report."
exit 0
fi

# ---------------------------------------------------------------------------
# Tally failures and collect a bounded, readable set of failure blocks.
# ---------------------------------------------------------------------------
total=0
blocks="" # up to MAX_BLOCKS failure blocks, across all regions
env_line="" # rustc / os / sha, recorded by the soak step
region_summary=""
MAX_BLOCKS=8
block_count=0

for log in "${logs[@]}"; do
[ -f "$log" ] || continue
n=$(grep -c '^FAILURE (seed' "$log" 2>/dev/null || true)
n=${n:-0}
total=$((total + n))

# The soak step records one `ENV ...` line at the top of each log.
if [ -z "$env_line" ]; then
env_line=$(grep -m1 '^ENV ' "$log" 2>/dev/null || true)
fi

base=$(grep -m1 '^SOAK start' "$log" 2>/dev/null | sed -n 's/.*base=\([0-9]*\).*/\1/p')
iters=$(grep -m1 '^SOAK done' "$log" 2>/dev/null | sed -n 's/.*iters=\([0-9]*\).*/\1/p')
region_summary+="- \`$(basename "$log")\`: base=${base:-?}, iters=${iters:-?}, failures=${n}"$'\n'

if [ "$n" -gt 0 ] && [ "$block_count" -lt "$MAX_BLOCKS" ]; then
# Extract each FAILURE block: the header plus its report lines, stopping at
# the next heartbeat / failure / summary line.
while IFS= read -r line; do
case "$line" in
"===BLOCK===")
block_count=$((block_count + 1))
[ "$block_count" -ge "$MAX_BLOCKS" ] && break
;;
*) blocks+="$line"$'\n' ;;
esac
done < <(awk '
/^FAILURE \(seed/ { if (inblk) print "===BLOCK==="; inblk=1; print; next }
inblk == 1 {
if ($0 ~ /^HEARTBEAT/ || $0 ~ /^SOAK/ || $0 == "") { inblk=0; print "===BLOCK==="; next }
print
}
END { if (inblk) print "===BLOCK===" }
' "$log")
fi
done

if [ "$total" -eq 0 ]; then
echo "Soak completed with 0 property failures. Nothing to file."
exit 0
fi

echo "Found $total property failure(s) across ${#logs[@]} region log(s); preparing issue."

# ---------------------------------------------------------------------------
# A ready-to-paste repro from the first failing seed. The soak's per-iteration
# seed is `base + iter`, so re-running with DDX_SOAK_BASE set to a failing
# `seed=` value reproduces that exact case as iteration 0 (same depth + wrt).
# ---------------------------------------------------------------------------
first_seed=$(printf '%s' "$blocks" | sed -n 's/^FAILURE (seed=\([0-9]*\).*/\1/p' | head -1)
first_seed=${first_seed:-0}

run_url="${GITHUB_SERVER_URL:-https://github.com}/${GITHUB_REPOSITORY:-xqlsystems/ddx}/actions/runs/${GITHUB_RUN_ID:-local}"
date_utc=$(date -u '+%Y-%m-%d %H:%M UTC')

body_file=$(mktemp)
{
echo "**Nightly simulation-soak fuzz found ${total} property failure(s).**"
echo
echo "- Run: [${GITHUB_RUN_ID:-local}](${run_url})"
echo "- Commit: \`${GITHUB_SHA:-unknown}\`"
echo "- When: ${date_utc}"
[ -n "$env_line" ] && echo "- Build: \`${env_line#ENV }\`"
echo
echo "### Reproduce"
echo
echo "The soak's per-iteration seed is \`base + iter\`, so setting \`DDX_SOAK_BASE\`"
echo "to a failing \`seed=\` value re-runs that exact case as iteration 0. On the"
echo "same platform as the build above (floating-point transcendentals are not"
echo "bit-identical across OS/toolchain):"
echo
echo '```bash'
echo "DDX_SOAK_SECS=15 DDX_SOAK_BASE=${first_seed} \\"
echo " cargo test -p ddx-core --test simulation --release \\"
echo " -- --ignored --nocapture soak_continuous_property_fuzz"
echo '```'
echo
echo "The full per-region logs are attached to the workflow run as artifacts."
echo
echo "### Per-region summary"
echo
printf '%s\n' "$region_summary"
echo "### Failure samples (up to ${MAX_BLOCKS})"
echo
echo '```'
printf '%s' "$blocks"
echo '```'
echo
echo "> Triage note: the finite-difference oracle is gated against float"
echo "> cancellation (magnitude cap) and truncation/aliasing (Richardson"
echo "> self-consistency), so a surviving \`[finite-diff]\` disagreement should be"
echo "> a real rule bug — but confirm by reproducing before assuming. \`[render]\`"
echo "> and \`[self-consumption]\` failures are always real."
echo
echo "<!-- fuzz-run: ${GITHUB_RUN_ID:-local} -->"
} > "$body_file"

# ---------------------------------------------------------------------------
# Ensure labels exist (idempotent), then dedup: comment on the single open
# fuzz-finding issue if one exists, else create a new one.
# ---------------------------------------------------------------------------
gh label create fuzz-finding --color B60205 \
--description "Found by the nightly simulation-soak fuzz" 2>/dev/null || true
gh label create needs-triage --color FBCA04 \
--description "Awaiting human triage" 2>/dev/null || true

existing=$(gh issue list --state open --label fuzz-finding \
--json number --jq '.[0].number // empty' 2>/dev/null || true)

if [ -n "$existing" ]; then
echo "Open fuzz-finding issue #${existing} exists; commenting instead of filing a duplicate."
{
echo "### Recurred — run ${GITHUB_RUN_ID:-local} (${date_utc})"
echo
cat "$body_file"
} | gh issue comment "$existing" --body-file -
echo "Commented on #${existing}."
else
title="Nightly fuzz: ${total} property failure(s) in ddx-core simulation soak"
url=$(gh issue create --title "$title" \
--label fuzz-finding --label needs-triage \
--body-file "$body_file")
echo "Filed new issue: $url"
fi

rm -f "$body_file"
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this addition! Consider also adding a Makefile or justfile which makes it super easy for developers to make these checks as code is created. This might also help with things like builds / etc.

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# SPDX-FileCopyrightText: 2026 Alexander Merose <al@merose.com> & ddx Authors
#
# SPDX-License-Identifier: Apache-2.0

# Standard PR checks for ddx: formatting, lints, and the deterministic test
# suite. The property/fuzz suite (crates/ddx-core/tests/simulation.rs) is
# intentionally NOT run here — it is a separate, on-demand / soak concern (see
# its module docs) rather than a blocking PR gate — so these checks stay fast
# and deterministic.
name: CI

on:
pull_request:
push:
branches: [main]

# Cancel superseded runs on the same ref (e.g. a new push to a PR branch).
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always

jobs:
lint:
name: rustfmt + clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: rustfmt (check only)
run: cargo fmt --all --check
- name: clippy (warnings are errors)
run: cargo clippy --workspace --all-targets -- -D warnings

test:
name: tests (non-simulation)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth testing more than stable?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth testing more than stable?

- uses: Swatinem/rust-cache@v2
# Everything except tests/simulation.rs (the property/fuzz suite). Split
# per target so a failure names the suite that broke.
- name: unit tests
run: cargo test --workspace --lib --bins
- name: integration tests
run: cargo test -p ddx-core --test rules --test rewrite --test roundtrip
- name: doctests
run: cargo test --workspace --doc
111 changes: 111 additions & 0 deletions .github/workflows/nightly-fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
name: Nightly fuzz (ddx-core simulation soak)

# Runs the seed-reproducible property-fuzz soak (crates/ddx-core/tests/
# simulation.rs :: soak_continuous_property_fuzz) over three disjoint random
# seed regions in parallel, then files/updates a single GitHub issue if any
# property (finite-difference numeric agreement, render fidelity, higher-order
# self-consumption) fails.
#
# Schedule: 08:00 America/Los_Angeles == 16:00 UTC (PST, UTC-8). GitHub cron is
# fixed-UTC and does NOT observe daylight saving, so during PDT (roughly
# Mar–Nov) this fires at 09:00 Pacific. Adjust the cron if you want it pinned to
# wall-clock Pacific year-round.
on:
schedule:
- cron: '0 16 * * *'
workflow_dispatch:
inputs:
duration_secs:
description: 'Soak wall-clock budget per region (seconds)'
required: false
default: '300'
base_override:
description: 'Base seed (optional; blank = derive a fresh one from the run id)'
required: false
default: ''

permissions:
contents: read
issues: write

concurrency:
group: nightly-fuzz
cancel-in-progress: false

jobs:
soak:
name: soak region ${{ matrix.region }}
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
region: [0, 1, 2]
steps:
- uses: actions/checkout@v4

- name: Toolchain info
run: rustc --version && cargo --version

- name: Build soak test binary
run: cargo test -p ddx-core --test simulation --release --no-run

- name: Run soak
run: |
set -o pipefail
SECS='${{ github.event.inputs.duration_secs || '300' }}'
OVERRIDE='${{ github.event.inputs.base_override }}'
REGION='${{ matrix.region }}'
# Disjoint region seed windows, monotonic across runs. run_number is a
# small, strictly-increasing counter — using it (not the ~10-digit
# run_id) keeps run_number * 1e8 far inside 64-bit range forever, and
# guarantees each region gets a fresh, non-overlapping seed window
# (a 5-min region explores < 1e7 seeds; regions are 1e7 apart, runs
# 1e8 apart).
if [ -n "$OVERRIDE" ]; then
BASE=$(( OVERRIDE + REGION * 10000000 ))
else
BASE=$(( GITHUB_RUN_NUMBER * 100000000 + REGION * 10000000 ))
fi
LOG="$PWD/soak-region-${REGION}.log"
echo "ENV rustc=$(rustc --version) os=${RUNNER_OS} sha=${GITHUB_SHA}" > "$LOG"
echo "Running soak: region=$REGION base=$BASE secs=$SECS"
# The soak asserts on failure (non-zero exit); don't let that abort the
# job — the report job decides what to do from the log contents.
set +e
DDX_SOAK_SECS="$SECS" DDX_SOAK_BASE="$BASE" DDX_SOAK_LOG="$LOG" \
cargo test -p ddx-core --test simulation --release \
-- --ignored --nocapture soak_continuous_property_fuzz
echo "soak exit code: $?"
set -e
echo "---- log tail ----"
tail -n 5 "$LOG" || true

- name: Upload region log
if: always()
uses: actions/upload-artifact@v4
with:
name: soak-log-${{ matrix.region }}
path: soak-region-${{ matrix.region }}.log
if-no-files-found: warn

report:
name: triage & file issue
needs: soak
if: always()
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- name: Download all region logs
uses: actions/download-artifact@v4
with:
path: logs

- name: Triage and file/update issue
env:
GH_TOKEN: ${{ github.token }}
run: |
chmod +x .github/scripts/report_fuzz_findings.sh
.github/scripts/report_fuzz_findings.sh logs
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ target
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/

# agents
.claude
.chainlink
.mcp.json
# auto-generated from chainlink
CHANGELOG.md
Loading
Loading