Skip to content

Repository files navigation

cnpg-infra

Local-first admin tooling for managing settings, teams, and CODEOWNERS across every repository in the cloudnative-pg GitHub organization.

Philosophy: laptop, not CI

Every script here assumes a human, locally-authenticated gh session with real org-admin scope (admin:org, repo, read:org). None of this runs from GitHub Actions or any other CI pipeline, on purpose: keeping org-admin credentials on an admin's laptop instead of in a pipeline secret means a compromised workflow or a malicious PR never has a path to org-wide admin access. The tradeoff is that these changes don't happen automatically — an admin has to actually run them — which is the point, not a limitation.

Getting started

  1. Create an empty directory and clone this repo as a sibling of every other cloudnative-pg repo you'll manage (or into a fresh empty directory — the next step populates it):

    mkdir cnpg.io && cd cnpg.io
    git clone https://github.com/cloudnative-pg/cnpg-infra.git
  2. Authenticate gh with sufficient scope:

    gh auth login
    gh auth refresh -h github.com -s admin:org
  3. From inside cnpg-infra/, run the bootstrap script. It checks you're an active member of the admins team, then clones every repo listed in generated/managed-repos.yaml that isn't already a sibling directory (existing clones are left completely untouched):

    cd cnpg-infra
    ./scripts/bootstrap.sh

The admin-team check is a fail-fast sanity check, not the real security boundary — GitHub's own API authorization is what actually stops a non-admin from doing anything privileged. It just avoids a confusing half-completed run before that happens.

What's here

Generated files, in generated/ — do not hand-edit, re-run the script that owns them:

File Rebuilt by Contents
generated/managed-repos.yaml scripts/update-managed-repos.sh Every non-archived cloudnative-pg/* repo cloned as a sibling of cnpg-infra, with its origin remote verified
generated/teams.yaml scripts/update-teams.sh Live GitHub team roster (org owners, org members, each team's members) — deliberately does NOT track per-repo access; see the policy files below for that

Hand-maintained policy files — the desired state the scripts below check/enforce against:

File Scope
repo-policy.yaml Per-repo exceptions to the settings baseline (e.g. a repo whose content is machine-published and so doesn't need a PR-review gate; a standalone review-count override for a repo not yet in repo-tiers.yaml; extra_team_permissions, a per-repo team grant on top of org-policy.yaml's org-wide floors, for a team a repo's CODEOWNERS names but that holds no access there)
repo-tiers.yaml Every managed repo's importance class (A = critical, B = important, C = low-stakes), governance subproject, and (optionally) a desired GitHub description. Class drives the settings floor: A gets 2 required reviews + code-owner review forced on, B gets 1 + code-owner review forced on, C gets the org default (1, untouched code-owner-review setting). A repo's description field, if set, is pushed to GitHub whenever it drifts — the only field in any policy file here that changes something a repo already has rather than just flooring it
componentowners-policy.yaml Per-repo CODEOWNERS content — both the catch-all * rule and any path-scoped rules, each as teams + users
org-policy.yaml Rules that apply identically to every repo (e.g. which teams get admin everywhere), so they don't need repeating per-repo
milestones-policy.yaml Desired "horizon" milestones (Now / Next / Later, title + description copied verbatim from cloudnative-pg/cloudnative-pg's own generic triage milestones), each one's due_offset_months (used to compute a rolling due date, not a fixed one), and the explicit, hand-picked list of repos it should exist in
labels-policy.yaml GitHub issue labels the org's issue templates depend on, org-wide or per-repo. A form silently drops a label the repository doesn't have, so every label a template references has to exist wherever that template can be used
gitvote-policy.yaml Per-repo exceptions to the gitvote configuration baseline (see below). Only the five org-control repos need an entry; every other managed repo gets the default template with no bespoke entry at all

Scripts (all in scripts/, YAML config stays in the repo root):

Script Effect
scripts/bootstrap.sh Checks admin-team membership, clones any managed repo missing as a sibling
scripts/create-new-repo.sh --name <repo> --class <A|B|C> --subproject <...> --description "<text>" --owners user1,user2,... [--apply] Onboards a brand-new repo end-to-end — see "Adding a new repo" below
scripts/update-managed-repos.sh Rebuilds generated/managed-repos.yaml from live GitHub state
scripts/update-teams.sh Rebuilds generated/teams.yaml from live GitHub state
scripts/check-repo-settings.sh [repo] Read-only audit of repo settings (branch protection, teams, collaborators, security features) against the policy files and the GitHub-API-checkable subset of the OSPS Baseline checklistrepo-settings-report.md
scripts/fix-repo-settings.sh <repo> [--apply] Remediates one repo against the policy files. Defaults to dry-run — always review the diff before re-running with --apply. Only ever raises settings, never lowers an existing stricter one
scripts/fix-all-repos.sh [--apply] Runs fix-repo-settings.sh across every repo in managed-repos.yaml, one at a time, with a summary at the end. Same dry-run-by-default safety model — nothing here is new logic, just a loop
scripts/render-component-owners.rb <repo> Renders that repo's COMPONENT_OWNERS.md from repo-tiers.yaml's owners: to stdout. Pure renderer, like render-codeowners.rb: touches nothing. The public, per-repo answer to "who owns this component" that a GitHub team can't be, since team membership is only visible to other org members
scripts/sync-ownership-files.sh <repo> [--apply] Writes everything cnpg-infra generates for a repo into its local sibling clone: CODEOWNERS, COMPONENT_OWNERS.md, CONTRIBUTORS.md where it has contributors, .gitvote.yml, and a copy of each org-wide issue template from cloudnative-pg/.github if the repo has an ISSUE_TEMPLATE folder of its own (GitHub stops serving org defaults to those repos, silently). One command stages a repository completely, so landing it is one PR and one diff to review rather than three passes. Defaults to dry-run, showing a unified diff per file. Local only: never commits, pushes, or calls GitHub, so the target repo's own PR review still gates the change
scripts/sync-reviewer-grants.rb [repo] [--apply] Reconciles the Write access that Reviewers need, against the individuals named in componentowners-policy.yaml's path rules. Direct grants rather than a team, since the Reviewer rung implies no org membership and teams can only hold members. Reports, but never revokes, a direct write grant no rule explains. Defaults to dry-run
scripts/sync-labels.rb [repo] [--apply] Reconciles GitHub issue labels against labels-policy.yaml: creates a missing label, updates a drifted colour or description. Never deletes, and never touches a label the policy doesn't name. Defaults to dry-run
scripts/validate-policy.rb Static validation of the YAML policy files: every file parses, every managed repo has a repo-tiers.yaml/componentowners-policy.yaml entry, class/subproject values are from the documented enum, every referenced team slug exists in generated/teams.yaml's last snapshot, and every repo referenced in milestones-policy.yaml is managed. No GitHub API calls — safe to run in CI, and also runs there (.github/workflows/lint.yml, alongside ShellCheck on every script)
scripts/sync-milestones.rb [repo] [--apply] Reconciles GitHub milestones against milestones-policy.yaml: creates a missing milestone (matched by exact title), updates its description and/or due date if either has drifted. Due date is a rolling window computed from due_offset_months and the date the script runs, not a fixed date — it advances on its own each time this next runs in a new calendar month. Never touches open/closed state or issue assignments. Defaults to dry-run — always review the plan before re-running with --apply
scripts/check-gitvote-config.sh [repo] Read-only audit of each repo's .gitvote.yml against gitvote-policy.yaml's baseline, plus a one-time check of the gitvote GitHub App's org-wide installation scope. Writes gitvote-config-report.md
scripts/fix-gitvote-config.sh <repo> [--apply] Lands the .gitvote.yml that repo's category calls for. Not a settings PATCH like fix-repo-settings.sh -- .gitvote.yml lives in the repo's own git history, so this opens a PR (branch, commit, gh pr create), except for cnpg-infra itself, which gets a direct commit to its default branch per its own documented bypass exception. Defaults to dry-run -- always review the diff (and, for a non-cnpg-infra repo, that a PR is about to be opened) before re-running with --apply

Adding a new repo

./scripts/create-new-repo.sh does this end-to-end for a genuinely new repo — creates it on GitHub from cnpg-template, clones it as a sibling, registers it in repo-tiers.yaml and componentowners-policy.yaml, regenerates generated/managed-repos.yaml, creates and populates its <repo>-owners team, renders and pushes its real CODEOWNERS, brings it up to the settings baseline (fix-repo-settings.sh), runs validate-policy.rb, and opens a PR on cloudnative-pg/.project adding it to project.yaml's repositories: list. Dry-run by default, like every other script here:

./scripts/create-new-repo.sh \
  --name <repo> \
  --class <A|B|C> \
  --subproject <core|supply-chain|community-ecosystem|extensibility|org-control|unclassified> \
  --description "<one-line GitHub description>" \
  --owners user1,user2,...
# review the plan, then:
./scripts/create-new-repo.sh ... --apply

New entries are appended to the end of each policy file's repositories: list, not re-sorted into their class/alphabetical grouping — that's a human tidy-up afterward, if you want one.

If the repo already exists on GitHub (an existing repo just joining cnpg-infra's management rather than a genuinely new one), use the older manual path instead:

  1. Clone it as a sibling of cnpg-infra: git clone https://github.com/cloudnative-pg/<name>.git
  2. Run ./scripts/update-managed-repos.sh — it'll now find the new sibling clone and add it to generated/managed-repos.yaml.
  3. Add policy entries for it in repo-tiers.yaml and componentowners-policy.yaml by hand.
  4. Run ./scripts/fix-repo-settings.sh <name> (dry-run first, review the diff, then --apply) to bring it up to the settings baseline.

GitVote configuration

gitvote is already installed org-wide with repository selection "all", so every managed repo, current and future, already has the app -- there's no per-repo install step. What each repo needs is its own .gitvote.yml, scoped to that repo's actual eligible voters rather than a stale or overly broad team.

A repo with its own <repo>-owners team (see repo-tiers.yaml's owners field) gets two profiles, both scoped to that team, mirroring CONTRIBUTOR_LADDER.md's two repository-level thresholds: default (simple majority, Contributor promotion/removal) and component-owner (two-thirds majority, Component Owner promotion/removal). Those render as pass_threshold: 50.01 and 66.66 rather than the round numbers, because gitvote passes a vote on in_favor_percentage >= pass_threshold: at a flat 50 an even-sized electorate splitting down the middle would pass its own tie, and a flat 66 sits just under a true two-thirds. The comment block above render_gitvote_config() has the full reasoning. The five org-control repos (governance, .project, .github, cnpg-infra, cnpg-template) get the same shape scoped to steering-committee instead, since their votes are Steering Committee business, not a subproject's.

gitvote-policy.yaml is where a repo's category (org-control today) or an outright exclusion is recorded; most repos need no entry there at all and just get the default per-repo-owners template. scripts/render-gitvote-config.sh <repo> prints what a given repo's .gitvote.yml should be; scripts/check-gitvote-config.sh audits every managed repo's live file against that render, and scripts/fix-gitvote-config.sh <repo> [--apply] lands it (a PR for most repos, a direct commit for cnpg-infra's own bypass exception). Same dry-run-by-default rule as everything else here: review the diff, then --apply.

governance is excluded outright (gitvote-policy.yaml's excluded: true) -- its own .gitvote.yml fix landed as its own PR in that repo, not from here.

Conventions

Same as every other repo in the org — DCO sign-off, Conventional Commits, Assisted-by: trailer for material AI assistance (see governance/AI_POLICY.md).

About

Local-first admin tooling for managing github.com/cloudnative-pg org settings, teams, and CODEOWNERS — run from an admin's laptop, never from CI

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages