# CI/CD

Running the CLI from a pipeline. The pieces you need are environment-variable
authentication, `--non-interactive`, `--json`, and exit codes.

## Authentication

`sinch auth login` prompts for a project ID, key ID and key secret, then writes
the secret to the OS keychain — neither of which works on a runner with no stdin
and no keychain. Set the three environment variables described in
[Configuration → Environment variable override](./configuration.md#environment-variable-override)
instead, and the CLI skips both the keychain and the login step entirely.

Of the three, only `SINCH_KEY_SECRET` is sensitive — mask it. The other two are
identifiers.

## Deploying a function

Every command works from the function's own directory. `deploy` reads
`sinch.json` out of the working directory and takes the name from it, so the job
only has to be in the right folder:

```sh
cd my-function
sinch functions deploy --non-interactive
```

There is a `--name` flag, but pass it only when you deliberately want to deploy
under a different name than `sinch.json` says. In a pipeline it is a liability:
rename the function in the repo and the job keeps deploying to the old name
without complaining.

`--non-interactive` answers every prompt with its default rather than blocking on
stdin, which a runner does not have. Without it the command hangs until the job
times out.

Deploy waits for the deployment to finish and exits non-zero if it fails. Add
`--no-wait` to return as soon as the upload is accepted — useful when a later
job polls, but it means a green step no longer means a running function.

Other flags that matter in a pipeline:

| Flag        | Effect                                                  |
| ----------- | ------------------------------------------------------- |
| `--public`  | Deploy to the public namespace (internet-accessible)    |
| `--private` | Deploy to the private namespace (internal access only)  |
| `--force`   | Deploy despite validation warnings instead of prompting |
| `--no-docs` | Skip doc generation                                     |

Pass `--public` or `--private` explicitly. Relying on the default means the
namespace can change under you without the pipeline saying so.

## GitHub Actions

```yaml
name: Deploy function

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm install -g @sinch/cli

      - name: Deploy
        env:
          SINCH_PROJECT_ID: ${{ secrets.SINCH_PROJECT_ID }}
          SINCH_KEY_ID: ${{ secrets.SINCH_KEY_ID }}
          SINCH_KEY_SECRET: ${{ secrets.SINCH_KEY_SECRET }}
        working-directory: my-function
        run: sinch functions deploy --non-interactive --public
```

## GitLab CI

```yaml
deploy:
  image: node:20
  only:
    - main
  before_script:
    - npm install -g @sinch/cli
  script:
    - cd my-function
    - sinch functions deploy --non-interactive --public
```

Set `SINCH_PROJECT_ID`, `SINCH_KEY_ID` and `SINCH_KEY_SECRET` under
**Settings → CI/CD → Variables**, with `SINCH_KEY_SECRET` marked _Masked_.

## Reading output in a script

`--json` prints the result to stdout with no spinner or table, so it can be
piped straight into `jq`:

```sh
sinch functions list --json | jq -r '.functions[] | select(.status != "Running") | .name'
sinch functions status --json | jq -r '.deployment.status'
sinch templates list --runtime node --json | jq -r '.templates[].name'
```

`functions deploy --json` prints one object and nothing else, so a pipeline can
pick the deployed URL straight out of it:

```sh
URL=$(sinch functions deploy --non-interactive --public --json | jq -r '.url')
curl --fail "$URL"
```

```json
{
  "name": "my-function",
  "functionId": "01M10PWQBM37KJSGJW0CZTP3CF",
  "runtime": "node",
  "status": "Running",
  "accessLevel": "public",
  "url": "https://supreme-hertz.fn.sinch.com"
}
```

`warnings` carries anything pre-flight validation flagged — a `.env` tracked in
git, for instance. The messages also go to stderr, so they are visible in the job
log even though stdout stays a single document. The key is absent when there are
none, so a job can fail closed on them:

```sh
sinch functions deploy --non-interactive --json | jq -e '.warnings == null'
```

`status` is the settled state — `--json` waits for the function to reach
`Running` or `Failed` before printing, so a URL in the payload is a URL you can
call. Under `--no-wait` there is no URL yet and the field is omitted rather than
reported as null.

On failure the object is `{"error": "...", "exitCode": 101}`, printed to stdout
with the process still exiting on that code.

## Update checks in CI

The CLI never checks for or installs updates in CI. The check is skipped when any of these is true:

- `CI` is set, or the process is running in Docker
- stdout is not a TTY (so anything piped or redirected)
- `--json` or `--non-interactive` is present

That is what keeps `--json` output clean: the update notice is written from module load, long
before a command parses its flags, so it is suppressed at the source rather than filtered later.

If you want to be explicit anyway, or you are running somewhere that does not set `CI`:

```yaml
env:
  SINCH_NO_AUTO_UPDATE: '1'
```

Pin an exact version in CI regardless, so a pipeline cannot change behaviour because a new release
landed:

```sh
npm install -g @sinch/cli@0.5.8
```

## Exit codes

The CLI follows BSD `sysexits` conventions plus a Sinch-specific range, so a
pipeline can tell a bad credential apart from a bad request.

| Code | Meaning         | Usual cause in CI                           |
| ---- | --------------- | ------------------------------------------- |
| 0    | Success         |                                             |
| 1    | Generic failure |                                             |
| 2    | Usage           | Malformed command line                      |
| 64   | Usage error     | Bad flag or argument                        |
| 65   | Data error      | Malformed credentials file                  |
| 66   | No input        | A named file does not exist                 |
| 69   | Unavailable     | API unreachable                             |
| 70   | Internal error  |                                             |
| 73   | Cannot create   |                                             |
| 74   | I/O error       |                                             |
| 77   | No permission   | Key lacks access to the project             |
| 78   | Config error    | Missing or contradictory configuration      |
| 100  | Auth required   | The three environment variables are not set |
| 101  | Auth failed     | Wrong or revoked key secret                 |
| 102  | Not found       | No such function or template                |
| 103  | Conflict        | Name already taken                          |
| 104  | Timeout         | Deployment did not become ready in time     |

`100` and `101` are the two worth branching on: both mean the pipeline's
credentials are wrong, and no retry will help.

```sh
sinch functions deploy --non-interactive
case $? in
  0)   echo "deployed" ;;
  100|101) echo "check SINCH_KEY_ID / SINCH_KEY_SECRET"; exit 1 ;;
  104) echo "deploy timed out; check sinch functions status"; exit 1 ;;
  *)   echo "deploy failed"; exit 1 ;;
esac
```

## Secrets a function needs at runtime

`sinch secrets add` writes to the OS keychain on the machine that runs it, which
is not where your deployed function reads from. Function secrets are set through
`sinch.json` and the deploy flow, not from the pipeline's own keychain. Do not
add a `sinch secrets add` step to CI expecting the deployed function to see it.

## Debugging a failing job

```sh
SINCH_DEBUG=2 sinch functions deploy --non-interactive
```

Level 2 logs HTTP method, URL and status; level 3 adds headers and response
bodies, redacted and truncated.

## Log output

Progress reporting animates only into a terminal. With no TTY — any CI runner,
or a local `> file` — each step prints once as a plain line instead, so the log
does not fill with spinner frames and cursor escapes. Nothing to configure.

Under `--json` there is no progress output at all: stdout carries the one
document and stderr stays empty. See [Configuration](./configuration.md) for the
full table.

## See also

- [Configuration](./configuration.md) — profiles, config file, global flags
- [functions command reference](./commands/functions.md)
