Thanks for considering it. This project optimises for long-term maintainability over implementation speed, so expect review to focus on design and on tests.
mise install # tools, pinned by mise.lock
mise run # fmt, lint, lint:actions, build, testTool versions are resolved to exact builds in mise.lock, which is committed.
CI installs from the same two files, so a tool release cannot change CI
behaviour on its own.
mise installs a prek hook that runs formatting, clippy and commitlint before
each commit.
| Task | What it does |
|---|---|
mise run |
Format, lint, lint the workflows, build, test |
mise run test |
cargo nextest run (accepts nextest selectors) |
mise run test:live |
The live suite, against GH_SETTINGS_TEST_REPO |
mise run test:live:setup |
Create or repair a sandbox for the live suite |
mise run cover |
Coverage via cargo llvm-cov |
mise run snapshots |
Review pending insta snapshots |
mise run lint |
Clippy with -D warnings |
mise run lint:actions |
actionlint over the workflows |
mise run spell |
typos |
mise run schema |
Regenerate the committed JSON Schema |
mise run schema:check |
Fail if the committed schema is stale |
mise run docs:reference |
Regenerate every document derived from the code |
mise run docs:check |
Fail if any generated document is stale (what CI runs) |
mise run docs |
Serve the documentation locally |
mise run dogfood |
gh ship validate — check the release setup |
This is the path the architecture is designed for, and it should not require touching the engine.
- Create
src/resources/<name>/withmod.rsandtests.rs, plus amodel.rsif the types are big enough to want their own file —topicsandautolinksare small enough that they are not. - Implement the
Resourcetrait. The associatedDesiredandCurrenttypes are yours to shape. - Write
normalized()first. GitHub rewrites what you send it — lowercasing, defaulting, reordering, adding server-only fields. Anything you fail to normalise becomes a diff that never converges. See ADR-002 for the traps found so far. - Declare a
Requirement. This single declaration drives the documentation table,gh settings doctor, and the hint attached to a403(ADR-015). If you cannot confirm a fine-grained permission mapping from GitHub's own reference, mark itConfidence::Unverifiedrather than guessing. - Add a variant to
ResourceIdand one line toRegistry::default(). - Add the configuration section to
config::settings::Settings. - Run
mise run schemaand commit the regenerated schema.
- Is
diffpure? It must be synchronous and total. Anything needing the network belongs inprepare(). - Does an omitted field stay unmanaged? Absence must never be read as "reset to default". This is what makes partial configuration files safe.
- Is deletion opt-in? Pruning defaults to off (ADR-005).
- Is it idempotent? Add a test proving that applying then re-planning yields nothing.
- Are the tests about behaviour?
a_case_only_colour_difference_is_not_a_changetells a reader why the code exists;test_normalize_2does not.
Three layers, described in ADR-012:
- Unit tests, inline under
#[cfg(test)].normalizeanddiffare pure, so this is where most coverage should live — no runtime, no stub, no network. - Integration tests in
tests/, driving the real binary against a stubghonPATH. Assert onoutput.writes()— which requests were issued, in which order — not only on what was printed. - Snapshot tests with
insta, for diagnostics and plans. Review withmise run snapshots.
The stub answers unregistered reads with an empty result but fails unregistered writes, so an unexpected mutation cannot slip through unnoticed.
A fourth, optional layer runs the real binary against a real repository
(ADR-019). It is #[ignore]d and skips
unless GH_SETTINGS_TEST_REPO names one, so an ordinary mise run never
touches the network.
CI runs it on every push to main that touches the code, nightly, and on
demand — never on a pull request, because forks cannot read the sandbox
credential and one repository cannot serve several runs at once. So a change
that only the real API can falsify is caught shortly after merge, not before
it. Running it yourself before opening the PR is the way to find out sooner.
Bring your own sandbox. The repository CI uses belongs to CI. The tests mutate whatever they are pointed at, and the pre-flight refuses a repository that already holds managed configuration — so two people sharing one do not interleave, they make each other's run abort.
mise run test:live:setup you/gh-settings-sandbox
export GH_SETTINGS_TEST_REPO=you/gh-settings-sandbox
mise run test:liveThe sandbox must be public: on the free plan a private repository answers
403 Upgrade to GitHub Pro for the rulesets endpoints, which is most of what
the suite is for. To keep the variable across shells, put it in
mise.local.toml (git-ignored):
[env]
GH_SETTINGS_TEST_REPO = "you/gh-settings-sandbox"Each test cleans up after itself from a destructor, so a failing test leaves the sandbox as it found it. A run that is killed — cancelled, timed out, OOM — runs no destructor, and the next run will then refuse to start:
refusing to run the live suite: you/sandbox already has rulesets
That is the safety check working, not a bug. mise run test:live:setup --yes
resets the sandbox — it goes wider than the tests' own cleanup, and it reports
what it could not reset rather than stopping at the first refusal.
Pages is the one thing neither can remove. On a public repository GitHub ties
the site to the gh-pages branch and refuses to deactivate it while that branch
exists — and the sandbox needs the branch, so a Pages site there is expected.
One test, live_declared_permissions_match_what_github_accepts, asks GitHub
what permissions each endpoint really requires and checks our declarations
against the answer. GitHub only answers fine-grained tokens, so it prints a skip
and passes on anything else. A skip there is expected, not a broken suite.
Conventional Commits, enforced by commitlint. The changelog and the next version number are derived from them, so the type and scope matter:
feat(labels): support renaming via new_name
fix(topics): normalise underscores to hyphens
docs(adr): record why secrets are out of scope
Anything that will still be visible in five years — a new dependency, a change
to the resource abstraction, a change to the configuration format — wants an
ADR in docs/adr/. Records are immutable: supersede rather than rewrite.
Orchestrated by gh-ship, which is why
commit messages matter: git cliff derives both the changelog and the next
version number from them.
The lifecycle is:
- push to
main→gh ship prepareopens or updates the Release PR, carrying the version bump and the changelog; - review the changelog and merge it;
gh ship releasetags the merge commit, drafts the release, attaches the cross-compiled binaries, and only then makes it public.
Maintainers do not tag by hand. gh ship validate runs in CI, so a workflow
that stops satisfying the contract fails on a pull request rather than
mid-release. See ADR-014.
The release credentials are a GitHub App, installed on the repository and
holding its Client ID and private key in the release environment. Each job
mints a short-lived installation token and the version bump is re-created
through the API so GitHub signs it — which is why the release commit is
Verified, authored by the App's bot and committed by GitHub. See
ADR-022.