Skip to main content

loha plan

loha plan shows what Loha would run for your .loha.yml on a given branch and event: which jobs, which steps, which images, how much CPU and memory, and which secrets each step may read. Nothing runs. No build is created, no secret is decrypted and no token is minted, so a plan is safe to run as often as you like.

It is the same assembly the cluster uses to start a real build, read from the Loha server, so the plan matches the build page and not your local reading of the file. The same inputs that a real run refuses (an image outside the allowed registries, a denied environment name, a matrix over its limit) are refused by the plan with the same message.

Local runs are on for every workspace

loha plan is part of the local-run feature, which is on by default. If a Loha platform administrator has switched it off for your workspace, the command stops with Local runs are not enabled for this workspace. Ask a workspace admin to turn on local_run. and exits with code 2. To run the plan on your own machine, see loha run --local.

Usage​

Run it from inside a checkout whose origin remote is a repository onboarded to Loha:

loha plan

Without flags the CLI reads the branch, the head commit and the repository from your git checkout, assumes a push event, and plans ./.loha.yml.

Plan for .loha.yml on main (push) at 3f9c2ab
On: would fire (Matches the on.push entry for branch main.)
Trigger: tr_8a1 (auto)

build [medium 2 / 4Gi]
run clone node:22
emu checkout alpine/git@sha256:aaaa…
↳ Replaced by the staged working tree.
run test node:22
secrets: NPM_TOKEN
skip lint-extra node:22
↳ Condition `matrix.skip_lint != 'true'` is false for branch main, event push.

release skipped: Condition `github.event == 'tag_push'` is false for branch main, event push.

loha-security-final-gate [platform]
skip security-gate
↳ The managed security gate runs only on the cluster.

! Step "notify" references secret "SLACK_URL" but no such secret is defined in scope

Variables: names only (token lacks variables:values:read)

The values above are examples. Image names, ids and the jobs you see depend on your workspace and pipeline. Blank lines separate the header, each job, the warnings and the Variables: line.

Flags​

FlagPurpose
--file <path>Pipeline file to plan (default .loha.yml)
--branch <name>Branch to plan for (default: the checked-out branch; a detached HEAD needs this flag)
--event <type>Event to plan for: push (default), pull_request, tag, manual or release (tag is sent as tag_push, which is also accepted). Job and step conditions are evaluated against it
--base <branch>Target branch of the pull request, to evaluate the on: block for it. Only with --event pull_request
--tag <tag>The tag that was pushed, to evaluate the on: block for it. Only with --event tag
--sha <sha>Commit to plan for (default: HEAD)
--repo <id>Repository to plan against (default: found from the git remote)
--trigger <id>Use this trigger's variables and settings
--var KEY=VALUERuntime variable injected as step env, repeatable
--jsonPrint the server's plan unchanged, for scripts and agents

Exit codes​

CodeMeaning
0The plan was rendered
1The configuration was rejected; the server's errors are printed exactly as the cluster would give them
2Usage or authentication error, local runs not enabled for the workspace, a request the server could not read (for example a malformed --sha), a stray argument, or server trouble (unknown repository or trigger, planner unavailable, rate limit, 5xx)

Reading a plan​

Header. Plan for <file> on <branch> (<event>) at <short sha>. For --event tag the event shows as tag_push.

On. On: would fire (<reason>) or On: would not fire (<reason>) says whether the pipeline's on: block would start a build for this event, branch, and (with --base or --tag) pull request target or tag, the server's answer to the same check a real push makes. Pass --event pull_request --base main or --event tag --tag v1.2.3 to ask about those events. For a release event the line reads On: not evaluated for release events. A server that does not report it leaves the line out. It is information only: a plan is rendered, and loha run --local runs, either way (a local run prints the same line first, as a notice: ! On: would fire (<reason>) or ! On: would not fire (<reason>)). --base with an event other than pull_request, or --tag with one other than tag, is a usage error (exit 2), so the answer never means something else than what you asked.

Trigger. Trigger: <id> (explicit) when you passed --trigger, and Trigger: <id> (auto) when exactly one enabled trigger on the repository reads this file and would fire for the event. Otherwise Trigger: none, and a warning lists the candidates so you can choose one with --trigger. A trigger matters because trigger-scoped variables and plan-level resource defaults come from it.

Jobs. Each job prints as <id> [<class> <cpu> / <memory>], the resources the cluster would give it. A job that would not run prints <id> skipped: <reason> (for example its condition is false for this branch). Platform jobs that Loha adds itself, such as the security gate, print [platform] without CPU or memory. Matrix jobs appear already expanded, one entry per combination, exactly as on the build page.

Steps. Every step starts with a badge:

BadgeMeaning
runRuns as written: same image, same script. loha run --local executes it unchanged.
emuEmulated: the cluster step relies on something a laptop does not have, so a local run substitutes it: a clone of the pushed commit, the cache, job outputs and artifacts (a local server answers the real scripts), an image build (your own engine builds it, nothing is pushed) or an approval gate (you answer in the terminal).
skipNot run on your machine. Either it changes remote state (push, deploy, notify, signing), or it is the platform security gate, or its if: is false for this branch and event. The reason is printed under the step.

The badge describes what a local run would do, not what the cluster does: for example a deploy step marked skip still deploys on the cluster. A line starting with ↳ under a step gives the reason, and secrets: A, B lists the secret names the step was granted.

Warnings. Lines starting with ! are things the plan could not decide for you, for example a step that names a secret which does not exist in scope, or a quota that would delay the build. Warnings do not make the plan fail.

Variables. The last line tells you whether stored variable values are in the plan, see below.

Variables and secrets in a plan​

A plan never contains a secret value. For each step it lists only the names of the secrets it was granted and that exist in scope.

Plain (non-secret) variables follow your token:

Your tokenThe plan shows
has variables:values:readPlain variable values (Variables: values included)
does notNames only (Variables: names only (token lacks variables:values:read))

variables:values:read is an opt-in personal access token scope. variables:read keeps its meaning (metadata, never values), guests cannot hold the new scope, and the "Read only" preset does not include it. Tokens created before it existed do not gain it; create a new token and select it if you want values in your plans.

A plan needs the pipelines:read scope and, like every workspace token, can only see the workspace it is bound to. A service account key bound to one project can only plan against that project's repositories.

No matching repository​

If the git remote does not match a repository in the active workspace, the plan is rendered without one and says so: No connected repository matches this checkout; planning without repository or trigger variables. Repository- and trigger-scoped variables are then absent from the plan. Pass --repo to name the repository yourself.

What a plan does and does not reproduce​

A plan is the cluster's own answer, so it covers everything the server decides. The following lists what is the same on a laptop, what would be emulated, and what is different, for the day steps run locally.

AspectLocal vs cluster
Parsing, conditions, matrix, env precedence, task rendering, image policy, denied env namesSame: all decided by the server
Step images, scripts, user id, capabilities, working directorySame
CPU and memorySame values; scheduling and node fit are not reproduced
SourceEmulated: your working tree instead of a clone of the pushed commit
ServicesEmulated: container network aliases instead of sidecars
Cache, job outputs, image builds, approval gatesEmulated: see loha run --local for how each one differs
NetworkDifferent: your network, not build-pod egress rules or cluster mirrors
ArchitectureDifferent unless a platform is chosen
Secrets, stored variablesDifferent source: secrets are never in the plan; plain variables only with the opt-in scope
Push, deploy, notify, signing, security gate, build recordNever locally

API​

loha plan calls POST /api/v1/pipelines/plan (scope pipelines:read). The body carries the YAML and the git context (yaml, branch, event, commitSha, and optionally repositoryId, triggerId, runtimeVars, targetBranch, tag); the response is { "plan": … }. An invalid document returns 422 with the same error list as loha validate; a request the server cannot read (for example a malformed commitSha) returns 400 with code: "VALIDATION_ERROR" and the field messages; a configuration the cluster would refuse returns 400 with its code and message (PIPELINE_CONFIG_ERROR); a Build Security refusal (for example IMMUTABLE_SOURCE_REQUIRED) or a configuration the plan cannot render (PLAN_PROJECTION_FAILED) returns 422 with its code and message; an unknown repository or trigger gets 404 (Repository not found or Trigger not found); a service account key bound to another project, or planning without a repository, gets 403 (Service account is not bound to this project); a workspace without the feature gets 403 with code: "FEATURE_DISABLED"; and any planner failure (the planner is down, or answers something other than a refusal of your request) gets 503 with code: "CONTROLLER_UNAVAILABLE" (The pipeline planner is unavailable. Try again shortly.). The full schema is in the OpenAPI document.

See also​