fix: runtime failure fields and Report-a-Problem popup blocking (#60)… #703
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' | |
| # 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 | |
| - 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 | |
| timeout 120 bash -c 'until curl -sf http://localhost:8080/api/settings > /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 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 — 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 | |
| - 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 | |
| # ── 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)." |