snapshots: stash BuildPlan on BuildContext #2280
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |