Skip to content

Repo settings as code: rulesets, settings JSON and setup-repo.py - #2

Merged
MattFisher merged 6 commits into
mainfrom
feat/repo-settings
Sep 11, 2026
Merged

MattFisher merged 6 commits into
mainfrom
feat/repo-settings

Conversation

@MattFisher

@MattFisher MattFisher commented Sep 11, 2026

Copy link
Copy Markdown

Ports the approach from Generality-Labs/cloudflare-worker-template#15 into this template and adapts it to the Python scaffold's CI check names.

Opt-in. A new use_repo_settings question, default no. Declining renders none of the files and no mention in the post-copy message, so copier update --defaults (the weekly template-update workflow) cannot land them in a downstream repo that didn't ask. An existing project opts in by setting use_repo_settings: true in .copier-answers.yml and running copier update. Nothing touches GitHub until someone runs the script.

Scaffold additions (when opted in)

  • .github/repo-settings.json: keys for PATCH /repos/{owner}/{repo} (merge methods, delete_branch_on_merge: true, wiki/projects off).
  • .github/rulesets/main.json (templated): protect the default branch. Changes via PR with 0 required approvals, no force-push or deletion, ci / Lint, type-check, and test required, plus frontend / Type-check and build when use_frontend. Repository Admins bypass.
  • scripts/setup_repo.py: standard-library Python applier using the developer's gh auth. It fetches the repo's current settings and each existing ruleset first and prints what would change: settings keys with current and desired values, and a unified diff per ruleset projected onto the keys the file sets (so ids, timestamps and GitHub's filled-in defaults never appear). It applies only after confirmation or --yes; --dry-run only shows; a non-terminal run without --yes refuses. Targets the origin remote or an explicit OWNER/REPO, never gh's default-repo guess (which picked the wrong repo on a two-remote checkout during testing). Only changed settings keys are sent; rulesets are created or updated in place by name.
  • tests/test_setup_repo.py in this repo exercises the script against a stubbed gh: no-op when everything matches, first-run create, drift diff and in-place update, dry-run, prompt yes/no, non-interactive refusal, plan-gate handling, missing settings file, explicit repo. Template CI runs it, plus render assertions for default, opt-in and frontend variants.

This repo carries its own copies and applies them with the scaffolded script (python3 'template/{% if use_repo_settings %}scripts{% endif %}/setup_repo.py' Generality-Labs/python-project-template).

Verified against the live API (the org is on Team, so no plan gate): protect-main exists on this repo (id 22861394) and on Generality-Labs/inspect-evals-lint (id 22861417); a dry run against either reports "Nothing to apply", and a dry run with deliberately drifted local files shows exactly the drifted keys and ruleset lines.

🤖 Generated with Claude Code

Posted by Claude Code on Matt's behalf.

MattFisher and others added 6 commits September 11, 2026 12:43
Scaffolds .github/repo-settings.json, .github/rulesets/main.json and
scripts/setup-repo.sh so a new repo's default branch gets protected from
files in the repo rather than by hand in the GitHub UI. Ported from the
worker template's open PR and adapted to this template's CI check names;
the template repo carries and applies its own copies.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gh's default-repo guess picked a fork on a checkout with two remotes and
reconfigured it silently. Resolve the 'origin' URL instead, and accept an
OWNER/REPO argument for cases like the template repo applying its own copies.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Off by default so copier update --defaults never lands the files, or the
post-copy invitation to run them, in a downstream repo that didn't ask.
Directory names carry the condition so declining leaves no empty dirs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The bash version had grown embedded jq programs and parallel arrays, and a
jq input() misuse made every ruleset look new. The stdlib Python script
fetches the repo's current settings and each ruleset, prints what would
change (settings keys, and a unified diff of each ruleset projected onto
the keys the file sets), and applies only after confirmation or --yes;
--dry-run only shows. Unit-tested against a stubbed gh in template CI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The smoke-test job runs pre-commit over every tracked file with ruff's
defaults (line length 88), while the scaffold formats at 100. Format so
both are satisfied.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The smoke-test job runs pre-commit over every tracked file, and without a
root config ruff formatted the template's own Python at its default line
length, disagreeing with the scaffold's 100. Mirror the scaffold settings
so the same file formats identically in both places.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@MattFisher MattFisher changed the title Repo settings as code: rulesets, settings JSON and setup-repo.sh Repo settings as code: rulesets, settings JSON and setup-repo.py Sep 11, 2026
@MattFisher
MattFisher merged commit 012d7b4 into main Sep 11, 2026
3 checks passed
@MattFisher
MattFisher deleted the feat/repo-settings branch September 11, 2026 03:54
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