ci: единый staged→tested→published путь установочных ассетов (#540)

`release-zip.yml` выкладывал `houseplan.zip` в ту же секунду, когда релиз
становился публичным — до Validate, Full Performance и E2E; `release.yml`
параллельно пересобирал `houseplan-card.js`, а E2E требовал публичного ZIP,
чтобы вообще начаться. Публикаторов было четыре, порядок — ни одного.

Теперь публикатор стабильных один — `release.yml`: закрепить SHA → релиз в
черновике (опубликованный руками немедленно возвращается в черновик) → гейты
на SHA (трейлер `Release: <tag>`, контракт `--stable`, Validate, Full
Performance, E2E на коммите-кандидате через tarball codeload) → одна сборка,
`git archive` ZIP из того же дерева, `SHA256SUMS` → загрузка в черновик →
публикация → скачать публичное и сверить с паспортом → анонс. Dispatch на
публичный тег — ремонт: догружается только недостающее, расходящийся хеш —
отказ. Беты кладут тот же паспорт; локальный публикатор больше не ждёт
републикаторов — их нет.

- `.github/workflows/release-zip.yml` удалён
- `scripts/release-assets.mjs` — паспорт ассетов (`sums`/`check`), чистые
  функции под юнитами
- `scripts/e2e-gate.mjs --ref=<sha>` — под тестом кандидат, `--tag` только
  для выбора `upgrade_from`
- `scripts/release-contract.mjs --stable`
- мутанты: независимый публикатор, снятая зависимость от гейта, релиз без
  возврата в черновик, `--clobber` в ремонте, E2E на теге, слепой паспорт

Issue: #540
User-Visible: no
This commit is contained in:
Claude
2026-09-13 07:42:23 +03:00
parent c53f9ffc02
commit eb77224e0c
18 changed files with 948 additions and 278 deletions
+34 -18
View File
@@ -378,11 +378,12 @@ operation; it does not modify the developer's Git configuration.
It creates or verifies an
annotated exact-SHA tag, builds `houseplan.zip` directly from that committed
tree, verifies its manifest and embedded frontend against the candidate hash,
stages a draft prerelease and uploads both assets. Only then does it make the
release public. It locates the Release, HACS-zip and Telegram runs by workflow
file plus exact tag/SHA, verifies the downloaded public asset contents and
stages a draft prerelease and uploads both assets plus their `SHA256SUMS`
passport. Only then does it make the release public. It verifies the downloaded
public asset contents against the candidate and the passport, checks the
paginated HACS prerelease order, and finally closes only the explicitly supplied
issues and strips their status label. Re-running the same command
issues and strips their status label. Nothing else re-uploads assets after
publication (#540): the bytes it verified are the bytes that stay. Re-running the same command
after a partial failure is safe when local/remote tags still resolve to the same
SHA: stale public assets are replaced and verified rather than accepted or left
for manual deletion. ZIP inspection is implemented in Node and does not depend
@@ -401,21 +402,36 @@ the workflow file exists on the default branch; until the next promotion to
closing them is the release manager's call, and the `close-merged` job does it
from the beta itself (#120).
The older release-event workflows remain supported as a recovery/manual
fallback. They still gate assets on the exact tagged SHA, so adopting the new
path does not weaken releases created through the old path.
Tag `vX.Y.Z` + GitHub Release → `.github/workflows/release.yml` resolves that
tag to its exact commit, waits for the latest non-cancelled Validate run of the
**Stable releases** go through `.github/workflows/release.yml`, the only
publisher of installable assets (#540). Run it with `workflow_dispatch` on
`main` with the exact tag: when the tag does not exist yet it is created on the
`main` tip; when it exists, its commit is the candidate. The workflow resolves
the tag to its exact commit, requires the `Release: <tag>` trailer on it,
checks the release contract (`release-contract.mjs --stable`), waits for the
latest non-cancelled Validate run of the
SHA to complete successfully (#511: a cancelled run is not a verdict, a later
re-run or another-baseline comparison refreshes an older result), then builds
and attaches `houseplan-card.js`. A missing, failed or one-hour-timed-out latest
Validate withholds the asset; stable releases additionally need the same for
Full Performance and a green E2E run on a real Home Assistant: `release.yml`
dispatches `e2e.yml` in `Matysh/houseplan-e2e` with the tag (the suite installs
the release's `houseplan.zip` into HA in docker) and waits for it (#514). A red
E2E withholds the assets — open the linked run, the Playwright traces and
screenshots are in its artifacts; fix, then cut a new tag. Bump the version
re-run or another-baseline comparison refreshes an older result), requires Full
Performance and a green E2E run on a
real Home Assistant — `e2e-gate.mjs --ref=<sha>` dispatches `e2e.yml` in
`Matysh/houseplan-e2e` on the **candidate commit**, whose
`custom_components/houseplan` tree the suite installs from the codeload tarball
(#514, #540) — then builds once, archives `houseplan.zip` from that same tree
(`git archive <sha>:custom_components/houseplan`, deterministic), writes
`SHA256SUMS`, uploads everything into a draft, publishes, downloads the public
assets back and checks them against the passport, and only then announces. The
tree hash printed in the run summary is the identity between what E2E installed
and what HACS downloads.
Publishing a stable release by hand in the GitHub form still works, but
fail-closed: `release: published` starts the same workflow, which immediately
turns the release back into a draft and walks the same path; nothing installable
is public while the gates run. A red gate leaves the draft in place — open the
linked run, the Playwright traces and screenshots are in its artifacts; fix,
then cut a new tag. Re-dispatching the workflow on an already public tag is a
**repair**: the gates run again on the SHA, missing assets are added, and an
existing asset whose hash differs from the rebuilt one fails the run instead of
being replaced. Hand-published betas are ignored by this workflow — prereleases
have their own staged path above. Bump the version
everywhere in sync: `src/houseplan-card.ts` (CARD_VERSION), `package.json`,
`custom_components/houseplan/manifest.json`, `custom_components/houseplan/const.py`.
+3 -3
View File
@@ -17,14 +17,14 @@ change must pass through a published beta/RC before stable. Stable release
commits are promotion-only (versions, generated bundles and release/changelog
metadata). Only an explicit owner-approved emergency hotfix may skip this gate.
## Snapshot (2026-09-12)
## Snapshot (2026-09-13)
| Item | State |
|---|---|
| Version | **v1.75.0** everywhere (manifest, const.py, package.json, package-lock in both places, CARD_VERSION in the card and the editor runtime) |
| Current local cycle | **Stable v1.75.0** — `main` is fast-forwarded to the tested `dev` SHA and the GitHub Release is public. The line aggregates from the stable v1.74.0 and carries one user-visible fix: the House Plan sidebar page could serve a previous version of the card for hours, because the panel entry fetched it through `./houseplan-card.js` — a relative specifier, and relative resolution does not inherit the `?v=` a dashboard gets from its Lovelace resource, while the entry files carry no `Cache-Control` at all. The panel now imports the implementation by its content-hashed name, so either the matching card arrives or the panel says out loud that the page is stale (#535). Internal in the line: #536 — the version banner asks the host to repaint when it drops the notice on disconnect, instead of relying on an unrelated update. Shipped as v1.75.0-beta.1 the same day; the stable is promotion-only on top of it. Known contradiction found while publishing: `scripts/release-contract.mjs` requires the grouped "small fixes" bullet unconditionally, while `npm run release:notes -- <tag> --verify` rejects it when every user-visible issue of the range is already itemised — the two rules deadlock any stable with a single user-visible issue. The contract won here because it is the automated gate; the verifier was run and its objection recorded rather than silenced. The banner still offers only "reload the page", which cannot help when the frontend is newer than the backend (files updated, Home Assistant not restarted) — not yet an issue. |
| Current local cycle | **Stable v1.75.0** — `main` is fast-forwarded to the tested `dev` SHA and the GitHub Release is public. The line aggregates from the stable v1.74.0 and carries one user-visible fix: the House Plan sidebar page could serve a previous version of the card for hours, because the panel entry fetched it through `./houseplan-card.js` — a relative specifier, and relative resolution does not inherit the `?v=` a dashboard gets from its Lovelace resource, while the entry files carry no `Cache-Control` at all. The panel now imports the implementation by its content-hashed name, so either the matching card arrives or the panel says out loud that the page is stale (#535). Internal in the line: #536 — the version banner asks the host to repaint when it drops the notice on disconnect, instead of relying on an unrelated update. Shipped as v1.75.0-beta.1 the same day; the stable is promotion-only on top of it. Known contradiction found while publishing: `scripts/release-contract.mjs` requires the grouped "small fixes" bullet unconditionally, while `npm run release:notes -- <tag> --verify` rejects it when every user-visible issue of the range is already itemised — the two rules deadlock any stable with a single user-visible issue. The contract won here because it is the automated gate; the verifier was run and its objection recorded rather than silenced. The banner still offers only "reload the page", which cannot help when the frontend is newer than the backend (files updated, Home Assistant not restarted) — not yet an issue. **Known gap of the public v1.75.0:** it carries `houseplan.zip` only — `release-zip.yml` uploaded the ZIP the moment the release was published, while `release.yml` withheld `houseplan-card.js` because Full Performance was red on that SHA (#537). The next patch release delivers the card asset and is the first live run of the single publisher from #540; a repair of v1.75.0 itself is impossible by design — the gates on `2c6410bb` stay red. |
| Hidden Alpha Stage | #89 Stage 1 ships in v1.63.0-beta.1, #122 Stage 2 in v1.64.0 and #160 Stage 3 in v1.73.0-beta.1. The same hidden `iso` view uses the fixed 4° camera, raised/tethered device-room-lock overlays, deeper openings and bounded theme materials; #471 removes the overlay plates from paint while retaining their safety geometry. Since #448 the experiment is enabled only through the single indefinite browser-local `hp_alpha` gate; it is not expiring and has no per-stage key. Flat remains default; editors, `houseplan-space-card`, floor effects, stored coordinates and HA actions remain unchanged. Stage 3 stays internal and is absent from public changelog/user documentation. |
| Workflow | Superseded 2026-08-12: the pre-1.62 rule of "local edits without tests or commits" is **dead** — since release 1.62 every product change follows `PROCESS.md` (issue in `S5-ready`+, branch `issue/<NN>-slug`, trailers on every commit, review pipeline; `AGENTS.md` is the summary). Release mechanics below remain current. A requested pre-release gets a production build plus the smallest targeted unit/smoke set covering the changed surfaces, one tested `dev` commit/tag and a GitHub Release with `prerelease=true`; `main` stays untouched. The complete local frontend/backend/smoke gate runs only before a stable release, after which `main` is fast-forwarded to the exact tested `dev` SHA and the GitHub Release uses `prerelease=false`. Release bodies are short and bilingual (Russian first); every bullet links its GitHub issue (#NN) so the #328 rules stay machine-checkable. A STABLE body aggregates the changelog since the PREVIOUS STABLE release (never since the last beta): features/fixes described across the line's beta changelogs must appear, while bugs that were introduced and fixed strictly inside the beta line (never shipped in any stable) are excluded — draft with `npm run release:notes -- <tag>`, curate by hand, then `npm run release:notes -- <tag> --verify` must pass. `Мелкие исправления и улучшения` / `Small fixes and improvements` is allowed only when the range really contains user-visible work not itemised in the body; a single-issue hotfix ships without it (the verifier enforces this). Every body ends with separate links to the Russian and English changelogs. Open or partially delivered issues are never presented as shipped. Telegram announcements are sent only for stable releases; beta and RC publication is silent. `docs/RELEASE-NOTES.md` is the current canonical body instance; `npm run release:prerelease -- <tag> --issues=… --yes` is the primary local publication path and the manual `Publish prerelease` workflow is its GitHub-only equivalent once present on `main`. Nothing is copied to the home instance by hand |
| Workflow | Superseded 2026-08-12: the pre-1.62 rule of "local edits without tests or commits" is **dead** — since release 1.62 every product change follows `PROCESS.md` (issue in `S5-ready`+, branch `issue/<NN>-slug`, trailers on every commit, review pipeline; `AGENTS.md` is the summary). Release mechanics below remain current. A requested pre-release gets a production build plus the smallest targeted unit/smoke set covering the changed surfaces, one tested `dev` commit/tag and a GitHub Release with `prerelease=true`; `main` stays untouched. The complete local frontend/backend/smoke gate runs only before a stable release, after which `main` is fast-forwarded to the exact tested `dev` SHA and the stable release is produced by `release.yml` (`workflow_dispatch` on `main` with the tag) — the only publisher of installable assets since #540: gates on the exact SHA (Validate, Full Performance, E2E on the candidate commit), one build, `houseplan.zip` archived from the committed tree, `SHA256SUMS`, draft → publish → read-back verification; a release published by hand in the GitHub form is turned back into a draft and walked through the same path, and a re-dispatch on a public tag is a repair that adds only missing assets. Release bodies are short and bilingual (Russian first); every bullet links its GitHub issue (#NN) so the #328 rules stay machine-checkable. A STABLE body aggregates the changelog since the PREVIOUS STABLE release (never since the last beta): features/fixes described across the line's beta changelogs must appear, while bugs that were introduced and fixed strictly inside the beta line (never shipped in any stable) are excluded — draft with `npm run release:notes -- <tag>`, curate by hand, then `npm run release:notes -- <tag> --verify` must pass. `Мелкие исправления и улучшения` / `Small fixes and improvements` is allowed only when the range really contains user-visible work not itemised in the body; a single-issue hotfix ships without it (the verifier enforces this). Every body ends with separate links to the Russian and English changelogs. Open or partially delivered issues are never presented as shipped. Telegram announcements are sent only for stable releases; beta and RC publication is silent. `docs/RELEASE-NOTES.md` is the current canonical body instance; `npm run release:prerelease -- <tag> --issues=… --yes` is the primary local publication path and the manual `Publish prerelease` workflow is its GitHub-only equivalent once present on `main`. Nothing is copied to the home instance by hand |
| GitHub | https://github.com/Matysh/houseplan-card — [Issues](https://github.com/Matysh/houseplan-card/issues) are the canonical task records; their labels carry priority and workflow status (`PROCESS.md` §9). GitHub Projects is no longer used. `main` carries stable releases; pre-release tags may point directly at `dev`. Work lands on `dev` and is merged into `main` for a stable release, so `dev` is normally equal to or ahead of `main`, never behind. Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) |
| CI | Prerelease publication requires a green exact-SHA Validate: frontend/backend, smoke (including the #73 rAF frame sampler), golden, HACS/Hassfest and a short absolute-ceiling performance smoke. Obsolete same-ref Validate runs are cancelled. Full seven-sample base/candidate performance moved to `performance.yml` (`main` push, weekly, manual); stable release assets fail closed unless Validate and Full Performance are green for the exact tagged SHA and the stable-only CDP compositor screencast finds no empty/black presented frame. |
| HACS | **In the default catalog since 2026-08-25** (hacs/default#9004 merged). Install = plain HACS search. `houseplan.zip` is attached to stable tags automatically (verified on v1.72.0); forum/4pda announcement still pending |
+8 -5
View File
@@ -68,11 +68,14 @@ dispatch-прогон на точном SHA; зелёный push-прогон и
## E2E на реальном Home Assistant (#514)
Репозиторий `Matysh/houseplan-e2e`: настоящий HA в docker, House Plan из
`houseplan.zip` релиза, 13 сценариев Playwright (боковая панель, дашборды,
роли, телефон, PDF, рестарт HA, первый запуск, обновление). Ночью — по
расписанию на последней бете; для **стабильного** релиза `release.yml`
запускает его на теге и ждёт зелёного (`scripts/e2e-gate.mjs`): красный —
ассеты не публикуются. В цикле разработки и на бетах не участвует.
релиза или из дерева коммита, 13 сценариев Playwright (боковая панель,
дашборды, роли, телефон, PDF, рестарт HA, первый запуск, обновление). Ночью —
по расписанию на последней бете; для **стабильного** релиза `release.yml`
запускает его на **SHA кандидата** (`scripts/e2e-gate.mjs --ref=<sha>`, #540:
ставится `custom_components/houseplan` из tarball коммита — то же дерево, из
которого `git archive` строит `houseplan.zip`) и ждёт зелёного: красный —
релиз остаётся черновиком, ассеты не публикуются. В цикле разработки и на
бетах не участвует.
## Версия в кадрах и попиксельная приёмка (#512)