gh fleet
A GitHub CLI extension that applies one change across your organization and follows every pull request to the end. Run it again and it updates, never duplicates.
gh extension install rossedman/gh-fleet
The problem
Pinning an action, bumping a Go version or adding a config file takes minutes in one repo. Across an organization it means cloning, branching, pushing, opening PRs, and then watching which ones fail checks, wait for review or never merge. gh fleet runs that loop for you and keeps the ledger.
apply updates open PRs instead of duplicating themretry picks up only thosestatus works with no local stateCore concepts
A campaign is one YAML file: which repos, what to run in each, how to open the PR. Everything else is three verbs.
Targets, a few lines of Starlark and a PR template. The name derives the branch and the hidden PR marker.
name: bump-go-1-27
targets:
orgs: [acme]
include:
files: [go.mod]
change:
steps:
- starlark: ./change.star
pr:
title: "build: go 1.27"
# change.star
load("@fleet//gomod.star", "set_go_version")
load("@fleet//gha.star", "set_input")
def change(repo):
set_go_version(repo, "1.27.3")
set_input(repo, "go-version", "1.27.3")
Runs the script in a fresh clone of each repo and prints the diff it would send. Repos with no diff are noops.
$ gh fleet plan --repo acme/api
── acme/api
- - uses: actions/checkout@v4
+ - uses: actions/checkout@08eb… # v4
1 changed · 1 skipped
Status reads every PR back from GitHub: state, checks and review decision, one batched query per 50 repos.
$ gh fleet status
3 pr open · 1 merged
1 noop · 1 skipped
Why gh fleet
PRs are found by the fleet/<name> branch and a hidden body marker. A second apply prints unchanged, not a duplicate.
plan runs your script with FLEET_DRY_RUN=true in fresh clones and shows each diff. Nothing is pushed or written.
Writes run at 1 per second by default. A secondary rate limit pauses every worker, and 5xx errors retry with backoff up to 5 attempts.
Delete the local state file and status still reports correctly, because it looks PRs up by branch and marker.
A repo that fails is recorded with its error and a log file. The others carry on, and retry reruns only the failed ones.
limits.maxOpenPRs stops new PRs at a ceiling, concurrency defaults to 8, and apply asks before it pushes unless you pass --yes.
Quick start
You need the GitHub CLI signed in with gh auth login and git on your PATH. gh fleet uses gh's authentication and never asks for a token.
$ gh extension install rossedman/gh-fleet
Writes fleet.yaml with every option shown and commented.
$ gh fleet init pin-actions --org acme
Lists each repo and the filter that matched it.
$ gh fleet targets
$ gh fleet plan --repo acme/api
$ gh fleet apply
$ gh fleet status
Documentation