Skip to content

A release gate in CI

A notebook whose exit code is the result. Checks evaluate every rule and report, a scoreboard shows every result, and raise_if_failed() at the end turns failures into exit code 1 — which any CI system turns red. On demo data the gate fails by design (the generator's weak cell trips the undervoltage floor), and one --param override flips it green.

You'll use: check.that() on series · CheckResults · raise_if_failed() · --param waivers · exit codes

Run it

zelos live demo --backfill 5m --duration 30m    # terminal 1: demo data

Copy the notebook below into release-gate.md, then:

zelos notebook run release-gate.md -o release-gate.html   # exit 1 — the weak cell fails the gate
zelos notebook run release-gate.md --param cell_floor=2.5   # exit 0 — floor relaxed

The notebook

---
description: A parameterized pass/fail runbook with exit codes CI understands.
requires-python: ">=3.10"
dependencies:
  - zelos-sdk[notebook]
params:
  window: "-3m"
  cell_floor: 3.0
  current_max: 40.0
---

Checks report, they don't raise — so every rule below runs and the page shows
the whole board, not just the first failure. The last cell is the gate: it
raises `AssertionError`, the run's outcome is `failed`, and the process exits
`1`.

This gate holds the battery pack to its undervoltage floor. On demo data it
**fails by design** — the generator plants one weak cell — and passes when
you relax the floor:

```python {.norun}
# In CI, or a terminal:
#   zelos notebook run release-gate.md -o release-gate.html      # exit 1: one cell sags
#   zelos notebook run release-gate.md --param cell_floor=2.5    # exit 0: floor relaxed
```

```python
from zelos_sdk import CheckResults, connect

agent = connect()
cells = agent.query("bus0/BMS_message/cells.*", start=params.window)
cells
```

## Every cell holds above the undervoltage floor

One rule per cell, all eight scored. `CheckResults` is a list that knows its
own pass/fail count, so it renders as a board once every rule is in it:

```python
results = CheckResults(
    agent.check.that(series, ">", params.cell_floor, name=path)
    for path, series in cells.short_names().items()
)
```

## Pack current stays inside its envelope

A check takes the series you already queried, so this rule is scored over the
same window as the eight above:

```python
current = agent.query("bus0/BMS_message/status.pack_current", start=params.window)
results.append(
    agent.check.that(
        current["bus0/BMS_message/status.pack_current"],
        "<",
        params.current_max,
        name="pack current",
    )
)
results
```

## The gate

One cell turns the scoreboard into an exit code. If anything failed or
errored, this raises `AssertionError` naming each one — the run prints the
traceback, its outcome is `failed`, and CI goes red with the rendered
notebook as the evidence:

```python
results.raise_if_failed()
```

Parameterize the thresholds, keep the notebook in the repo next to the code
it guards, and the gate evolves in review like everything else. The
`--param` override is the escape hatch for waivers — explicit, logged in the
run command, and visible in the rendered notebook.

What to notice

  • Every rule always runs. Because checks report instead of raising, the board shows all results — the gate cell at the end is the only place failure becomes an exception. See Notebooks as tests.
  • raise_if_failed() raises a plain AssertionError, which is what makes the run's outcome failed rather than errored. A single rule needs no scoreboard: assert agent.check.that(...).
  • Exit codes are the contract: 0 succeeded, 1 failed or errored, 2 infrastructure failure, 130 interrupted. A CI step needs nothing else.
  • --param is the explicit waiver. Relaxing a threshold happens on the command line, visible in the run and in the rendered notebook — never by editing the rulebook.
  • -o release-gate.html writes the evidence: a self-contained page with every board, ready to attach to the release.
  • Each rule's window is the query's window, so the gate's coverage is whatever params.window says. See find the weak cell.