name: Validate on: push: # The branch commit is the release-gate authority. An annotated tag points # to the same SHA and must not duplicate the browser validation jobs. branches: - '**' pull_request: # A new push supersedes an unfinished validation for the same branch or PR. # Exact-SHA release gates never depend on an obsolete commit. concurrency: group: validate-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: { node-version: 22 } - name: Validate public documentation run: node scripts/check-docs.mjs --external provenance: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: { fetch-depth: 0 } - uses: actions/setup-node@v7 with: { node-version: 22 } - name: Validate commit trailers and hook mode env: EVENT_NAME: ${{ github.event_name }} BEFORE_SHA: ${{ github.event.before }} BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.sha }} DEVELOPMENT_BRANCH: dev run: | git fetch -q origin "refs/heads/$DEVELOPMENT_BRANCH:refs/remotes/origin/$DEVELOPMENT_BRANCH" node scripts/validate-commit-provenance.mjs --check-hook-mode --github-range # Догоняющая проверка процесса (PROCESS.md §10.3). Хуки ловят нарушение на # машине автора, но их можно обойти `--no-verify`, а коммиты идут прямо в dev # без PR — GitHub на своей стороне не блокирует ничего. Это последнее место, # где нарушение правила №1 ловится машиной. Job независимый: краснеет сам и # не роняет остальные, откат — удалить его отсюда, скрипт остаётся рабочим. process-gate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: { fetch-depth: 0 } - uses: actions/setup-node@v7 with: { node-version: 22 } - name: Process gate env: EVENT_NAME: ${{ github.event_name }} BEFORE_SHA: ${{ github.event.before }} BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.sha }} DEVELOPMENT_BRANCH: dev TARGET_REF: ${{ github.ref }} # Публичный репозиторий: штатного токена хватает на чтение issue. GH_TOKEN: ${{ github.token }} run: | git fetch -q origin "refs/heads/$DEVELOPMENT_BRANCH:refs/remotes/origin/$DEVELOPMENT_BRANCH" node scripts/process-gate.mjs --github-range --issues # Классификация изменённых путей: тяжёлые job идут только там, где менялось # относящееся к ним. НА DEV ФИЛЬТРОВ НЕТ: гейт беты принимает «зелёный Validate # на точном SHA», и если объём прогона зависит от diff, «зелёный» перестаёт # значить одно и то же — кандидат релиза (манифесты + changelog) пропустил бы # браузерные тесты, а прогон с пропущенными job всё равно success. Фильтры # экономят на ветках задач, где Validate — ранний сигнал: настоящую приёмку # там делает код-ревью, которое гоняет гейты само (#127). changes: runs-on: ubuntu-latest outputs: frontend: ${{ steps.classify.outputs.frontend }} backend: ${{ steps.classify.outputs.backend }} integration: ${{ steps.classify.outputs.integration }} steps: - uses: actions/checkout@v7 with: { fetch-depth: 0 } - id: classify env: EVENT_NAME: ${{ github.event_name }} BEFORE_SHA: ${{ github.event.before }} BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.sha }} REF: ${{ github.ref }} run: | if [ "$REF" = "refs/heads/dev" ]; then echo "dev: без фильтров, всё true" printf 'frontend=true\nbackend=true\nintegration=true\n' >> "$GITHUB_OUTPUT" exit 0 fi zero=$(printf '%040d' 0) base="$BEFORE_SHA" if [ "$EVENT_NAME" = "pull_request" ]; then base="$BASE_SHA"; fi # Новая ветка: before нулевой, диапазон считается от merge-base с dev, # иначе классифицировалась бы вся история. if [ -z "$base" ] || [ "$base" = "$zero" ] \ || ! git cat-file -e "$base" 2>/dev/null; then git fetch -q origin dev base=$(git merge-base origin/dev "$HEAD_SHA" || echo "$HEAD_SHA~1") fi files=$(git diff --name-only "$base" "$HEAD_SHA") printf '%s\n' "$files" | head -50 has() { printf '%s\n' "$files" | grep -qE "$1" && echo true || echo false; } { echo "frontend=$(has '^(src/|demo/|test/|dist/|custom_components/houseplan/frontend/|package(-lock)?\.json$|rollup\.config\.mjs$|tsconfig)')" echo "backend=$(has '^(custom_components/.*\.py$|tests_backend/|pytest\.ini$)')" echo "integration=$(has '^(custom_components/houseplan/manifest\.json$|hacs\.json$|custom_components/.*\.py$|custom_components/.*/translations/)')" } >> "$GITHUB_OUTPUT" hacs: needs: changes if: needs.changes.outputs.integration == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: HACS validation uses: hacs/action@main with: category: integration hassfest: needs: changes if: needs.changes.outputs.integration == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Hassfest validation uses: home-assistant/actions/hassfest@master frontend: needs: changes if: needs.changes.outputs.frontend == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 22 cache: npm - 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: # Gated on `frontend` so a typecheck failure does not burn browser minutes. needs: frontend runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 22 cache: npm - run: npm ci - name: Install Chromium for Playwright run: npx playwright install --with-deps chromium - name: Build a fresh bundle for the smokes 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@v7 with: name: smoke-logs path: /tmp/smoke-logs golden: # Deterministic visual correctness stays in every prerelease gate: it is # inexpensive and catches a different class of regressions than timings. needs: frontend runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 22 cache: npm - 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@v7 with: name: golden-images path: artifacts/golden performance_smoke: # Candidate-only catastrophic-regression guard for ordinary pushes and # prereleases. The expensive same-runner comparison lives in performance.yml. needs: frontend runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 22 cache: npm - run: npm ci - name: Install pinned Chromium run: npx playwright install --with-deps chromium - name: Build the exact candidate source run: npm run build && cp dist/houseplan-card.js demo/srv/assets/houseplan-card.js - name: Capture the heaviest Glow state run: | npm run benchmark:glow -- --profile=large-house-glow-overlay-v1 --variants=60 --samples=3 --warmups=1 --output=artifacts/performance-smoke/candidate.json - name: Enforce absolute smoke ceilings run: | npm run benchmark:compare -- --absolute-only --budgets=demo/performance/budgets-glow-smoke.json --candidate=artifacts/performance-smoke/candidate.json --output=artifacts/performance-smoke/comparison.json - name: Upload performance smoke report if: always() uses: actions/upload-artifact@v7 with: name: performance-smoke path: artifacts/performance-smoke backend: needs: changes if: needs.changes.outputs.backend == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 # 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@v7 with: { node-version: 22 } - uses: actions/setup-python@v7 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