CLI
Every command Octo has, and every flag. The octo binary ships inside @quadnix/octo-build, and a
scaffolded project has it at node_modules/.bin/octo.
octo <command> [options]
octo --help
octo --version
Every command reads octo.yaml from the working directory. To read another file, set
OCTO_DEFINITION_FILE_PATH; there is no flag for it.
OCTO_DEFINITION_FILE_PATH=octo.prod.yaml node_modules/.bin/octo generate -o .octo-tf
The lifecycle
Four commands, in this order. The Lifecycle covers what each one is for.
The last two write shared state, so they require the app lock. Take it with
octo lock and pass the ID it prints. generate and validate write nothing shared and
need no lock.
npm run octo:generate
npm run octo:validate
LOCK_ID=$(npm run --silent octo:lock)
npm run octo:apply -- --lockId $LOCK_ID
npm run octo:commit -- --lockId $LOCK_ID # releases the lock
generate
Writes the Terragrunt module folders for the infrastructure your definition file describes, and saves the model state.
| Flag | ||
|---|---|---|
-o, --outputDir | required | Where to write the folders. Folders are overwritten in place, never wiped; a module deleted from intent leaves an emptied folder so a later apply destroys its resources. |
octo generate -o .octo-tf
A successful generate prints only what the listeners report. Silence is success.
With RemoteStateProvider, generate first brings
the state directory up to the newest state a finished run left in the backend.
validate
Runs terraform plan on every folder and correlates the plans with Octo's own diff. Creates
nothing.
| Flag | ||
|---|---|---|
--terragruntDir | required | The generated directory to plan. |
--terraformBinary | Override the binary Terragrunt invokes. Sets TG_TF_PATH. | |
--terragruntBinary | Override the Terragrunt binary. | |
--timeoutInMs | Per-command timeout. Default 60 minutes. |
Exits non-zero only when a folder could not be planned. Drift and destructive changes are findings, not failures. See Reading a Validate Report.
Planning a folder that holds an external
resource runs
octo get-response for it, which reads Octo state and calls nothing. validate
puts the binary on PATH for that, the same way apply does for run-action.
apply
The only command that changes live infrastructure, and the only one that does not boot Octo: it loads none of your modules and reads no model or resource state. It does read the definition file, because the state provider named there is where Terraform's own state is checked out of and back into around the apply.
| Flag | ||
|---|---|---|
--terragruntDir | required | The generated directory to apply. |
--lockId | required | The app lock ID from octo lock. |
--auto-approve | Skip the destroy check. Required to destroy anything. | |
--terraformBinary | Override the binary Terragrunt invokes. | |
--terragruntBinary | Override the Terragrunt binary. | |
--timeoutInMs | Give the apply a time limit. None by default. |
Verifies the lock before planning, so a run without it fails having done nothing. It does not
release the lock, because the apply is the middle of a run and commit is the end of it.
Plans first and refuses if anything would be destroyed, or if a folder could not be planned. Each
destroy of a resource holding data, a table or a bucket, is marked - the data it holds is lost with it.
A resource generate moved into another module folder, because its moduleId was renamed, has its
Terraform state moved into the new folder before the plan, so it is not listed. Only the apply checks
that move into the state provider; a plan or a validate runs on it and puts the state back.
An apply that fails part way prints the octo commit --partial that records what it did build,
with this run's directory and lock ID already in it. A run that fails before applying anything does
not, because there is nothing to record.
It runs without a time limit unless --timeoutInMs gives it one. How long a stack legitimately
takes is a fact about that stack, and a limit that fires part way through leaves infrastructure
half changed, which is worse than an apply that took longer than you expected. The limits that
belong here are your CI job's own, and the terminal you can interrupt. validate and commit only
read, so both keep the sixty-minute default that ends a hang.
npm run octo:apply -- --lockId $LOCK_ID --auto-approve
octo apply --help prints the flag as --autoApprove, which is the same flag. Yargs accepts either
spelling, and these pages use the hyphenated one because it matches Terraform's.
commit
Reads every folder's Terraform outputs and records them into Octo's state. All or nothing: a missing or null output throws before any state is written.
| Flag | ||
|---|---|---|
--terragruntDir | required | The generated directory, after an apply. |
--lockId | required | The app lock ID from octo lock. Released when the commit succeeds. |
--partial | Record what did apply, after an apply that failed. | |
--terraformBinary | Override the binary Terragrunt invokes. | |
--terragruntBinary | Override the Terragrunt binary. | |
--timeoutInMs | Per-command timeout. Default 60 minutes. |
This is the command that ends a run, so a commit that succeeds releases the lock. A commit that
throws keeps it: fix what it reported and run commit again with the same --lockId. A normal run
never leaves a lock behind.
--partial is for after an apply that failed. It plans every folder first, and records only the
folders whose plan is now empty: a folder whose apply failed still holds the outputs of its last
apply, which would otherwise record a change that never reached the cloud. Within those folders it
records every resource whose outputs did all resolve. Every other resource, and every resource
standing on one, keeps what the last commit recorded of it, or stays out of Octo's state if it was
never committed. Those come back as warnings
naming what was skipped. The next run plans them again, which Terraform reconciles against its own
state: recording less than reality is recoverable, recording more is not.
The app lock
Octo's state has one writer at a time, and the lock is how that is enforced. apply and commit
both require it; generate and validate do not, because neither writes shared state.
lock
Takes the lock and prints its ID, and nothing else on stdout, so a shell can capture it.
LOCK_ID=$(npm run --silent octo:lock)
Fails if a lock is already held, naming the ID that holds it. Nothing else runs, so a second runner
stops before it reads state or plans anything. With
RemoteStateProvider it also fails when another
run finished after your generate, so there generate comes first. It also fails while the state
directory holds journal files a released
lock could not archive, naming them.
| Flag | |
|---|---|
--lockId | Resume the run holding this lock instead of taking a new one. |
There is no timeout on a lock. An expiring one would let a second apply start while the first might
still be running, and from outside you cannot tell whether a dead run applied nothing, everything,
or half of it. A run that died is finished rather than released: stop its process if it is still
there, resume it with the ID the error named, here in HELD_LOCK_ID, then generate, apply and
commit as usual. Recovering a Failed Run
covers what to check before the apply.
LOCK_ID=$(npm run --silent octo:lock -- --lockId "$HELD_LOCK_ID")
npm run octo:generate
npm run octo:apply -- --lockId $LOCK_ID
npm run octo:commit -- --lockId $LOCK_ID
With a remote state provider, the run can be resumed from any machine, by the account that took the lock or by an organization administrator, and resuming reads everything the dead run had written. Stop the dead run's process first: its lock is still good, so a process that wakes up keeps writing into the same state.
unlock
Finishes a run as it stands and releases its lock.
| Flag | ||
|---|---|---|
--lockId | required | The ID to release, as octo lock printed it. |
A successful commit releases its own lock, so this is for a run that will not reach one: a run
that never applied, or one somebody finished by hand. With a remote state provider, unlocking makes
what the run wrote the newest state, which the next generate plans against. Like commit, it
seals the run into archives/<commitId>/: a copy of the state, and the journal moved
beside it.
Scaffolding
create-app
Creates a project from a template, and runs npm install in it.
| Flag | ||
|---|---|---|
-n, --name | required | The app name. A directory of this name is created. |
-t, --template | required | aws-s3-website, aws-ecs-server, or aws-dynamodb-service. |
-p, --path | Where to create it. Defaults to .. |
npx @quadnix/octo-build create-app -t aws-s3-website -n shop
create-module
Writes a module and its input schema, and exports it from src/index.ts. The model and its action
are a separate command, create-model.
| Flag | ||
|---|---|---|
-n, --name | required | Module name, in kebab-case. |
-t, --type | required | The model type: account, app, deployment, environment, execution, filesystem, image, pipeline, region, server, service, subnet. |
--package | required | Your library's package name, e.g. @shop. |
-p, --path | Project root. Defaults to .. |
create-model
Writes a model, its schema, and a model action, inside an existing module.
| Flag | ||
|---|---|---|
-n, --name | required | The module the model belongs to, in kebab-case. |
-t, --type | required | The base model this extends. Same list as above. |
--package | required | Your library's package name. |
-p, --path | Project root. |
create-terraform-resource
Writes a resource whose lifecycle Terraform manages.
| Flag | ||
|---|---|---|
-n, --name | required | Resource name, in kebab-case. |
--package | required | Your library's package name. |
-p, --path | Project root. |
create-external-resource
Writes a resource whose lifecycle runs outside Terraform, plus its add, update, and delete actions.
| Flag | ||
|---|---|---|
-n, --name | required | Resource name, in kebab-case. |
--package | required | Your library's package name. |
-p, --path | Project root. |
create-anchor
| Flag | ||
|---|---|---|
-n, --name | required | Anchor name, in kebab-case. |
--family | required | The directory under src/anchors/ to group it in. |
-t, --type | required | The model type it attaches to, or overlay. |
--package | required | Your library's package name. |
-p, --path | Project root. |
create-overlay
| Flag | ||
|---|---|---|
-n, --name | required | The module the overlay lives inside, in kebab-case. |
--overlay | required | The overlay's own name, in kebab-case. |
-t, --type | required | The model type the overlay is for. |
--package | required | Your library's package name. |
-p, --path | Project root. |
Others
run-action
Runs one resource action for an external resource. Terraform invokes this mid-apply through a generated provisioner; you do not call it.
| Flag | ||
|---|---|---|
--resourceId | required | The resource whose action to run. |
--input | A parent's response value, as <parentResourceId>.<responseKey>=<value>. Repeatable. |
Its stdout carries only the action's JSON result, because Terraform reads it as a data "external"
response. Octo's own listeners are routed to stderr for this command.
What the action produced is recorded against the resource as soon as it succeeds, and octo commit
folds that record into the resource graph and clears it. The record is also what keeps the action to
one run: Terraform never updates a null_resource in place, so a changed trigger destroys and
recreates it and the provisioner is invoked twice for one change. The second invocation finds the
work already done and returns what the first produced.
get-response
Prints one resource's recorded response as JSON. Terraform reads it as a data "external" for an
external resource, on every plan and every
apply of the folder that resource sits in; you do not call it.
| Flag | ||
|---|---|---|
--resourceId | required | The resource whose response to print. |
It reads Octo state and nothing else: no modules are composed and nothing is called, which is what
makes it safe during a plan. The response comes from what octo run-action recorded when the commit
has not folded it in yet, and from the committed resource graph after that. A resource that has
neither is an error, and the plan or apply that asked for it fails with that error.
build
A small job runner, unrelated to the infrastructure lifecycle. It reads a build.yml describing
jobs and their dependencies, and runs them with a concurrency limit.
| Flag | ||
|---|---|---|
-b, --buildFilePath | The build file, relative to sourcePath. Defaults to build.yml. | |
-s, --sourcePath | The project source. Defaults to .. | |
-c, --concurrency | How many jobs at once. Defaults to 5. | |
--dryRun | Resolve the jobs without running them. | |
--logsDir | Write each job's log to this directory. | |
--timeout | Overall limit, in minutes. 0 for none. |
Environment
OCTO_DEFINITION_FILE_PATH | The definition file, octo.yaml when unset. Relative paths resolve against the working directory, and everything the definition names resolves against the definition's own directory. |
OCTO_ENV_<key>, for an env key | Overrides that key's declared value. See The Definition File. |