# CI/CD (/guides/operate/ci-cd)

Your deployment must use a contract published in its recuut environment. Automate the contract, generated-code, and application-build checks, and run them with one environment key at a time. The paid test and promotion stay manual; [Go live](/guides/go-live/) covers them.

## Keep deployment values separate

Configure these values in each deployment environment:

| Value | Purpose |
| --- | --- |
| `RECUUT_API_KEY` | Secret test or live key. Keep the two keys in separate secret scopes. |
| `RECUUT_API_URL` | recuut API origin. The HTTP check requires it; the Python SDK defaults to `https://api.recuut.com`. |
| `RECUUT_REVISION_KEY` | Published `rvk_…` revision key for direct HTTP, raw revisions, and the selected-revision check below. |

The snapshot check below applies to every integration. Set `RECUUT_REVISION_KEY` in CI even when the application uses pulled contracts. Pulled-contract applications also run the generated-code check.

## Check the published contract

Choose the example that matches your application. Both call `GET /merchant/v1/contracts?include_resource_key=true`, require the configured revision to exist, and print that revision's stable JSON for review.

### HTTP

The HTTP example needs `curl` and `jq`:

```bash title="check_contract.sh"
#!/usr/bin/env bash
set -euo pipefail

: "${RECUUT_API_URL:?Set RECUUT_API_URL for this environment}"
: "${RECUUT_API_KEY:?Set RECUUT_API_KEY for this environment}"
: "${RECUUT_REVISION_KEY:?Set RECUUT_REVISION_KEY for this environment}"

contracts="$(
  curl --fail-with-body --silent --show-error --get \
    --max-time 30 \
    --url "${RECUUT_API_URL%/}/merchant/v1/contracts" \
    --data-urlencode "include_resource_key=true" \
    --header "Authorization: Bearer $RECUUT_API_KEY"
)"

if ! contract="$(
  printf '%s\n' "$contracts" | jq --exit-status --sort-keys \
    --arg revision_key "$RECUUT_REVISION_KEY" \
    '.components.schemas[$revision_key]'
)"; then
  echo "Published revision $RECUUT_REVISION_KEY was not found in this environment." >&2
  exit 1
fi

printf '%s\n' "$contract"
```

Run `bash check_contract.sh`.

### Python

The Python example uses the recuut client:

```python title="check_contract.py"
import json
import os

from recuut import Recuut

revision_key = os.environ["RECUUT_REVISION_KEY"]

with Recuut() as client:
    document = client.contracts.list(include_resource_key=True)

contracts = document.model_dump(mode="json", by_alias=True)
contract = contracts["components"]["schemas"].get(revision_key)
if contract is None:
    raise SystemExit(
        f"Published revision {revision_key} was not found in this environment."
    )

print(json.dumps(contract, ensure_ascii=False, indent=2, sort_keys=True))
```

Run `python check_contract.py`.

Create a snapshot directory with `mkdir -p contracts`. Save the sorted output as a reviewed snapshot for that environment, such as `contracts/test.json` or `contracts/live.json`. For example, run `bash check_contract.sh > contracts/test.json`, review the file, and commit it. The Python example produces the same sorted JSON.

In CI, run `bash check_contract.sh > /tmp/recuut-contract.json && diff -u contracts/test.json /tmp/recuut-contract.json`. Replace the first command with `python check_contract.py` for the Python path. A missing revision or changed reporting contract then fails the job.

The scripts also fail when the key is invalid or the configured revision is absent. An empty workspace or a key for the wrong workspace cannot pass just because the API returned valid JSON. Review `x-recuut-resource-key` and `x-recuut-revision-key` in each snapshot to confirm that test and live represent the intended product contract.

Run the check with each key: the same revision key can resolve to separate contracts, so test and live need their own reviewed snapshots.

The snapshot proves the published reporting contract and revision identity. It does not contain the Price or access setting, so it cannot prove that the revision is payable.

## Check generated Python contracts

For Python generated contracts, also run `recuut pull-contracts --check` against the key used to generate the committed package:

```bash
recuut pull-contracts --check
```

The generated package uses the Resource key and public revision key, without embedding the environment-specific Resource ID. A promoted revision with the same contract can use the same generated Python names in test and live, and run the same application artifact. `pull-contracts --check` compares every published revision visible to its key, so a package generated from test may fail that check with a live key when the catalogs differ. For live, use the selected-revision snapshot check above.

## Build the application artifact

Build the application artifact from the checked commit, keeping the API key out of the artifact. Inject the API key through the target deployment environment. Direct HTTP and raw-revision applications also need `RECUUT_REVISION_KEY`; pulled contracts already bind the revision. Deploy the same artifact to test and live.

For API errors, retry limits, and `Retry-After`, use [Errors and retries](/guides/operate/errors/). For the environment boundary, read [Test and live environments](/guides/concepts/test-live-environments/).
