Skip to content

import

Export existing repository settings as YAML. Useful for bootstrapping gh-infra configuration from an existing repository.

Terminal window
gh infra import <owner/repo> [owner/repo ...]
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)
Terminal window
# Import and save to a file
gh infra import babarot/my-project > repos/my-project.yaml
# Import multiple repositories
gh infra import babarot/my-project babarot/my-cli
# Import and review
gh 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 manifests
gh 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.

Terminal window
# Import specific repos into manifests
gh infra import <owner/repo> [owner/repo ...] --into=<path>
# Import all repos defined in manifests
gh 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.

  1. Parse local manifests at the given path
  2. Match targets to resources in the manifests — either from arguments or all repos in manifests when no arguments are given
  3. Fetch the current state from GitHub (in parallel)
  4. Diff local vs GitHub, field by field
  5. Display the plan (repo setting changes + file changes with diff stats)
  6. Confirm with interactive diff viewer (for file changes) or simple prompt (repo-only changes)
  7. Write approved changes to local files
Terminal window
# Pull GitHub state for all repos defined in manifests
gh infra import --into=repos/
# Pull GitHub state into a specific manifest file
gh infra import babarot/my-project --into=repos/my-project.yaml
# Pull from a directory of manifests
gh infra import babarot/my-project --into=repos/
# Import multiple repositories at once
gh infra import babarot/my-project babarot/my-cli --into=repos/

After the plan is displayed, the confirmation prompt offers three keys:

> Apply import changes? y yes n no d diff

Press 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.

The confirmation needs a terminal, so in CI or from an AI agent pass --auto-approve:

Terminal window
gh infra import --into=repos/ --auto-approve

The 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.

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:
  • 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 Tab in the diff viewer to switch to write or patch

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.

These two skip modes mean very different things:

  • default skip
    • the file is importable
    • gh-infra chooses skip as the safest default for this run
    • you can still press Tab and switch to write or patch
  • 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_only files are usually default skip
  • github:// 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

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:

  1. renders the local template for the target repository
  2. compares that rendered content with the GitHub file
  3. 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 as go 1.26.1 are 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.

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)

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)
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.

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.

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.