Verify a deployment environment
tenchi preflight answers a different question from tenchi check:
| Command | Question | Expected environment |
|---|---|---|
tenchi check | Is this source tree internally valid? | Deterministic and local |
tenchi preflight | Can this release safely use the environment it is about to enter? | The target deployment environment |
| Health and readiness routes | Can this running process serve traffic now? | A live application process |
tenchi task run | Should an authorized operator change application state? | An explicitly selected operational environment |
Use preflight after configuration and migrations are available but before the new application receives traffic. Typical checks cover database connectivity, the deployed schema version, secret-manager permissions, required outbound services, and worker heartbeats.
Declare read-only observations
For the generated application's SQLite database, first add a connection factory that opens an existing database in read-only mode:
# app/infra/preflight_database.py
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
import aiosqlite
@asynccontextmanager
async def open_preflight_connection(
database_path: str,
) -> AsyncIterator[aiosqlite.Connection]:
uri = f"{Path(database_path).resolve().as_uri()}?mode=ro"
async with aiosqlite.connect(uri, uri=True) as connection:
yield connectionCreate app/server/preflight.py exposing checks, the module tenchi preflight loads by default (tenchi new --full generates an empty one). Each
observation is a zero-argument async function that returns None on success
and raises on failure. This example assumes your migrations set SQLite's
user_version to 7; replace that value with the version your release
requires. The starter does not set a migration version for you.
# app/server/preflight.py
from app.infra.preflight_database import open_preflight_connection
from app.server.runtime import DATABASE_PATH
from tenchi.preflight import preflight_check, preflight_group
async def database_connectivity() -> None:
async with open_preflight_connection(DATABASE_PATH) as connection:
await connection.execute("SELECT 1")
async def database_schema() -> None:
async with open_preflight_connection(DATABASE_PATH) as connection:
cursor = await connection.execute("PRAGMA user_version")
row = await cursor.fetchone()
if row is None or int(row[0]) != 7:
raise RuntimeError("unexpected database schema")
checks = preflight_group(
preflight_check(
"database.connectivity",
database_connectivity,
description="Open the configured database in read-only mode.",
failure_code="DATABASE_UNAVAILABLE",
),
preflight_check(
"database.schema",
database_schema,
description="Verify the schema required by this release.",
failure_code="DATABASE_SCHEMA_MISMATCH",
),
)Checks default to a five-second timeout. Set a shorter or longer declaration timeout only when the dependency has a known response budget:
preflight_check(
"workers.email",
email_worker_heartbeat,
timeout=2.0,
failure_code="EMAIL_WORKER_NOT_READY",
)Tenchi runs the checks concurrently and reports results in declaration order. Naming and shape rules are listed under Constraints.
Keep the boundary read-only
Preflight does not receive an application context and does not start the application lifespan. Each check must open the narrowest read-only client it needs. For a database, use a read-only connection or account. For a secret manager, request metadata or describe a known secret instead of returning its value. For a worker, read its heartbeat rather than enqueueing probe work.
Python cannot prove that an arbitrary async function has no side effects.
Tenchi enforces the narrow function shape, supplies no stateful context,
marks the MCP tool read-only, and keeps operational mutations in
tenchi task run. Your adapter permissions are the final enforcement
boundary. Use credentials that cannot write.
Do not migrate schemas, repair records, rotate secrets, enqueue jobs, or call business endpoints from preflight. Put migrations in the release process and authorized repairs or backfills in operational tasks.
Check the production concerns
Keep each dependency independently named so a failed rollout points to one owner:
- Database connectivity: open a read-only connection and run a minimal query.
- Schema compatibility: read the migration table or required columns and compare them with what this release expects.
- Secret-manager access: describe required secret references with an identity that can read metadata without printing values.
- Outbound dependencies: call a documented, non-mutating readiness or metadata operation with the production client configuration.
- Worker readiness: read a durable heartbeat or consumer-group status and reject stale workers.
Preflight should prove only what must be true before traffic shifts. Leave ongoing dependency monitoring to health checks and observability.
Run the deployment gate
From the application root:
uv run tenchi preflight
uv run tenchi preflight --json
uv run tenchi preflight --timeout 3--timeout caps every declared timeout; it can make the gate stricter but
never extend a check's own limit. The command exits zero only when every check
passes. Human output names each status and stable failure code. JSON returns
the same versioned result model as the MCP tool:
{
"schema_version": 12,
"root": "/srv/api",
"target": "app.server.preflight:checks",
"ok": false,
"counts": {
"passed": 1,
"failed": 1,
"timed_out": 0,
"total": 2
},
"duration_seconds": 0.031,
"checks": [
{
"name": "database.connectivity",
"description": "Open the configured database in read-only mode.",
"status": "passed",
"duration_seconds": 0.012,
"failure_code": null
},
{
"name": "database.schema",
"description": "Verify the schema required by this release.",
"status": "failed",
"duration_seconds": 0.03,
"failure_code": "DATABASE_SCHEMA_MISMATCH"
}
]
}Exception types, exception messages, and return values never appear in the result.
The CLI and MCP tool discard direct stdout and stderr from preflight code. They cannot intercept handlers that send logs directly to files, sockets, or telemetry backends. Configure dependency logging for production and never include secret values in application log records.
Use a different module only when the application cannot follow the generated convention:
uv run tenchi preflight --preflight my_app.release:checksFor MCP, pass the same target when the server starts:
uv run tenchi mcp --preflight my_app.release:checksThe MCP preflight tool is available without enabling task execution. It uses
the credentials and environment captured by the MCP server process, so call it
only when that process is connected to the intended deployment target.
Place it in the rollout
A safe release sequence is:
- Validate the source and contracts with
tenchi checkand the OpenAPI, job-message, application-tool, and evaluation-policy compatibility gates. - Load production configuration without printing secret values.
- Run backward-compatible migrations once.
- Start the new generation without traffic.
- Run
tenchi preflightfrom that generation's target environment. - Wait for live readiness, then shift traffic gradually.
- Watch request and worker telemetry throughout the rollout.
Continue with deployment for health routes, server limits, rollout, and rollback guidance.
Constraints
- Check names use dotted
snake_caseand remain stable for deployment automation. Failure codes useSCREAMING_SNAKE_CASE; when omitted, Tenchi derives one such asPREFLIGHT_DATABASE_SCHEMA_FAILED. - Each check is a zero-argument async function that returns
Noneon success and raises on failure. A check that returns a value is reported as failed and the value is discarded. - A check must propagate cancellation so its client and connection cleanup can finish when it times out.
--timeoutcaps every declared timeout and never extends one.- Descriptions and failure codes come from the declaration and appear in results, so keep them free of credentials and tenant data.
- Preflight receives no application context and does not start the application lifespan.