YAML-config-driven GitHub repository manager
Project description
Repo Management
Declarative, YAML-driven management of GitHub repository configuration. Describe how a
repo should be configured in a YAML file; repo-management reads the live state, shows
you the diff, and reconciles it through the GitHub API (via PyGithub).
It is declarative and idempotent: re-running when nothing has changed does nothing. A section you don't mention is left unmanaged; a section you do declare is authoritative — it's the complete desired set, so anything on the repo not listed in it is removed.
Install
uv sync # create the venv + install everything
Authentication uses a GitHub token, read from $GITHUB_TOKEN (or --token). The token
needs the scopes for whatever you manage (repo administration, Actions secrets, etc.).
Usage
export GITHUB_TOKEN=ghp_...
repo-management validate -c examples/repos.yaml # check the YAML (no network)
repo-management plan -c examples/repos.yaml # show the diff (read-only)
repo-management apply -c examples/repos.yaml # reconcile (prompts before writing)
repo-management list-repos # the managed fleet across config/*.yml (no network)
Useful flags:
--repo owner/name— limit the run to a single repo from the config.--yes/-y— skip the confirmation prompt onapply.--token— pass a token explicitly instead of using$GITHUB_TOKEN.
plan prints one line per change: + create, ~ update, - delete. Secret values are
always redacted.
list-repos prints the managed fleet — the union of the repos: lists across every applied
config/*.yml (the *.yaml layer files are bases, not configs, and are skipped). --format names emits a single comma-separated line of owner-relative names, used to scope the central
Renovate runner's App token (see Fleet automation).
Config format
A config file lists the repositories to manage and one shared block of config sections applied to every one of them:
repos:
- owner/repo
- owner/other
settings: { ... }
rulesets: [ ... ]
labels: [ ... ]
Composing configs with extends
A file may extends: one or more base files (a string or a list, relative paths, resolved
recursively). The bases are merged underneath, then this file is merged on top:
- scalars — the override wins;
- list sections (
rulesets,labels.items,collaborators,webhooks,secrets) — merge by each item's natural key (ruleset/label/secretname, collaboratorusername, webhookurl): a same-key item in the override replaces the base's item, and new items are appended.
# repos.yaml
extends: base.yaml
repos: [owner/repo]
settings:
description: Managed by repo-management # added on top of base.settings
See examples/base.yaml + examples/repos.yaml
for a fully-annotated, working pair.
extends:reads and merges local files by relative path, so only point it at config you trust — treat a base file the same as the config that includes it.
What it manages
| Section | Manages |
|---|---|
settings |
description, homepage, topics, visibility, features (issues/wiki/projects/discussions), merge options (squash/merge/rebase, auto-merge, delete-branch-on-merge), default branch |
rulesets |
repository rulesets (branch/tag): the full rule set (pull_request, required_status_checks, required_linear_history, non_fast_forward, deletion, creation, update, required_deployments, merge_queue, required_signatures, the *_pattern rules, file_path/extension/size restrictions, workflows, code_scanning), plus bypass_actors and ref-name conditions |
labels |
create/update/delete labels to match the listed set exactly |
collaborators |
add/re-permission direct collaborators; remove those not listed |
webhooks |
create/update/delete webhooks, matched by URL |
secrets |
Actions secrets (write-only; libsodium-encrypted by PyGithub); delete those not listed |
variables |
Actions repository variables (plain text; updated only when the value differs); delete those not listed |
Design notes / limitations
- A declared section is authoritative. Each section you include is the complete desired set: items present on the repo but absent from the section are removed (labels, webhooks, secrets, variables deleted; direct collaborators removed; repo rulesets deleted). A section you omit is left entirely unmanaged. This means a declared section can revoke access or delete a secret by omission — declare deliberately.
- Only directly-granted collaborator access is managed. Pruning uses
affiliation="direct", so access inherited from org or team membership is never listed and never touched (the repo API can't revoke it anyway). - Rulesets are matched by name and updated to the full declared spec. PyGithub has no
ruleset support, so this is driven through its authenticated requester against the REST
rulesets API. On update, a ruleset's rules/conditions/bypass actors are PUT to exactly
what the config declares. A declared ruleset's lists (rules, bypass actors, ref-name
patterns) must match the live ones exactly — a manually-added rule is drift that triggers
an update — while server-supplied metadata the config never sets (item
integration_id, bypassactor_id, timestamps) is ignored, so it can't cause churn. Listing passesincludes_parents=false, so inherited org/enterprise rulesets are never matched or deleted through the repo. - Secrets and webhook secrets are write-only. The API never returns a secret value, so
a change to only a secret can't be diffed: an Actions secret is re-sent on every apply,
and a webhook with a configured secret is always re-sent (shown as
(set)in the plan). Values are sourced from the environment (value_from_env/secret_from_env) and never printed. A literalvalue:is supported for secrets but should never be committed. - Variables, by contrast, are readable. An Actions variable value is returned by the
API, so a variable is updated only when its value actually differs and the value is shown
in plain text in the plan. Variables take a literal
value:orvalue_from_env:(same shape as secrets, minus the secrecy).
Fleet automation
Beyond the CLI, this control-plane repo runs scheduled GitHub Actions that operate on the whole fleet, authenticating as the CI GitHub App (see each workflow's header comment for the details):
apply-config/plan-config— reconcile the live repos toconfig/*.yml:applyon push tomain,planas a read-only PR preview.renovate— a central, self-hosted Renovate runner that opens dependency-update PRs across the managed fleet on a schedule, replacing the hosted Renovate app (and unlockingpostUpgradeTasks, so a bumped binary's checksum is refreshed inside Renovate's own commit). It scopes its App token to exactly the fleet —repo-management list-repos --format namesderives that set fromconfig/*.yml, so the same config that drivesapplyis the single source of truth for what Renovate touches. Its global behaviour (autodiscover, onboarding off, thepostUpgradeTaskscommand allowlist) lives in.github/renovate-global.json, separate from this repo's own dependency policy in.github/renovate.json. Dispatch it manually withdryRunto preview the scope and proposed changes without opening any PRs.
Development
uv run pytest # tests + coverage (gate: 90%; currently 100%)
uvx prek run --all-files # the full quality gate (ruff, format, REUSE, typos, …)
The project carries the shared quality spine: prek hooks (git hygiene, gitleaks, typos,
rumdl, SPDX/REUSE headers, ruff) that run identically locally and in CI. Conventional
Commits (gitmoji) are enforced at commit-msg time. A few hooks shell out to system
tools prek can't bootstrap — install them locally too (most are in Homebrew):
hawkeye, taplo, osv-scanner.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file repo_management-1.2.2.tar.gz.
File metadata
- Download URL: repo_management-1.2.2.tar.gz
- Upload date:
- Size: 114.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3e5fb2b6137da9bd83dc7a99551a34a5a10fd6d850105d3e3249af7acb21eb77
|
|
| MD5 |
9bb2c2a210866666ec18dc7f2d93e304
|
|
| BLAKE2b-256 |
52b8d6b7d032b76c6c208b3cc08d10e196ce331bc4dcf494c3000d591ee36247
|
File details
Details for the file repo_management-1.2.2-py3-none-any.whl.
File metadata
- Download URL: repo_management-1.2.2-py3-none-any.whl
- Upload date:
- Size: 32.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d3c77cca6d26f17fff21b6cb0090ba9df5b62450532df294ec7c51846525e430
|
|
| MD5 |
eda86d650f980155e6fd2a1bfcb8f363
|
|
| BLAKE2b-256 |
f12e1c8ac3e22258a3956ea9d47647b5fb52cf49fc5f1563d7ae16d88baddbf3
|