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.
Full documentation lives at nivintw.github.io/repo-management; the source code, issue tracker, and annotated example configs live in the nivintw/repo-management repository on GitHub.
Install
uv tool install repo-management # or: pip install repo-management
Requires Python 3.14+. 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
Write a config file first — the repos.yaml below; see Config format
for the schema and the repository's annotated examples/ for a working
pair. Then:
export GITHUB_TOKEN=ghp_...
repo-management validate -c repos.yaml # check the YAML (no network)
repo-management plan -c repos.yaml # show the diff (read-only)
repo-management apply -c repos.yaml # reconcile (prompts before writing)
repo-management list-repos # the managed fleet across a config dir (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 *.yml file in a config directory (--config-dir, default config/; *.yaml
files are treated as base layers, not applied configs, and are skipped). --format names
emits a single comma-separated line of owner-relative names, which the
repo-management repository uses to scope its 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,collaborators,webhooks,secrets,variables,deploy_keys,autolinks,environments) — merge by each item's natural key (ruleset/label/secret/variable/environmentname, collaboratorusername, webhookurl, deploy keykey, autolinkkey_prefix): 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 in the
repository 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, squash/merge commit title & message, web commit signoff), default branch, template/archived flags |
actions |
Actions enablement and allowed-actions policy (enabled, allowed_actions, selected_actions patterns), default workflow permissions (default_workflow_permissions), and workflow approval permission (can_approve_pull_request_reviews) |
security |
secret scanning + push protection, Dependabot vulnerability alerts, automated security fixes, and private vulnerability reporting |
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 |
deploy_keys |
create/delete deploy keys, matched by key content; delete those not listed |
autolinks |
create/delete autolink references, matched by key prefix; delete those not listed |
pages |
GitHub Pages build type, source branch/path, custom domain, and HTTPS enforcement; enabled: false disables it |
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 |
environments |
deployment environments — wait timer, required reviewers, self-review prevention, branch policy — plus environment-scoped secrets/variables |
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 already present on the repo
is left untouched by default (pass
--force-secretsto re-push values for rotation), 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). - Deploy keys and autolinks have no update endpoint. GitHub's APIs for both only
support create/list/delete, so changing an existing entry (e.g. a deploy key's
titleor an autolink'surl_template) is planned as a delete of the old entry paired with a create of the new one, not a single in-place update. - Environments reuse the secrets/variables logic per-environment. An environment's
secrets/variablesfollow the same authoritative-set semantics as the top-level sections, scoped to that environment. ATeamreviewer resolves itsslugthrough the repo's owning org (a config error on a non-org-owned repo); aUserreviewer resolves itslogindirectly. Becausecreate_environmentis a single PUT covering wait timer, reviewers, self-review prevention, and branch policy together, an unset field on an existing environment is preserved at its live value rather than reset — the API has no partial-update form. - Pages has no explicit "disable" by default omission. Leaving
pages:out entirely leaves Pages unmanaged; declaring it withenabled: falsedisables it if currently on. Creating a new Pages site withcname/https_enforcedset takes two API calls (GitHub's create endpoint only acceptsbuild_type/source), planned as one change.
Fleet automation
Beyond the CLI, the nivintw/repo-management repository — the tool's home — is itself a working deployment: a control plane that manages its author's repositories (its "fleet") with scheduled GitHub Actions, authenticating as a CI GitHub App. It doubles as a reference for running the tool this way:
apply-config/plan-config— reconcile the live repos to the repository'sconfig/*.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 from the sameconfig/*.ymlthat drivesapply, so one config is the single source of truth for what Renovate touches. Its global behaviour lives in the repository's.github/renovate-global.json, separate from the repository's own dependency policy in.github/renovate.json. Dispatch it manually withdryRunto preview the scope and proposed changes without opening any PRs.
Development
Clone the repository, then:
uv sync # create the venv + install everything
uv run pytest # tests + coverage (gate: 90%; currently 100%)
uvx prek run --all-files # the full quality gate (ruff, format, REUSE, typos, …)
Quality checks run identically locally and in CI via prek hooks: git hygiene, gitleaks,
typos, rumdl, SPDX/REUSE headers, and ruff. 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.5.0.tar.gz.
File metadata
- Download URL: repo_management-1.5.0.tar.gz
- Upload date:
- Size: 157.6 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 |
d3a7a6f7e12a471e828671468fe10c5c0fb18e46dda33af7f30b3318a6b381b5
|
|
| MD5 |
e12b159ec7726f31048cbe34c5438aed
|
|
| BLAKE2b-256 |
e10b0ee02d55de07971f2da5384fd411d000f23e9187da3e2d977a255196db3c
|
File details
Details for the file repo_management-1.5.0-py3-none-any.whl.
File metadata
- Download URL: repo_management-1.5.0-py3-none-any.whl
- Upload date:
- Size: 50.6 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 |
7047f1a7ff4611dbf3055f556e48777c3298b48d5504b4e96d0b4490d40f2038
|
|
| MD5 |
4fab48f3e38cb7110292c22073620838
|
|
| BLAKE2b-256 |
dae7ac514662038b0661c2e4a86cded69752d344bb7a24781e3eec0067b5b876
|