import
Export existing repository settings as YAML. Useful for bootstrapping gh-infra configuration from an existing repository.
gh infra import <owner/repo> [owner/repo ...]Arguments
Section titled “Arguments”| Argument | Example | Behavior |
|---|---|---|
<owner/repo> |
gh infra import babarot/my-project |
Import that repository |
| Multiple repos | gh infra import babarot/my-project babarot/my-cli |
Import each repository |
When using --into, arguments are optional. If omitted, all repositories defined in the manifests are targeted.
| Flag | Description |
|---|---|
--into <path> |
Pull GitHub state into existing local manifests (directory or file path) |
--concurrency <n> |
Maximum number of gh processes (GitHub API calls) at once, across the whole command. Default 8, or GH_INFRA_CONCURRENCY. Lower it if you hit GitHub’s secondary rate limit or network timeouts |
--auto-approve |
With --into, write without asking, using each file’s default action (see Without a terminal) |
Examples
Section titled “Examples”# Import and save to a filegh infra import babarot/my-project > repos/my-project.yaml
# Import multiple repositoriesgh infra import babarot/my-project babarot/my-cli
# Import and reviewgh infra import babarot/my-project
# Pull GitHub state into existing manifests (specific repo)gh infra import babarot/my-project --into=repos/my-project.yaml
# Pull GitHub state for all repos defined in manifestsgh infra import --into=repos/The output is a complete Repository YAML manifest reflecting the current state of the repository on GitHub.
--into: Pull GitHub State into Local Manifests
Section titled “--into: Pull GitHub State into Local Manifests”With --into, import works in the reverse direction of plan/apply: it fetches the current GitHub state and updates your existing local YAML manifests to match.
# Import specific repos into manifestsgh infra import <owner/repo> [owner/repo ...] --into=<path>
# Import all repos defined in manifestsgh infra import --into=<path>The path can be a single YAML file or a directory containing manifests.
When no owner/repo arguments are given, all repositories defined in the manifests (from Repository, RepositorySet, and FileSet resources) are targeted automatically.
How It Works
Section titled “How It Works”- Parse local manifests at the given path
- Match targets to resources in the manifests — either from arguments or all repos in manifests when no arguments are given
- Fetch the current state from GitHub (in parallel)
- Diff local vs GitHub, field by field
- Display the plan (repo setting changes + file changes with diff stats)
- Confirm with interactive diff viewer (for file changes) or simple prompt (repo-only changes)
- Write approved changes to local files
Examples
Section titled “Examples”# Pull GitHub state for all repos defined in manifestsgh infra import --into=repos/
# Pull GitHub state into a specific manifest filegh infra import babarot/my-project --into=repos/my-project.yaml
# Pull from a directory of manifestsgh infra import babarot/my-project --into=repos/
# Import multiple repositories at oncegh infra import babarot/my-project babarot/my-cli --into=repos/Interactive Diff Viewer
Section titled “Interactive Diff Viewer”After the plan is displayed, the confirmation prompt offers three keys:
> Apply import changes? y yes n no d diffPress d to open a full-screen diff viewer for file-level changes:
| Key | Action |
|---|---|
↑/↓ or j/k |
Select file |
Tab |
Cycle write / patch / skip for the selected file |
d/u |
Scroll diff pane |
q/Esc |
Return to confirmation |
Repository setting changes (description, visibility, features, etc.) are shown in the terminal plan output, not in the diff viewer.
Without a terminal
Section titled “Without a terminal”The confirmation needs a terminal, so in CI or from an AI agent pass --auto-approve:
gh infra import --into=repos/ --auto-approveThe plan is still printed, then the changes are written with each file’s default action, as if you confirmed without opening the diff viewer: write for a file used by one target, patch for a shared source, and skip for files that are skipped by default (for example reconcile: create_only files without patches, which are therefore not written). Without --auto-approve and without a terminal, import fails before writing anything and suggests the flag.
write / patch / skip
Section titled “write / patch / skip”The import diff viewer does not just let you approve or reject a file change. For each file, you can choose how the change should be written back:
write- update the file’s normal write-back target
- inline entries update the manifest
content: |block - source-backed entries update the local source file
patch- store the drift as a manifest patch under
patches:
- store the drift as a manifest patch under
skip- do not apply that file change in this import run
The default action depends on the file shape:
| File shape | Default | Allowed |
|---|---|---|
| Inline content | write |
write, skip |
| Local source (single-use) | write |
write, patch, skip |
| Local source shared by multiple repos | patch |
write, patch, skip |
Existing patches: entry |
patch |
write, patch, skip |
Simple <% .Repo.* %> substitutions |
write/patch/skip based on file shape |
shown in viewer |
| Files whose remote content cannot be safely written back to the template | skipped in plan | not shown in viewer |
github:// source |
skipped in plan | not shown in viewer |
This is especially useful for shared source files:
- the safe default is
patch, so one repo’s drift does not immediately rewrite the shared source - but if you intentionally want to update the shared source/template itself, switch that entry to
write
There are two kinds of skip behavior:
- hard skip: the file cannot be written back safely, so it is shown only in the plan with a skip reason
- default skip: the file is skipped by default, but you can press
Tabin the diff viewer to switch towriteorpatch
For example, files with reconcile: create_only default to skip, while files whose remote content cannot be safely written back to the template are hard-skipped.
Hard Skip vs Default Skip
Section titled “Hard Skip vs Default Skip”These two skip modes mean very different things:
- default skip
- the file is importable
- gh-infra chooses
skipas the safest default for this run - you can still press
Taband switch towriteorpatch
- hard skip
- gh-infra could not produce a safe local write-back result
- the file is shown in the terminal plan only
- it does not appear in the diff viewer and cannot be toggled
In practice:
reconcile: create_onlyfiles are usually default skipgithub://sources are always hard skip- template-backed files may be either:
- importable, if gh-infra can preserve placeholders and safely apply only the literal drift
- hard-skipped, if the remote file can no longer be mapped back to the original template without risk
Template-Backed Files
Section titled “Template-Backed Files”For files containing gh-infra template placeholders such as <% .Repo.Name %> or <% .Repo.FullName %>, import does not compare the raw template source directly against the GitHub file.
Instead, gh-infra:
- renders the local template for the target repository
- compares that rendered content with the GitHub file
- tries to reconstruct updated template source while preserving the original placeholders
This means simple cases can be imported safely. For example:
module github.com/<% .Repo.FullName %>can stay templated while nearby literal lines such asgo 1.26.1are updated/<% .Repo.Name %>can still be preserved if only the surrounding punctuation changes in a safe way
However, gh-infra hard-skips a template-backed file when the remote file no longer has a structure that can be safely mapped back to the original template. Typical examples are:
- the remote file removed or rewrote the placeholder-backed lines entirely
- the remote file changed placeholder-derived content in a way that is ambiguous to reverse
- the template uses unsupported control flow or complex template syntax
So the rule of thumb is:
- if placeholders are still recognizable and only literal drift changed, import can usually proceed
- if the remote file effectively stopped looking like the original template, gh-infra hard-skips it
For implementation details and exact safety rules, see Import Into Manifests.
What Gets Imported
Section titled “What Gets Imported”| Resource | Behavior |
|---|---|
| Repository settings | Field-by-field comparison and YAML patch |
| RepositorySet entries | Minimal override reconstruction (preserves defaults/override separation) |
Files with local source (source: ./path) |
Local file overwritten with GitHub content |
Files with inline content (content: |) |
YAML content block updated in-place |
Files with reconcile: create_only |
Imported (updates the local master template for future repos) |
RepositorySet entries
Section titled “RepositorySet entries”For a RepositorySet entry, import writes only what differs from defaults.spec. When the repository has none of a collection that the defaults have (labels, rulesets, branch_protection, variables, topics, milestones), import writes it as [] so the entry does not inherit the defaults:
~ my-org/my-cli ~ labels (defaults) → []An entry cannot drop single default items. When the repository has only some of the defaults’ labels, rulesets, branch protection rules or variables, import cannot write that, and shows a warning naming the missing items instead. plan will then create them. To keep the repository without them, change the defaults or manage the repository with its own Repository manifest.
⚠ my-org/my-cli (repos/set.yaml) cannot be fully imported ⚠ labels in defaults but not on GitHub: kind/docs (an entry cannot drop default items, so plan will create them)What Gets Skipped
Section titled “What Gets Skipped”| Source | Reason |
|---|---|
| Files whose remote content cannot be safely written back to the template | The remote content cannot be mapped back to the original template source without risking an incorrect rewrite |
Files from GitHub source (source: github://...) |
No local file to write back to |
| Secrets | GitHub API does not return secret values; local values are preserved |
Skipped files are shown in the plan output with a warning icon and the skip reason displayed dimmed.
Repositories That Cannot Be Read
Section titled “Repositories That Cannot Be Read”A file that does not exist on GitHub has nothing to import. When a repository, or a file of it, cannot be read for another reason (no permission for the repository or its contents, a server error, a rate limit that resets too late), the repository is reported as an error and left out: none of its settings or files are written, and the other repositories are still imported. The run ends with Some repositories were skipped due to errors above. when nothing else changed. An invalid token (401), or a 403 when reading a repository’s settings, stops the import before anything is written.
Shared Source Files
Section titled “Shared Source Files”When a source file is shared across multiple repositories in a FileSet, the default import action is patch, not write.
This means:
- by default, drift from one repository is captured in manifest patches
- the shared source file is left unchanged
- other repositories are not affected immediately
If you intentionally want to make the shared source authoritative and propagate that change, switch the entry to write in the diff viewer.