Skip to content

octoform plan

plan loads configuration, discovers repositories, resolves effective policy, observes requested state, and reports executable and blocked differences. It never mutates GitHub.

octoform plan --config octoform.yml
octoform plan --config octoform.yml --owner example-org
octoform plan --config octoform.yml --repo example-org/octoform
octoform plan --config octoform.yml --type npm-package
octoform plan --config octoform.yml --format json
octoform plan --config octoform.yml --out plan.json --expires-in 30

Selection

--owner limits the run to the named accounts and is repeatable. --repo selects one exact discovered repository, and accepts a qualified owner/name when the same repository name exists under more than one selected account. --type selects every repository with one resolved type. Supplying none of them evaluates the complete managed set after exclusions. Use a narrow selector for initial rollout and incident diagnosis.

What a plan covers

A plan covers the selected repositories and the account above them. An organization block produces changes in the same plan, confirmed the same way, grouped under a heading that cannot be mistaken for a repository name — a repository really can be called the same thing as the organization that owns it:

3 change(s) across 2 repositories:

  (example-org: the organisation itself)
    organization.members.base_permission: write -> read
    organization.teams.platform-oncall: (unset) -> platform-oncall (Platform on-call); closed; under platform

  example-service
    access.teams.platform: (unset) -> maintain

Owner-level work is planned and applied before any repository, so a change to the floor under every repository is never briefly wider than the file asks for.

Operation identity

Every operation carries:

  • a stable identifier, unchanged between two runs that plan the same change, so results can be correlated across runs and across output formats;
  • the account it belongs to, and the repository when it belongs to one;
  • an operation kind: create, update, attach, detach or delete;
  • a risk level: normal, sensitive, destructive or cost;
  • its prerequisites, and its before and after values.

Identifiers address a change in Octoform's own model. They are never a GitHub object identity.

Changed in 0.5.0

attach, detach and delete are new operation kinds, and destructive is a new risk level. A change belonging to the account itself has no repository at all: Change.repo is optional, and inventing a value for it would make the change group and count as though it belonged to a repository. Programmatic callers that read change.repo as a string have to handle its absence.

Risk levels

Level What it means Examples
normal Metadata A description, a label colour, a team's notification setting
sensitive It affects access, merge safety, or reaches beyond what the file names base_permission, an organization ruleset, an organization role, any access grant, branch protection, visibility, a repository property value
destructive Something is removed and GitHub does not keep it A deleted team, a deleted property definition, a revoked grant, a deleted label
cost It has a billing consequence Nothing Octoform manages today

Risk is carried on every change in --format json and in a saved plan. Text output does not print it; it prints the reason a change is blocked or warned, which is what a reader acts on.

Determinism and concurrency

Two runs against unchanged state produce the same operations in the same order, regardless of the order the API happened to answer in. Ordering is part of the contract, not an accident of pagination.

--concurrency <n> bounds how many repositories are worked on at once within one account; it defaults to 4. Accounts themselves run one at a time, in the order the configuration declares them, so their reports never interleave.

A failure inside one account does not stop another: by default the run continues and reports every account's outcome. --fail-fast stops at the first account that fails instead. A repository whose plan cannot be computed at all is reported as a distinct failure and never treated as "no changes".

Plan categories

Result Meaning
Executable change Current state is readable, differs from policy, and the operation is available.
Warning The change can execute but has a consequence requiring attention, such as workflow references during branch rename.
Blocked change Desired and current state cannot be reconciled safely, or the capability is unavailable. It will never reach the applier.
No change The field matches or is not managed.

Blocked changes remain visible with their reason. Unreadable state is not converted into a guessed current value.

Available since 0.4.1

A reason explains itself from what was observed. Where repository visibility settles the question — GitHub hides scanning settings outside a public repository without Advanced Security — the report names that; where it does not, it says only that the value could not be read, rather than guessing at a commercial plan.

Reading the summary

The per-account counts put each repository in exactly one bucket, so they sum to the number scanned. A repository that has both an executable change and a blocked one is therefore counted as changed.

Available since 0.4.1

Because that would understate how much of a run cannot be applied, the number of repositories carrying blocked work is stated alongside the totals whenever it is larger:

Total — scanned: 49, changed: 18, blocked: 9, failed: 0, unchanged: 22
16 repositories carry blocked work; 7 of them are counted above as
changed because they also have changes to apply.

PlanSummary exposes the same number as blockedRepositories.

Saving a plan

--out <path> writes the reviewed plan to a versioned JSON file, with owner-only permissions where the platform supports them. The file records the schema version, the version of Octoform that produced it, the authenticated actor, each target account's numeric identity, a digest of the resolved configuration and of every source file that contributed to it, the observation time, an expiry, and the operations themselves.

--expires-in <minutes> sets that expiry; it defaults to 60. apply --plan re-checks every one of those claims before it touches anything.

The plan file is exactly as sensitive as the configuration it came from: it names private repositories and their settings. It carries no credential.

Exit and output

Situation Exit
Nothing to do 0
Executable changes found 1
One or more changes blocked 4
One or more repositories could not be examined 5
Configuration or selector error 2
Authentication or permission failure 3

Text output is human-oriented; do not depend on its layout as a stable machine API. Use --format json for a versioned envelope instead, described in the execution contract.