> For the complete documentation index, see [llms.txt](https://developer.celigo.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.celigo.com/cli/local-tree/workflow.md).

# Teams, git, and CI

How to run `celigo pull`, `status`, `diff`, `push`, and `lint` as a team: one repository, feature branches, pull-request review, a deploy job. [The local tree](/cli/local-tree.md) documents the tree itself; this page is the recommended setup, with the reason wherever a rule is not obvious.

## Pick a mode, one per repository

A **standard tree** is faithful to one account: files carry their `_id`s and every reference is a raw id. It is the right shape for one environment per repository (or per directory), and it is what this page assumes. A [**neutral tree**](/cli/local-tree/neutral-trees.md) carries no ids in its tracked files, so one permanent git branch per environment merges cleanly. The two have opposite commit rules for `.celigo/`, so pick one mode per repository and say which in its README.

## Repository layout

One repository per integration (a scoped tree, `--integration <id>`) or per team (an account tree). Scoped where you can: a scoped tree is smaller, pulls faster, and never writes into another integration unless you pass `--allow-dependencies`.

```
repo/
  .celigo/manifest.json      COMMIT — the shared optimistic-lock tokens and file hashes
  .celigo/.gitignore         written by pull: local/, tmp/, bindings.json, *.tmp
  .celigo/local/             never committed — per-machine state (last pull, lint feedback, the tree lock)
  .celigo/storage/*.json     commit (storage metadata, no secrets) — or .celigoignore storage
  .celigo/targets/           commit — one file per promote target
  .gitattributes             written by pull once: *.json/*.js/*.md text eol=lf;
                             .celigo/manifest.json -diff linguist-generated merge=binary
  .celigoignore              optional, commit: what this repository must never push
  connections/ exports/ imports/ scripts/ integrations/<Int>/…   commit
```

| Path                          | Standard tree                                               | Neutral tree                              |
| ----------------------------- | ----------------------------------------------------------- | ----------------------------------------- |
| `.celigo/manifest.json`       | **commit** — the team's shared lock tokens and hashes       | never (per-environment ids)               |
| `.celigo/.gitignore`          | written by pull: `local/`, `tmp/`, `bindings.json`, `*.tmp` | written by pull: `*`                      |
| `.celigo/local/`              | never — per-machine state                                   | never                                     |
| `.celigo/bindings.json`       | (absent)                                                    | never — this environment's credential ids |
| `.celigo/storage/*.json`      | commit, or `.celigoignore` storage                          | commit                                    |
| `.celigo/targets/<name>.json` | commit                                                      | (promote does not apply)                  |
| `.gitattributes`              | written by pull once, at the tree root                      | same                                      |
| `.celigoignore`               | optional, commit                                            | optional, commit                          |
| resource folders              | commit                                                      | commit                                    |

The rule of thumb: anything two clones at the same commit would compute differently is per-machine and lives under `.celigo/local/`. The manifest is the shared half of the tree's state — a teammate's push is what refreshes the lock tokens for everyone — so it is committed, and it is deterministic so that a no-op pull leaves the checkout clean.

**First pull of a git-tracked tree:**

```bash
celigo pull --integration <id> --no-symlinks
git add -A && git commit -m "Pull <integration> from <account>"
```

`--no-symlinks` writes `.link.json` pointer files instead of symlinks. The style is sticky, and pointer files survive every platform's checkout — a Windows checkout of a symlink is a text file. The pull also writes the `.gitattributes` above: the tree's text files stay LF on every platform, and the manifest merges as binary, so git stops on a manifest conflict instead of interleaving two JSON documents. A CRLF checkout is not an edit either way — change detection compares `.json`, `.js`, and `.md` files line-ending-insensitively, and since celigo-cli 2026.10.1 mixed line endings and a JSON document a formatter rewrote read clean too. Detection only: a script edited in a CRLF checkout is pushed with its `\r\n`, so keep the `.gitattributes` (or `core.autocrlf=input`) wherever scripts are edited on Windows.

## Day to day

Bind your shell to the account's profile once, instead of passing `--profile` on every command:

```bash
export CELIGO_PROFILE=acme-prod      # per terminal or agent session; --profile still overrides
celigo status                        # offline: tree vs manifest
celigo diff                          # live: only the files you changed
celigo lint                          # offline schema check
celigo push --dry-run                # the exact change set; one GET /v1/tokenInfo first
celigo push                          # writes; the manifest write-back follows
```

`CELIGO_PROFILE` is the per-session binding. `celigo profile use` is machine-wide and redirects every other terminal and agent session on the machine. Neither is what binds the tree to an account — that is the manifest, checked by `pull`, `diff`, and `push` however the profile was selected.

## The account binding

The manifest records two facts about the account a tree was pulled from: the API host (`baseUrl`) and the token owner's user id (`accountUserId`, from `GET /v1/tokenInfo`). Every `pull` into an existing tree, every `diff`, and every `push` checks both; `push --dry-run` checks the host offline and, whenever a token is configured and the plan has items, runs the same identity check as the push, before its first plan line. The verdicts, in order:

| The selected token…                                                                                                                                             | Result                                                                                                                                                                                                                                                                                                                                                                                  |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| targets another host (`api.eu…`, a forgotten `CELIGO_BASE_URL` defaulting to production)                                                                        | **Refused** before any request: `This tree is bound to <baseUrl> (account user <id>) but profile '<p>' targets <url> — refusing to push to a different account …`                                                                                                                                                                                                                       |
| same host; `tokenInfo` answers with a user id that differs from the manifest's                                                                                  | **Refused** before anything of that account is read or written: `This tree is bound to account user <id> but the token of profile '<p>' belongs to <other> …`                                                                                                                                                                                                                           |
| same host; `tokenInfo` answers with the manifest's user id                                                                                                      | Allowed. A different profile name adds `note: same account — the token of profile '<p>' belongs to this tree's account user (<id>).`                                                                                                                                                                                                                                                    |
| same host; the tree never recorded a user id (pulled while the probe was failing); the selected profile is the one the tree was pulled with                     | Allowed and **bound**: the id is recorded and checked from then on, announced as `note: bound this tree to account user <id> …`. `diff` never binds — it writes nothing.                                                                                                                                                                                                                |
| same host; the tree never recorded a user id; the selected profile is a **different** profile                                                                   | `push` and `push --dry-run` are **refused** — `refusing to push: a stale --profile for another account on <host> looks exactly like this` — naming two exits: `--allow-account-mismatch` to bind the tree to this account (then commit the bound manifest so CI needs no flag), or a re-pull with the recorded profile. `pull` and `diff` warn and continue on the host match, unbound. |
| same host; `tokenInfo` fails (a known platform issue makes it return `500 Internal Server Error` for some production tokens)                                    | Allowed on the host match alone: `warning: same host (account identity could not be verified — GET /v1/tokenInfo failed: …); relying on the host match (<host>).`                                                                                                                                                                                                                       |
| same host; `tokenInfo` answers without naming the account — no `ownerUser`, the answer an OAuth sign-in gets when the platform cannot confirm its account grant | Treated as a failed probe (since celigo-cli 2026.10.1): allowed on the host match, never bound or compared, with `warning: same host (account identity could not be verified — GET /v1/tokenInfo named no account: no ownerUser in the answer); relying on the host match (<host>).`                                                                                                    |

So the binding detects a wrong host with certainty, and a wrong account on the same host whenever the token's `tokenInfo` works. It cannot detect a wrong account on the same host for a token whose `tokenInfo` fails or names no account; there, updates still fail harmlessly (the lock `GET` finds no such resource), but a create would land in the other account, and `--force-remote` skips the lock `GET`. The probe runs on every binding command and nothing about a failed probe is persisted, so the check takes effect the day the endpoint answers. Until then, for production tokens: read the warning, keep one profile per shell (`CELIGO_PROFILE`), and prefer env-only CI — one token, one base URL, no profile to get stale — for anything that writes.

`--allow-account-mismatch` on `pull` and `push` is the only override (none on `diff`). It prints the target it is about to touch, and binding under it also re-records the host.

## After a push, the file is the server's copy

Updates and creates alike write the server's response back and record its hash in the manifest, so `status` is clean *because* the tree is server-identical, a typed secret is masked again immediately, and the pull right after a push is a no-op. Two edits the tree cannot carry. `_registeredConnectionIds`, `_registeredLookupCacheIds`, and `_registeredTopicIds` are ignored by `PUT` (use `celigo integrations register-connections` / `register-lookup-caches` / `register-topics`; push reports the drop as `note: … server dropped …`); the one exception, since celigo-cli 2026.10.1, is what the integration's `aliases[]` need, which push registers itself before the `PUT` and the plan announces. And an Integration App's documents are pull-only: its settings values are saved through the app's connector code — `celigo push --allow-connector-settings` for the ones edited in its `integration.json` (since celigo-cli 2026.10.1), or `celigo integrations persist-settings` one at a time.

**Secrets.** Type the real value in place of `"******"`, push, and the write-back masks it again. Between the edit and the push the plaintext is on disk — do not commit in between. `celigo diff` renders it as `****** → (set)`, in JSON output too.

## Branches, review, rollback

`main` mirrors the account. Work happens on feature branches; the pull request is the review; a merge deploys.

* **Feature commits touch resource files only.** The manifest changes in exactly two places: the deploy job's write-back commit after a `celigo push`, and a release manager's "sync" commit after a `celigo pull` (drift from the UI, a rename, a resource created there). Keeping the manifest out of feature commits keeps `git revert <feature>` a valid rollback and keeps merges off the token lines.
* **The diff is the review artifact.** Files are canonical JSON — stable key order, volatile fields stripped — so a two-edit PR is two files with a few changed lines. For "what will this do to the account", post `celigo push --dry-run` and `celigo diff --exit-code` output as the PR comment. `diff` with no refs compares only the files the PR touched; to check specific resources against the account, name them.
* **Rollback = revert the feature, then deploy.** `git revert <merge>` and let the deploy job push it, or by hand: `git checkout <good> -- exports imports integrations scripts connections && celigo push`. A *content* checkout, never a wholesale `git checkout <old>`: that also rewinds the manifest, every hash then matches, and `status` and `push` see nothing to do while the server differs.
* **Merge conflicts on the manifest** are resolved the same way every time: take one side, then run `celigo pull` — the manifest is derived state, and a pull rebuilds the hashes and tokens for whatever the tree holds. A half-merged resource file — a JSON document, a `script.js`, an `instructions.md` — is named as such by every verb, with the line of the first conflict marker; `lint`, `push --dry-run`, and `diff` fail on it rather than skipping it. A half-merged `.link.json` pointer sends nothing and is rewritten by the next pull, but `status` lists it as `unreadable`.

## One writer per tree at a time

The committed manifest is a lock **across time**: a push from a checkout whose tokens are stale — because someone else pushed and committed the write-back since — is refused per file (`remote changed since pull …`), and after merging their write-back commit it succeeds. It is **not** a lock across simultaneous pushes from the same baseline: the platform has no conditional write, so two clones pushing the same edits at the same moment can both report `updated` while one silently loses. Push re-reads each document once after its `PUT` and, when the server already holds something else, says so (`warning: <path> was overwritten by another writer right after this push …`) and records the server's hash so `status` shows the file modified — that catches a writer landing inside the `PUT`→`GET` gap, not one landing later. Therefore:

* Deploy from CI, not from laptops, for any tree more than one person edits; the deploy job's `concurrency` group below serializes it.
* Within one clone, two concurrent `pull`/`push` runs are refused by the tree lock (`.celigo/local/lock`); a lock whose process is gone, or older than six hours, is taken over automatically, and `--force-lock` is the escape for a run that died on another machine sharing the directory.
* Interrupting a push is safe: updates and deletes are idempotent, and creates record their intent before the `POST`, so the next run adopts them instead of duplicating them. Since celigo-cli 2026.10.1 a lookup cache's data sync records its writes before the first one, so the next run finishes it, and a pull or push killed in the middle of a rename leaves the resource [`unfinished`](/cli/local-tree/status-and-diff.md#celigo-status) rather than missing, so a later `push --prune` cannot delete it; `celigo pull` finishes it.
* The same new files pushed from two clones are the case no intent record can see, so push asks the server immediately before each `POST`: an untracked namesake holding exactly this document, or one in the same integration, refuses the create (`… another clone of this tree pushed the same file? Run celigo pull to track it, then remove the draft …`). The residual window is two clones running the same push in lockstep. It is a mitigation, not a lock — deploy from CI, where the `concurrency` group is the lock.

## CI: validate on pull requests, deploy on merge

Both jobs are env-only: a token plus `CELIGO_BASE_URL` is a complete identity, with no config file and no `profile add`, because the tree is bound to the account and not to a profile name. The PR check needs a read-mode token; the deploy needs a full-mode one. The CLI detects CI (`CI`, `GITHUB_ACTIONS`, `GITLAB_CI`, `JENKINS_URL`, `CIRCLECI`, `BUILDKITE`, …) and skips its background tasks — the update check, the skills install, and the lint schema refresh.

Since celigo-cli 2026.10.1 `CELIGO_MODE` only tightens a profile's mode: a session runs in the stricter of the profile's stored mode and the variable. An env-only runner has no profile, so the `full` default applies there and the variable names the ceiling each step runs under, as the jobs below set it; on a machine with a profile stored in `read`, `CELIGO_MODE=full` does not lift it. See [Permission modes](/cli/getting-started/profiles.md#permission-modes).

`celigo profile whoami` prints the `GET /v1/tokenInfo` envelope, which since the platform's October release names the account owner and the acting user, emails included. The jobs below project what a log needs with `--jq`.

> ⚠️ **Store tokens in the CI secret store.** Never commit a token or pass `--token` on a command line that gets logged. Both token types from **Resources → API tokens** work; a personal access token carries its user's full permissions and expires after 90 days by default, so for CI prefer an account-level API token with a custom scope.

```yaml
name: celigo-pr-check
on:
  pull_request:
    branches: [main]
permissions:
  contents: read
jobs:
  validate:
    runs-on: ubuntu-latest
    env:
      CELIGO_API_TOKEN: ${{ secrets.CELIGO_API_TOKEN }}      # a read-mode token is enough here
      CELIGO_BASE_URL: ${{ secrets.CELIGO_BASE_URL }}        # https://api.integrator.io, api.eu…, api.au…, api.ca…
      CELIGO_FORMAT: table
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm install -g @celigo/celigo-cli
      - name: Who am I (proves the token and shows the account)
        # GET /v1/tokenInfo can return 500 for some production tokens; the steps below make
        # real calls with the token, so a bad token still fails the job there.
        continue-on-error: true
        # tokenInfo also names the account owner and the acting user, emails included; the
        # projection keeps them out of the job log.
        run: |
          celigo profile whoami --jq '{_userId, account: .ownerUser.company,
            environment: .environment.name, credential: .token.type}'
      - name: Lint (offline; a half-merged or unparseable file is an error, not a skip)
        run: celigo lint
      - name: Local changes since the last pull (offline; informational on a PR that edits resources)
        run: celigo status --exit-code || true
      - name: What this PR changes on the account (only files the PR touched)
        run: celigo diff --exit-code
        # The PR's changes are SUPPOSED to differ, so this step exits 1 by design; read the diff,
        # not the annotation.
        continue-on-error: true
        env:
          CELIGO_MODE: read
      - name: Plan (gated like push; exit 1 when any item is refused)
        run: celigo push --dry-run --format json
        env:
          CELIGO_MODE: full
```

The deploy job **must commit the manifest write-back**: a successful push refreshes the lock tokens, and without that commit the next deploy touching the same resource is rejected as `remote changed since pull`.

```yaml
name: celigo-deploy
on:
  push:
    branches: [main]
    paths-ignore: [".github/**"]
permissions:
  contents: write                                   # the manifest write-back commit
concurrency:
  group: celigo-deploy-prod
  cancel-in-progress: false
jobs:
  deploy:
    runs-on: ubuntu-latest
    # A push made with the default GITHUB_TOKEN never triggers `on: push`, so the write-back
    # commit below starts no run by itself. The guard matters when the write-back is pushed
    # with a PAT or a bot token, which WOULD trigger a run.
    if: "${{ !startsWith(github.event.head_commit.message, 'ci celigo write-back') }}"
    env:
      CELIGO_API_TOKEN: ${{ secrets.CELIGO_API_TOKEN_FULL }}
      CELIGO_BASE_URL: ${{ secrets.CELIGO_BASE_URL }}
      CELIGO_MODE: full
      CELIGO_FORMAT: table
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.ref_name }}
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm install -g @celigo/celigo-cli
      - name: Who am I (a deploy with nothing to push makes no API call; this proves the token every run)
        continue-on-error: true
        # tokenInfo also names the account owner and the acting user, emails included; the
        # projection keeps them out of the job log.
        run: |
          celigo profile whoami --jq '{_userId, account: .ownerUser.company,
            environment: .environment.name, credential: .token.type}'
      - run: celigo push --dry-run
      - name: Push (no --prune, no creates from CI; exit 1 on any failed file)
        # `shell: bash` turns on pipefail, so a failed file fails the step through tee.
        shell: bash
        run: celigo push --yes --no-create --format json | tee push-report.json
      - name: The tree is server-identical after the push (the write-back landed)
        run: celigo status --exit-code
      - name: Commit the manifest write-back
        run: |
          git config user.name celigo-ci
          git config user.email celigo-ci@users.noreply.github.com
          git add .celigo/manifest.json
          git diff --cached --quiet && exit 0
          git commit -m "ci celigo write-back after deploy of ${GITHUB_SHA::7}"
          git push origin HEAD:${GITHUB_REF_NAME}
```

Every write prints `celigo: profile 'default' → api.integrator.io` on stderr first, so the target sits in the job log next to the write, and the push refuses outright if the token's account is not the manifest's. Branch protection that blocks the write-back push: run the deploy from a bot account allowed to bypass it, or protect a release branch and deploy from `main`. Pass `--dir <path>` on every command when the tree is not the repository root. Without `--prune`, nothing is ever deleted and no confirmation prompt is triggered; updates whose server copy changed since the pull are rejected — re-pull and re-commit the tree rather than passing `--force-remote` in CI.

A tree pulled while the identity probe was failing has no recorded user id, and only the profile it was pulled with can bind it. CI's built-in `default` profile is a different name, so on such a tree a CI `push` continues on the host match while the probe keeps failing — and refuses the day the probe answers. Bind it once from a developer machine with the pulling profile (`celigo pull`, then commit `.celigo/manifest.json`) or, deliberately, with `celigo push --allow-account-mismatch` in the job, and commit the bound manifest so the flag can come off.

### Which gates are usable today

| Gate                        | Usable  | Notes                                                                                                                                                                                                                                                                                                                                                                                                                        |
| --------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `celigo lint`               | yes     | Errors: the guards a create introduces, invalid or half-merged files, the `responseMapping` wrapper, and — since celigo-cli 2026.10.1 — the logic rules (a hook naming a function its script lacks, a script that does not parse, a `lookupName` with no lookup, a dangling `nextRouterId`, a Handlebars template that does not compile), which are errors only in files the change touched and warnings on a pristine pull. |
| `celigo lint --strict`      | not yet | The published schemas are stricter than the API on real data — a pristine pull of a real account produces warnings, so `--strict` fails everything until the specs catch up.                                                                                                                                                                                                                                                 |
| `celigo push --dry-run`     | yes     | Exit `1` when any planned item is refused — since celigo-cli 2026.10.1 a logic-lint error on a planned document included, which `--allow-lint-errors` prints as allowed instead; `0` when changes are merely withheld. Schema findings never gate a push.                                                                                                                                                                    |
| `celigo status --exit-code` | yes     | Git-style: informational without the flag.                                                                                                                                                                                                                                                                                                                                                                                   |
| `celigo diff --exit-code`   | yes     | Git-style: informational without the flag.                                                                                                                                                                                                                                                                                                                                                                                   |

## Promote on merge: two tokens

A deploy job that promotes needs the **target's** full-mode token and nothing else — the tree's source token is not used. The two identities have two names, and the CLI keeps them apart by variable:

| Variable                                            | Effect                                                                                                                                                                                                                                                                                                                                                               |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CELIGO_API_TOKEN`, `CELIGO_BASE_URL`               | The **source** session: what `pull`, `diff`, and `push` use. Promote itself never uses them, but a `push --dry-run` gate in the same job does.                                                                                                                                                                                                                       |
| `CELIGO_TARGET_API_TOKEN`, `CELIGO_TARGET_BASE_URL` | The **target** that `--to` names, on a runner with no profile of that name. `CELIGO_TARGET_BASE_URL` defaults to `https://api.integrator.io`. A `note:` says the variable stands in.                                                                                                                                                                                 |
| `CELIGO_MODE`                                       | Tightens both sides and never loosens either (since celigo-cli 2026.10.1). Exported job-wide as `read` — for a `diff` step, say — it tightens the promote's target to `read` too, and the promote is refused before any request: set it on the steps that only read, not job-wide. A target with no profile on the runner is `full` unless the variable tightens it. |
| `CELIGO_CONCURRENCY`                                | Requests in flight at once for `pull`, `diff`, `push`, and `promote` (1–16, default 8).                                                                                                                                                                                                                                                                              |

A job that sets only `CELIGO_API_TOKEN` and runs `promote --to production` is refused (`target profile 'production' has no API token on this machine and CELIGO_TARGET_API_TOKEN is unset …`) rather than writing the tree back into the source account under the target's name. `--to production` resolves to the committed `.celigo/targets/production.json` first — its recorded profile name is a label, its recorded host and account user are what is verified — and, unlike `push`, a target already bound to an account user is refused when the identity probe fails, not carried on the host match. The target variables stand in for a profile only behind that committed file: run the first promote on a machine where the target has a profile, commit `.celigo/targets/production.json`, and a runner with only the variables promotes to it from then on. With neither the file nor a profile, the promote is refused and says so.

```yaml
name: celigo-promote
on:
  push:
    branches: [main]
permissions:
  contents: write                                   # the target-file write-back commit
concurrency: celigo-promote-production            # one writer per target at a time
jobs:
  promote:
    runs-on: ubuntu-latest
    env:
      CELIGO_TARGET_API_TOKEN: ${{ secrets.CELIGO_PRODUCTION_TOKEN }}   # the TARGET's full-mode token
      CELIGO_TARGET_BASE_URL: ${{ secrets.CELIGO_PRODUCTION_BASE_URL }} # omit for https://api.integrator.io
      CELIGO_FORMAT: table
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm install -g @celigo/celigo-cli
      - run: celigo promote --to production --dry-run      # exit 1 on any refusal: stop here
      - run: celigo promote --to production -y
      - name: Commit the target file write-back
        run: |
          git config user.name celigo-ci
          git config user.email celigo-ci@users.noreply.github.com
          git add .celigo/targets/production.json
          git diff --cached --quiet || git commit -m "Promote to production [skip ci]" && git push
```

Two-stage pipelines (merge → sandbox, tag → production) are two such jobs with two target files and two secrets. The PR check stays as above against the source, plus `celigo promote --to sandbox --dry-run` or `celigo diff --target sandbox` when the sandbox token is available to the PR job as `CELIGO_TARGET_API_TOKEN` — the two pairs sit side by side in one job without crossing. `promote --dry-run` needs full mode on the target like the promote itself, so keep `CELIGO_MODE: read` on the steps that only read, as the PR check above does for its `diff` step; `diff --target` reads in any mode. Keep one writer per target: promote holds the tree lock like push, and the workflow-level `concurrency` key keeps two merges from racing.

## Related

* [The local tree](/cli/local-tree.md) — the tree, its layout, what it never carries.
* [Push changes](/cli/local-tree/push.md) — the plan, the lock, the write-back.
* [Promote to another account](/cli/local-tree/promote.md) — the target file and the first promote into an existing account.
* [Neutral trees](/cli/local-tree/neutral-trees.md) — one branch per environment.
* [Scripting patterns](/cli/using-the-cli/scripting.md) and [Configuration reference](/cli/getting-started/configuration.md) — the environment variables and CI recipes for the rest of the CLI.
