Roadmap¶
Where gh-settings is, where it is going, and what it will deliberately never
do. Decisions already settled live in the architecture decision
records; this page tracks what is left.
The guiding ambition: if it is under the repository Settings page, it should eventually be manageable here.
Supported settings¶
| Resource | Status | Notes |
|---|---|---|
| Repository metadata | ✅ | description, homepage, features, merge strategies, commit title/message, default branch |
| Repository security | ✅ | secret scanning, push protection, Dependabot updates, advanced security |
| Topics | ✅ | |
| Labels | ✅ | including renames, which preserve issue assignments |
| Autolinks | ✅ | changes are delete-and-recreate; GitHub has no update endpoint |
| Rulesets | ✅ | unknown rule types round-trip untouched rather than being dropped |
Inheritance (extends:) |
✅ | single level, ref pinned, prune never inherited |
| Environments | ✅ | protection rules, reviewers and deployment branch policies |
| Actions general settings | ✅ | allowed actions, workflow permissions, artifact and log retention, fork PR approval |
| Actions variables | ✅ | repository and environment scope; values are readable, so they diff |
| Pages | ✅ | build type, source, custom domain and HTTPS; declaring the section enables the site, nothing ever disables it. The public flag is reported by the API but not settable through it, so it is not offered |
| Custom properties | planned | |
| Webhooks | planned | |
| Collaborators & teams | planned | |
| Branch protection (legacy) | planned | rulesets are the modern equivalent and already supported |
| Repository interactions | planned | |
| Secrets | ✖ never | ADR-009 — values are write-only, so they cannot be diffed, exported, or made idempotent |
Every section of the configuration file is optional, and an absent section is unmanaged. Adoption is incremental: you can manage labels alone and nothing else will move.
Next¶
Roughly in priority order. Each entry says why, so it can be dropped when the reason stops applying.
Verification¶
- A live test suite against a real throwaway repository,
#[ignore]d and gated onGH_SETTINGS_TEST_REPO. Covers each resource's create → update → prune cycle, re-planning after every mutation, plus one that asks GitHub which permissions each endpoint really requires. Runs on every push tomainthat touches the code, nightly, and on demand; refuses to start against a repository that already has managed configuration. Run it yourself withGH_SETTINGS_TEST_REPO=you/sandbox mise run test:live— the sandbox must be public, since a private repository on the free plan answers403 Upgrade to GitHub Profor rulesets. - Ruleset apply-path tests through the stub. Create, update, delete and prune, asserting the request log — including that an update addresses the server id rather than the name, which nothing else checked, and that server-only fields never travel back.
- Repository security PATCH test — covered by the live suite.
- Generic idempotency contract — the live suite re-plans after every mutation and asserts the plan is empty, for every resource. Checked against reality rather than against our own fixtures, which is the only version of this assertion that would have caught the ruleset permanent diff.
- Snapshots for
plan,doctorandexport. Includingplan --verbose, which is the reason there is no separatediffcommand, anddoctor's "unknown" verdict. The sharedassert_cli_snapshot!applies the stabilising filters, because forgetting them fails only on someone else's machine. -
--continue-on-errorintegration test. Exercised through the binary: every change attempted, partial success preserved, and every failure reported in the JSON output rather than just the first.
Behaviour¶
Two span bugs were found while preparing this and fixed first: ruleset findings
underlined whichever rule sorted into the position rather than the one the user
wrote, and a configuration using only repository.topics panicked a debug build.
Requirement::verdict in src/resources/requirement.rs is the single place a
credential is judged. doctor renders it and sync refuses on it, so the two
cannot disagree about what a token can do.
-
syncpre-flight permission check. Refuses before the first request when a change is certain to be rejected, naming the permission. Stays conservative: onlyCapability::Impossibleblocks, and an unintrospectable token proceeds so the real error can speak. There is no flag to overrule a refusal, which is why the bar for making one is proof. - Attach the requirement table to a
403. Per failing resource, so a failed label write no longer points atAdministration: write. The GITHUB_TOKEN note appears only inside Actions, where it is the answer rather than a false lead. - Help with ruleset rule parameters. GitHub requires all parameters of
a rule, not the subset you want to change: a
pull_requestrule missing one field is rejected withInvalid property /rules/1: data matches no possible input, which names neither the rule nor the field. Validation could name the rule from the index, and list the parameters a known rule type expects.
Features¶
-
A composite action —
uses: noirbizarre/gh-settings@mainor an exact release tag until 1.0,@v1after. Maps exit code 2 to achangedoutput rather than a failed job, writes the plan to the job summary, and annotates a 403 with the token explanation. See GitHub Actions. -
Inheritance (
extends:) — share labels, autolinks and rulesets across repositories. See ADR-017.Collections replace the whole item by identity rather than merging field by field:
Label::colorandRuleset::target/enforcementare notOption, so a parsed document cannot distinguish an omitted field from one written to its default, and a field-wise merge would repaint an inherited label grey the moment a child named it.prunenever inherits. The ref is required. A base may not itself extend. Reading a base needsContents: readon the other repository, which the ActionsGITHUB_TOKENdoes not have —doctorsays so.Still open: making those three fields
Optionwould allow field-wise merging, and is the way to revisit the trade-off above.
Documentation¶
- Installation page with per-platform notes and upgrading.
- Quick start: export → validate → plan → sync on an existing repository.
- Migration guide from
safe-settings— what is read, what has to be removed before the file will parse (org-levelsuborgs/overrides, and the sections listed in ADR-006:branches,collaborators,teams,milestones), and above all that pruning is off by default, so the first run is non-destructive. The README and the index both say so; what is missing is the step-by-step. - FAQ: why not safe-settings, why no secrets, why a
403, why a PAT in CI.
Infrastructure¶
- Harden
${{ inputs.tag }}interpolation inpublish-release.yamlby passing throughenv:. Only reachable by someone who already has write access, so defence in depth rather than a hole.
Not planned, and why¶
These are settled. Reopening one means superseding its record, not re-arguing it.
| Decision | |
|---|---|
| Secrets | ADR-009 — write-only values cannot be diffed, exported or made idempotent. Use gh secret set. |
Org-level suborgs / overrides |
ADR-006 — safe-settings compatibility is one-way and per-repository. Sharing configuration between repositories now exists as extends:, on its own merits rather than as compatibility; suborgs and overrides themselves remain out of scope. |
A diff command |
plan --verbose already shows field-level before/after; a second command for the same output is surface without substance. |
| A typed enum of ruleset rules | GitHub ships new rule types faster than any client tracks them. The untyped passthrough is deliberate: an unrecognised rule round-trips untouched rather than being silently dropped on the next sync. |
| GraphQL | ADR-004 — autolinks, topics, rulesets, environments and custom properties have no GraphQL mutations at all. |
| Publishing to crates.io | ADR-014 — gh-settings is distributed as a GitHub CLI extension; a second install path is a second thing to keep working. |
Manual verification checklist¶
Automated tests assert the shape of the requests we send. They cannot assert that GitHub accepts them, so these paths need a human and a throwaway repository. Several are destructive.
- Add a topic
-
export -
plan -
sync -
export→planreports zero changes - Sync a ruleset with rules and
conditions.ref_name— found a422; GitHub requires every parameter of apull_requestrule, not a subset - Update that ruleset (change a rule parameter) and re-sync — exercises
PUT /rulesets/{id}and canonical rule ordering - A ruleset with
bypass_actors: [{ team: … }]on an organisation repository — slug-to-id resolution; impossible to test on a personal repo -
repository.security.secret_scanning: true - Change an autolink's
url_templateand re-sync — the recreate path -
sync --pruneafter removing a label — the destructive path and its prompt - Run any
synctwice; the second must report "up to date" — this is what caught the ruleset permanent diff -
doctorinside GitHub Actions withsecrets.GITHUB_TOKEN— should report labels and Pages as manageable, everything else refused, and say why -
gh extension install noirbizarre/gh-settingson a clean machine
A 422 means a payload-shape bug, and GitHub will not say which field.