# Branch protection for `main` (checklist) Why this matters here: `deploy.yml` auto-deploys `main` to the production host that serves the wallet and `/downloads`. The `workflow_run` gate makes deploy run only *after* CI succeeds — but that gate protects **deploy**, not **merge**. Without a branch-protection rule, a red-CI or force-pushed commit can still land on `main`; branch protection is what stops an unvalidated commit from becoming `main` in the first place. The two together mean: **`main` only ever advances through a CI-passing commit, and prod only ever runs a CI-validated commit.** Apply this in the GitHub web UI (no local change; it's a repo setting). Repo: `moonbitecoin/moonbite-network`. You need admin on the repo, and a working `gh`/web session — note the invalid `GITHUB_TOKEN` env var breaks `gh`, so use the browser or `unset GITHUB_TOKEN` first. --- ## Do this first: run CI once so the check names exist Required-status-check pickers only list checks GitHub has **seen recently**. PRs #2/#3 should have produced them. The CI job is `test` on a 3.11/3.12 matrix, so the checks are named: - `test (3.11)` - `test (3.12)` If the dropdown doesn't show them yet, let one CI run finish on a PR, then come back. --- ## Option A — Branch protection rule (classic) **Settings → Branches → Add branch protection rule.** - [ ] **Branch name pattern:** `main` - [ ] **Require a pull request before merging** — blocks direct pushes to `main`. - [ ] Required approvals: **0** (solo maintainer — GitHub won't let you approve your own PR; set 0 so you aren't locked out. Raise to 1 only once a second trusted maintainer exists.) - [ ] *(optional)* Require conversation resolution before merging - [ ] **Require status checks to pass before merging** - [ ] Require branches to be **up to date before merging** (forces a re-run against the latest `main`, so a check can't pass stale) - [ ] Select the required checks: **`test (3.11)`** and **`test (3.12)`** (these include the pytest suite *and* the release-checksum guard, both steps of the `test` job) - [ ] **Do not allow bypassing the above settings** (a.k.a. "Include administrators") — **on**. This is what actually guarantees the deploy safety for a solo admin; otherwise you can push past your own rule and the gate means nothing. (You can toggle it off for a genuine emergency, then back on.) - [ ] **Allow force pushes:** **off** (never force-push `main` — matches the project rule; a force-push rewrites what prod will deploy) - [ ] **Allow deletions:** **off** - [ ] *(optional, recommended given release signing)* **Require signed commits** - [ ] *(optional)* **Require linear history** (if you prefer rebase/squash merges and no merge commits on `main`) Leave everything else default. Save. --- ## Option B — Repository ruleset (newer equivalent) GitHub's **Settings → Rules → Rulesets → New branch ruleset** is the modern form and can target `main` by pattern with the same effect: - [ ] **Enforcement status:** Active - [ ] **Target branches:** Default branch (`main`) - [ ] **Bypass list:** empty (add nobody — the equivalent of "include administrators") - [ ] Rules: - [ ] Restrict deletions - [ ] Block force pushes - [ ] Require a pull request before merging (required approvals: 0 for solo) - [ ] Require status checks to pass → add `test (3.11)` and `test (3.12)`, and tick **Require branches to be up to date** - [ ] *(optional)* Require signed commits / linear history Rulesets and a classic rule can coexist; pick one to avoid confusing overlap. --- ## Fastest path: one `gh` command block (do this, then verify) - [ ] Run exactly this (requires `gh auth status` = `moonbitecoin`, admin on the repo; `unset GITHUB_TOKEN GH_TOKEN` first if a stale token is exported): ```bash unset GITHUB_TOKEN GH_TOKEN gh auth status # require CI (both matrix legs, up to date), require a PR, enforce for admins, # no force-push, no delete gh api -X PUT repos/moonbitecoin/moonbite-network/branches/main/protection \ -H "Accept: application/vnd.github+json" --input - <<'JSON' {"required_status_checks":{"strict":true,"contexts":["test (3.11)","test (3.12)"]}, "enforce_admins":true, "required_pull_request_reviews":{"required_approving_review_count":0,"dismiss_stale_reviews":true}, "restrictions":null,"allow_force_pushes":false,"allow_deletions":false,"required_linear_history":false} JSON gh api repos/moonbitecoin/moonbite-network/branches/main/protection \ --jq '{checks:.required_status_checks.contexts, strict:.required_status_checks.strict, admins:.enforce_admins.enabled, pr:.required_pull_request_reviews.required_approving_review_count, force:.allow_force_pushes.enabled, delete:.allow_deletions.enabled}' ``` - [ ] Expected: `checks` lists both legs, `strict:true`, `admins:true`, `pr:0`, `force:false`, `delete:false`. ## Optional: apply via API instead of the UI (same call, longer form) After clearing the bad token (`unset GITHUB_TOKEN GH_TOKEN`) and confirming `gh auth status` shows `moonbitecoin` active: ```bash gh api -X PUT repos/moonbitecoin/moonbite-network/branches/main/protection \ --input - <<'JSON' { "required_status_checks": { "strict": true, "contexts": ["test (3.11)", "test (3.12)"] }, "enforce_admins": true, "required_pull_request_reviews": { "required_approving_review_count": 0 }, "restrictions": null, "allow_force_pushes": false, "allow_deletions": false } JSON ``` Verify: ```bash gh api repos/moonbitecoin/moonbite-network/branches/main/protection \ --jq '{checks: .required_status_checks.contexts, admins: .enforce_admins.enabled, force: .allow_force_pushes.enabled}' ``` --- ## After enabling — sanity check - [ ] Open a throwaway PR that deliberately fails CI (e.g. a broken test) and confirm the **Merge** button is blocked until checks pass. - [ ] Confirm a direct `git push origin main` is rejected (PR required). - [ ] Confirm `deploy.yml` still fires only after a green CI run on `main` (it deploys `github.event.workflow_run.head_sha`). ## Not covered here (separate hardening, see RELEASE-SIGNING.md and deploy.yml) - Non-root deploy user with a narrow sudoers entry. - SHA-pinned GitHub Actions and signed-tag-only deploys. - The minisign release-signing key ceremony — **OPEN, owner-only task**: no signing key exists yet and none should be generated by automation or an agent. Until the ceremony is done, `publish-node-binaries.sh` emits checksums without a signature and `deploy/moonbite-release.pub` is a placeholder. Track it as a launch blocker in LAUNCH_CHECKLIST.md.