docs

Deploy from the CLI

Declare deployments from CI with unhandled deploy, including flags, environment variables, CI auto-detection, exit codes and examples.

unhandled deploy is the shortest way to declare a deployment from a pipeline. It ships in the @unhandled/cli package, reads what it can from your CI provider, and sends one request to the Deployments API. It needs Node 24 or later. For installation and the other commands in the package, see CLI.

Add the package as a dev dependency so CI installs it with the rest of your project:

Terminal
npm install --save-dev @unhandled/cli
npx --no-install unhandled deploy --service shop-api --environment production

--no-install makes npx fail instead of fetching an unrelated package if the dependency is missing.

Requirements

  • A deploy key for the project, created by an admin under Settings > Deploy keys. It starts with dk_. A DSN key does not work here.
  • A version or a commit. The command uses --version or UNHANDLED_VERSION when set, and finds the commit on its own in CI or in a local git checkout.

Flags

FlagDefault or fallbackNotes
--service <name>noneRequired. The service's telemetry name.
--environment <slug>UNHANDLED_ENVIRONMENTRequired one way or the other. production, staging or development.
--version <version>UNHANDLED_VERSIONOptional.
--commit <sha>CI provider, then local .gitSent as commitSha.
--status <status>succeededstarted, succeeded, failed or canceled.
--started-at <time>noneISO 8601 with a timezone, Z or an offset like +02:00.
--finished-at <time>noneSame format as --started-at.
--external-id <id>derived from the CI runOutside CI, a fresh local: ID per call.
--actor <actor>the CI user
--ci-url <url>the CI run URL
--api-key <key>UNHANDLED_API_KEYThe deploy key.
--url <url>UNHANDLED_URL, then https://unhandled.netBase URL. The command appends /api/v1/deployments.
--strictoffExit 1 on warnings instead of 0.
--dry-runoffPrint the payload and where each field came from. Sends nothing.

A flag always wins over a value detected from CI or git. Passing a flag with an empty value is an error.

The repository URL is only sent when a CI provider is detected; there is no flag for it. The CLI does not send metadata. Use the API directly if you need either.

Environment variables

VariableUsed for
UNHANDLED_API_KEYDeploy key, when --api-key is not passed.
UNHANDLED_ENVIRONMENTEnvironment, when --environment is not passed.
UNHANDLED_VERSIONVersion, when --version is not passed.
UNHANDLED_URLBase URL, when --url is not passed. A self-hosted instance sets it to its APP_URL, not the DSN host.

Empty variables count as unset.

CI detection

The command recognizes six CI providers and reads the commit, actor, run URL, repository URL and an external ID from their environment:

ProviderDetected byCommit from
GitHub ActionsGITHUB_ACTIONS=trueGITHUB_SHA
GitLab CIGITLAB_CI=trueCI_COMMIT_SHA
CircleCICIRCLECI=trueCIRCLE_SHA1
BuildkiteBUILDKITE=trueBUILDKITE_COMMIT
Bitbucket PipelinesBITBUCKET_BUILD_NUMBER is setBITBUCKET_COMMIT
Cloudflare Workers BuildsWORKERS_CI=1WORKERS_CI_COMMIT_SHA

The external ID combines the provider, the run (and attempt or job where the provider has one), the service and the environment. Two calls in the same run for the same service and environment share it, so a started call followed by a succeeded call updates one deployment instead of creating two. A GitHub Actions re-run has a new attempt number and creates a new deployment. If your steps would derive different IDs, pass the same --external-id to both.

Outside CI, the command reads the commit from the .git directory without running git, including worktrees and packed refs. It generates a new external ID for every call.

The detected commit is always sent as commitSha, never as version. If neither a version nor a commit is found, the command warns and sends nothing.

Exit codes and retries

The command is built not to break your deploy. A missing API key, a missing version and commit, a network failure or an error response print a warning: line and exit 0. With --strict the same cases exit 1.

Usage errors always exit 1: an unknown flag, a missing --service or environment, an invalid --status, a timestamp without a timezone, an invalid --url, or an empty flag value.

Each request times out after 5 seconds. After a network error, a timeout or a 5xx response, the command retries once. It never retries a 4xx. On success it prints the deployment ID, status, whether it merged with a detected release, and a link.

Dry run

--dry-run resolves every field, prints the request it would send and the source of each value, and exits. It needs no API key, which makes it useful for checking CI detection on a branch.

Terminal
npx --no-install unhandled deploy --service shop-api --environment staging --dry-run

The source column shows flag, env:<NAME>, ci:<provider>, git, default or generated.

GitHub Actions example

This workflow deploys a storefront API, declares the deployment as started, and reports the outcome either way.

yaml
name: release
on:
  push:
    branches: [main]
jobs:
  ship:
    runs-on: ubuntu-latest
    env:
      UNHANDLED_API_KEY: ${{ secrets.UNHANDLED_DEPLOY_KEY }}
      UNHANDLED_ENVIRONMENT: production
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
      - run: npm ci
      - run: npx --no-install unhandled deploy --service shop-api --status started
      - id: rollout
        run: ./scripts/rollout.sh
      - if: success()
        run: npx --no-install unhandled deploy --service shop-api --status succeeded
      - if: failure() && steps.rollout.outcome == 'failure'
        run: npx --no-install unhandled deploy --service shop-api --status failed

The three calls share the run's external ID, so the history shows one deployment that moves from started to succeeded or failed.

GitLab CI example

yaml
deploy:
  stage: deploy
  image: node:24
  rules:
    - if: $CI_COMMIT_TAG
  variables:
    UNHANDLED_ENVIRONMENT: staging
  script:
    - npm ci
    - export UNHANDLED_VERSION="$CI_COMMIT_TAG"
    - npx --no-install unhandled deploy --service shop-worker --status started
    - ./scripts/rollout.sh
    - npx --no-install unhandled deploy --service shop-worker --status succeeded

The job only runs for tags, so the tag doubles as the version. Store the deploy key as a masked CI/CD variable named UNHANDLED_API_KEY.