# Deployment checks

Deployment checks hold a release back from production until what you require of it (tests, a scan, a review) has been reported as passed. This page explains where required checks are declared, how CI satisfies them, who can bypass them and how that is recorded. It also describes progressive rollout, declared in the same file.

## Concepts

- **Check**: a requirement named by a stable identifier you choose (`unit-tests`, `static-analysis`). It describes no command: your CI reports it, under that identifier, whatever its provider.
- **Required**: a required check holds the release back; an informational check is shown and holds nothing.
- **State** of a check on a release: `pending`, `passed` or `bypassed`.
- **Release on hold**: built (`built`) and not served, because it is waiting for its checks.

## Declare checks

Checks are declared in the `.bastide/controles.yaml` file, at the project's build root (the « Racine du projet » setting, or the repository root). There is no other place: neither the console nor the API configures them, and switching CI provider changes nothing here.

```yaml
version: 1
controles:
  - id: unit-tests
  - id: end-to-end-tests
  - id: static-analysis
    obligatoire: false
    description: Informational only
```

| Field | Rule |
| --- | --- |
| `version` | Required, equals `1`. |
| `controles` | Twenty at most, no duplicates. |
| `id` | A lowercase letter or digit, then up to 62 lowercase letters, digits, dots, underscores or hyphens. |
| `obligatoire` | True by default: forgetting the line holds the release back rather than letting it through. |
| `description` | For whoever reads the file, 200 characters at most; neither stored nor served. |

The file weighs 16 KiB at most. If it is absent, the release requires nothing. If it is present but unreadable (invalid YAML, unknown field, identifier outside the grammar, duplicate, symbolic link), the build is refused with `controles_refuses`: a production gate the platform cannot read is not ignored.

Checks are frozen onto the release when it is built. Rebuilding the same commit does not reset a check that has already been reported.

## What checks hold back

- **Towards production**, a release with a required check neither passed nor bypassed stays on hold: it is built, and the live version does not change. A check that has reported nothing holds as much as a failure.
- **Outside production** (preview, staging, custom environment), checks are shown and hold nothing back: a preview is precisely where you watch them fail.

A release's checks are read in `checks` from `deployment_read`, and `enAttente` says whether it is on hold. The console does not show checks yet: the promotion screen that will carry them is still to come.

## Satisfy checks

Once your jobs have passed, promote the release while declaring the checks passed, with `deployment_create`, `releaseId` and `passedChecks`:

```json
{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "environmentId": "env_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "releaseId": "rel_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "passedChecks": ["unit-tests", "end-to-end-tests"],
  "idempotencyKey": "promotion-rel-2026-10-28"
}
```

The same promotion from a CI job, under a [service account](service-accounts.md) holding the `deploy:write` scope:

```sh
export BASTIDE_TOKEN=<service account token>
bastide deployment create --project-id prj_01JQ8ZK4M7NP2RSTVWXYZ34567 \
  --environment-id env_01JQ8ZK4M7NP2RSTVWXYZ34567 \
  --release-id rel_01JQ8ZK4M7NP2RSTVWXYZ34567 \
  --passed-checks unit-tests --passed-checks end-to-end-tests
```

Promotion rebuilds nothing: production serves the exact image that was built (see [Deploy](deploy.md)). If a required check is still pending, the promotion is refused with `controles_non_satisfaits`, which names the missing checks; the live version does not change.

`passedChecks` and `bypassChecks` only apply together with `releaseId`.

## Who can report, and who can bypass

| Action | A person | A service account | An agent (agent session, or the MCP channel) |
| --- | --- | --- | --- |
| Report passed checks (`passedChecks`) | yes | yes | no |
| Bypass checks (`bypassChecks`) | yes | no | no |

An agent does not declare the checks that block it as passed itself, and does not bypass them: it proposes, the person decides. The refusal comes as `contournement_reserve` (403), before anything is written, and the attempt is logged. The role required is that of `deployment_create`: developer (`developer`).

### Bypass

Bypassing is the act of a person who answers for it, with a reason of 10 to 500 characters:

```json
{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "environmentId": "env_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "releaseId": "rel_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "bypassChecks": { "reason": "Urgent security fix, tests rerun after going live." }
}
```

It covers this release's required checks that are still pending. Each moves to `bypassed`, with the person's name and the reason; a bypassed check stays bypassed, even if a job later reports it passed.

### How it is recorded

- The promotion's [audit log](audit.md) entry names the person, the promoted release, the original deployment, the bypassed checks and the reason.
- Each check on the release keeps who reported or bypassed it, and when.
- An attempt refused with `contournement_reserve` is logged too.

Read these entries on the project's **Audit** screen, or with `audit_read`.

## Progressive rollout

The same file can declare that a production deployment first receives a share of the traffic, and is cancelled on its own if errors or latency exceed a threshold:

```yaml
version: 1
mise_en_service:
  paliers: [10, 50, 100]
  erreurs_5xx_max_pct: 2
  p95_max_ms: 1000
  palier_minutes: 5
  echantillon_min: 50
controles: []
```

| Field | Meaning | Default |
| --- | --- | --- |
| `paliers` | Successive traffic shares, in percent, increasing, the last at 100; ten at most. | 10, 50, 100 |
| `erreurs_5xx_max_pct` | Rate of 5xx responses beyond which the release is cancelled. | 2 |
| `p95_max_ms` | p95 latency beyond which it is cancelled. | 1,000 ms |
| `palier_minutes` | Duration of a step. | 5 |
| `echantillon_min` | Number of requests below which a measurement does not decide. | 50 |

Declaring the block is enough: each field has its default. It only applies in production, and only when a previous release is already serving. A threshold crossed on a sufficient sample cancels at once, and the previous release takes back all the traffic. If there is no traffic after three steps, the share moves on, and the platform says so.

## Common errors

| Code | Remedy |
| --- | --- |
| `controles_refuses` | `.bastide/controles.yaml` is unreadable: read the reason (line and code), fix it, redeploy. Removing it removes the checks. |
| `controles_non_satisfaits` | Report the missing checks with `passedChecks`, or have a person bypass them. |
| `contournement_reserve` | An agent tried to report or bypass: ask a person to promote, through the API or the command line. |
| `promotion_impossible` | Release already served, environment pinned, not routable, or a deployment of the same commit in progress. |

See also: [Deploy](deploy.md), [Builds](builds.md), [Service accounts](service-accounts.md), [Agent safety](agent-safety.md), [Audit](audit.md).
