> ## Documentation Index
> Fetch the complete documentation index at: https://docs.phoneuse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CLI in CI

> How to run the CLI in your own pipeline, and what ours does with it today.

This page has two halves. The first states what our own pipeline runs today, so
you can see the CLI used in a real one. The second is guidance for putting it
into yours.

## What this repository's CI runs today

On every pull request and on pushes to the trunk, one job runs these steps in
order:

```bash theme={null}
corepack pnpm install --frozen-lockfile
corepack pnpm typecheck
corepack pnpm check:docs
corepack pnpm gen:docs
git diff --exit-code
corepack pnpm check:content -- --allow-missing-registry
corepack pnpm build
corepack pnpm test
```

That is the whole of it. Read what it implies:

* **The docs regenerate and the tree must not move.** `gen:docs` rebuilds the
  reference pages from the code, and `git diff --exit-code` fails if that
  produced any change. A tool, scope, error code, route or CLI command that
  changed without a regeneration turns this red. It is the only thing stopping
  these pages from quietly describing an older surface.

* **The CLI is not executed in CI.** Nothing in that list runs a command
  against a device. `typecheck` covers the CLI's source, so it is compile
  checked, and the unit tests cover the argument parser, which is pure and
  needs no process. Neither is the same as running it.

* **An end to end script exercises the whole sequence.** `corepack pnpm e2e:cli`
  starts a control plane and a stand in device, then runs the full sequence from
  registering a row through acquiring, observing, acting and releasing, plus the
  negative cases. It is run on demand rather than on every commit.

Documentation publishing is a separate workflow. It is decoupled from this one
on purpose, so that fixing a typo does not rebuild the application.

Releasing the CLI to npm is a third workflow, and it is manual: someone
dispatches it, and it publishes the version currently written into the source.
It runs the typecheck, the unit tests and the build first, and it refuses a
version that is already on the registry. So a published version has passed the
same gates as any commit on the trunk, and no release ever silently replaces
another.

## Using the CLI in your own pipeline

Nothing above stops you from running it. These are the constraints that
actually apply.

### Install it as a package

Your pipeline does not need a checkout of anything. Install the published CLI
and call it by name:

```bash theme={null}
npm install -g phonebase
phonebase --version
```

Pin the version if your pipeline should not pick up a new release on its own:
`npm install -g phonebase@<version>`, with a version from the package's own
[npm page](https://www.npmjs.com/package/phonebase). `phonebase --version`
reports what a runner actually installed. The examples below call `phonebase`
directly.

### Authenticate from the environment

A runner has nobody to approve `phonebase login`, so put an API key in a
secret and export it. `PHONEBASE_API_KEY` takes precedence over any key a
login stored, and the CLI never reads a credential from a flag, so a key cannot
leak into a command line that a build log echoes.

```bash theme={null}
export PHONEBASE_API_KEY="$PHONEBASE_API_KEY"
phonebase whoami
phonebase devices --json
```

The hosted control plane is the default; pass `--server <url>`, or set
`PHONEBASE_SERVER`, for any other.

The server must be `https` unless it is loopback. Sending a credential in the
clear to a remote host is refused rather than warned about.

### Give the key only what the job needs

Issue a separate key per pipeline, scoped to what that pipeline does and, if
the job only ever touches one device, narrowed to that device. A key restricted
to a subset cannot see the rest of the fleet, so a mistake in a script cannot
reach a device the job was never meant to touch. See
[Authentication](/mcp/auth).

### Release what you acquire

A lease is exclusive, and a job that exits without releasing blocks the next
one until the lease expires on its own. Release in whatever your runner calls a
cleanup step, not on the happy path.

```bash theme={null}
LEASE=$(phonebase acquire "$DEVICE" --json | jq -r .leaseId)
trap 'phonebase release "$DEVICE" "$LEASE" >/dev/null 2>&1 || true' EXIT
```

Releasing a device you no longer hold is a no operation, so the trap is safe
even when the lease already expired.

### Parse the machine readable output

Use `--json` for anything a script reads. It prints the structured result the
tool returned, whose shape is published in the
[tool reference](/mcp/reference). The default human summary is written for a
person and is free to change.

### Expect to queue

Devices are exclusive and physical hardware is scarce, so a job may wait for a
device rather than getting one immediately. Build for that: fail the step with
a clear message rather than retrying an acquire in a tight loop.

### Watch a run to completion

If your job hands a goal to the agent tier rather than driving each step,
`run` returns a run id immediately. Pass `--watch` to poll until the run
reaches a terminal state, and the exit code follows the outcome.

```bash theme={null}
phonebase run "$DEVICE" "open settings" --lease "$LEASE" --executor phonebase --watch --json
```

Name `--executor phonebase` in CI. An API key's default executor is
`self_hosted`, which needs your own worker and a worker URL that a CI log must
not hold. From CLI 0.3.0, `--watch --json` refuses an
explicit `self_hosted` run before starting it, and cancels a run the default
made `self_hosted`. Both exit 2. On 0.2.1, an API key running `--watch`
without `--executor` gets a `self_hosted` run that stays queued until its
deadline, holding the device, because no worker ever connects.

`--max-steps` can only lower the budget. The control plane's own ceiling
cannot be raised from a client. See [Runs](/agent/runs).

## Do not run two control planes over one device set

Worth stating because CI is where it happens. Two control plane processes over
the same devices would each keep their own view of who holds a lease and could
hand the same physical device to two callers. The receipt ledger is locked by
a single process, so the second one refuses to start rather than racing the
first.

If a job needs its own control plane, give it its own devices, or its own
ledger path.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.