How it works¶
Every target repo goes through the same short pipeline, and each repo can end in a different state without affecting the others.

The pipeline¶
- Resolve targets. The campaign's
orgsare listed 100 repos per page, 4 pages at a time (cached for 10 minutes), unless a codesearchhas already named the only repos that can match. Cheap metadata filters run first, thenfileschecks in batched queries of 50 repos, 4 at a time, split in half whenever GitHub times out. Exclusion always wins. - Look up existing PRs. For
apply, gh fleet finds PRs by the head branch (fleet/<name>), in batched queries of 50 repos, and falls back to a search for the hidden<!-- gh-fleet:campaign=<name> -->marker in the PR body. - Skip what hasn't changed. For
apply, a repo the lastplanorapplyfound nothing to change in, within the last day, is skipped without cloning, as long as it has no campaign PR, its base branch is on the same commit (checked in batched queries of 50 repos) and the change is the same. See Skipping unchanged repos. - Prepare a clone. A shallow, single-branch clone of the base branch (one commit), with the campaign branch checked out. Its files come from a git cache kept between runs: each run fetches only what changed in the base since the last one into the cache, and the clone borrows the cache's files instead of downloading them, while having its own
.gitdirectory andoriginpointing at GitHub as before. If the cache can't be used, the repo is cloned from GitHub directly;--no-git-cachealways does that. Performance has more on the cache. Where the campaign branch is in every target is looked up before the run, 50 repos to a GraphQL query, rather than asked of each repo's git server. If the branch already exists on GitHub it is rebased onto the base; on a conflict it is reset to the base, because the script, not the old branch, is the source of truth. - Run the script. At the repo root, with the
FLEET_*environment and a timeout. See Writing changes. - Decide. No difference from the base is a
noop. A non-zero exit isfailed. Otherwise the change is committed, pushed, and the PR is opened or updated.
Skipping unchanged repos¶
Most repos in a big campaign need nothing, and running the change in each of them means a clone and a script run. So when plan or apply finds nothing to change in a repo, gh fleet records the commit the base branch was on and a fingerprint of the change, and the next apply skips the repo while all of these hold:
- it was run less than a day ago (a skip doesn't count, so a repo is always rechecked daily);
- it has no campaign PR;
- its base branch is still on the same commit;
- the change is the same: every file in the campaign's directory (hidden directories aside), any script or Starlark file it uses outside it, the repo's variables, topics and properties, and the gh fleet version.
Running plan and then apply therefore clones only the repos the plan found changes in. A skipped repo shows as noop with the detail unchanged since <time>.
A change that depends on something outside the repo, such as the latest release of a tool fetched while it runs, can need doing even though nothing above moved. Pass --recheck to run every repo. A Starlark file that loads a remote module at a branch rather than a version or commit, or a campaign directory too big to be a campaign (over 2,000 files or 50 MB), turns skipping off.
Where each repo ends up¶
| Status | Meaning |
|---|---|
pr open |
A PR exists and is open. Its detail says created, unchanged or what was updated. |
merged |
The PR was merged. Later runs skip the repo. |
closed |
The PR was closed without merging. Later runs leave it alone unless you pass --reopen. |
noop |
The script produced no difference from the base branch. |
changed |
The script made a change but it was not pushed, for example because limits.maxOpenPRs was reached. retry picks it up. |
failed |
The script, git or the API failed. The error is kept and retry runs the repo again. |
skipped |
The repo matched but you cannot push to it. |
pending |
Known to the campaign but not yet processed. |
Source of truth¶
Local state under $XDG_STATE_HOME/gh-fleet/ is a cache plus error history. status re-reads every PR from GitHub by branch and marker, so it stays correct if the state file is deleted, and a PR recorded locally that no longer exists on GitHub is reset to pending.