From 1326b3f79972a31e28b5f8623bdb63e36e420945 Mon Sep 17 00:00:00 2001 From: Musa Musa Date: Sun, 16 Aug 2026 22:08:11 +0100 Subject: [PATCH] chore: protect main and the release tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main had no protection at all — a force-push or an accidental delete would have destroyed history on a repo that is now public and published. Three rulesets, split deliberately rather than bundled into one: main: no force-push, no deletion no bypass, not even the owner main: PR + green checks bypass: repository admin release tags are immutable no bypass, v* cannot move or be deleted The split is the point. Rewriting main is the only mistake here that destroys work rather than just making a mess, so it has no escape hatch. The PR rule does, because a solo maintainer cannot approve their own pull request and a hard gate would mean self-blocking or fake reviews — remove the bypass actor when there is a second maintainer. Tags are immutable because a published npm version is. v0.5.0 must keep pointing at the commit whose provenance attestation says it built resilix@0.5.0. Two checks are deliberately NOT required: Deploy to Pages and release are push-triggered only, so they never report on a pull request, and requiring a check that cannot run blocks every merge forever. Every rule was tested rather than assumed, which caught a bad test of my own: `git push --force` of a descendant commit is a fast-forward, not a force, so it proved nothing and pushed an empty commit to main. The real test is a rewind — `push --force origin/main~1:main` — which is correctly rejected. Deletion of main and of v0.5.0 are both rejected too. Also closes the stale Version Packages PR. It was built from the four changesets consumed manually during the 0.5.0 release, so it had gone conflicting and would have rolled package.json back to 0.2.0. --- CONTRIBUTING.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c0a54a0..5a470fe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -106,3 +106,38 @@ limiter and the retry/throttling work both gated on theirs. If you are adding a comes first, and it should carry the provenance of every default it proposes. Add a changeset (`pnpm changeset`) for anything user-visible. + +## Branch protection + +Three rulesets guard `main` and the release tags. They are configured on GitHub, so this is the +copy that explains *why*. + +| Ruleset | Rule | Bypass | +|---|---|---| +| `main: no force-push, no deletion` | no non-fast-forward, no deletion | **none — not even the owner** | +| `main: PR + green checks` | PR required (0 approvals), 9 status checks green | repository admin | +| `release tags are immutable` | `v*` cannot be deleted or moved | **none** | + +**Nobody can rewrite `main`.** This is the one rule with no escape hatch, because a force-push is +the only mistake here that destroys work rather than just making a mess. Verified by attempting a +rewind, which is rejected. + +**The PR rule is bypassable by the admin on purpose.** A solo maintainer cannot approve their own +pull request, so a hard gate would mean either self-blocking or fake reviews. The rule makes PRs +the default path and keeps a hotfix possible. Remove the bypass actor when there is a second +maintainer. + +**Release tags are immutable** because a published npm version is immutable. `v0.5.0` must keep +pointing at the commit whose provenance attestation says it built `resilix@0.5.0`; a moved tag +makes that attestation a lie. + +### Required checks, and the two that are deliberately absent + +Required: `build`, `node 18`, `node 20`, `node 22`, `node 24`, `bun`, `deno`, +`cloudflare workers`, `Build site`. + +**Not required, and must never be:** `Deploy to Pages` and `release`. Both are `push`-triggered +only, so they never report on a pull request — requiring a check that cannot run blocks every +merge forever. `Deploy to Pages` shows as `SKIPPED` on PRs, which is correct. + +If you add a workflow, only add it to the required list if it runs `on: pull_request`.