Skip to content

fix: runtime failure fields and Report-a-Problem popup blocking (#60)… #703

fix: runtime failure fields and Report-a-Problem popup blocking (#60)…

fix: runtime failure fields and Report-a-Problem popup blocking (#60)… #703

Workflow file for this run

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)."