Skip to content

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

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 checklist → repo-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-repo.sh <repo> [--apply] Runs every per-repo reconcile step below in order (validate, owners team, Reviewer grants, settings, labels, milestones, ownership files). The single command to run after a policy change. Same dry-run-by-default model, no logic of its own
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 repo-tiers.yaml's reviewers: lists (naming them on componentowners-policy.yaml paths is optional routing). 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

After you change a policy file: what to run

Merging a change here only updates the desired state. GitHub and the target repos change when you run a script. For any change to one repo, run:

./scripts/sync-repo.sh <repo>            # dry run of every step
./scripts/sync-repo.sh <repo> --apply

It validates the policy files, then runs the steps below in order: owners team, Reviewer grants, repo settings, labels, milestones (only for repos milestones-policy.yaml names) and the generated ownership files. Each step is idempotent and only raises, so running all of them is safe when only one was needed. Afterwards, commit the regenerated files in <repo> and open a PR there (the script stops at the working tree).

The table says which step a given edit actually exercises, for running one script on its own. Every script is dry-run by default.

You changed Run, in this order Then
repo-tiers.yaml owners: (add or remove a Component Owner) sync-project-owner-teams.sh <repo>, then sync-ownership-files.sh <repo> Commit and open a PR in <repo> for the regenerated COMPONENT_OWNERS.md. CODEOWNERS only changes if its policy changed too, since it references the <repo>-owners team. A person who is not yet an org member gets a pending invite, not access, until they accept
repo-tiers.yaml contributors: sync-ownership-files.sh <repo> PR in <repo> for CONTRIBUTORS.md. No GitHub access is granted by this list
componentowners-policy.yaml (path rules, Reviewers) sync-reviewer-grants.rb <repo>, then sync-ownership-files.sh <repo> PR in <repo> for CODEOWNERS and COMPONENT_OWNERS.md
people.yaml (name, organization, country) sync-ownership-files.sh <repo> for each repo that lists the person Display data only, grants nothing. The rendered files in the repo change, nothing on GitHub does
repo-tiers.yaml class: or description: fix-repo-settings.sh <repo> Class sets the review floor, description is pushed to GitHub
repo-policy.yaml or org-policy.yaml fix-repo-settings.sh <repo> for one repo, fix-all-repos.sh for an org-wide rule
gitvote-policy.yaml sync-ownership-files.sh <repo> (writes .gitvote.yml) or fix-gitvote-config.sh <repo> Then check-gitvote-config.sh to confirm
labels-policy.yaml sync-labels.rb [repo]
milestones-policy.yaml sync-milestones.rb [repo]
A repo was cloned, archived or created update-managed-repos.sh See "Adding a new repo"
A team's membership changed outside this repo update-teams.sh Refreshes the snapshot validate-policy.rb checks team slugs against

When a script lands files in a sibling clone, git fetch origin and branch from origin/main before committing, and cite the passed vote issue in the PR body for any ownership change. check-repo-settings.sh [repo] is the read-only way to confirm the end state afterwards.

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