name: Validate on: push: # The branch commit is the release-gate authority. An annotated tag points # to the same SHA and must not duplicate every expensive browser/perf job. branches: - '**' pull_request: schedule: - cron: "0 4 * * 1" jobs: hacs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: HACS validation uses: hacs/action@main with: category: integration hassfest: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Hassfest validation uses: home-assistant/actions/hassfest@master frontend: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 22 } - run: npm ci - name: Typecheck run: npm run typecheck - name: Unit tests run: npm test - name: Build run: npm run build - name: Card bundle snapshots in sync run: | cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js smoke: # audit T2: the end-to-end layer used to run only when a human remembered. # Gated on `frontend` so a typecheck failure does not burn browser minutes. needs: frontend runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 22 } - run: npm ci - name: Install Chromium for Playwright run: npx playwright install --with-deps chromium - name: Build a FRESH bundle for the smokes # Never depend on a previous job's artefact; build the exact source # under test again before the browser suite. run: npm run build && cp dist/houseplan-card.js demo/srv/assets/houseplan-card.js - name: Smoke suite run: | fail=0 mkdir -p /tmp/smoke-logs for f in demo/smoke_*.mjs; do name=$(basename "$f" .mjs) if node "$f" > "/tmp/smoke-logs/$name.log" 2>&1; then echo "ok $name" else echo "FAIL $name" tail -20 "/tmp/smoke-logs/$name.log" fail=1 fi done exit $fail - name: Upload smoke logs if: failure() uses: actions/upload-artifact@v4 with: name: smoke-logs path: /tmp/smoke-logs golden: # Separate deterministic pixel layer. Before the first reviewed baseline # set it captures candidates; once baselines exist the same job becomes a # blocking comparison and uploads actual/diff/report on failure. needs: frontend runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 22 } - run: npm ci - name: Install pinned Chromium run: npx playwright install --with-deps chromium - name: Build the exact source under review run: npm run build && cp dist/houseplan-card.js demo/srv/assets/houseplan-card.js - name: Capture or verify golden matrix id: golden run: | if find demo/golden/baselines -maxdepth 1 -name '*.png' -print -quit | grep -q .; then echo "has_baselines=true" >> "$GITHUB_OUTPUT" npm run golden:verify else echo "has_baselines=false" >> "$GITHUB_OUTPUT" npm run golden:capture fi - name: Upload golden candidates/diffs if: failure() || steps.golden.outputs.has_baselines == 'false' uses: actions/upload-artifact@v4 with: name: golden-images path: artifacts/golden performance: # Candidate and base SHA run sequentially on the same hosted runner with # one Playwright/Chromium installation. This avoids turning a developer # laptop or cross-runner variance into a timing budget. needs: frontend runs-on: ubuntu-latest steps: - name: Check out candidate uses: actions/checkout@v4 with: path: candidate fetch-depth: 2 - name: Resolve comparison SHA id: base working-directory: candidate env: EVENT_NAME: ${{ github.event_name }} PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} PUSH_BEFORE_SHA: ${{ github.event.before }} run: | if [ "$EVENT_NAME" = "pull_request" ] && [ -n "$PR_BASE_SHA" ]; then sha="$PR_BASE_SHA" elif [ "$EVENT_NAME" = "push" ] && [ -n "$PUSH_BEFORE_SHA" ] && ! printf '%s' "$PUSH_BEFORE_SHA" | grep -Eq '^0+$'; then sha="$PUSH_BEFORE_SHA" else sha="$(git rev-parse HEAD^)" fi # The first main promotion after HP-PERF-01 can legitimately point # at an older stable commit which predates the benchmark's embedded # bundle fingerprint. Comparing against that legacy tree would fail # the freshness guard before any measurements are taken. Fall back # to the candidate's direct parent, which is the last dev revision # included in the promotion and already has the current harness. git fetch --no-tags --depth=1 origin "$sha" if ! git cat-file -e "${sha}:demo/bundle-freshness.mjs" 2>/dev/null; then echo "comparison $sha predates HP-PERF-01; using HEAD^" >&2 sha="$(git rev-parse HEAD^)" fi echo "sha=$sha" >> "$GITHUB_OUTPUT" - name: Check out base SHA uses: actions/checkout@v4 with: ref: ${{ steps.base.outputs.sha }} path: baseline - uses: actions/setup-node@v4 with: node-version: 22 cache: npm cache-dependency-path: | candidate/package-lock.json baseline/package-lock.json - name: Install candidate and baseline dependencies run: npm ci --prefix candidate && npm ci --prefix baseline - name: Install pinned Chromium working-directory: candidate run: npx playwright install --with-deps chromium - name: Build both exact source trees run: | npm --prefix candidate run build cp candidate/dist/houseplan-card.js candidate/demo/srv/assets/houseplan-card.js npm --prefix baseline run build cp baseline/dist/houseplan-card.js baseline/demo/srv/assets/houseplan-card.js - name: Capture base and candidate profiles working-directory: candidate run: | npm run benchmark:large-house -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/baseline.json npm run benchmark:large-house -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/candidate.json npm run benchmark:glow -- --profile=large-light-blend-v1 --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/blend-baseline.json npm run benchmark:glow -- --profile=large-light-blend-v1 --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/blend-candidate.json npm run benchmark:glow -- --profile=large-house-glow-overlay-v1 --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/overlay-baseline.json npm run benchmark:glow -- --profile=large-house-glow-overlay-v1 --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/overlay-candidate.json if ! grep -q "glow_enabled" ../baseline/src/logic.ts; then echo "Base predates independent Glow; bootstrap relative overlay baseline, keep absolute gate" cp ../artifacts/performance/overlay-candidate.json ../artifacts/performance/overlay-baseline.json fi - name: Enforce performance budgets working-directory: candidate run: | npm run benchmark:compare -- --baseline=../artifacts/performance/baseline.json --candidate=../artifacts/performance/candidate.json --output=../artifacts/performance/comparison.json npm run benchmark:compare -- --budgets=demo/performance/budgets-large-light-blend.json --baseline=../artifacts/performance/blend-baseline.json --candidate=../artifacts/performance/blend-candidate.json --output=../artifacts/performance/blend-comparison.json npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-glow-overlay.json --baseline=../artifacts/performance/overlay-baseline.json --candidate=../artifacts/performance/overlay-candidate.json --output=../artifacts/performance/overlay-comparison.json - name: Upload performance reports if: always() uses: actions/upload-artifact@v4 with: name: large-house-performance path: artifacts/performance backend: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Browser fixtures are generated by their real ESM factories and then # validated through the Python CONFIG_SCHEMA/LAYOUT_SCHEMA in the same test. - uses: actions/setup-node@v4 with: { node-version: 22 } - uses: actions/setup-python@v5 with: { python-version: "3.13" } - run: pip install pytest voluptuous pytest-homeassistant-custom-component home-assistant-frontend - name: Backend unit tests (pure + HA harness) run: python -m pytest tests_backend/ -q