Skip to main content

sidebar_key: pulumi-github-fleet tags: [python, devops]

ghfleet

Build Opensource ByJG Install MCP Server PyPI GitHub license

Declare a fleet of GitHub repositories from one configuration file, with Pulumi: repository settings, Actions secrets, workflow permissions and branch protection rulesets.

It exists because the interesting part of managing dozens of repositories is not the API calls, it is answering "which repositories get this, and what is it exactly?" in a file you can read, review and diff.

Install

pip install ghfleet     # or: uv add ghfleet

Use

import pulumi

from ghfleet import apply, load_config

apply(load_config("config.yml"))

pulumi.export("summary", "GitHub repositories configured successfully")

That is the whole Pulumi program. apply takes the configuration as a plain dictionary, so it does not care whether it came from YAML, JSON or was built in code.

The two conventions are yours to change

apply would otherwise impose where a token and a secret value come from. Both are keyword arguments, with the obvious behaviour as the default:

Argument Default
token_for(owner) reads GITHUB_ACCESS_TOKEN_<owner> from the environment, raising when unset
secret_value(secret) the entry's own value, else the environment variable named after the secret
apply(
    load_config("config.yml"),
    token_for=lambda owner: vault.read(f"github/{owner}").token,
    secret_value=lambda secret: vault.read(secret["name"]).value,
)

The configuration

lists:
  services:
    - api-gateway
    - billing
    - notifications

repository:
  - name: services
    owner: acme
    list:
      - services
    repositories:
      - one-off-repo
    repository_settings:
      allow_update_branch: true
    workflow_permissions:
      default: write
      can_approve_pull_request_reviews: false
    branch_protection:
      enabled: true
      bypass_actor_ids: [5]
    secrets:
      - name: REGISTRY_USER
        value: acme
      - name: REGISTRY_TOKEN
  • lists — named lists of repository names, so a repository set is written once and referenced by several entries.
  • repository — the entries. Each one names an owner, the repositories it covers (through list, repositories, or both), and what they get. A repository named by more than one entry receives the union.

repository_settings

Declare only the settings that must be guaranteed. The accepted keys are in REPOSITORY_SETTINGS_KEYS; an unknown one aborts the run.

A repository that declares this block becomes a github.Repository resource, adopted into the state on the first run that declares it — no import step. The resource covers the whole repository, so everything the block does not declare (description, topics, visibility, the other merge options) is read back from the github.get_repository data source and handed to the resource unchanged. Two consequences:

  • pulumi preview shows a diff only for the keys you declared, which is what makes it a drift report;
  • an undeclared property is not enforced: change the description on GitHub and the next run simply follows it.

The resource is declared with retain_on_delete, so neither pulumi destroy nor removing a line from the configuration deletes a repository. Its secrets and ruleset are removed — only the repository is retained.

pages is not an accepted key, and that absence is meaningful: the provider reads it as "no Pages site" and disables Pages on any repository that still has one. The one exception is the owner's user site, the repository named <owner>.github.io, which is recognised by name and left alone (see "GitHub says no", below).

branch_protection

branch_protection:
  enabled: true
  enforcement: active          # active | evaluate | disabled
  bypass_actor_ids: [5]        # 2 = Triage, 3 = Write, 4 = Maintain, 5 = Admin
  rules:
    pull_request:
      required_approving_review_count: 2
      require_code_owner_review: true
      allowed_merge_methods: [squash]
    required_status_checks:
      strict_required_status_checks_policy: true
      required_checks:
        - context: "Build"
    deletion: true
    non_fast_forward: true

rules, ref_name and bypass_actors are handed to the provider exactly as written — the resource accepts plain dictionaries — so every rule github.RepositoryRuleset supports is reachable from the configuration without this package knowing about it. The keys are the provider's, in snake_case.

Left out, rules falls back to DEFAULT_BRANCH_PROTECTION_RULES: one approving review, dismiss stale reviews on push, all three merge methods, no branch deletion and no force push. It is a default, not a rule of the package — writing rules replaces it entirely rather than merging into it.

Other keys: name (default Protect <default branch>), target (branch or tag), ref_name ({includes, excludes}, default the default branch), and bypass_actors for anything that is not a repository role. An unknown key aborts the run: on a branch protection block, a silent typo means a rule that is not enforced.

Note that rulesets on private repositories require a paid plan. On the free plan GitHub answers 403, so the ruleset is skipped with a warning instead of failing the run.

secrets

secrets:
  - name: REGISTRY_USER
    value: acme          # in the file, for values that are not secret
  - name: REGISTRY_TOKEN # no value: taken from the environment

The run aborts before declaring anything if a secret has no value anywhere. That check is not politeness: an empty value would overwrite the real secret with an empty string in every repository that declares it.

A secret goes to GitHub Actions unless it says otherwise. apps lists where it is made available, and env names the environment variable to read when it is not the name of the secret:

secrets:
  - name: REGISTRY_TOKEN                # GitHub Actions, as above
  - name: REGISTRY_TOKEN
    apps: [dependabot]                  # the same name, for Dependabot pull requests
    env: REGISTRY_READ_TOKEN            # ... with another value: a read-only token
  - name: REGISTRY_USER
    value: acme
    apps: [actions, dependabot, codespaces]
apps value The secret is available to
actions (default) GitHub Actions workflows
dependabot Workflows triggered by Dependabot pull requests, which cannot read Actions secrets
codespaces Codespaces created for the repository

Each app keeps its own secrets, so the same name can hold a different value in each. That is the point of the example: a workflow reads secrets.REGISTRY_TOKEN either way, and on a Dependabot pull request -- which runs dependency code nobody reviewed yet -- it gets the read-only token instead of the one that can publish.

GitHub also has secrets for agents; the Pulumi GitHub provider cannot declare them yet, so apps: [agents] aborts the run, as any unknown value does.

Sharing blocks between entries

Entries repeat themselves. Use YAML anchors — a feature of the file format, so nothing here has to implement it:

x-defaults:
  branch_protection: &protected
    enabled: true
    bypass_actor_ids: [5]

repository:
  - name: services
    branch_protection: *protected

  - name: legacy
    branch_protection:
      <<: *protected            # inherit, then override
      enforcement: "disabled"

Precedence stays local and explicit: a key written next to <<: wins.

GitHub says no

Two settings answer 422 whatever you send, and both cost a failed run to discover.

allow_forking on a private repository of a user account. The repository-level forking policy only exists for private repositories owned by an organization, and even there an organization owner has to allow forking of private repositories first. On a public repository the setting means nothing. It is therefore not among the accepted keys, which is also what the provider recommends — its schema says to "leave unset for the default behaviour". The restriction is not in the REST reference; it is in the forking policy guide.

Deactivating GitHub Pages on the owner's user site. The repository named <owner>.github.io cannot have its Pages site unpublished:

DELETE /repos/<owner>/<owner>.github.io/pages: 422 Deactivating GitHub pages
for this repository is not allowed

This one is documented nowhere — neither the REST reference nor the "Unpublishing a GitHub Pages site" guide mentions it, and that guide draws no distinction between a user site and a project site. It is only what the API does, so the package recognises the repository by name and keeps pages out of its diff.

What this is not

An ordinary Python package, not a Pulumi component: it declares its resources into the stack that calls it, and can only be consumed from Python.

License

MIT. See LICENSE.


Open source ByJG

Metadata

Release files for ghfleet 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ghfleet 0.2.0
File Size Uploaded
ghfleet-0.2.0.tar.gz 72.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ghfleet 0.2.0
File Interpreter ABI Platform
ghfleet-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 86.5 kB

Release files / ghfleet-0.2.0.tar.gz

Download URL ghfleet-0.2.0.tar.gz
Size 72.1 kB
Tags Source
SHA-256 checksum
How to use checksums
7abd3fa50f000614dca8c131f17e6b20b1a5388c47d3751a07b0250df787df00
BLAKE2b-256 checksum
How to use checksums
2cda75fcd9950ed7375ba704ecaa836806e118bb25e57342ef39c013a75ba3f6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.

Transparency log

Release files / ghfleet-0.2.0-py3-none-any.whl

Download URL ghfleet-0.2.0-py3-none-any.whl
Size 14.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6d5a249b59fe403981c32954a733982927364144ab105c79374443e73b59a69f
BLAKE2b-256 checksum
How to use checksums
b4cab337acc866b492533d6799f52909f477471d353dac525ec5f48b2109a319
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page