Out Plane

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,url

Branch 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 ;;
esac

Deploying 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.

FormBehaviour
deploy createReturns immediately. The step passes while the build may still fail.
deploy create --waitBlocks until the deployment reaches a final state. Usually what a pipeline wants.
deploy create --followBlocks 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,url

For an application the platform builds from a repository, send no image and the branch is built again:

      - run: outplane deploy create checkout --follow --timeout 20m

Configuration 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 --deploy

Pushing 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

CommandWhy
outplane loginNeeds a browser or a person to paste a token. Use OUTPLANE_TOKEN.
outplane link, team useWrite to the checkout or the machine. The token already names its team.
outplane app shellNeeds a terminal at both ends. Exits 2 behind a pipe.
Destructive commandsNever 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 }}

On this page