master is the default branch - what the Home Assistant Supervisor clones - and until today it had no branch protection at all, while every gate this repo built (CI-01, CI-9) 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 (0f14c2a, the only commit on master with no (#NN) suffix).
SEC-2 applied to master the same rule release/1.0 already had: enable_push: false, required context checks / unit (pull_request), block_admin_merge_override: true, required_approvals: 0. The default branch was not changed. release/1.0s rule was not touched and re-reads byte-identical.
That is a forge setting and cannot live in a commit. This PR is the part that can: the procedure it implies, so the next release is not attempted the old way and does not fail confusingly at the push.
docs/RELEASE.md covers:
master is protected; releases go via PR release/1.0 -> master
the store must be reloaded (ha store reload) before the Supervisor offers the update
the before/after protection state of both branches
how the gate was proven to actually refuse rather than merely badge green
Documentation only - no code, no workflow change. Do not merge on my say-so.
`master` is the default branch - what the Home Assistant Supervisor clones - and until today it had **no branch protection at all**, while every gate this repo built (CI-01, CI-9) 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 (`0f14c2a`, the only commit on `master` with no `(#NN)` suffix).
SEC-2 applied to `master` the same rule `release/1.0` already had: `enable_push: false`, required context `checks / unit (pull_request)`, `block_admin_merge_override: true`, `required_approvals: 0`. The default branch was **not** changed. `release/1.0`s rule was not touched and re-reads byte-identical.
That is a forge setting and cannot live in a commit. This PR is the part that can: the **procedure** it implies, so the next release is not attempted the old way and does not fail confusingly at the push.
`docs/RELEASE.md` covers:
- `master` is protected; releases go via PR `release/1.0` -> `master`
- the store must be reloaded (`ha store reload`) before the Supervisor offers the update
- the before/after protection state of both branches
- how the gate was proven to actually refuse rather than merely badge green
**Documentation only** - no code, no workflow change. Do not merge on my say-so.
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
Review: verified against the forge, not against the document's account of itself.
Everything below I re-checked myself; I did not change any protection rule, and both rules re-read with their original updated_at after my testing (release/1.0 2026-08-30T16:00:05, master 2026-09-23T03:09:21).
Protection state — §2's "After" table is exact.GET /branch_protections returns two rules and they are field-for-field identical apart from branch_name/created_at: enable_push: false, enable_push_whitelist: false, enable_force_push: false, enable_status_check: true, status_check_contexts: ["checks / unit (pull_request)"], required_approvals: 0, block_on_rejected_reviews: false, block_admin_merge_override: true, protected_file_patterns: "". The master rule was created 2026-09-23T03:09:21 — consistent with "until 2026-09-23, no protection of any kind". default_branch is still master, unchanged as stated.
The empty-PR 405 trap — reproduced independently. I built my own control rather than trusting PR #110: two throwaway branches at the same SHA (sec2rev-base/sec2rev-head, both off ci9-gateproof, both unprotected), PR #111 between them, which Gitea created with changed_files: 0. Merge POST with a deliberately wrong head_commit_id:
empty PR, UNPROTECTED base -> 405 {"message":"Please try again later"}
Then I added one commit to the head branch and re-probed the same PR, same wrong head:
non-empty PR, UNPROTECTED base -> 409 {"message":"head out of date"}
So 405 on its own carries no information about protection at all — on an unprotected base it is purely a statement about emptiness. §1's "Read the message, not just the code" is the correct instruction and the two messages are quoted accurately. PR #111 is closed and both branches are deleted.
The "literal release PR is empty right now" correction — confirmed.git merge-base --is-ancestor origin/release/1.0 origin/master succeeds: release/1.0 (b0b24e5) is a parent of master (0f14c2a). PR #109 is the proof that Gitea opens it anyway: base master, head release/1.0, commits: 0, changed_files: 0, additions: 0. §1's closing note is right.
Step 3's "a master base matches the workflow" — confirmed as a real run, not a laundered skip. Statuses on PR #107's head cb7e45b3 show checks / unit (pull_request) going Waiting to run -> In progress -> success with Successful in 7m56s against run 814/job 1938. That is a genuine execution; the skipped-with-no-target_url shape described at the top of checks.yml does not appear.
This PR's own gate. Head 9d593429 carries one context, checks / unit (pull_request), success, Successful in 7m56s, run 818/job 1942 — again a real run.
Scope. The diff is two files: docs/RELEASE.md (new, 116 lines) and a 2-line addition to README.md's file map. No code, no workflow change.
What I could not verify, and am not asking anyone to:
The 405 Not all required status checks successful half of the discriminator on a protected base. Reproducing it would mean protecting a throwaway branch, and I am not touching branch protection. I take it on PR #107's record.
The exact pre-receive hook declined text in step 2, for the same reason — it matches the string Gitea emits for a blocked push, but I did not make it happen.
**Review: verified against the forge, not against the document's account of itself.**
Everything below I re-checked myself; I did not change any protection rule, and both rules re-read with their original `updated_at` after my testing (`release/1.0` 2026-08-30T16:00:05, `master` 2026-09-23T03:09:21).
**Protection state — §2's "After" table is exact.** `GET /branch_protections` returns two rules and they are field-for-field identical apart from `branch_name`/`created_at`: `enable_push: false`, `enable_push_whitelist: false`, `enable_force_push: false`, `enable_status_check: true`, `status_check_contexts: ["checks / unit (pull_request)"]`, `required_approvals: 0`, `block_on_rejected_reviews: false`, `block_admin_merge_override: true`, `protected_file_patterns: ""`. The `master` rule was created 2026-09-23T03:09:21 — consistent with "until 2026-09-23, no protection of any kind". `default_branch` is still `master`, unchanged as stated.
**The empty-PR `405` trap — reproduced independently.** I built my own control rather than trusting PR #110: two throwaway branches at the same SHA (`sec2rev-base`/`sec2rev-head`, both off `ci9-gateproof`, both unprotected), PR #111 between them, which Gitea created with `changed_files: 0`. Merge `POST` with a deliberately wrong `head_commit_id`:
```
empty PR, UNPROTECTED base -> 405 {"message":"Please try again later"}
```
Then I added one commit to the head branch and re-probed the same PR, same wrong head:
```
non-empty PR, UNPROTECTED base -> 409 {"message":"head out of date"}
```
So `405` on its own carries no information about protection at all — on an unprotected base it is purely a statement about emptiness. §1's "Read the *message*, not just the code" is the correct instruction and the two messages are quoted accurately. PR #111 is closed and both branches are deleted.
**The "literal release PR is empty right now" correction — confirmed.** `git merge-base --is-ancestor origin/release/1.0 origin/master` succeeds: `release/1.0` (`b0b24e5`) is a parent of `master` (`0f14c2a`). PR #109 is the proof that Gitea opens it anyway: base `master`, head `release/1.0`, `commits: 0`, `changed_files: 0`, `additions: 0`. §1's closing note is right.
**Step 3's "a `master` base matches the workflow" — confirmed as a real run, not a laundered skip.** Statuses on PR #107's head `cb7e45b3` show `checks / unit (pull_request)` going `Waiting to run` -> `In progress` -> `success` with `Successful in 7m56s` against run 814/job 1938. That is a genuine execution; the `skipped`-with-no-`target_url` shape described at the top of `checks.yml` does not appear.
**This PR's own gate.** Head `9d593429` carries one context, `checks / unit (pull_request)`, `success`, `Successful in 7m56s`, run 818/job 1942 — again a real run.
**Scope.** The diff is two files: `docs/RELEASE.md` (new, 116 lines) and a 2-line addition to `README.md`'s file map. No code, no workflow change.
**What I could not verify, and am not asking anyone to:**
- The `405 Not all required status checks successful` half of the discriminator on a *protected* base. Reproducing it would mean protecting a throwaway branch, and I am not touching branch protection. I take it on PR #107's record.
- The exact `pre-receive hook declined` text in step 2, for the same reason — it matches the string Gitea emits for a blocked push, but I did not make it happen.
Finding — §1: the (#NN) test is false, and it makes a habit look like a one-off. Non-blocking, but it should be corrected.
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).
The conclusion is true. The identifying test is not.
274 of master's 297 commits carry no (#NN) suffix. The suffix is a squash-merge artefact from PRs landing on release/1.0; it says nothing about how a commit reached master.
Two things a reader gets wrong from this sentence:
Auditing. Someone asking "what else bypassed the gate?" applies the stated test and gets 274 hits, which answers nothing, and may conclude the test is broken rather than the claim.
How bad it was. The sentence reads as a single lapse. master's first-parent chain says otherwise — the commit directly beneath 0f14c2a is 6f5e8e3 PKG-15: bring release/1.0 to master at 0.10.0, so the default branch is the add-on, a merge commit pushed straight to master by exactly the same route, one release earlier. 0.10.1 was not the first; it was the second, and direct-push promotion was the established habit. §1 exists to stop that habit, so understating it undercuts the document.
What is true and is checkable. The Gitea PR list is the clean discriminator: only two pull requests in this repository's history have ever had base master — #107 and #109, both throwaway probes opened today by SEC-2 itself, neither merged. No release has ever reached master through a pull request. That is a stronger statement than the one in the text, it is one API call to verify, and it cannot be confused with a squash artefact.
Suggested replacement for the parenthetical: (commit 0f14c2a; no release has ever reached master through a pull request — before SEC-2's own throwaway probes #107 and #109, the PR list contains no pull request with base master at all. 0.10.0's promotion, 6f5e8e3, went the same way one release earlier.)
I am not blocking on this — it is background, not procedure, and the forward path in §1 is correct without it. But it is a verifiably false claim in the document that is now this project's authority on how releases work, so it wants a follow-up line change rather than being left.
**Finding — §1: the `(#NN)` test is false, and it makes a habit look like a one-off. Non-blocking, but it should be corrected.**
> 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).
The conclusion is true. The identifying test is not.
```
$ git log --format='%s' origin/master | grep -vcE '\(#[0-9]+\)$'
274
$ git rev-list --count origin/master
297
```
274 of `master`'s 297 commits carry no `(#NN)` suffix. The suffix is a squash-merge artefact from PRs landing on `release/1.0`; it says nothing about how a commit reached `master`.
Two things a reader gets wrong from this sentence:
1. **Auditing.** Someone asking "what else bypassed the gate?" applies the stated test and gets 274 hits, which answers nothing, and may conclude the test is broken rather than the claim.
2. **How bad it was.** The sentence reads as a single lapse. `master`'s first-parent chain says otherwise — the commit directly beneath `0f14c2a` is `6f5e8e3 PKG-15: bring release/1.0 to master at 0.10.0, so the default branch is the add-on`, a merge commit pushed straight to `master` by exactly the same route, one release earlier. 0.10.1 was not the first; it was the second, and direct-push promotion was the established habit. §1 exists to stop that habit, so understating it undercuts the document.
**What is true and is checkable.** The Gitea PR list is the clean discriminator: only two pull requests in this repository's history have ever had base `master` — #107 and #109, both throwaway probes opened today by SEC-2 itself, neither merged. **No release has ever reached `master` through a pull request.** That is a stronger statement than the one in the text, it is one API call to verify, and it cannot be confused with a squash artefact.
Suggested replacement for the parenthetical: *(commit `0f14c2a`; no release has ever reached `master` through a pull request — before SEC-2's own throwaway probes #107 and #109, the PR list contains no pull request with base `master` at all. 0.10.0's promotion, `6f5e8e3`, went the same way one release earlier.)*
I am not blocking on this — it is background, not procedure, and the forward path in §1 is correct without it. But it is a verifiably false claim in the document that is now this project's authority on how releases work, so it wants a follow-up line change rather than being left.
The default is merge, so the default path is safe — which is why this is not blocking.
But the habit on this repo pulls the other way. Every feature PR into release/1.0 lands squashed; that is where the (#NN) suffixes come from. A releaser who has merged twenty PRs on this repo has clicked "squash" twenty times, and step 4 gives them no reason to do anything different on the one merge where it matters.
If the release PR is squashed, master gets a new commit whose parent is only the old master. release/1.0 stops being an ancestor of master, and §1's own closing note —
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.
— stops being true, permanently. The next release PR then shows a diff containing everything already shipped, which looks exactly like a corrupted branch. Nothing in the document tells the reader what they are looking at, and with enable_force_push: false on both branches (correctly noted in §3) the obvious reflex to fix it is unavailable. The recovery is another merge, which is not obvious from a diff that appears to be re-shipping the last release.
Suggested step 4: Merge the pull request as a merge commit — not a squash. Squashing detaches master from release/1.0's history and every later release PR then shows the whole shipped tree as a diff. required_approvals is 0, so a green check is the whole gate; there is no second pair of eyes unless someone asks for one.
That is one clause, it is the difference between the document's stated invariant holding and not holding, and it lands in the same sentence that already warns about the approvals gap.
**Finding — §1 step 4 does not say *how* to merge, and a squash there quietly breaks the invariant §1 itself relies on. Non-blocking; one clause fixes it.**
Step 4 says only "Merge the pull request." The repository allows all three styles:
```
allow_merge_commits: true
allow_rebase: true
allow_squash_merge: true
default_merge_style: merge
```
The default is `merge`, so the default path is safe — which is why this is not blocking.
But the habit on this repo pulls the other way. Every feature PR into `release/1.0` lands squashed; that is where the `(#NN)` suffixes come from. A releaser who has merged twenty PRs on this repo has clicked "squash" twenty times, and step 4 gives them no reason to do anything different on the one merge where it matters.
If the release PR is squashed, `master` gets a *new* commit whose parent is only the old `master`. `release/1.0` stops being an ancestor of `master`, and §1's own closing note —
> 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.
— stops being true, permanently. The next release PR then shows a diff containing everything already shipped, which looks exactly like a corrupted branch. Nothing in the document tells the reader what they are looking at, and with `enable_force_push: false` on both branches (correctly noted in §3) the obvious reflex to fix it is unavailable. The recovery is another merge, which is not obvious from a diff that appears to be re-shipping the last release.
Suggested step 4: *Merge the pull request as a **merge commit** — not a squash. Squashing detaches `master` from `release/1.0`'s history and every later release PR then shows the whole shipped tree as a diff. `required_approvals` is 0, so a green check is the whole gate; there is no second pair of eyes unless someone asks for one.*
That is one clause, it is the difference between the document's stated invariant holding and not holding, and it lands in the same sentence that already warns about the approvals gap.
The suffix is Gitea's merge-commit title for a PR, matching default_merge_style: merge. Two consequences, and the second one costs me my third comment:
1. My first comment (the (#NN) finding) stands, with a better explanation. The count is unchanged and independently checked — 274 of master's 297 commits carry no suffix — but the reason is not squashing. Commits lack the suffix when they are branch-side commits, pre-history, or merge commits created locally rather than by Gitea. That last category is the interesting one, and master's first-parent chain makes the point sharply:
0f14c2a parents=[6f5e8e3 b0b24e5] PKG-16: ship 0.10.1 to master ...
6f5e8e3 parents=[5184a2c 8a57361] PKG-15: bring release/1.0 to master at 0.10.0 ...
5184a2c parents=[1b85eb4] Rest the meter just below zero, not at zero
Exactly two merge commits have ever been pushed to master, both hand-made locally and pushed direct, both promotions of release/1.0: 0f14c2a (0.10.1) and 6f5e8e3 (0.10.0). So the document's sentence is wrong in a specific way — 0f14c2a is not the only such commit, it is the second of two, and 0.10.0 went the same way. The suggested replacement in that comment is still what I would write.
2. My third comment (merge style) should be treated as withdrawn. I argued from "the habit on this repo is squash, so a releaser will reflexively squash the release PR." That premise is false — the habit is merge commits, and default_merge_style is already merge. Squash remains enabled, and squashing a release PR would still detach master from release/1.0's history in the way I described, so the underlying hazard is real; but with both the default and the established practice pointing at a merge commit, it is a remote hazard and not worth a line in a document whose whole virtue is that it is short. Please do not act on that comment. It was a right-ish observation resting on a reason I made up rather than checked, which is the defect this project already has a name for.
Neither correction changes the verdict: the procedure in §1 is correct and followable, and the 405 handling is accurate.
**Correction to my own two comments above — I asserted a reason I had not checked, and it is wrong.**
In both comments I claimed the `(#NN)` suffix is a *squash*-merge artefact. It is not. Every `(#NN)` commit in this repository is a two-parent **merge commit**:
```
b0b24e5 b9f7bd0 eb88646 PKG-16: cut 0.10.1 ... (#105)
b9f7bd0 8a57361 43c144b SAFETY-57: ... (#104)
8a57361 16b1d6c 5aba220 PKG-15: ... (#103)
16b1d6c 186a044 ca8fd01 CI-9: ... (#95)
```
The suffix is Gitea's merge-commit title for a PR, matching `default_merge_style: merge`. Two consequences, and the second one costs me my third comment:
**1. My first comment (the `(#NN)` finding) stands, with a better explanation.** The count is unchanged and independently checked — 274 of `master`'s 297 commits carry no suffix — but the reason is not squashing. Commits lack the suffix when they are branch-side commits, pre-history, or merge commits created locally rather than by Gitea. That last category is the interesting one, and `master`'s first-parent chain makes the point sharply:
```
0f14c2a parents=[6f5e8e3 b0b24e5] PKG-16: ship 0.10.1 to master ...
6f5e8e3 parents=[5184a2c 8a57361] PKG-15: bring release/1.0 to master at 0.10.0 ...
5184a2c parents=[1b85eb4] Rest the meter just below zero, not at zero
```
Exactly two merge commits have ever been pushed to `master`, both hand-made locally and pushed direct, both promotions of `release/1.0`: `0f14c2a` (0.10.1) and `6f5e8e3` (0.10.0). So the document's sentence is wrong in a specific way — `0f14c2a` is not the only such commit, it is the second of two, and 0.10.0 went the same way. The suggested replacement in that comment is still what I would write.
**2. My third comment (merge style) should be treated as withdrawn.** I argued from "the habit on this repo is squash, so a releaser will reflexively squash the release PR." That premise is false — the habit is merge commits, and `default_merge_style` is already `merge`. Squash remains *enabled*, and squashing a release PR would still detach `master` from `release/1.0`'s history in the way I described, so the underlying hazard is real; but with both the default and the established practice pointing at a merge commit, it is a remote hazard and not worth a line in a document whose whole virtue is that it is short. Please do not act on that comment. It was a right-ish observation resting on a reason I made up rather than checked, which is the defect this project already has a name for.
Neither correction changes the verdict: the procedure in §1 is correct and followable, and the `405` handling is accurate.
admin
merged commit 239e120bf6 into release/1.02026-09-23 04:08:19 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
masteris the default branch - what the Home Assistant Supervisor clones - and until today it had no branch protection at all, while every gate this repo built (CI-01, CI-9) sat onrelease/1.0, the branch that does not ship. 0.10.1 went straight ontomasteron 2026-09-22 with no PR, no CI and no reviewer, and it succeeded (0f14c2a, the only commit onmasterwith no(#NN)suffix).SEC-2 applied to
masterthe same rulerelease/1.0already had:enable_push: false, required contextchecks / unit (pull_request),block_admin_merge_override: true,required_approvals: 0. The default branch was not changed.release/1.0s rule was not touched and re-reads byte-identical.That is a forge setting and cannot live in a commit. This PR is the part that can: the procedure it implies, so the next release is not attempted the old way and does not fail confusingly at the push.
docs/RELEASE.mdcovers:masteris protected; releases go via PRrelease/1.0->masterha store reload) before the Supervisor offers the updateDocumentation only - no code, no workflow change. Do not merge on my say-so.
381a7c84ceto9d593429d3Review: verified against the forge, not against the document's account of itself.
Everything below I re-checked myself; I did not change any protection rule, and both rules re-read with their original
updated_atafter my testing (release/1.02026-08-30T16:00:05,master2026-09-23T03:09:21).Protection state — §2's "After" table is exact.
GET /branch_protectionsreturns two rules and they are field-for-field identical apart frombranch_name/created_at:enable_push: false,enable_push_whitelist: false,enable_force_push: false,enable_status_check: true,status_check_contexts: ["checks / unit (pull_request)"],required_approvals: 0,block_on_rejected_reviews: false,block_admin_merge_override: true,protected_file_patterns: "". Themasterrule was created 2026-09-23T03:09:21 — consistent with "until 2026-09-23, no protection of any kind".default_branchis stillmaster, unchanged as stated.The empty-PR
405trap — reproduced independently. I built my own control rather than trusting PR #110: two throwaway branches at the same SHA (sec2rev-base/sec2rev-head, both offci9-gateproof, both unprotected), PR #111 between them, which Gitea created withchanged_files: 0. MergePOSTwith a deliberately wronghead_commit_id:Then I added one commit to the head branch and re-probed the same PR, same wrong head:
So
405on its own carries no information about protection at all — on an unprotected base it is purely a statement about emptiness. §1's "Read the message, not just the code" is the correct instruction and the two messages are quoted accurately. PR #111 is closed and both branches are deleted.The "literal release PR is empty right now" correction — confirmed.
git merge-base --is-ancestor origin/release/1.0 origin/mastersucceeds:release/1.0(b0b24e5) is a parent ofmaster(0f14c2a). PR #109 is the proof that Gitea opens it anyway: basemaster, headrelease/1.0,commits: 0,changed_files: 0,additions: 0. §1's closing note is right.Step 3's "a
masterbase matches the workflow" — confirmed as a real run, not a laundered skip. Statuses on PR #107's headcb7e45b3showchecks / unit (pull_request)goingWaiting to run->In progress->successwithSuccessful in 7m56sagainst run 814/job 1938. That is a genuine execution; theskipped-with-no-target_urlshape described at the top ofchecks.ymldoes not appear.This PR's own gate. Head
9d593429carries one context,checks / unit (pull_request),success,Successful in 7m56s, run 818/job 1942 — again a real run.Scope. The diff is two files:
docs/RELEASE.md(new, 116 lines) and a 2-line addition toREADME.md's file map. No code, no workflow change.What I could not verify, and am not asking anyone to:
405 Not all required status checks successfulhalf of the discriminator on a protected base. Reproducing it would mean protecting a throwaway branch, and I am not touching branch protection. I take it on PR #107's record.pre-receive hook declinedtext in step 2, for the same reason — it matches the string Gitea emits for a blocked push, but I did not make it happen.Finding — §1: the
(#NN)test is false, and it makes a habit look like a one-off. Non-blocking, but it should be corrected.The conclusion is true. The identifying test is not.
274 of
master's 297 commits carry no(#NN)suffix. The suffix is a squash-merge artefact from PRs landing onrelease/1.0; it says nothing about how a commit reachedmaster.Two things a reader gets wrong from this sentence:
master's first-parent chain says otherwise — the commit directly beneath0f14c2ais6f5e8e3 PKG-15: bring release/1.0 to master at 0.10.0, so the default branch is the add-on, a merge commit pushed straight tomasterby exactly the same route, one release earlier. 0.10.1 was not the first; it was the second, and direct-push promotion was the established habit. §1 exists to stop that habit, so understating it undercuts the document.What is true and is checkable. The Gitea PR list is the clean discriminator: only two pull requests in this repository's history have ever had base
master— #107 and #109, both throwaway probes opened today by SEC-2 itself, neither merged. No release has ever reachedmasterthrough a pull request. That is a stronger statement than the one in the text, it is one API call to verify, and it cannot be confused with a squash artefact.Suggested replacement for the parenthetical: (commit
0f14c2a; no release has ever reachedmasterthrough a pull request — before SEC-2's own throwaway probes #107 and #109, the PR list contains no pull request with basemasterat all. 0.10.0's promotion,6f5e8e3, went the same way one release earlier.)I am not blocking on this — it is background, not procedure, and the forward path in §1 is correct without it. But it is a verifiably false claim in the document that is now this project's authority on how releases work, so it wants a follow-up line change rather than being left.
Finding — §1 step 4 does not say how to merge, and a squash there quietly breaks the invariant §1 itself relies on. Non-blocking; one clause fixes it.
Step 4 says only "Merge the pull request." The repository allows all three styles:
The default is
merge, so the default path is safe — which is why this is not blocking.But the habit on this repo pulls the other way. Every feature PR into
release/1.0lands squashed; that is where the(#NN)suffixes come from. A releaser who has merged twenty PRs on this repo has clicked "squash" twenty times, and step 4 gives them no reason to do anything different on the one merge where it matters.If the release PR is squashed,
mastergets a new commit whose parent is only the oldmaster.release/1.0stops being an ancestor ofmaster, and §1's own closing note —— stops being true, permanently. The next release PR then shows a diff containing everything already shipped, which looks exactly like a corrupted branch. Nothing in the document tells the reader what they are looking at, and with
enable_force_push: falseon both branches (correctly noted in §3) the obvious reflex to fix it is unavailable. The recovery is another merge, which is not obvious from a diff that appears to be re-shipping the last release.Suggested step 4: Merge the pull request as a merge commit — not a squash. Squashing detaches
masterfromrelease/1.0's history and every later release PR then shows the whole shipped tree as a diff.required_approvalsis 0, so a green check is the whole gate; there is no second pair of eyes unless someone asks for one.That is one clause, it is the difference between the document's stated invariant holding and not holding, and it lands in the same sentence that already warns about the approvals gap.
Correction to my own two comments above — I asserted a reason I had not checked, and it is wrong.
In both comments I claimed the
(#NN)suffix is a squash-merge artefact. It is not. Every(#NN)commit in this repository is a two-parent merge commit:The suffix is Gitea's merge-commit title for a PR, matching
default_merge_style: merge. Two consequences, and the second one costs me my third comment:1. My first comment (the
(#NN)finding) stands, with a better explanation. The count is unchanged and independently checked — 274 ofmaster's 297 commits carry no suffix — but the reason is not squashing. Commits lack the suffix when they are branch-side commits, pre-history, or merge commits created locally rather than by Gitea. That last category is the interesting one, andmaster's first-parent chain makes the point sharply:Exactly two merge commits have ever been pushed to
master, both hand-made locally and pushed direct, both promotions ofrelease/1.0:0f14c2a(0.10.1) and6f5e8e3(0.10.0). So the document's sentence is wrong in a specific way —0f14c2ais not the only such commit, it is the second of two, and 0.10.0 went the same way. The suggested replacement in that comment is still what I would write.2. My third comment (merge style) should be treated as withdrawn. I argued from "the habit on this repo is squash, so a releaser will reflexively squash the release PR." That premise is false — the habit is merge commits, and
default_merge_styleis alreadymerge. Squash remains enabled, and squashing a release PR would still detachmasterfromrelease/1.0's history in the way I described, so the underlying hazard is real; but with both the default and the established practice pointing at a merge commit, it is a remote hazard and not worth a line in a document whose whole virtue is that it is short. Please do not act on that comment. It was a right-ish observation resting on a reason I made up rather than checked, which is the defect this project already has a name for.Neither correction changes the verdict: the procedure in §1 is correct and followable, and the
405handling is accurate.