Skip to content

snapshots: stash BuildPlan on BuildContext #2280

snapshots: stash BuildPlan on BuildContext

snapshots: stash BuildPlan on BuildContext #2280

Workflow file for this run

name: Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
schedule:
# Run nightly tests every Sunday at 2 AM UTC (9 PM Saturday PST)
- cron: '0 2 * * 0'
workflow_dispatch: # Allow manual trigger any time
concurrency:
group: tests-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
performance-evidence:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Validate Performance Evidence section
run: python scripts/check_performance_evidence.py --github-event "$GITHUB_EVENT_PATH"
# Detect whether code paths changed — skip tests for docs-only PRs
detect-changes:
if: github.event_name == 'pull_request' || github.event_name == 'push'
runs-on: ubuntu-latest
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
- uses: actions/checkout@v7
with:
# For PRs the base SHA is fetched automatically; for pushes we only
# need the previous commit. Fall back to full clone only when the
# shallow history doesn't contain the base SHA.
fetch-depth: 20
- name: Check for code changes
id: filter
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE=${{ github.event.pull_request.base.sha }}
else
BASE=${{ github.event.before }}
fi
# List changed files between base and head
CHANGED=$(git diff --name-only "$BASE" HEAD 2>/dev/null || echo "FORCE_RUN")
# If git diff failed (e.g. force push with unknown base), run everything
if echo "$CHANGED" | grep -q "FORCE_RUN"; then
echo "code=true" >> $GITHUB_OUTPUT
echo "Could not determine base — running all jobs"
exit 0
fi
# Check if any changed file is outside docs-only paths
CODE_CHANGED="false"
while IFS= read -r file; do
case "$file" in
site/*|docs/*|plan/*|skills/*|*.md|LICENSE|.context/*) ;;
*) CODE_CHANGED="true"; break ;;
esac
done <<< "$CHANGED"
echo "code=$CODE_CHANGED" >> $GITHUB_OUTPUT
if [ "$CODE_CHANGED" = "false" ]; then
echo "Docs-only change — skipping test jobs"
else
echo "Code changed — running test jobs"
fi
# Fast check: unit tests only for quick PR feedback
fast-check:
needs: detect-changes
if: needs.detect-changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.14t"
- name: Cache uv dependencies
uses: actions/cache@v6
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
- name: Install dependencies
run: uv sync --no-sources --group dev
- name: Fast test suite (unit tests + coverage)
timeout-minutes: 25
env:
CI: true
BENGAL_CI_FAST: "1"
OMP_NUM_THREADS: "1"
PYTHON_GIL: "0"
run: |
echo "Running fast test suite (unit tests only, excluding slow)..."
uv run pytest -n 4 -q --tb=short --maxfail=5 --dist worksteal --max-worker-restart=3 --cov=bengal --cov-report=xml tests/unit tests/core tests/test_thread_safety.py -m "not slow and not known_gap"
- name: Upload coverage
uses: codecov/codecov-action@v7
with:
files: coverage.xml
# Baseurl spot-checks (parallel with downstream test jobs)
baseurl:
needs: fast-check
if: ${{ !cancelled() && needs.fast-check.result == 'success' }}
runs-on: ubuntu-latest
strategy:
matrix:
baseurl: ["/bengal", "https://example.com/sub"]
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.14t"
- name: Cache uv dependencies
uses: actions/cache@v6
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
- name: Install dependencies
run: uv sync --no-sources --group dev
- name: Run targeted baseurl portability tests
timeout-minutes: 10
env:
BENGAL_BASEURL: ${{ matrix.baseurl }}
CI: true
BENGAL_CI_FAST: "1"
OMP_NUM_THREADS: "1"
PYTHON_GIL: "0"
run: |
echo "Running with BENGAL_BASEURL='${BENGAL_BASEURL}'"
uv run pytest -q --tb=short --maxfail=5 -n auto tests/unit/config/test_config_loader_baseurl_env.py tests/unit/rendering/test_asset_url_baseurl.py tests/unit/rendering/test_template_links_baseurl.py tests/integration/test_baseurl_builds.py
# E2E tests (build + validate + check links)
e2e:
needs: fast-check
if: ${{ !cancelled() && needs.fast-check.result == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.14t"
- name: Cache uv dependencies
uses: actions/cache@v6
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
- name: Install dependencies
run: uv sync --no-sources --group dev
- name: Run E2E tests
env:
CI: true
BENGAL_CI_FAST: "1"
OMP_NUM_THREADS: "1"
PYTHON_GIL: "0"
run: |
echo "Running E2E tests..."
uv run pytest \
tests/integration/test_cli_smoke.py \
tests/integration/test_documentation_builds.py \
tests/integration/test_blog_scenarios.py \
tests/integration/test_golden_output.py \
tests/integration/test_full_site_url_consistency.py \
-v --tb=short -n auto --dist worksteal --max-worker-restart=3
# Integration tests (excluding warm_build/ — see warm-build job)
integration:
needs: fast-check
if: ${{ !cancelled() && needs.fast-check.result == 'success' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
group: [1, 2, 3, 4, 5, 6]
# 15m was too tight for slowest shards (runner cancels mid-run). 25m + 6 shards
# keeps wall time per shard well under the limit.
timeout-minutes: 25
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.14t"
- name: Cache uv dependencies
uses: actions/cache@v6
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
- name: Install dependencies
run: uv sync --no-sources --group dev
- name: Integration tests (shard ${{ matrix.group }}/6, excludes warm_build/)
env:
CI: true
BENGAL_CI_FAST: "1"
OMP_NUM_THREADS: "1"
PYTHON_GIL: "0"
PYTHONUNBUFFERED: "1"
run: |
echo "Running integration tests (group ${{ matrix.group }}/6)..."
set -o pipefail
uv run pytest \
--splits 6 --group ${{ matrix.group }} \
--splitting-algorithm least_duration \
-n 1 -vv -ra --tb=short --maxfail=3 --durations=40 \
--timeout=180 --timeout-method=thread \
--max-worker-restart=3 \
tests/integration \
--ignore=tests/integration/warm_build \
-m "not slow and not performance and not stateful and not autodoc and not known_gap" \
--junitxml=integration-results.xml \
2>&1 | tee integration-tests.log
- name: Upload integration diagnostics
if: always()
uses: actions/upload-artifact@v7
with:
name: integration-diagnostics-group${{ matrix.group }}
path: |
integration-tests.log
integration-results.xml
if-no-files-found: warn
# Warm-build integration tests (isolated; heavy full/incremental site builds)
warm-build:
needs: fast-check
if: ${{ !cancelled() && needs.fast-check.result == 'success' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
group: [1, 2]
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.14t"
- name: Cache uv dependencies
uses: actions/cache@v6
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
- name: Install dependencies
run: uv sync --no-sources --group dev
- name: Warm build tests (shard ${{ matrix.group }}/2)
env:
CI: true
BENGAL_CI_FAST: "1"
OMP_NUM_THREADS: "1"
PYTHON_GIL: "0"
PYTHONUNBUFFERED: "1"
run: |
echo "Running warm_build integration tests (group ${{ matrix.group }}/2)..."
set -o pipefail
uv run pytest \
--splits 2 --group ${{ matrix.group }} \
--splitting-algorithm least_duration \
-n auto -vv -ra --tb=short --maxfail=1 --durations=20 \
--timeout=60 --timeout-method=thread \
--dist worksteal --max-worker-restart=3 \
tests/integration/warm_build/ \
-m "not slow and not performance and not stateful" \
--junitxml=warm-build-results.xml \
2>&1 | tee warm-build-tests.log
- name: Upload warm-build diagnostics
if: always()
uses: actions/upload-artifact@v7
with:
name: warm-build-diagnostics-group${{ matrix.group }}
path: |
warm-build-tests.log
warm-build-results.xml
if-no-files-found: warn
# Render output regression sentinels (autodoc + fallback path)
render-output-regression:
needs: fast-check
if: ${{ !cancelled() && needs.fast-check.result == 'success' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.14t"
- name: Cache uv dependencies
uses: actions/cache@v6
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
- name: Install dependencies
run: uv sync --no-sources --group dev
- name: Run render output regression tests
timeout-minutes: 10
env:
CI: true
OMP_NUM_THREADS: "1"
PYTHON_GIL: "0"
run: |
uv run pytest -n 0 -q --tb=short -m performance \
tests/performance/test_autodoc_render_regression.py \
tests/performance/test_asset_fallback_cost.py
- name: Upload render output metrics
if: always()
uses: actions/upload-artifact@v7
with:
name: render-output-metrics-${{ github.sha }}
path: |
.pytest_cache/render_output_metrics.json
.pytest_cache/fallback_parser_metrics.json
if-no-files-found: warn
# Slow tests (memory profiling, bytecode cache, etc.)
slow-tests:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.14t"
- name: Cache uv dependencies
uses: actions/cache@v6
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
- name: Install dependencies
run: uv sync --no-sources --group dev
- name: Run slow tests
timeout-minutes: 30
env:
CI: true
OMP_NUM_THREADS: "1"
PYTHON_GIL: "0"
run: |
# Serial: -n auto OOM'd memory-intensive tests. Ignore tests/performance
# because package-level pytestmark there does not apply to modules, so
# @pytest.mark.slow memory tests leaked into this job.
uv run pytest -n 0 -q --tb=short \
-m "(slow or known_gap) and not stateful and not performance" \
--ignore=tests/performance
# Stateful workflows (weekly/manual)
stateful:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.14t"
- name: Cache uv dependencies
uses: actions/cache@v6
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
- name: Install dependencies
run: uv sync --no-sources --group dev
- name: Run stateful tests
timeout-minutes: 30
env:
CI: true
OMP_NUM_THREADS: "1"
PYTHON_GIL: "0"
run: |
# Serial: xdist + --forked + PYTHON_GIL=0 SIGKILL'd Hypothesis machines.
uv run pytest -n 0 -q --tb=short -m stateful
# Performance suite (weekly/manual)
performance:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.14t"
- name: Cache uv dependencies
uses: actions/cache@v6
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
- name: Install dependencies
run: uv sync --no-sources --group dev
- name: Run performance tests
timeout-minutes: 30
env:
CI: true
OMP_NUM_THREADS: "1"
PYTHON_GIL: "0"
run: |
uv run pytest -n auto -q --tb=short -m performance --dist worksteal --max-worker-restart=2
# Gate job for branch protection — succeeds when tests pass OR were skipped (docs-only)
ci-ok:
if: always()
needs:
- detect-changes
- fast-check
- baseurl
- e2e
- integration
- warm-build
- render-output-regression
- stateful
- slow-tests
- performance
runs-on: ubuntu-latest
steps:
- name: Decide status
run: |
# If detect-changes decided no code changed, all test jobs are skipped — that's OK
if [ "${{ needs.detect-changes.outputs.code }}" = "false" ]; then
echo "Docs-only change — all tests skipped, CI passes"
exit 0
fi
# Skipped jobs (docs-only, or weekly jobs on PR/push) are not failures.
# Schedule/dispatch runs stateful/slow-tests/performance; those must pass.
if [ "${{ needs.fast-check.result }}" = "failure" ] || \
[ "${{ needs.baseurl.result }}" = "failure" ] || \
[ "${{ needs.e2e.result }}" = "failure" ] || \
[ "${{ needs.integration.result }}" = "failure" ] || \
[ "${{ needs.warm-build.result }}" = "failure" ] || \
[ "${{ needs.render-output-regression.result }}" = "failure" ] || \
[ "${{ needs.stateful.result }}" = "failure" ] || \
[ "${{ needs.slow-tests.result }}" = "failure" ] || \
[ "${{ needs.performance.result }}" = "failure" ]; then
echo "One or more test jobs failed"
exit 1
fi
echo "All test jobs passed"