mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
528 lines
26 KiB
YAML
528 lines
26 KiB
YAML
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:
|
||
- '**'
|
||
# Документы ревью конвейер пишет пачками — 340 коммитов за месяц, и каждый
|
||
# гонял лёгкую половину Validate впустую (≈15 часов раннера в месяц).
|
||
# Релизного кандидата это не затрагивает: тег всегда стоит на коммите,
|
||
# который меняет версию и бандл, а не только `docs/reviews/**`.
|
||
paths-ignore:
|
||
- 'docs/reviews/**'
|
||
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
|
||
|
||
# Конвейер читает `process.yml` из ветки по умолчанию, поэтому файл обязан
|
||
# совпадать в `main` и `dev`. До этой проверки совпадение держалось на
|
||
# дисциплине: каждая правка требовала двух пушей и ручной сверки.
|
||
process-workflow-sync:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
fetch-depth: 0
|
||
- name: process.yml идентичен в main и dev
|
||
run: |
|
||
git fetch --quiet origin main dev
|
||
if diff <(git show origin/main:.github/workflows/process.yml) \
|
||
<(git show origin/dev:.github/workflows/process.yml); then
|
||
echo "main и dev идентичны"
|
||
else
|
||
echo "РАСХОЖДЕНИЕ: process.yml в main и dev различаются."
|
||
echo "Конвейер исполняет версию из ветки по умолчанию, поэтому"
|
||
echo "правку нужно отправить в обе ветки."
|
||
exit 1
|
||
fi
|
||
|
||
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"
|
||
|
||
# Переиспользование результата тяжёлой job (#208). Ключ = входы поведения
|
||
# (sourceFingerprint: src/**, demo/fixtures, demo/golden/*.mjs, манифесты
|
||
# сборки) ПЛЮС оснастка именно этой job. Маркер в кэше пишет только успешный
|
||
# прогон с тем же ключом, поэтому попадание доказывает: job с побайтово теми
|
||
# же входами уже завершилась успешно.
|
||
#
|
||
# Это НЕ фильтр путей из job `changes` (на dev они отключены намеренно): там
|
||
# объём прогона угадывается по путям и «зелёный» начинает значить разное,
|
||
# здесь эквивалентность входов доказана хешем.
|
||
#
|
||
# Свойство, снимающее главный риск: релизный кандидат бампает версию, а
|
||
# CARD_VERSION и package.json входят в фингерпринт, поэтому ключи кандидата
|
||
# заведомо новые и полный набор гейтов перед бетой и релизом идёт всегда.
|
||
reuse:
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
smoke: ${{ steps.probe.outputs.smoke }}
|
||
golden: ${{ steps.probe.outputs.golden }}
|
||
performance_smoke: ${{ steps.probe.outputs.performance_smoke }}
|
||
backend: ${{ steps.probe.outputs.backend }}
|
||
smoke_key: ${{ steps.keys.outputs.smoke }}
|
||
golden_key: ${{ steps.keys.outputs.golden }}
|
||
performance_smoke_key: ${{ steps.keys.outputs.performance_smoke }}
|
||
backend_key: ${{ steps.keys.outputs.backend }}
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
- uses: actions/setup-node@v7
|
||
with: { node-version: 22 }
|
||
- name: Ключи переиспользования
|
||
id: keys
|
||
run: |
|
||
for job in smoke golden performance_smoke backend; do
|
||
key=$(node scripts/gate-reuse.mjs --job="$job")
|
||
echo "$job=$key" >> "$GITHUB_OUTPUT"
|
||
echo "$job: $key"
|
||
done
|
||
# lookup-only: маркер только проверяется, но не восстанавливается —
|
||
# сохранять его в этой job нечего, она ничего не прогоняла.
|
||
- name: Маркер smoke
|
||
id: m_smoke
|
||
uses: actions/cache/restore@v6
|
||
with:
|
||
path: .reuse-marker
|
||
key: reuse-smoke-${{ steps.keys.outputs.smoke }}
|
||
lookup-only: true
|
||
- name: Маркер golden
|
||
id: m_golden
|
||
uses: actions/cache/restore@v6
|
||
with:
|
||
path: .reuse-marker
|
||
key: reuse-golden-${{ steps.keys.outputs.golden }}
|
||
lookup-only: true
|
||
- name: Маркер performance_smoke
|
||
id: m_perf
|
||
uses: actions/cache/restore@v6
|
||
with:
|
||
path: .reuse-marker
|
||
key: reuse-performance_smoke-${{ steps.keys.outputs.performance_smoke }}
|
||
lookup-only: true
|
||
- name: Маркер backend
|
||
id: m_backend
|
||
uses: actions/cache/restore@v6
|
||
with:
|
||
path: .reuse-marker
|
||
key: reuse-backend-${{ steps.keys.outputs.backend }}
|
||
lookup-only: true
|
||
- name: Что переиспользуем
|
||
id: probe
|
||
env:
|
||
SMOKE: ${{ steps.m_smoke.outputs.cache-hit }}
|
||
GOLDEN: ${{ steps.m_golden.outputs.cache-hit }}
|
||
PERF: ${{ steps.m_perf.outputs.cache-hit }}
|
||
BACKEND: ${{ steps.m_backend.outputs.cache-hit }}
|
||
run: |
|
||
# Пропуск обязан быть громким: молчаливый skip — тот самый тихий
|
||
# успех, который уже дважды стоил нам дня (#171, #207).
|
||
waive() {
|
||
if [ "$2" = "true" ]; then
|
||
echo "$1=true" >> "$GITHUB_OUTPUT"
|
||
echo "::notice::$1 не прогоняется: входы побайтово те же, что в предыдущем успешном прогоне (#208)"
|
||
echo "- **$1** переиспользована: входы не менялись" >> "$GITHUB_STEP_SUMMARY"
|
||
else
|
||
echo "$1=false" >> "$GITHUB_OUTPUT"
|
||
echo "- $1: прогоняется" >> "$GITHUB_STEP_SUMMARY"
|
||
fi
|
||
}
|
||
echo "### Переиспользование гейтов (#208)" >> "$GITHUB_STEP_SUMMARY"
|
||
waive smoke "$SMOKE"
|
||
waive golden "$GOLDEN"
|
||
waive performance_smoke "$PERF"
|
||
waive backend "$BACKEND"
|
||
|
||
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, reuse]
|
||
if: needs.reuse.outputs.smoke != 'true'
|
||
runs-on: ubuntu-latest
|
||
# Смоки шардируются: последовательный прогон занимал ~7.5 минут и был
|
||
# критическим путём всего Validate. Три шарда режут его примерно вдвое;
|
||
# цена — трижды `npm ci` и сборка, около двух оплаченных минут раннера.
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
shard: [1, 2, 3]
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: 22
|
||
cache: npm
|
||
- run: npm ci
|
||
# Браузеры кэшируются, а apt не запускается вовсе: на GitHub-раннере
|
||
# системные библиотеки Chromium уже в образе, а --with-deps тратил минуты
|
||
# и подолгу перебирал недоступное azure-зеркало (#175, #206). Если
|
||
# библиотека когда-нибудь исчезнет из образа, Chromium не запустится с
|
||
# внятной ошибкой — тогда флаг вернуть.
|
||
- name: Кэш браузеров Playwright
|
||
id: pw
|
||
uses: actions/cache@v6
|
||
with:
|
||
path: ~/.cache/ms-playwright
|
||
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||
- name: Install pinned Chromium
|
||
if: steps.pw.outputs.cache-hit != 'true'
|
||
run: npx playwright install 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 (шард ${{ matrix.shard }} из 3)
|
||
env:
|
||
SHARD: ${{ matrix.shard }}
|
||
SHARDS: '3'
|
||
run: |
|
||
fail=0
|
||
index=0
|
||
ran=0
|
||
mkdir -p /tmp/smoke-logs
|
||
# Деление по порядковому номеру файла: список отсортирован, поэтому
|
||
# разбиение детерминировано и не зависит от времени прогона.
|
||
for f in demo/smoke_*.mjs; do
|
||
index=$((index + 1))
|
||
if [ $(( (index - 1) % SHARDS + 1 )) -ne "$SHARD" ]; then continue; fi
|
||
ran=$((ran + 1))
|
||
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
|
||
echo "--- шард ${SHARD}/${SHARDS}: прогнано ${ran} из ${index}"
|
||
# Пустой шард — признак, что деление сломалось, а не что работы нет.
|
||
if [ "$ran" -eq 0 ]; then echo "шард пуст: проверьте деление"; exit 1; fi
|
||
exit $fail
|
||
- name: Upload smoke logs
|
||
if: failure()
|
||
uses: actions/upload-artifact@v7
|
||
with:
|
||
name: smoke-logs-${{ matrix.shard }}
|
||
path: /tmp/smoke-logs
|
||
|
||
# Маркер переиспользования пишется ОДИН раз и только когда прошли все шарды:
|
||
# частично прогнанная матрица не имеет права выглядеть как выполненная работа.
|
||
smoke_done:
|
||
needs: [smoke, reuse]
|
||
if: needs.reuse.outputs.smoke != 'true'
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Записать маркер успеха
|
||
run: |
|
||
printf '%s\n' "smoke прогнана успешно (3 шарда)" \
|
||
"SHA: ${{ github.sha }}" \
|
||
"прогон: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
||
> .reuse-marker
|
||
- uses: actions/cache/save@v6
|
||
# Гонка двух прогонов с одинаковым ключом даёт «Cache already exists».
|
||
# Это не отказ гейта: работа выполнена, маркер уже записал сосед — job
|
||
# краснеть из-за этого не должна.
|
||
continue-on-error: true
|
||
with:
|
||
path: .reuse-marker
|
||
key: reuse-smoke-${{ needs.reuse.outputs.smoke_key }}
|
||
|
||
golden:
|
||
# Deterministic visual correctness stays in every prerelease gate: it is
|
||
# inexpensive and catches a different class of regressions than timings.
|
||
needs: [frontend, reuse]
|
||
if: needs.reuse.outputs.golden != 'true'
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: 22
|
||
cache: npm
|
||
- run: npm ci
|
||
# Браузеры кэшируются, а apt не запускается вовсе: на GitHub-раннере
|
||
# системные библиотеки Chromium уже в образе, а --with-deps тратил минуты
|
||
# и подолгу перебирал недоступное azure-зеркало (#175, #206). Если
|
||
# библиотека когда-нибудь исчезнет из образа, Chromium не запустится с
|
||
# внятной ошибкой — тогда флаг вернуть.
|
||
- name: Кэш браузеров Playwright
|
||
id: pw
|
||
uses: actions/cache@v6
|
||
with:
|
||
path: ~/.cache/ms-playwright
|
||
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||
- name: Install pinned Chromium
|
||
if: steps.pw.outputs.cache-hit != 'true'
|
||
run: npx playwright install 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
|
||
# Маркер пишется последним шагом: он существует только если всё выше
|
||
# прошло. Кэш сохраняется post-шагом, то есть тоже лишь при успехе job.
|
||
- name: Записать маркер успеха
|
||
run: |
|
||
printf '%s\n' "golden прогнана успешно" \
|
||
"SHA: ${{ github.sha }}" \
|
||
"прогон: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
||
> .reuse-marker
|
||
- uses: actions/cache/save@v6
|
||
# Гонка двух прогонов с одинаковым ключом даёт «Cache already exists».
|
||
# Это не отказ гейта: работа выполнена, маркер уже записал сосед — job
|
||
# краснеть из-за этого не должна.
|
||
continue-on-error: true
|
||
with:
|
||
path: .reuse-marker
|
||
key: reuse-golden-${{ needs.reuse.outputs.golden_key }}
|
||
|
||
performance_smoke:
|
||
# Candidate-only catastrophic-regression guard for ordinary pushes and
|
||
# prereleases. The expensive same-runner comparison lives in performance.yml.
|
||
needs: [frontend, reuse]
|
||
if: needs.reuse.outputs.performance_smoke != 'true'
|
||
runs-on: ubuntu-latest
|
||
# 15 минут не хватало, когда установка браузера шла через apt: замер
|
||
# начинался на исходе окна (#206). Запас на холодный кэш — при попадании
|
||
# job укладывается в те же минуты, что и раньше.
|
||
timeout-minutes: 20
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: 22
|
||
cache: npm
|
||
- run: npm ci
|
||
# Браузеры кэшируются, а apt не запускается вовсе: на GitHub-раннере
|
||
# системные библиотеки Chromium уже в образе, а --with-deps тратил минуты
|
||
# и подолгу перебирал недоступное azure-зеркало (#175, #206). Если
|
||
# библиотека когда-нибудь исчезнет из образа, Chromium не запустится с
|
||
# внятной ошибкой — тогда флаг вернуть.
|
||
- name: Кэш браузеров Playwright
|
||
id: pw
|
||
uses: actions/cache@v6
|
||
with:
|
||
path: ~/.cache/ms-playwright
|
||
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||
- name: Install pinned Chromium
|
||
if: steps.pw.outputs.cache-hit != 'true'
|
||
run: npx playwright install 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
|
||
# Маркер пишется последним шагом: он существует только если всё выше
|
||
# прошло. Кэш сохраняется post-шагом, то есть тоже лишь при успехе job.
|
||
- name: Записать маркер успеха
|
||
run: |
|
||
printf '%s\n' "performance_smoke прогнана успешно" \
|
||
"SHA: ${{ github.sha }}" \
|
||
"прогон: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
||
> .reuse-marker
|
||
- uses: actions/cache/save@v6
|
||
# Гонка двух прогонов с одинаковым ключом даёт «Cache already exists».
|
||
# Это не отказ гейта: работа выполнена, маркер уже записал сосед — job
|
||
# краснеть из-за этого не должна.
|
||
continue-on-error: true
|
||
with:
|
||
path: .reuse-marker
|
||
key: reuse-performance_smoke-${{ needs.reuse.outputs.performance_smoke_key }}
|
||
|
||
backend:
|
||
needs: [changes, reuse]
|
||
if: needs.changes.outputs.backend == 'true' && needs.reuse.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
|
||
# Маркер пишется последним шагом: он существует только если всё выше
|
||
# прошло. Кэш сохраняется post-шагом, то есть тоже лишь при успехе job.
|
||
- name: Записать маркер успеха
|
||
run: |
|
||
printf '%s\n' "backend прогнана успешно" \
|
||
"SHA: ${{ github.sha }}" \
|
||
"прогон: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
||
> .reuse-marker
|
||
- uses: actions/cache/save@v6
|
||
# Гонка двух прогонов с одинаковым ключом даёт «Cache already exists».
|
||
# Это не отказ гейта: работа выполнена, маркер уже записал сосед — job
|
||
# краснеть из-за этого не должна.
|
||
continue-on-error: true
|
||
with:
|
||
path: .reuse-marker
|
||
key: reuse-backend-${{ needs.reuse.outputs.backend_key }}
|