Skip to content
## [1.4] — 2026-03-17

### New Features

#### `docker_exec` Step

Standalone step for executing commands inside Docker containers — a compact
shorthand for `docker_op: exec` with full `save`/`save_regex`/`save_map` support:

```yaml
- docker_exec: "cat /opt/app/status.txt"
  services: ["clientd-1", "clientd-2"]
  expect: success
  save: status_output
  save_regex: "state=(\\w+)"
```

#### `save_regex` / `save_map` — Structured Output Extraction

CLI, RPC, Bash, and docker_exec steps now support regex-based extraction:

```yaml
- cli: "net srv -net {{network_name}} dump"
  node: node-1
  save: raw_output
  save_regex: "address:\\s+(\\S+)"
  save_map:
    order_hash: "hash:\\s+(0x[a-f0-9]+)"
    price: "price:\\s+(\\d+\\.\\d+)"
```

Centralized via `RuntimeContext.save_output()` — supports first capture group
or full match fallback, with warning on no-match.

#### `wait: docker_exec` Polling

Poll a command inside a Docker container until a condition is met:

```yaml
- wait: docker_exec
  cmd: "test -S /opt/app/run/app.sock && echo ready"
  services: ["clientd-1"]
  contains: "ready"
  timeout: 20
  interval: 1
```

#### `build` / `devices` / `volumes` in `docker_compose_gen`

Generated Docker Compose services now support custom image builds,
device mappings, and volume mounts. The first generated service carries the
full `build` definition; siblings reuse the image via `pull_policy: never`:

```yaml
docker_compose_gen:
  - prefix: kelvpn-client
    count: "{{client_count}}"
    build:
      context: ../docker
      dockerfile: Dockerfile.client
    devices: ["/dev/net/tun:/dev/net/tun"]
    volumes: ["../configs:/etc/app:ro"]
    cap_add: [NET_ADMIN]
    environment:
      APP_MODE: client
```

`INSTANCE_ID` env var is automatically injected (1, 2, …N).

#### `env_file` for Suites

Suite descriptor supports `env_file` to inject environment variables
into all provisioned nodes:

```yaml
env_file: .env.stage
```

#### `docker_devices` / `environment` in Node Specs

`NodeComposeSpec` and `NodeComposeGenSpec` now accept `docker_devices`
and `environment` fields, and `cap_add` supports a `docker_capabilities`
YAML alias:

```yaml
node_compose:
  vpn-server-1:
    role: full
    docker_capabilities: [NET_ADMIN, SYS_PTRACE]
    docker_devices: ["/dev/net/tun:/dev/net/tun"]
    environment:
      VPN_MODE: server
```

#### `param_map` in `ConfigStep`

Conditionally populate config keys from truthy scenario variables:

```yaml
- config: "cellframe-node.cfg.d/debug-overrides.cfg"
  nodes: "{{_all_svc}}"
  param_map:
    DEBUG_REACTOR: "general.debug_reactor"
    DEBUG_HTTP: "general.debug_http"
```

Only variables with truthy values (`"true"`, `"1"`, `"yes"`) generate
entries. If none match, the step is silently skipped.

#### Generalized `cfg.d` Paths

Config step supports arbitrary `.cfg.d/` paths, not just
`cellframe-node.cfg.d/`:

```yaml
- config: "network/vpntest.cfg.d/role.cfg"
  nodes: ["node-1"]
  set:
    role: "master"
```

#### `--param` / `-P` CLI Option for `run_tests`

Pass ad-hoc KEY=VALUE parameters from the command line directly
into scenario variables:

```bash
stage_env.py run-tests tests/ -P vpn_client_count=5 -P debug=true
```

#### `--yes` / `-y` Flag for `start --clean`

Skip the interactive confirmation prompt in CI pipelines:

```bash
stage_env.py start --clean --yes
```

#### Per-Role / Per-Node Docker Customizations

New `[role_docker_*]` and `[node_docker_*]` sections in `stage-env.cfg`
for declarative Docker configuration:

```ini
[role_docker_root]
capabilities = NET_ADMIN,SYS_PTRACE
devices = /dev/net/tun:/dev/net/tun
environment = VPN_DEBUG=1

[node_docker_3]
capabilities = NET_RAW
extra_config = {"privileged": true}
```

#### CI Pipeline Improvements

- SIGTERM/SIGHUP handler — `finally` blocks run on pipeline cancellation
- PID file moved to `/tmp/` — system-wide lock, writable without root
- Temp config support — avoid writing to read-only `/opt/`
- Path redirection to CI work directory

### Bug Fixes

- **Image build race** — build ALL missing images before `docker compose up`,
  not just the first service; correctly detect `build` directives
- **Zombie prevention** — `init: true` added to all Docker containers
- **Monotonic timeouts** — `wait: cli` and `wait: bash` polling loops
  use `time.monotonic()` with dynamic per-attempt timeouts instead of
  accumulated `elapsed += interval`
- **Template-variable timeouts** — `timeout: "{{var}}"` resolved via
  `_resolve_timeout()` across all wait types
- **curl fail-fast** — `-f` flag added to mirror_tester HTTP requests
- **Absolute cache_dir** — normalized to relative path for Docker volumes
- **Default base_http_port** — changed to 9079 to avoid collisions
- **SnapshotManager init** — fixed constructor argument ordering bug
- **Python 3.9 compatibility** — removed 3.10+ syntax constructs
- **cfg.d routing** — any path containing `.cfg.d/` is handled as INI
  snippet (not just `cellframe-node.cfg.d/`)

### Stats

- **24 files** changed
- **+833 / −127** lines
- **70 commits**

---