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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
5.1 KiB
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:
-
Merge the work into
release/1.0as usual, via a pull request that goes green. Bumpversion:ingoodwe_controller/config.yamlon that branch — the Supervisor keys the built image byversion:, so without a bump nothing is offered and nothing changes. -
Open a pull request
release/1.0→master. Do not push tomaster; 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) -
Wait for
checks / unit (pull_request)to go green on that pull request. The workflow in.gitea/workflows/checks.ymlcarries nobranches:filter, so amasterbase matches and a real run happens — verified on throwaway PR #107 (basemaster), which queued a genuine run.⚠️ Green means green. A
skippedstatus is laundered into asuccessrollup 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. -
Merge the pull request.
required_approvalsis 0, so a green check is the whole gate; there is no second pair of eyes unless someone asks for one. -
Reload the store. Merging to
masterdoes not, by itself, offer the update. The Supervisor only sees the new version after:ssh root@192.168.2.158 "ha store reload && ha addons update local_goodwe_controller"Until
ha store reloadruns, 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.