Skip to content
Merged
568 changes: 562 additions & 6 deletions AUDIT_OPEN.md

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@

All notable changes to teploy are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [0.1.37] - 2026-09-22

### Fixed

- **Webhook deploys build the authenticated commit.** A delivery now
pins the build to the payload's exact commit (fetch-verify-reset);
if that commit was force-pushed away the deploy fails loudly naming
both commits instead of silently building the moving branch tip.
The pin rides the durable admission ledger through supersede and
crash-resume.
- **Preview updates no longer take the preview down.** A preview update
now runs blue/green: the candidate starts under a version-suffixed
name with its own network alias, passes a readiness gate, the route
switches, and only then is the predecessor retired — a failed
candidate leaves the old preview serving. `teploy preview prune`
prunes expired previews across all apps (both record eras,
idempotent, 72h default TTL) and is cron-able; the deploy-time prune
uses the same core.
- **SSH host-key mismatches now name what was presented and what
known_hosts holds** (key algorithms included), instead of a bare
mismatch error (found via ship's delivery provisioning).

### Added

- **Crash-recovery evidence for deploys (C01 design obligations):**
readiness receipts (exact candidate IDs + probe outcomes) and
predecessor snapshots (exact container IDs) persist per attempt at
the moment they become true, so recovery can distinguish
compensable states from inspect-only ones and restore exactly what
was displaced; the deploy log records DEGRADED outcomes (traffic
switched but retirement partially failed) instead of clean success.

## [0.1.36] - 2026-09-22

### Fixed
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,25 @@ processes:
web: "npm start"
worker: "npm run worker"

# Readiness gate — what "healthy" means before traffic switches, and how
# long to wait. mode selects the probe:
# http — status-based only: GET path, 200 = ready. A 404/redirect FAILS
# (no fallback). Best when the app has a real health endpoint.
# tcp — a TCP dial against the published port; nothing is fetched.
# For apps with no HTTP surface (game servers, TCP brokers).
# Setting `path` alongside is rejected — nothing would fetch it.
# auto — compatibility default (also what an omitted mode means): HTTP
# GET first; a 404/3xx falls back to a TCP dial. The historical
# behavior, kept so existing configs deploy identically.
# timeout_seconds is the TOTAL deadline for the gate (not per-try); the
# deploy output states the mode and deadline before the gate runs, e.g.
# "Readiness: HTTP GET /healthz (30s deadline)".
health:
mode: http # http | tcp | auto (default auto/compat)
path: /healthz # default /health (http/auto only)
timeout_seconds: 30 # total gate deadline (default 30)
interval_seconds: 1 # time between attempts (default 1)

# Per-process HEALTHCHECK overrides. disable: true passes --no-healthcheck
# so the container ignores the image's HEALTHCHECK — useful when a worker
# shares an image with web but has no HTTP listener for the inherited probe.
Expand Down Expand Up @@ -316,6 +335,7 @@ teploy log # deploy history
teploy exec <server> <cmd> # run a command on the server (SSH)
teploy app exec -- <cmd> # run a command in the app container (migrations, etc.)
teploy validate # check config and server readiness
teploy doctor [--server <name>] # read-only diagnostics: toolchain, SSH, Docker, registry, Caddy, disk, compatibility, repair debt (--json for machines; exit 1 if any check fails, never 2)
teploy scale <count> # multi-server deploy + LB update
teploy version / update # version info and self-update
```
Expand Down
63 changes: 63 additions & 0 deletions contracts/MANIFEST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Teploy contracts corpus — MANIFEST

The machine-interface fixture corpus (X02 S2; ADR `_internal/
X02_RESOURCE_CONTRACT_ADR_2026-09-22.md` §4, adopted by
DELEGATED_DECISIONS_2026-09-23 D15). teploy-cli owns the corpus because it
produces the envelopes and sits at the bottom of the stack with no
Neutron/Nucleus dependency and a public mirror.

## Revision table

| Corpus rev | Emitting CLI | Machine Interface | Notes |
|---|---|---|---|
| 1 | post-v0.1.37 main (S2 skeleton) | 1 | First goldens: version handshake, app-list envelope (MI + pre-MI legacy), error envelope (config-invalid, internal, invalid-code), release-record, attempt-name grammar, preview-state eras. |

## Artifact status

| Artifact | Schema | Fixtures | Producer |
|---|---|---|---|
| version-handshake | yes | valid (real `writeVersion` encoder) | teploy-cli |
| app-list-envelope | yes | valid (real DTO tags) + legacy pre-MI | teploy-cli |
| server-status-envelope | yes (appStatus root) | pending S2 tail (live `server status` capture) | teploy-cli |
| error-envelope | yes | valid x2 + invalid code | teploy-cli |
| release-record | yes | valid container | teploy-cli |
| attempt-name | yes (pattern) | valid + invalid examples | teploy-cli |
| preview-state | yes (canonical/legacy) | valid + legacy + ambiguous | teploy-cli |
| observation-envelope | yes | pending S6 (dash encoder) | teploy-dash |
| operation-record | yes | pending S5/S6 (dash) | teploy-dash |

## Rules

- Fixtures under `valid/` and `legacy/` are GENERATED from the real
encoders where a CLI producer exists (`internal/cli/
contracts_golden_test.go`, run with `TEPLOY_UPDATE_CONTRACTS=1` to
rewrite). Hand-authored fixtures say so in this file. Never edit a
generated fixture by hand.
- `invalid/` and `ambiguous/` fixtures MUST fail schema validation /
adoption respectively — they pin refusals, not shapes.
- A corpus change lands in the SAME commit as the code that changed the
contract, with this manifest's revision table bumped. Non-additive
changes bump `machine_interface` (D8) and are coordinated with
teploy-dash's decoder first.
- Legacy fixtures are first-class forever: an id-less server, a pre-MI
envelope, a slug-keyed preview are states real deployments carry.

## Regeneration

```
cd teploy-cli
TEPLOY_UPDATE_CONTRACTS=1 go test ./internal/cli/ -run TestContracts
```

CI runs the same test WITHOUT the env var: any drift between the corpus
and the encoders fails the build.

## Known downgrade hazard (from the ADR §5 row 1)

An older CLI rewriting `~/.teploy/servers.yml` silently drops unknown
fields, so an `id` minted by a newer CLI can vanish on downgrade. The
file itself cannot enforce it; the mitigation is consumer-side (dash
treats id-vanished as ambiguous-legacy requiring explicit re-binding,
never auto-re-mint). Consumers MUST NOT treat a missing
`machine_interface` field as MI 0 — it means "pre-MI producer", the
legacy decode path.
6 changes: 6 additions & 0 deletions contracts/fixtures/app-list-envelope/legacy/pre-mi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"apps": [],
"errors": null,
"host": "srv.example.com",
"observed_at": "2026-09-23T12:00:00Z"
}
41 changes: 41 additions & 0 deletions contracts/fixtures/app-list-envelope/valid/mi1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"machine_interface": 1,
"host": "srv.example.com",
"apps": [
{
"app": "myapp",
"domain": "myapp.example.com",
"type": "container",
"ingress": "caddy",
"current_release": {
"version": "3",
"ports": [
3000
]
},
"previous_release": {
"version": "",
"ports": null
},
"containers": [
{
"id": "9f31c02",
"name": "myapp-web-3",
"image": "nginx:1.27",
"state": "running",
"status": "Up 4 minutes",
"created_at": "2026-09-23T11:55:00Z",
"process": "web",
"version": "3"
}
],
"processes": null,
"lock": null,
"maintenance": false,
"observed_at": "2026-09-23T12:00:00Z",
"errors": null
}
],
"observed_at": "2026-09-23T12:00:00Z",
"errors": null
}
7 changes: 7 additions & 0 deletions contracts/fixtures/attempt-name/invalid/examples.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
"deadb17ecafef00d",
"ABC1234.deadb17ecafef00d",
"abc1234.DeadB17eCafef00d",
"abc1234.deadb17ecafef00",
"../escape.attempt0000000"
]
4 changes: 4 additions & 0 deletions contracts/fixtures/attempt-name/valid/examples.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
"abc1234.deadb17ecafef00d",
"9f31c02.0123456789abcdef"
]
5 changes: 5 additions & 0 deletions contracts/fixtures/error-envelope/invalid/unknown-code.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"machine_interface": 1,
"code": "kaboom",
"message": "x"
}
6 changes: 6 additions & 0 deletions contracts/fixtures/error-envelope/valid/config-invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"machine_interface": 1,
"code": "config-invalid",
"message": "invalid teploy configuration",
"detail": "teploy.yml: services.0.name: required"
}
6 changes: 6 additions & 0 deletions contracts/fixtures/error-envelope/valid/internal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"machine_interface": 1,
"code": "internal",
"message": "command failed",
"detail": "dial tcp: connection refused"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": "myapp-p-feature-login",
"era": "legacy",
"app": "myapp",
"candidates": [
{"branch": "feature/login", "canonical_id": "myapp-p-08e81639"},
{"branch": "feature-login", "canonical_id": "myapp-p-cb4bdf9a"}
],
"rule": "refuse adoption; require explicit binding (C06 ambiguity contract - never auto-adopt)"
}
5 changes: 5 additions & 0 deletions contracts/fixtures/preview-state/legacy/slug-keyed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"id": "myapp-p-feature-login",
"era": "legacy",
"app": "myapp"
}
6 changes: 6 additions & 0 deletions contracts/fixtures/preview-state/valid/canonical.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"id": "myapp-p-08e81639",
"era": "canonical",
"app": "myapp",
"branch": "feature/login"
}
12 changes: 12 additions & 0 deletions contracts/fixtures/release-record/valid/container.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"schema_version": 1,
"app": "myapp",
"hash": "abc1234.deadb17ecafef00d",
"created_at": "2026-09-23T12:00:00Z",
"deployment_type": "container",
"ingress_mode": "caddy",
"domain": "myapp.example.com",
"image_ref": "nginx:1.27",
"image_digest": "sha256:0000",
"manifest_sha256": "sha256:beef"
}
22 changes: 22 additions & 0 deletions contracts/fixtures/version-handshake/valid/mi1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"capabilities": [
"app-list-machine",
"autodeploy-redeploy",
"doctor-diagnostics",
"env-set-stdin",
"error-envelope",
"health-modes",
"kv-set-stdin",
"preview-blue-green",
"preview-canonical-id",
"provenance-records",
"readiness-receipts",
"repair-debt",
"server-rename",
"server-status-machine",
"server-update",
"template-var-stdin"
],
"machine_interface": 1,
"version": "v0.0.0-contracts"
}
Loading
Loading