Browse / github.com/storytold/gridcraft / patch
CI: nightly command-sequence fuzz soak
CI: nightly command-sequence fuzz soak Adds .github/workflows/fuzz.yml: a nightly (03:17 UTC) and manually dispatchable job that builds the engine tests in release and runs the command-sequence fuzzer at 200 seeds by 200 steps with GRIDCRAFT_FUZZ_KEEP_GOING and GRIDCRAFT_FUZZ_SHRINK. A failing seed fails that run only (not main, not a required check, never runs on pull requests) and uploads a fuzz-failures artifact: the full log, failing-seeds.txt and each seed's shrunk command sequence. The run summary prints how to replay a seed. Conventions follow the shared gate workflow: ubuntu-24.04, the same SHA-pinned checkout, rust-cache
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Harrison-Booth (imported from pull request #2692, open)
- Base
f6b0cec67e328f2f6352768b5cb2feb7e562c5db- Size
- +109 −4 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
1ac63175cbdfeb067e79a56e
How it went
No other operator has reported using it yet.
Diff
raw · .github/workflows/fuzz.yml ROADMAP.md docs/releasing.md
new file mode 100644@@ -0,0 +1,97 @@+name: Fuzz++# Nightly soak of the command-sequence fuzzer (`crates/engine/tests/it/command_sequence_fuzz.rs`):+# random command sequences checked against undo/redo, XLSX round-trip and full-recalc invariants.+# `cargo xtask ci` runs it with a tiny budget; this job runs 200 seeds of 200 steps so a sequence bug+# is found without anyone remembering to soak by hand. It is not a required check. A failing seed+# fails this run only (the job and the Actions tab), never `main`, and uploads the failing seeds with+# their shrunk command sequences as an artifact. A pull request that touches this file or the harness+# runs a small budget (20 by 50) so the workflow is exercised before it reaches `main`.+on:+ pull_request:+ paths: [".github/workflows/fuzz.yml", "crates/engine/tests/it/command_sequence_fuzz.rs"]+ schedule:+ - cron: "17 3 * * *"+ workflow_dispatch:+ inputs:+ seeds:+ description: "Seeds to run (GRIDCRAFT_FUZZ_SEEDS)"+ default: "200"+ steps:+ description: "Commands per seed (GRIDCRAFT_FUZZ_STEPS)"+ default: "200"++permissions:+ contents: read++concurrency:+ group: fuzz-${{ github.ref }}+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}++jobs:+ fuzz:+ runs-on: ubuntu-24.04+ timeout-minutes: 90+ env:+ CARGO_TERM_COLOR: never+ CARGO_INCREMENTAL: 0+ CARGO_NET_RETRY: 10+ RUST_BACKTRACE: 1+ # The engine and its dependencies have no egui, fonts or system libraries, so no apt packages+ # and no craft-fonts checkout.+ GRIDCRAFT_FUZZ_SEEDS: ${{ inputs.seeds || (github.event_name == 'pull_request' && '20' || '200') }}+ GRIDCRAFT_FUZZ_STEPS: ${{ inputs.steps || (github.event_name == 'pull_request' && '50' || '200') }}+ GRIDCRAFT_FUZZ_KEEP_GOING: "1"+ GRIDCRAFT_FUZZ_SHRINK: "1"+ steps:+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1+ with:+ persist-credentials: false++ # The channel in rust-toolchain.toml wins, as in the suite's gate workflow.+ - name: Install Rust+ run: |+ set -euo pipefail+ toolchain=stable+ if [ -f rust-toolchain.toml ]; then+ channel=$(sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' rust-toolchain.toml)+ toolchain=${channel:-$toolchain}+ fi+ rustup toolchain install "$toolchain" --profile minimal --no-self-update+ rustup default "$toolchain"+ rustc --version++ # Only the default branch writes the cache, so manual runs on other branches only read it.+ - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3+ with:+ prefix-key: fuzz-rust+ key: ${{ hashFiles('Cargo.toml') }}+ save-if: ${{ github.ref_name == github.event.repository.default_branch }}+ cache-on-failure: true++ - name: Fuzz command sequences+ run: |+ set -o pipefail+ mkdir -p fuzz-out+ # A zero count builds an empty seed list and passes without running anything.+ for count in "$GRIDCRAFT_FUZZ_SEEDS" "$GRIDCRAFT_FUZZ_STEPS"; do+ if [[ ! "$count" =~ ^[0-9]+$ ]] || (( 10#$count == 0 )); then+ echo "Fuzz counts must be positive integers, got '$count'" >&2+ exit 1+ fi+ done+ cargo test --release -p gridcraft-engine --test it command_sequence_fuzz:: 2>&1 | tee fuzz-out/fuzz.log++ - name: Collect failing seeds+ if: failure()+ run: |+ grep -oE '^command-sequence fuzz failure: seed [0-9]+' fuzz-out/fuzz.log | grep -oE '[0-9]+$' > fuzz-out/failing-seeds.txt || true+ echo "Failing seeds: $(tr '\n' ' ' < fuzz-out/failing-seeds.txt)" | tee -a "$GITHUB_STEP_SUMMARY"+ echo "Replay one with: GRIDCRAFT_FUZZ_SEED=<seed> GRIDCRAFT_FUZZ_STEPS=$GRIDCRAFT_FUZZ_STEPS GRIDCRAFT_FUZZ_SHRINK=1 cargo test -p gridcraft-engine --test it command_sequence_fuzz::" >> "$GITHUB_STEP_SUMMARY"++ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2+ if: failure()+ with:+ name: fuzz-failures+ path: fuzz-out/+ retention-days: 30@@ -135,6 +135,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | What landed | |---|---|+| 2026-10-26 | A nightly workflow soaks the command-sequence fuzzer at 200 seeds by 200 steps and uploads the failing seeds with shrunk sequences (`.github/workflows/fuzz.yml`; not a required check) | | 2026-10-25 | AutoSum over a selection does what Excel does: a selected row gets one total to its right (in its last cell if that is empty), not a one-cell total under each cell; a block whose last column is empty gets each row's total there, one whose last row is empty each column's, and with both the corner gets the total of the block; otherwise totals go under each column as before | | 2026-10-24 | Go To lists the named ranges of every sheet (`Sheet2!Name` for a name scoped to another sheet; hidden names, constants and formulas left out) and the references entered before, a double-click goes to an entry, and the Reference box takes several names or references separated by commas (`Price, Type`, `B14:C22,F19:G30`) | | 2026-10-24 | Status bar: a right-click opens a menu that sets which selection statistics show (Average, Count and Sum by default; Numerical Count, Minimum and Maximum are new), kept with the preferences (`view.statusBar`) |@@ -286,8 +286,15 @@ row in `ATTRIBUTION.md` (`cargo xtask assets`). - `.github/workflows/windows-arm64.yml` (on `release` pushes): packages ARM64 on x64, then installs, runs and uninstalls the MSI on a Windows 11 ARM64 runner. - `.github/workflows/freebsd.yml`: FreeBSD build + tests (manual run from `release`).--**Only the `release` branch builds.** No build workflow runs on `main` or pull requests; every-build job checks `github.ref == 'refs/heads/release'`, so a manual run from another branch is-skipped. Only `packaging-lint.yml` (no build, no secrets) runs on `main` and PRs.+- `.github/workflows/fuzz.yml` (nightly at 03:17 UTC, or run it by hand from the Actions tab): builds+ in release and soaks the command-sequence fuzzer at 200 seeds by 200 steps with+ `GRIDCRAFT_FUZZ_KEEP_GOING` and `GRIDCRAFT_FUZZ_SHRINK`. It is not a required check; a pull request that touches the workflow+ or the fuzz harness runs it at 20 seeds by 50 steps. A failure fails that run only (never `main`) and uploads the `fuzz-failures`+ artifact: the full log, `failing-seeds.txt` and each failing seed's shrunk command sequence. Replay+ with the `GRIDCRAFT_FUZZ_SEED=<seed>` line in the run summary.++**Only the `release` branch builds installers.** No installer build workflow runs on `main` or pull+requests; every build job checks `github.ref == 'refs/heads/release'`, so a manual run from another+branch is skipped. Only `packaging-lint.yml` (no build, no secrets) runs on `main` and PRs, besides+the CI gate and the test-only `fuzz.yml` (nightly, and on PRs that touch it or the fuzz harness). - Locally: `cargo xtask ci` (fmt, clippy, tests, assets, layers, wasm, parity check).