Skip to content

Add CI workflow and main-branch ruleset#5

Merged
lionuncle merged 1 commit into
mainfrom
chore/branch-protection
Apr 9, 2026
Merged

Add CI workflow and main-branch ruleset#5
lionuncle merged 1 commit into
mainfrom
chore/branch-protection

Conversation

@lionuncle
Copy link
Copy Markdown
Contributor

Sets up everything needed to harden main for the eventual public flip. Two pieces:

  1. .github/workflows/ci.yml — runs on every PR to main. Becomes active immediately on merge.
  2. scripts/setup-branch-protection.sh — idempotent script that creates/updates a repository ruleset. Cannot run yet — see "Why not now" below — but the file is staged so it's a one-line invocation the moment you flip the repo to public.

What the CI workflow checks (8 jobs)

  • python (3.9) / (3.10) / (3.11) / (3.12) — runs python src/rpr/cli.py --help, then does an editable install and exercises import rpr, rpr --help, and python -m rpr --help. Specifically catches the PEP 604 regression we fixed in Audit fixes: Python 3.9 compat, version sync, doc cleanup #4 and any future Python-version drift.
  • version-sync — verifies pyproject.toml, npm/package.json, and src/rpr/__init__.py all report the same version. Same logic as the release workflow's preflight check, but runs on PRs so drift is caught before merge instead of at release time.
  • json-lintjson.load() on examples/config.json and npm/package.json.
  • shellcheck — lints install.sh, scripts/bump.sh, and scripts/setup-branch-protection.sh.
  • npm-pack — runs npm pack --dry-run from npm/, exercising the prepack hook so a broken copy-python.js or package.json is caught before release time.

concurrency: ci-${{ github.ref }} cancels in-progress runs when you push new commits to a branch — saves runner minutes.

What the ruleset enforces (when applied)

Defined in scripts/setup-branch-protection.sh and applied via gh api -X POST /repos/dedev-llc/rpr/rulesets:

  • No deletion of main
  • No force-push (non_fast_forward)
  • Linear history — squash and rebase merges only, no merge commits inside PRs
  • All changes through PR — direct pushes to main are blocked
  • Conversation resolution required before merge
  • Stale review approvals dismissed when new commits are pushed
  • All 8 CI checks must pass before merge (the names in the script match the job names in ci.yml exactly — if you rename a job, update both)
  • Org admins may bypass (OrganizationAdmin actor, bypass_mode: always) — escape hatch for emergency hotfixes. Set bypass_mode: pull_request later if you want admins to still go through PRs but skip the checks.

required_approving_review_count: 0 because you're a solo maintainer and can't approve your own PRs. The PR-required-ness is what enforces the workflow; the 0 just lets you self-merge.

Why we can't apply the ruleset right now

I tried both legacy branch protection and rulesets via the API:

$ gh api repos/dedev-llc/rpr/branches/main/protection
403 Upgrade to GitHub Pro or make this repository public to enable this feature.

$ gh api repos/dedev-llc/rpr/rulesets
403 Upgrade to GitHub Pro or make this repository public to enable this feature.

GitHub gates both systems behind a paid plan (Pro / Team / Enterprise) for private repos. Public repos get them free. Since you've said the repo is going public eventually, the cleanest path is: stage everything now, run the script the moment you flip to public.

Going-public checklist

When you're ready to flip the switch:

# 1. Make the repo public
gh repo edit dedev-llc/rpr --visibility public --accept-visibility-change-consequences

# 2. Apply the ruleset
scripts/setup-branch-protection.sh

# 3. (Optional) Verify
gh api repos/dedev-llc/rpr/rulesets --jq '.[] | {name, enforcement, target}'

The script is idempotent — if a ruleset named main-protection already exists it'll update it in place rather than create a duplicate. So if you later add or remove CI jobs, just update the REQUIRED_CHECKS array at the top of the script and re-run it.

Test plan

  • bash -n scripts/setup-branch-protection.sh — syntax valid
  • chmod +x on the new script committed via git index (mode 100755 confirmed)
  • Structural sanity check on ci.yml — all 8 jobs present, top-level keys correct
  • JSON payload generated by the setup script is valid (test-ran the python heredoc with a stub CONTEXTS)
  • After merge: open a no-op PR and verify all 8 CI checks run and pass
  • Once the repo goes public: run scripts/setup-branch-protection.sh and verify the ruleset shows up at https://github.com/dedev-llc/rpr/rules

Things to know about the protection model

  • The release workflow keeps working. It pushes git tags (refs/tags/v0.1.x), not branches. Tags aren't covered by branch rulesets, so tag-and-release is unaffected.
  • You can self-merge your own PRs because required_approving_review_count is 0. The PR requirement is purely about routing changes through CI, not about getting a second human reviewer (which doesn't exist yet).
  • External contributors will get the full check set the moment you go public — fork → PR → 8 checks run → you review and merge. No manual setup needed for them.
  • enforce_admins-equivalent: rulesets use bypass_actors instead. Currently set to allow any org admin to bypass any rule. If you want stricter enforcement (e.g. force yourself through PRs too), set the bypass_actors array to [] in the script and re-run.

Notes on the python matrix

CI runs Python 3.9 through 3.12 to match the classifiers in pyproject.toml. Python 3.13 isn't included because the project doesn't claim to support it yet — if you bump the floor or add 3.13 support, update both pyproject.toml classifiers AND the matrix in ci.yml.

🤖 Generated with Claude Code

Sets up everything needed to protect main once the repo flips to public.

CI workflow (.github/workflows/ci.yml) runs on every PR:
- python (3.9-3.12) — verifies cli.py imports + editable install works
  on every supported Python version. Catches the 3.9 PEP 604 regression
  the audit pass fixed and any future drift.
- version-sync — verifies pyproject.toml, npm/package.json, and
  src/rpr/__init__.py agree.
- json-lint — validates examples/config.json and npm/package.json.
- shellcheck — lints install.sh, scripts/bump.sh, and the new
  setup-branch-protection.sh.
- npm-pack — exercises the npm prepack hook to make sure the package
  still builds.

Branch protection (scripts/setup-branch-protection.sh) is an idempotent
script that creates or updates a repository ruleset enforcing:
- No deletion or force-push to main
- Linear history (squash/rebase merges only)
- All changes through pull request, conversations resolved
- All eight CI checks must pass before merge
- Org admins (dedev-llc maintainers) may bypass in an emergency

GitHub gates rulesets behind a paid plan for private repos on the free
tier — verified by testing both 'gh api branches/main/protection' and
'gh api rulesets', both 403 with 'Upgrade to GitHub Pro'. So the script
can't run today; it's staged for the maintainer to invoke once the repo
goes public. CONTRIBUTING.md has the one-line instructions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lionuncle lionuncle merged commit 1a16e35 into main Apr 9, 2026
7 of 8 checks passed
@lionuncle lionuncle deleted the chore/branch-protection branch April 15, 2026 12:45
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.

1 participant