Continuous Integration
Running the CLI where there is no terminal, no keychain and nobody to ask.
A pipeline is not a small terminal. There is no browser to open, no keychain to store a token, and nobody to answer a prompt. The CLI is built for that, and four rules cover almost all of it.
The Four Rules
Do Not Sign In
Set OUTPLANE_TOKEN. It outranks everything else, and the token names its own team, so
nothing needs linking or selecting.
export OUTPLANE_TOKEN=${{ secrets.OUTPLANE_TOKEN }}Create the token under API Tokens. It is shown once, at
creation. Never pass it as --token: arguments appear in process lists and in most
runners' logs.
Pin the Version
A runner that installs the newest release on every run changes behaviour without a commit.
OUTPLANE_VERSION=v0.2.9 sh -c "$(curl -fsSL https://outplane.com/install.sh)"Ask for the Format
Output is JSON in a pipe already, but a step that sometimes runs attached and sometimes not should say what it wants.
outplane app get --json --fields status,urlBranch on the Exit Status
Every command exits with one of the codes in the table, and they are a contract: appended to, never reused.
outplane deploy create --wait --timeout 15m || case $? in
7) echo "::error::plan limit reached"; exit 1 ;;
124) echo "::error::deploy did not finish in time"; exit 1 ;;
*) exit 1 ;;
esacDeploying from a Pipeline
deploy create returns as soon as the build is queued. A queued build is not a shipped
deploy, so a pipeline that stops there is reporting success it has not seen.
| Form | Behaviour |
|---|---|
deploy create | Returns immediately. The step passes while the build may still fail. |
deploy create --wait | Blocks until the deployment reaches a final state. Usually what a pipeline wants. |
deploy create --follow | Blocks and streams the build output, so the runner's log holds it. |
Both waiting forms take --timeout, and both exit 124 when it expires. The build
keeps going on the platform: a timeout is this side giving up, not the deployment being
cancelled.
GitHub Actions
Building the image yourself and telling Out Plane which tag to run:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
env:
OUTPLANE_TOKEN: ${{ secrets.OUTPLANE_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Install the CLI
run: OUTPLANE_VERSION=v0.2.9 sh -c "$(curl -fsSL https://outplane.com/install.sh)"
# ... build and push your image here ...
- name: Deploy and wait
run: |
outplane deploy create checkout \
--image ghcr.io/acme/checkout:${{ github.sha }} \
--follow --timeout 15m
- name: Report where it landed
run: outplane app get checkout --json --fields status,urlFor an application the platform builds from a repository, send no image and the branch is built again:
- run: outplane deploy create checkout --follow --timeout 20mConfiguration from a Pipeline
Variables, ports and build settings are saved without restarting anything. Pass
--deploy when the change is meant to reach the running application in the same step:
outplane env push .env.production --deploy
outplane port set 3000:http:public --deployPushing a file is not a synchronisation: a variable set on the application and missing
from the file is left alone. outplane env unset is what removes one.
What Will Not Work
| Command | Why |
|---|---|
outplane login | Needs a browser or a person to paste a token. Use OUTPLANE_TOKEN. |
outplane link, team use | Write to the checkout or the machine. The token already names its team. |
outplane app shell | Needs a terminal at both ends. Exits 2 behind a pipe. |
| Destructive commands | Never prompt, and stop with exit 4. See below. |
Destructive Commands in a Pipeline
They never prompt and never proceed on a flag alone. Without confirmation they exit 4
and return the exact command that would proceed, in the error's confirm_command.
That is deliberate: it puts the approval in the pipeline's own gate, an environment protection rule or a manual step, rather than in a terminal nobody is watching. When a job genuinely should delete something, spell it out:
outplane app delete checkout-preview-${{ github.event.number }} \
--yes --confirm-name checkout-preview-${{ github.event.number }}