Skip to content

JSON output

plan and apply can print their result as JSON for CI jobs and AI agents:

Terminal window
gh infra plan ./repos/ --output json
gh infra apply ./repos/ --output json --auto-approve

With --output json, stdout has only the JSON document; the progress, the plan table and the summary are not printed. apply needs --auto-approve, since the confirmation prompt cannot be shown. Exit status is the same as with text output.

{
"format_version": 1,
"command": "plan",
"summary": { "create": 1, "update": 2, "delete": 0 },
"changes": [
{
"target": "babarot/my-cli",
"resource": "Repository",
"action": "update",
"field": "description",
"before": "old",
"after": "new"
},
{
"target": "babarot/my-cli",
"resource": "Repository",
"action": "update",
"field": "features",
"details": [
{ "field": "wiki", "action": "update", "before": true, "after": false }
]
},
{
"target": "babarot/my-cli",
"resource": "Ruleset",
"action": "create",
"name": "main-protection",
"after": "main-protection",
"details": [
{ "field": "enforcement", "action": "create", "after": "active" }
]
},
{
"target": "babarot/my-cli",
"resource": "File",
"action": "update",
"path": ".github/workflows/ci.yaml",
"fileset": "babarot",
"via": "push",
"lines": { "added": 3, "removed": 1 }
}
],
"warnings": [],
"errors": []
}
Field Description
format_version 1. Raised only when a field is removed or changes meaning; new fields keep it
command plan or apply
summary Number of changes by action. apply adds applied, failed and skipped
changes One entry per change, in the order plan prints them. The counts in summary match these entries
warnings Deprecations found while parsing, and notes such as a commit that will not be signed. target is set when it is about one repository
errors Repositories that could not be planned, with the error. They are left out of changes
changed_since Only with --changed-since: ref, and selected / total, the number of resources planned and found (each RepositorySet entry counts once, before --repo). selected is 0 when nothing changed, which tells “nothing was planned” from “planned, no changes”
api GitHub API usage of the run: calls (gh processes run, retries included; one call can make several HTTP requests with --paginate), waited_seconds for rate limits, and remaining with core (REST) and graphql, each remaining, limit and reset (RFC 3339). remaining is left out when gh api rate_limit is not available
Field Description
target Repository (owner/repo)
resource Repository, Actions, BranchProtection, Ruleset, Label, Milestone, Secret, Variable, or File
action create, update, or delete
name Name of a named resource: the ruleset, branch protection pattern, label, milestone, secret, or variable
field Setting name for repository settings, such as description or features
path File path, for File
before, after The values plan shows. They are display values, not API payloads (for example "3 actors" for bypass actors). Secret values never appear
details Changes inside a grouped setting, with the same field, action, before and after
lines Added and removed lines, for File
mode before and after file mode, for File when the mode changes
diff Unified diff, for File with plan --diff (up to 500 lines)
fileset, via FileSet owner and delivery method (push or pull_request), for File
status apply only: applied, failed, or skipped (no content change on GitHub)
error apply only: the error when status is failed
pr_url apply only: the pull request for a File delivered via pull_request

Fail a workflow when a plan would delete something:

Terminal window
gh infra plan ./repos/ -o json | jq -e '.summary.delete == 0'

List every change on one line each, with grouped settings flattened into one line per setting:

Terminal window
gh infra plan ./repos/ -o json | jq -r '
.changes[] | . as $c | (.details // [{}])[]
| [$c.target, $c.resource, ($c.name // $c.path // ""), ([$c.field, .field] | map(select(.)) | join(".")), (.action // $c.action)]
| @tsv'

Show the changes that failed in an apply:

Terminal window
gh infra apply ./repos/ -o json --auto-approve | jq '.changes[] | select(.status == "failed") | {target, resource, name, field, error}'

Errors that stop the command before a plan is made, such as an invalid manifest, are printed to stderr as usual, with no JSON on stdout.