fix: let a sandboxed session read the agent list, which silently truncates #1212
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: CI | |
| on: | |
| pull_request: | |
| push: | |
| branches: [main] | |
| jobs: | |
| # ── Detect which areas changed ────────────────────────────────── | |
| changes: | |
| name: Detect changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| backend: ${{ steps.filter.outputs.backend }} | |
| frontend: ${{ steps.filter.outputs.frontend }} | |
| algorithm: ${{ steps.algorithm.outputs.changed }} | |
| e2e: ${{ steps.filter.outputs.e2e }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - uses: dorny/paths-filter@v3 | |
| id: filter | |
| with: | |
| filters: | | |
| backend: | |
| - 'backend/**' | |
| - 'core/bess/**' | |
| - 'pyproject.toml' | |
| - 'requirements-dev.txt' | |
| frontend: | |
| - 'frontend/**' | |
| e2e: | |
| - 'backend/**' | |
| - 'core/bess/**' | |
| - 'frontend/**' | |
| - 'e2e/**' | |
| - 'scripts/mock_ha/**' | |
| - 'docker-compose.ci.yml' | |
| - 'docker-compose.prod-test.yml' | |
| - 'Dockerfile' | |
| - 'backend/Dockerfile' | |
| - 'build.json' | |
| - 'package-addon.sh' | |
| # Algorithm filter uses a script instead of dorny because dorny's | |
| # negation patterns don't work correctly with the default 'some' | |
| # quantifier — any unrelated file matches '!core/bess/excluded.py', | |
| # so algorithm always triggers. This script checks whether any | |
| # core/bess/ file changed that is NOT in the I/O-only exclusion list. | |
| - name: Check algorithm-relevant changes | |
| id: algorithm | |
| run: | | |
| # I/O-only files excluded from algorithm tests (fully mocked in slow suite) | |
| EXCLUDED=( | |
| core/bess/ha_api_controller.py | |
| core/bess/sensor_collector.py | |
| core/bess/weather.py | |
| core/bess/octopus_energy_source.py | |
| core/bess/official_nordpool_source.py | |
| core/bess/debug_data_exporter.py | |
| core/bess/debug_report_formatter.py | |
| core/bess/health_check.py | |
| core/bess/influxdb_helper.py | |
| core/bess/prediction_analyzer.py | |
| ) | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| BASE="${{ github.event.pull_request.base.sha }}" | |
| HEAD="${{ github.event.pull_request.head.sha }}" | |
| else | |
| BASE="${{ github.event.before }}" | |
| HEAD="${{ github.sha }}" | |
| fi | |
| CHANGED="false" | |
| for file in $(git diff --name-only "$BASE" "$HEAD" -- 'core/bess/'); do | |
| SKIP="false" | |
| for ex in "${EXCLUDED[@]}"; do | |
| if [ "$file" = "$ex" ]; then | |
| SKIP="true" | |
| break | |
| fi | |
| done | |
| if [ "$SKIP" = "false" ]; then | |
| echo "Algorithm-relevant change: $file" | |
| CHANGED="true" | |
| fi | |
| done | |
| echo "changed=$CHANGED" >> "$GITHUB_OUTPUT" | |
| echo "Algorithm tests needed: $CHANGED" | |
| # ── Fast tests: backend API + non-optimizer unit tests ────────── | |
| test-fast: | |
| name: Fast tests | |
| needs: changes | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| if: needs.changes.outputs.backend == 'true' | |
| - uses: actions/setup-python@v5 | |
| if: needs.changes.outputs.backend == 'true' | |
| with: | |
| python-version: "3.13" | |
| cache: pip | |
| - name: Install dependencies | |
| if: needs.changes.outputs.backend == 'true' | |
| run: | | |
| pip install -r backend/requirements.txt | |
| pip install -r requirements-dev.txt | |
| - name: Run fast tests | |
| if: needs.changes.outputs.backend == 'true' | |
| run: | | |
| pytest -m "not slow" --tb=short -q \ | |
| --cov=backend --cov=core/bess --cov-report=term-missing \ | |
| --cov-report=xml | |
| - name: Coverage summary | |
| if: needs.changes.outputs.backend == 'true' | |
| run: | | |
| { | |
| echo "### Coverage (fast tests)" | |
| echo '```' | |
| python -c "import xml.etree.ElementTree as ET; print(f'Total: {float(ET.parse(\"coverage.xml\").getroot().get(\"line-rate\")) * 100:.1f}%')" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: No backend changes | |
| if: needs.changes.outputs.backend != 'true' | |
| run: echo "No backend changes — skipping" | |
| # ── Slow tests: full optimizer / scenario / integration ───────── | |
| test-algorithm: | |
| name: Algorithm tests | |
| needs: changes | |
| if: needs.changes.outputs.algorithm == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.13" | |
| cache: pip | |
| - name: Install dependencies | |
| run: | | |
| pip install -r backend/requirements.txt | |
| pip install -r requirements-dev.txt | |
| - name: Run algorithm tests | |
| run: pytest -m slow --tb=short -q | |
| # ── Frontend: type-check + lint ───────────────────────────────── | |
| test-frontend: | |
| name: Frontend checks | |
| needs: changes | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| if: needs.changes.outputs.frontend == 'true' | |
| - uses: actions/setup-node@v4 | |
| if: needs.changes.outputs.frontend == 'true' | |
| with: | |
| node-version: "20" | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install and test | |
| if: needs.changes.outputs.frontend == 'true' | |
| run: cd frontend && npm ci && npm test && npm run type-check && npm run lint | |
| - name: No frontend changes | |
| if: needs.changes.outputs.frontend != 'true' | |
| run: echo "No frontend changes — skipping" | |
| # ── E2E: Playwright against docker-compose mock HA ─────────────── | |
| e2e: | |
| name: E2E tests | |
| needs: changes | |
| runs-on: ubuntu-latest | |
| env: | |
| RUN_E2E: ${{ needs.changes.outputs.e2e }} | |
| steps: | |
| - name: No E2E changes | |
| if: env.RUN_E2E != 'true' | |
| run: echo "No E2E-relevant changes — skipping" | |
| - uses: actions/checkout@v5 | |
| if: env.RUN_E2E == 'true' | |
| - uses: actions/setup-node@v4 | |
| if: env.RUN_E2E == 'true' | |
| with: | |
| node-version: "20" | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Build frontend | |
| if: env.RUN_E2E == 'true' | |
| run: cd frontend && npm ci && npm run build | |
| - name: Install Playwright | |
| if: env.RUN_E2E == 'true' | |
| run: cd e2e && npm ci && npx playwright install chromium --with-deps | |
| # Phase 1: Smoke / navigation / dashboard tests (pre-configured system) | |
| - name: "E2E: Start environment (normal day)" | |
| if: env.RUN_E2E == 'true' | |
| run: | | |
| SCENARIO=ci-normal-day docker compose -f docker-compose.ci.yml up -d | |
| # Gate on an endpoint behind _require_configured_system, NOT on | |
| # /api/settings: startup runs in a background thread (app.py | |
| # start_in_background) and the first schedule build can take | |
| # 10-60+ seconds, while /api/settings answers as soon as uvicorn | |
| # binds. /api/dashboard-health-summary is no good either -- it | |
| # deliberately returns 200 during startup. Waiting on the wrong | |
| # signal let the smoke tests fire before startup_complete and get | |
| # 503 "System is starting up" from every guarded endpoint. | |
| timeout 180 bash -c 'until curl -sf http://localhost:8080/api/system-health > /dev/null 2>&1; do sleep 2; done' | |
| - name: "E2E: Run smoke & navigation tests" | |
| if: env.RUN_E2E == 'true' | |
| run: cd e2e && npx playwright test --project=chromium | |
| - name: "E2E: Stop environment" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| # Phase 1b: Growatt VPP schedule build (regression for #399, #469 -- | |
| # fresh install, VPP remote-control mode, official Nordpool integration). | |
| # mock_time is pinned to a fixed past date, so the container's wall | |
| # clock must be faked to match or date-anchored price lookups fail. | |
| - name: "E2E: Start environment (Growatt VPP)" | |
| if: env.RUN_E2E == 'true' | |
| run: | | |
| BESS_FAKETIME_PRELOAD=/usr/local/lib/libfaketime.so.1 \ | |
| BESS_FAKETIME="2025-01-15 08:30:00" \ | |
| SCENARIO=ci-growatt-vpp BESS_SETTINGS=./e2e/ci-bess-settings-growatt-vpp.json \ | |
| docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/settings > /dev/null 2>&1; do sleep 2; done' | |
| - name: "E2E: Verify Growatt VPP schedule builds without error" | |
| if: env.RUN_E2E == 'true' | |
| run: | | |
| sleep 5 | |
| curl -sf http://localhost:8080/api/dashboard-health-summary > /dev/null | |
| if docker compose -f docker-compose.ci.yml logs bess | grep -q "Failed to update battery schedule"; then | |
| echo "Growatt VPP schedule build failed -- see logs below" | |
| docker compose -f docker-compose.ci.yml logs bess | |
| exit 1 | |
| fi | |
| - name: "E2E: Stop environment (Growatt VPP)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| # Phase 2: Setup wizard tests (all scenario combinations) | |
| # Each step resets bess_settings.json to empty so the wizard triggers fresh. | |
| - name: "E2E: Wizard — Nordpool + MIN" | |
| if: env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-nordpool-min BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-nordpool-min npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (Nordpool + MIN)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - name: "E2E: Wizard — Nordpool + SPH" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-nordpool-sph BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-nordpool-sph npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (Nordpool + SPH)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - name: "E2E: Wizard — Nordpool + SolaX" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-nordpool-solax BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-nordpool-solax npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (Nordpool + SolaX)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - name: "E2E: Wizard — Octopus + MIN" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-octopus BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-octopus npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (Octopus + MIN)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - name: "E2E: Wizard — ENTSO-e / Belpex + MIN" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-entsoe BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-entsoe npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (ENTSO-e + MIN)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - name: "E2E: Wizard — ENTSO-e + SolaX Growatt MIN (Frank #126)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-entsoe-frank-126 BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-entsoe-frank-126 npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (ENTSO-e Frank #126)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - name: "E2E: Wizard — SolaX with disabled lifetime counters (#549)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-solax-disabled-lifetime BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-solax-disabled-lifetime npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (disabled lifetime counters)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - name: "E2E: Wizard — no price provider installed (#549)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-no-price-provider BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options-no-pricing.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-no-price-provider npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (no price provider)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - name: "E2E: Wizard — Full integrations" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-full BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-full npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (Full)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - name: "E2E: Wizard — Nordpool HACS" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-nordpool-hacs BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-nordpool-hacs npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (Nordpool HACS)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - name: "E2E: Wizard — Octopus + SPH" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-growatt-sph-cloud-octopus BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-growatt-sph-cloud-octopus npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (Octopus + SPH)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - name: "E2E: Wizard — Both providers" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-both-providers BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-both-providers npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (Both providers)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - name: "E2E: Wizard — Solis (solis_modbus)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-solis BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-solis npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (Solis)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - name: "E2E: Wizard — Growatt VPP (GEN3 + SolaX Modbus)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-growatt-vpp BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-growatt-vpp npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (Growatt VPP)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - name: "E2E: Wizard — Growatt VPP (real config, ridax67 / #118)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-growatt-vpp-ridax-118 BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-growatt-vpp-ridax-118 npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (Growatt VPP real config)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - name: "E2E: Wizard — Growatt MIN (GEN4 + SolaX Modbus)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: | | |
| echo '{}' > ./e2e/ci-wizard-settings.json | |
| SCENARIO=ci-wizard-growatt-modbus BESS_SETTINGS=./e2e/ci-wizard-settings.json BESS_OPTIONS=./e2e/ci-wizard-options.json docker compose -f docker-compose.ci.yml up -d | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/setup/status > /dev/null 2>&1; do sleep 2; done' | |
| cd e2e && SCENARIO=ci-wizard-growatt-modbus npx playwright test --project=wizard | |
| - name: "E2E: Stop wizard (Growatt MIN Modbus)" | |
| if: always() && env.RUN_E2E == 'true' | |
| run: docker compose -f docker-compose.ci.yml down | |
| - uses: actions/upload-artifact@v4 | |
| if: failure() && env.RUN_E2E == 'true' | |
| with: | |
| name: playwright-report | |
| path: e2e/playwright-report/ | |
| # ── Quality gate: linting + formatting ────────────────────────── | |
| quality: | |
| name: Code quality | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.13" | |
| cache: pip | |
| - name: Install tools | |
| run: pip install black ruff | |
| - name: Black formatting | |
| run: black --check . --exclude="/(build|\.venv|node_modules)/" | |
| - name: Ruff linting | |
| run: ruff check . --exclude="build,.venv,node_modules" | |
| # ── Docker build & boot: verify production image starts ────────── | |
| docker-build-test: | |
| name: Docker build & boot | |
| needs: changes | |
| if: needs.changes.outputs.e2e == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Build production image | |
| run: docker compose -f docker-compose.prod-test.yml build | |
| - name: Start and verify process runs | |
| run: | | |
| BESS_PORT=8080 docker compose -f docker-compose.prod-test.yml up -d | |
| sleep 10 | |
| # Verify the bess container is still running (not crashed from ImportError etc.) | |
| STATUS=$(docker compose -f docker-compose.prod-test.yml ps bess --format '{{.State}}') | |
| echo "Container state: $STATUS" | |
| if [ "$STATUS" != "running" ]; then | |
| echo "FAIL: bess container is not running (state: $STATUS)" | |
| docker compose -f docker-compose.prod-test.yml logs bess | |
| exit 1 | |
| fi | |
| # Verify app logged successful initialization (not just an import crash) | |
| docker compose -f docker-compose.prod-test.yml logs bess 2>&1 | grep -q "BESS Controller initialized" | |
| echo "Production image boots correctly — no missing imports or COPY entries." | |
| - name: Dump logs on failure | |
| if: failure() | |
| run: docker compose -f docker-compose.prod-test.yml logs | |
| - name: Stop environment | |
| if: always() | |
| run: docker compose -f docker-compose.prod-test.yml down | |
| # The step above builds ./Dockerfile — what release-addon.yml publishes | |
| # to GHCR. A local install goes down a different path entirely: | |
| # deploy.sh → package-addon.sh, which ships backend/Dockerfile and lets | |
| # the supervisor build it against build.json. That path had no CI | |
| # coverage, which is how a base image with Python 3.11 and a numpy pin | |
| # needing >=3.12 reached users. Build it here the way the supervisor | |
| # does, using the real base from build.json. | |
| - name: Build the add-on image the supervisor builds | |
| run: | | |
| ./package-addon.sh | |
| BASE=$(jq -r '.build_from.amd64' build.json) | |
| echo "Building build/bess_manager/Dockerfile on $BASE" | |
| docker build \ | |
| --build-arg BUILD_FROM="$BASE" \ | |
| --build-arg BUILD_VERSION=ci \ | |
| -t bess-addon-supervisor-test \ | |
| build/bess_manager | |
| docker run --rm --entrypoint /app/venv/bin/python bess-addon-supervisor-test \ | |
| -c "import sys, numpy, pandas, fastapi; print('py', sys.version.split()[0], 'numpy', numpy.__version__)" | |
| # ── Merge gate: single required check for branch protection ────── | |
| # Branch protection cannot require the conditional jobs (test-algorithm, | |
| # docker-build-test) by name: when their path filter skips them, the | |
| # required status never reports and the PR is blocked forever. Instead | |
| # this gate always runs, depends on every job, and fails only if one | |
| # genuinely failed or was cancelled (skipped is allowed). Require ONLY | |
| # "Merge gate" in branch protection — it transitively enforces the rest, | |
| # including the slow Algorithm job whenever core/bess/ actually changed. | |
| ci-gate: | |
| name: Merge gate | |
| if: always() | |
| needs: | |
| - changes | |
| - test-fast | |
| - test-algorithm | |
| - test-frontend | |
| - e2e | |
| - quality | |
| - docker-build-test | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Fail if any required job failed or was cancelled | |
| if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') | |
| run: | | |
| echo "A required CI job failed or was cancelled — blocking merge." | |
| echo "Job results: ${{ join(needs.*.result, ', ') }}" | |
| exit 1 | |
| - name: Gate passed | |
| run: echo "All required CI jobs passed (skipped jobs allowed)." |