Skip to content

security(repo): protect main with reviewed PRs and blocked force-pushes (#484) - #614

Open
timo126 wants to merge 1 commit into
Sub-Rosa-Issue:mainfrom
timo126:security/484-protect-main
Open

timo126 wants to merge 1 commit into
Sub-Rosa-Issue:mainfrom
timo126:security/484-protect-main

Conversation

@timo126

@timo126 timo126 commented Oct 27, 2026

Copy link
Copy Markdown

Summary

Closes #484.

main had no protection at all — the ruleset list was empty and
GET /repos/{owner}/{repo}/branches/main/protection returned
404 Branch not protected — so a mistaken direct push or force-push could bypass
review and reintroduce unsafe history.

This PR creates an active repository ruleset named Protect main on the
default branch and records the final settings in docs/BRANCH_PROTECTION.md.
No source code, workflow, or dependency changes are included.

Ruleset

Field Value
Name / ID Protect main / 25968240
Target branch
Enforcement active
Conditions ref_name.include = ["~DEFAULT_BRANCH"] (resolves to main, follows renames)

Acceptance criteria → settings

Criterion Setting
Require changes through pull requests pull_request rule
At least one approving review required_approving_review_count: 1
Dismiss stale approvals after new pushes dismiss_stale_reviews_on_push: true
Require the existing CI checks before merge required_status_checks: check, coverage, round-contract, web
Block force pushes non_fast_forward rule
Block branch deletion deletion rule
Bypass limited to minimum administrators one actor: RepositoryRole admin (actor_id: 5)

Required status checks

Contexts are the GitHub Actions job names. Five workflows gate every PR
through four contexts, because Bindings check and Line lengths check both
name their job check. GitHub matches checks by context name and requires all
runs sharing a context to pass, so check covers both:

Context Workflow Job
check bindings-check.yml (Bindings check) check
check line-lengths.yml (Line lengths check) check
coverage coverage.yml (TypeScript coverage) coverage
round-contract contract.yml (Contract tests) round-contract
web web.yml (Web checks) web

Deliberately not required: Docs link check (check-links) and
Environment template check (validate) are scoped with
on: pull_request: paths:. GitHub keeps a required check from a path-filtered
workflow in Pending when the paths don't match, which would block every
unrelated PR indefinitely. They remain advisory until they run on every PR.

Bypass: exactly one actor — the repository admin role. No users, teams,
apps, deploy keys, or the write/maintain roles can bypass, keeping an
emergency escape hatch without widening bypass access.

Where it is applied

The ruleset is applied and verified on timo126/sub-rosa-issue, which is the
repository where the automation account holds admin. The upstream
Sub-Rosa-Issue/sub-rosa-issue reports permissions.admin = false for the same
token, so an org admin must apply the identical payload there:
gh api --method POST repos/Sub-Rosa-Issue/sub-rosa-issue/rulesets --input ruleset-main.json
(the full JSON is in docs/BRANCH_PROTECTION.md).

Verification

$ gh api repos/timo126/sub-rosa-issue/rules/branches/main
[{"type":"deletion","ruleset_source_type":"Repository","ruleset_source":"timo126/sub-rosa-issue","ruleset_id":25968240},
 {"type":"non_fast_forward","ruleset_source_type":"Repository","ruleset_source":"timo126/sub-rosa-issue","ruleset_id":25968240},
 {"type":"pull_request","parameters":{"required_approving_review_count":1,"dismiss_stale_reviews_on_push":true,...},"ruleset_id":25968240},
 {"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"check"},{"context":"coverage"},{"context":"round-contract"},{"context":"web"}]},"ruleset_id":25968240}]

That endpoint only returns rules from rulesets with active/evaluate
enforcement, so it is direct evidence that merges into main now require an
approved, CI-green pull request and that force pushes and deletions are blocked.
Live ruleset page: https://github.com/timo126/sub-rosa-issue/rules/25968240.

Supply-chain review

  • No package scripts or dependencies changed.
  • No .vscode/, workflow, or other editor/config file changes.
  • No lockfile changes.
  • No binary or font asset changes.
  • I confirm no automatic task (such as preinstall/postinstall) was added.
  • I confirm no hidden executable payload (encoded blob, obfuscated command, or unexpected binary) was added.
  • Any changed executable or binary was reviewed by a maintainer. (None changed.)

Testing

  • Applied the ruleset via the REST API and re-fetched it
    (GET /repos/timo126/sub-rosa-issue/rulesets/25968240) — the recorded JSON in
    docs/BRANCH_PROTECTION.md is that response verbatim.
  • Confirmed the rules resolve against main with
    GET /repos/timo126/sub-rosa-issue/rules/branches/main.
  • Confirmed the required-check contexts match the job names GitHub Actions
    actually reports (GET /repos/Sub-Rosa-Issue/sub-rosa-issue/commits/main/check-runs).
  • node scripts/check-links.js → OK 43 local link(s) checked, 0 allowlisted.
  • node scripts/check-line-lengths.mjs → pass (longest new line: 109 chars).
  • The note is documentation only; Docs link check is the only changed-path
    workflow that applies to this PR.

Closes #484

…ue#484)

The default branch had neither a ruleset nor a classic branch-protection
rule, so a mistaken direct push or force-push could bypass review and
reintroduce unsafe history. Record the applied `Protect main` ruleset —
pull-request-only merges, one approving review with stale approvals
dismissed, the always-on CI checks, blocked force pushes and deletions,
and admin-only bypass — so the setting is auditable and reproducible
upstream.

Closes Sub-Rosa-Issue#484

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security(high/easy): protect main with reviewed PRs and blocked force-pushes

1 participant