# CI image deploy (tested digest)

Deploy the **exact container image your CI tested** to an existing
image-backed Bex service. Source pin: Bex `0eb2036e2`
(`deploys/rest.go` `imageUrl`, store deploy statuses).

Downloadable from the public site (no private CMS checkout required):
[`/examples/ci-image-deploy/`](https://bex.co/examples/ci-image-deploy/README.md).

## Prerequisites

1. An **existing image-backed** service (`runtime: image`) with working
   registry pull credentials — see [Docker deploys](https://bex.co/docs/platform/docker-deploys).
   Repository-backed services reject `imageUrl`; they rebuild from source.
2. A workspace **machine API key** (developer role) — see
   [REST & GraphQL API](https://bex.co/docs/platform/api).
3. Registry **push** credentials for CI (for example `GITHUB_TOKEN` → GHCR).
   Push permission is separate from Bex runtime **pull** credentials.
4. Billing/setup required by your installation for deploys — see
   [Billing setup](https://bex.co/docs/platform/billing-setup) when hosted.

## Files

| Path | Role |
| --- | --- |
| `deploy.mjs` | Machine-token exchange, `POST …/deploys` with `imageUrl`, poll that deployment ID |
| `.github/workflows/deploy-tested-image.yml` | Build → test **that** image → push digest → deploy |
| `Dockerfile` + `app/` | Minimal Node sample under test |
| `fixtures/stack.mjs` | Offline loopback API (no live Bex resources) |
| `scripts/handoff-harness.mjs` | Offline proof that failed tests skip deploy |

## Local offline check

```bash
BEX_CI_FIXTURE=1 BEX_IMAGE_URL='ghcr.io/example/app@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \
  node deploy.mjs

node scripts/handoff-harness.mjs
```

Fixture tests do **not** create hosted deployments or push to a registry.

## Adopt the workflow in another repo

1. Copy this directory (or curl the public files) into your application repo.
2. Set repository variables `BEX_SERVICE_ID` (and optional API/token origins).
3. Set secrets `BEX_KEY_ID` and `BEX_KEY_SECRET`.
4. Ensure GHCR (or your registry) package write permission is available to Actions.
5. Keep the workflow's single-platform scope (`linux/amd64`) unless you test every platform you publish.

Concurrency is `bex-image-deploy-<repo>-<serviceId>` with
`cancel-in-progress: false`. That orders this workflow's runs for one service;
it does not disable dashboard, hook, or other CI deploys. Canceling a GitHub job
does **not** cancel or roll back the platform deployment — use the printed
`deploymentId` and [logging](https://bex.co/docs/platform/logging) /
[rollbacks](https://bex.co/docs/platform/rollbacks).

## Client behavior

- Rejects tag-only / mutable refs; requires `@sha256:<64-hex>`.
- Exchanges `client_credentials` for a bearer token; never prints secrets.
- Submits `{ "imageUrl": "<digest-ref>" }` once. Ambiguous POST transport
  failures exit nonzero and are **not** auto-retried.
- Polls `GET /v1/services/{serviceId}/deploys/{deploymentId}` until `live`, a
  known terminal failure (`build_failed`, `pre_deploy_failed`, `update_failed`,
  `canceled`), unknown status, auth failure, or deadline.

## Live deploy (your credentials)

```bash
cp .env.example .env
# fill BEX_KEY_ID, BEX_KEY_SECRET, BEX_SERVICE_ID, BEX_IMAGE_URL=@sha256:…
set -a && source .env && set +a
node deploy.mjs
```
