# Scripts and CI

After this guide, the CLI runs in a pipeline without a profile file. Your script reacts to exit codes instead of text, fetches lists completely, limits wait times, and retries calls that change something without triggering an operation twice.

> **Type:** guide
> **For:** Anyone who runs the CLI in a CI pipeline or a cron job
> **Prerequisite:** the built CLI available in the run, the base URL of the environment, and an API key `ak_…` with the permissions of the commands, stored as a CI secret and not as a file
> **Where:** command line

## Steps

1. Store the address and key as secrets of your CI and pass them into the run as `SZENERA_BASE_URL` and `SZENERA_API_KEY`. Together, the two variables carry a complete profile. No file on disk is needed, and the profile is not write-protected.

2. Call the CLI directly through the built file, never through `pnpm run`, or your script sees the code `1` for every error:

   ```bash
   export SZENERA_BASE_URL="https://<basis-adresse>"
   export SZENERA_API_KEY="$CI_GEHEIMNIS_SIGNAGE"
   szenera() { node packages/cli/dist/main.js "$@"; }
   ```

3. At the start of every pipeline, check the environment and the credential with two calls that separate network from key:

   ```bash
   szenera health --json > /dev/null || { echo "Umgebung nicht bereit"; exit 1; }
   szenera whoami --json | jq -r '.tenantId'
   ```

4. Evaluate the exit code, not the message. On failure, standard output carries nothing, so it can safely go into a file:

   ```bash
   szenera asset upload --file=plakat.png --json > ergebnis.json
   case $? in
     0)     echo "fertig: $(jq -r '.id' ergebnis.json)" ;;
     8|10)  sleep 30; echo "später erneut versuchen" ;;   # throttled or no network
     11)    echo "Vorgang endete rot — Befund lesen"; exit 1 ;;
     12)    echo "Zeitlimit — erst nachsehen, nicht blind wiederholen"; exit 1 ;;
     *)     exit 1 ;;
   esac
   ```

5. Process the JSON output with `jq`. Lists carry `items` and, as long as pages follow, `nextCursor`:

   ```bash
   szenera device list --json | jq -r '.items[] | select(.status=="active") | .id'
   ```

## Options

**Fetch lists completely.** `--all` follows all pages until there are no more and returns one concatenated list without `nextCursor`. For large collections, combine it with `--limit=200`, the cap per page:

```bash
szenera scene list --all --limit=200 --json | jq -r '.items[].id'
```

**Limit wait times.** Commands that wait for an operation (`asset upload`, `asset reprocess`, `device screenshot`, `device send-command --wait`) accept `--timeout=<dauer>` (`90s`, `20m`, `2h`, or a number in seconds) and some also `--interval=<dauer>`. `--no-wait` only triggers the operation and returns immediately, without knowing the outcome. Exit 12 always means that the outcome is unknown and the operation may still be running.

**Make retries safe.** For the escape hatch `api post|put|patch|delete`, the CLI generates a fresh idempotency key for every call. `--idempotency-key=<schluessel>` is needed only to continue an aborted retry, and then with the same value as on the first attempt. Commands that are idempotent by themselves say so in their help: sending `player set-program` twice is not a second operation.

```bash
szenera api post /v1/playlists @playlist.json --idempotency-key="import-${CI_LAUF_ID}"
```

**Protection against irreversible actions.** Commands that do something irreversible require `--yes` and abort with exit 1 without the flag: `asset purge`, `asset delete`, `scene delete`, `api-key revoke`, and more. `szenera <befehl> --help` names the flag. For profiles in a file, `readOnly: true` remains the lock: a command that changes something then aborts with exit 2 before a network packet is created. A profile made only of environment variables does not have this lock.

## Check the result

- `szenera health` and `szenera whoami` run at the start of the pipeline with exit 0. `whoami` names the expected organization under `tenantId`.
- A deliberately wrong call (`szenera device list --state=active`) ends with exit 1 and a message on standard error. The redirected file stays empty.
- After an upload, `szenera asset get <ast_…> --json` shows the state `ready`.

## If something goes wrong

| Symptom | Meaning | Solution |
|---|---|---|
| Every error ends with `1`, even a 401 | The call runs through `pnpm run`. | Call the file directly through `node` (step 2). |
| `Keine Konfiguration gefunden` (no configuration found) (exit 2) | `SZENERA_BASE_URL` is missing or empty. | Set both variables in the run. See [Profiles and credentials](/cli/profiles-and-credentials). |
| Exit 3 on every command except `health` | The key is not valid: revoked, expired, or issued for another environment. | Create a new key and replace the secret. |
| Exit 4 on a single command | The key does not carry the permission, or the permission is not meant for keys. | Check the “Key” column under [Commands for your organization](/cli/commands-organization). Otherwise use a profile with password sign-in. |
| Exit 12 after an upload | The time limit was too tight. Processing continues. | Set `--timeout=20m` and check with `asset get` before a retry. |

## Related

- [JSON output and exit codes](/cli/output-and-exit-codes)
- [Profiles and credentials](/cli/profiles-and-credentials)
- [Idempotency from the start](/getting-started/idempotency)
