Keeps every repo in the OpenShock org in the same state. GitHub only offers org-wide rulesets and policies on paid plans, so this does the same job from the outside: one config file describes the desired state, and a script compares every repo against it.
Nothing is cloned.
Everything goes through the GitHub API using the gh CLI's login, and file changes arrive as GitHub-signed commits.
pip install pyyaml # the only dependency, besides gh
python -m orgsync plan # read-only: show all drift
python -m orgsync plan --diff --only API # with file diffs, one repo
python -m orgsync plan --check rulesets # one check
python -m orgsync apply # one PR per repo, plus settings and rulesets
python -m orgsync apply --direct # commit to default branches instead of opening PRs
python -m orgsync apply --check branches # delete orphaned branchesplan exits with 1 when there is drift, so it can run on a schedule.
apply prints the plan first and asks before doing anything.
It refuses to run unless gh is logged in as the bot_account from org.toml.
A full plan costs about 230 of the 5,000 hourly REST requests and about 30 of the 5,000 hourly GraphQL points, and prints what it used.
A run does not start with less than 1,000 left of either.
Requests that change something are spaced one second apart.
When GitHub answers with a rate limit error the request is retried: after the hourly window resets if the budget is exhausted, otherwise after a minute, doubling on each repeat.
All of them are configured in org.toml.
| Check | Keeps even | How it is applied |
|---|---|---|
files |
Managed files such as SECURITY.md and renovate.json, copied from templates/ |
PR |
community |
Rules about community files: required, forbidden, must contain, must not contain; issue template security link | Notes, PR for a missing issue template config.yml |
actions |
An allowlist of actions, and banned actions | Notes, PR for a rewritten banned action |
msbuild |
Selected properties in Directory.Build.props |
PR |
rulesets |
Repository rulesets, shared ones and per-repo ones | API |
settings |
Merge policy and security features | API |
branches |
Deletes orphaned feat/, fix/, chore/, ... branches |
API, only with --check branches |
All file changes for a repo arrive together: one commit on the chore/org-sync branch and one PR per repo, with the description grouped by check.
Running apply again while that PR is open adds a commit to it instead of opening a second one.
Lines starting with ! in the plan are notes: things the tool noticed but will not change on its own.
SECURITY.md is a short stub that links to the full policy in the .github repo.
A repo-level SECURITY.md replaces the org default entirely, so the stub never restates scope or rules that could drift.
.github, API, Frontend and Firmware are excluded because they carry hand-written policies; the community check verifies those still contain the shared reporting block.
Only repos that already have a renovate.json are kept in step, Renovate is never added to a repo.
renovate/ presets, read by Renovate straight from this repo
templates/renovate/
shared/renovate.json the file every repo gets, it only extends the presets
repos/<Repo>/renovate.json the whole file for a repo with needs of its own
Renovate opens a pull request for every update and merges minor, patch and digest updates itself once CI passes. Major updates always wait for a human.
Actions that are neither from an allowed owner nor individually allowed are reported, and so are banned ones.
softprops/action-gh-release is also rewritten into a gh release create step when the tool understands every input of the step, and left for a human otherwise.
The rewritten step is stricter than the action in two ways: it fails if the release already exists, and it fails if a file pattern matches nothing.
Action versions and SHA pins are not touched, see below.
rulesets/
shared/ used by many repos, assigned to repos in org.toml
repos/<Repo>/ for that one repo, picked up automatically
Rulesets are matched by the name field inside the file, the filename does not matter.
A ruleset in a repo's folder with the same name as a shared one replaces the shared one for that repo.
python -m orgsync import-rulesets copies rulesets that exist on GitHub but not here into rulesets/repos/, without changing anything on GitHub.
Rulesets on GitHub that have no file are deleted, or only reported when prune = false is set in org.toml.
A folder in rulesets/repos/ that names no active repo stops the run, so a renamed repo cannot lose its rulesets unnoticed.
| Shared ruleset | Repos | Effect |
|---|---|---|
main-branches |
all | No force-push and no deletion of master, main, develop, beta and the default branch |
squash-merges |
public | The default branch only takes squash merges, through a pull request |
pull-requests |
listed in org.toml |
Main branches need one code owner approval |
Merge methods can only be limited per target branch.
Only the default branch is limited to squash, so develop can still be merged into master with its history.
Private repos on the free plan cannot have rulesets and are reported.
A branch is a candidate only if its name starts with one of the configured prefixes and it is not protected.
| Situation | Result |
|---|---|
| Has an open PR | kept |
| Its PR was merged and nothing was pushed since | deleted |
| Has no commits that are missing from the default branch | deleted |
dependabot/* without an open PR |
deleted |
Unmerged work, older than stale_days |
reported, deleted only with delete_stale_unmerged = true |
| Unmerged work, recent | kept |
The plan prints the commit each branch pointed at, so a deleted branch can be recreated.
Dependency updates belong to Renovate, which is installed on the org.
That covers package versions, GitHub Actions versions and their SHA pins, and Docker image tags.
org-sync does not generate dependabot.yml, does not bump or pin actions, and leaves renovate.json alone.
Add a module in orgsync/checks/ with a check(repo, cfg) function returning a Result, and register it in orgsync/checks/__init__.py.
A Result holds file changes, API calls and notes; delivery is handled for you.
python -m unittest discover -s tests