name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, reopened, synchronize]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
CARGO_INCREMENTAL: 0
jobs:
changes:
name: Detect changes
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
rust: ${{ steps.filter.outputs.rust }}
tuika: ${{ steps.filter.outputs.tuika }}
docs: ${{ steps.filter.outputs.docs }}
eval-analyzers: ${{ steps.filter.outputs.eval-analyzers }}
steps:
- uses: actions/checkout@v7
- uses: dorny/paths-filter@v4
id: filter
with:
filters: |
rust:
- '**/*.rs'
- '**/Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- '**/build.rs'
- '.github/workflows/ci.yml'
- 'crates/tuika/benches/check_iai.py'
- '**/iai-baseline.json'
# The dedicated MSRV check only compiles `-p tuika`, which depends on
# neither yolop, yolop-yep, nor tuika-codeformatters. Scope it to
# tuika's own sources plus the workspace-wide inputs that reach it
# (root manifest, lockfile, this workflow). A yolop-only change still
# compiles tuika via the workspace `test` job — it just skips this
# extra 1.88 + gallery-asset leg.
tuika:
- 'crates/tuika/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/ci.yml'
docs:
- '**/*.md'
# The regression-gate analyzers under harness_basic are pure Python
# and decide whether the nightly Search Efficiency Eval passes. They
# never touch the Rust toolchain, so give them their own fast leg that
# runs whenever an analyzer or its tests change.
eval-analyzers:
- 'evals/harness_basic/*.py'
- 'evals/harness_basic/tests/**'
- '.github/workflows/ci.yml'
tuika-msrv:
name: Tuika MSRV
needs: changes
if: needs.changes.outputs.tuika == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- name: Install Rust 1.88
uses: dtolnay/rust-toolchain@1.88.0
- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
shared-key: "tuika-msrv"
- name: Check Tuika
run: cargo check --locked -p tuika --all-targets
- name: Verify component gallery assets
run: cargo run --locked -p tuika --example demo -- check
docs-boundary:
name: Docs boundary
needs: changes
if: needs.changes.outputs.docs == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- name: Validate release and knowledge metadata
run: |
python3 scripts/validate_okf.py knowledge --check-links
python3 scripts/test_validate_okf.py
python3 scripts/test_publish_order.py
test "$(python3 scripts/publish_order.py | tail -1)" = yolop
- name: Public docs boundary
run: |
if grep -R -n -E --include='*.md' '\]\([^)]*(knowledge/|\.agents/)' README.md docs; then
echo "::error::Public documentation must not link to internal knowledge/ or .agents/ files."
exit 1
fi
eval-analyzers:
name: Eval analyzers (Python)
needs: changes
if: needs.changes.outputs.eval-analyzers == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- name: Run analyzer unit tests
run: python3 -m unittest discover -s evals/harness_basic/tests -v
lint:
name: Format + Clippy
needs: changes
if: needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.94.0
with:
components: rustfmt, clippy
- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
shared-key: "debug"
- name: Format check
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
test:
name: Build + Tests + Offline Smoke
needs: changes
if: needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v7
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.94.0
- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
shared-key: "debug"
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@v2
with:
tool: cargo-llvm-cov
- name: Build
run: cargo build --locked --all-targets
- name: Unit and integration tests with coverage
run: |
cargo llvm-cov --no-report --all-features --workspace
cargo llvm-cov report --lcov --output-path lcov.info
cargo llvm-cov report --summary-only
- name: Upload coverage to Codecov
if: always()
uses: codecov/codecov-action@v7
with:
files: lcov.info
fail_ci_if_error: ${{ github.event_name == 'push' }}
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload coverage artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: lcov-coverage
path: lcov.info
if-no-files-found: error
live-smoke:
name: Live Provider Smoke (Doppler)
runs-on: ubuntu-latest
timeout-minutes: 15
if: (github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) && github.actor != 'dependabot[bot]'
needs: [lint, test]
env:
DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }}
steps:
- uses: actions/checkout@v7
- name: Verify DOPPLER_TOKEN is set
run: |
if [ -z "${DOPPLER_TOKEN}" ]; then
echo "::error::DOPPLER_TOKEN is required for the live smoke job but is not set. Configure the repo secret or update the job's outer 'if:' to skip this environment."
exit 1
fi
- name: Install Doppler CLI
uses: dopplerhq/cli-action@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.94.0
- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
shared-key: "debug"
- name: Build the test binary (offline)
run: cargo test --no-run --test integration
- name: Live provider smoke tests
env:
YOLOP_REQUIRE_LIVE_TESTS: "1"
run: doppler run -- cargo test --test integration -- --nocapture
sandbox-contract:
name: Sandbox Contract (${{ matrix.os }})
needs: changes
if: needs.changes.outputs.rust == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v7
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.94.0
- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
shared-key: "sandbox-${{ runner.os }}"
- name: Run sandbox contract suite
run: cargo test sandbox -- --nocapture
benchmarks:
name: Benchmarks (archive)
needs: changes
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && needs.changes.outputs.rust == 'true')
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.94.0
- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
shared-key: "debug"
- name: Run benchmarks
run: |
cargo bench --locked -p tuika -p tuika-codeformatters -- \
--warm-up-time 2 --measurement-time 5 --noplot | tee bench-output.txt
- name: Upload benchmark results
if: always()
uses: actions/upload-artifact@v7
with:
name: criterion-benchmarks
path: |
target/criterion/**
bench-output.txt
if-no-files-found: warn
iai:
name: iai-callgrind (instruction counts)
needs: changes
if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v7
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.94.0
- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
shared-key: "debug"
- name: Install Valgrind
run: sudo apt-get update && sudo apt-get install -y valgrind
- name: Install iai-callgrind-runner
run: cargo install iai-callgrind-runner --version 0.16.1 --locked
- name: Run instruction-count benchmarks
run: |
rm -rf target/iai
cargo bench --locked -p tuika --bench markdown_iai --bench scroll_iai \
-p tuika-codeformatters --bench highlight_iai -- --save-summary=json
- name: Check against committed baseline
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
python3 crates/tuika/benches/check_iai.py --update
else
python3 crates/tuika/benches/check_iai.py
fi
- name: Upload regenerated baseline
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v7
with:
name: iai-baseline
path: '**/iai-baseline.json'
if-no-files-found: error
windows:
name: Windows Build + Shell Smoke
needs: changes
if: needs.changes.outputs.rust == 'true'
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.94.0
- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
shared-key: "windows"
- name: Build the yolop binary
run: cargo build --locked -p yolop
- name: Sandbox + shell tests
run: cargo test -p yolop sandbox -- --nocapture
- name: Offline smoke (PowerShell shell)
run: cargo run -p yolop -- --provider llmsim -p "hi"