# 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`.