From 9d593429d31e8944c361a7b23813e82b6f5eaeb1 Mon Sep 17 00:00:00 2001 From: glenn schrooyen Date: Wed, 23 Sep 2026 03:13:49 +0200 Subject: [PATCH] SEC-2: write down the release path, now that master refuses a direct push master is the default branch, which is what the Home Assistant Supervisor clones - so whatever sits on it runs on a real 5 kW inverter. Until today it had no branch protection at all, while every gate this repo built sat on release/1.0, the branch that does not ship. 0.10.1 went straight onto master on 2026-09-22 with no PR, no CI and no reviewer, and it succeeded. master now carries the same rule as release/1.0 - enable_push false, required context `checks / unit (pull_request)`, block_admin_merge_override, required_approvals 0. That is a forge setting and cannot live in a commit, so the part that can is this: the procedure it implies. Without it the next release is attempted the old way and fails confusingly at the push. docs/RELEASE.md records the path (merge to release/1.0, bump version, PR release/1.0 -> master, wait for green, merge, then `ha store reload` before the Supervisor offers anything), the before/after protection state of both branches, and how the gate was proven to bite rather than merely badge green. Documentation only - no code, no workflow change. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU --- README.md | 2 + docs/RELEASE.md | 116 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 docs/RELEASE.md diff --git a/README.md b/README.md index fdd9eb5..cd500c7 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ estop/rs485_log.py optional RS485 e-stop / bus witness docs/GoodWe-RS485-Field-Guide.pdf the guide techs carry (10pp, A4) docs/build_guide.py its single source - rebuilds the HTML and PDF FIELD-GUIDE.md how to edit and rebuild the guide +docs/RELEASE.md how a change reaches the battery - `master` is + protected, releases go via PR from `release/1.0` ``` **Picking this up after a break? Read `HANDOVER.md` first** — current state, diff --git a/docs/RELEASE.md b/docs/RELEASE.md new file mode 100644 index 0000000..ff5d899 --- /dev/null +++ b/docs/RELEASE.md @@ -0,0 +1,116 @@ +# Releasing — how a change reaches the battery + +`master` is the **default branch**, and the default branch is what the Home +Assistant Supervisor clones when it installs or updates this add-on. Whatever +sits on `master` is what runs on a real 5 kW inverter. Treat a push to `master` +as a deployment, because that is what it is. + +--- + +## 1. `master` is protected — releases go through a pull request + +Until 2026-09-23 `master` had **no protection of any kind**. Every gate this +repo built (CI-01, CI-9) sat on `release/1.0`, the branch that does *not* ship. +On 2026-09-22 version 0.10.1 went straight onto `master` — no pull request, no +CI run, no reviewer — and it succeeded (commit `0f14c2a`, the only commit on +`master` that carries no `(#NN)` merge suffix). SEC-2 closed that. + +**The release path is now:** + +1. Merge the work into `release/1.0` as usual, via a pull request that goes + green. Bump `version:` in `goodwe_controller/config.yaml` on that branch — + the Supervisor keys the built image by `version:`, so without a bump nothing + is offered and nothing changes. +2. Open a pull request **`release/1.0` → `master`**. Do not push to `master`; + it will be refused by the pre-receive hook: + + ``` + remote: error: Not allowed to push to protected branch master + ! [remote rejected] ... (pre-receive hook declined) + ``` + +3. Wait for `checks / unit (pull_request)` to go **green** on that pull request. + The workflow in `.gitea/workflows/checks.yml` carries **no `branches:` + filter**, so a `master` base matches and a real run happens — verified on + throwaway PR #107 (base `master`), which queued a genuine run. + + ⚠️ Green means green. A `skipped` status is laundered into a `success` + rollup by this Gitea and the merge gate accepts it — see the long note at + the top of `.gitea/workflows/checks.yml`. Never add an event filter there. + +4. Merge the pull request. `required_approvals` is **0**, so a green check is + the whole gate; there is no second pair of eyes unless someone asks for one. + +5. **Reload the store.** Merging to `master` does not, by itself, offer the + update. The Supervisor only sees the new version after: + + ```bash + ssh root@192.168.2.158 "ha store reload && ha addons update local_goodwe_controller" + ``` + + Until `ha store reload` runs, the add-on page still shows the old version + and the release looks like it did nothing. + +Note that `release/1.0` is normally an *ancestor* of `master` between releases, +so a `release/1.0 → master` pull request is empty until new commits land on +`release/1.0`. That is expected; it means there is nothing to release yet. + +--- + +## 2. Branch protection — before and after SEC-2 + +Forge is Gitea at `192.168.2.147:3000`, repo `admin/goodwe-addon`. Read it back +with `GET /api/v1/repos/admin/goodwe-addon/branch_protections`. + +**Before (2026-09-23):** + +| branch | protected | status check required | context | approvals | +|---|---|---|---|---| +| `master` (default) | **no rule at all** | — | — | — | +| `release/1.0` | yes | yes | `checks / unit (pull_request)` | 0 | + +**After:** + +| branch | protected | status check required | context | approvals | +|---|---|---|---|---| +| `master` (default) | yes | yes | `checks / unit (pull_request)` | 0 | +| `release/1.0` | yes (unchanged) | yes | `checks / unit (pull_request)` | 0 | + +Both rules are otherwise identical: `enable_push: false`, +`enable_force_push: false`, `block_admin_merge_override: true`, no whitelists, +no file patterns. `required_approvals` stays at **0** on purpose — raising it +would block the review agent from merging, and nobody asked for it. + +The default branch was **not** changed. Changing it would change what the +Supervisor clones, which is a different decision from this one. + +### How the gate was proven to actually bite + +A protection rule on this forge can *appear* satisfied while having checked +nothing, so the rule was tested rather than trusted. The discriminator is a +merge `POST` carrying a deliberately wrong `head_commit_id`: Gitea evaluates +branch protection *before* it checks the head, so + +* `409 head out of date` → the request **cleared** protection; +* `405 Not all required status checks successful` → protection **refused** it. + +Read the *message*, not just the code: `405` is also returned as +`Please try again later` for a pull request with **no commits**, which has +nothing to do with protection. An empty pull request on an unprotected base +gives that same `405` (verified, throwaway PR #110), so an empty +`release/1.0 → master` reports it too. + +Validated against a control on an unprotected base (PR #106), which answered +`409` while its check was still pending — i.e. protection was no obstacle +there. On `master` (PR #107) the same probe answered `405` while pending, and +`409` once the check was genuinely green. A direct `git push` to `master` was +rejected by the pre-receive hook with `master`'s SHA unmoved. + +--- + +## 3. Rolling back + +There is no force-push escape hatch: `enable_force_push` is `false` on both +branches. To undo a bad release, land a revert on `release/1.0`, bump the +version again, and take it through §1. The `/addons/` rollback for the +reference install is in `HANDOVER.md`.