Skip to content

sync: CLI defect fixes (L4) + preview readiness probe (004743c) - #25

Merged
im-tyler merged 11 commits into
mainfrom
sync/orch-004743c
Sep 24, 2026
Merged

im-tyler merged 11 commits into
mainfrom
sync/orch-004743c

Conversation

@im-tyler

Copy link
Copy Markdown
Contributor

Mirror sync from Forgejo main at 004743c (tree identical): --host resolves servers.yml names; TCP readiness requires a live listener (deploy gate, heal, preview); ID-created containers report their tag and rollback keeps the release image (contracts rev 7); env set refuses secret-backed keys; maintained quickstart fixture + extended make quickstart.

🤖 Generated with Claude Code

im-tyler and others added 11 commits September 23, 2026 21:10
…llow-ip (corpus rev 6)

Implements the teploy-cli half of Tyler's 2026-09-24 preview ruling
(DELEGATED_DECISIONS_2026-09-23 section 10): previews served on plain HTTP
under <target-tailnet-ip>.sslip.io, gated to tailnet addresses.

- preview deploy flags: --base-domain <domain> (hostname base instead of
  the app domain; hyphen or dot sslip form), --http-only (explicit http://
  site address, no tls directive - Caddy never attempts ACME), --allow-ip
  <ip|cidr> (repeatable/comma; maps to caddy.Firewall.AllowIPs). Validated
  before connecting and again in Manager.Deploy before any mutation.
- caddy.TLS gains HTTPOnly (wins over Cert/Key/Internal).
- Mode persisted in the preview record as base_domain / http_only /
  allow_ips (all omitempty). Updates inherit each field unless the deploy
  overrides it (--http-only=false, --allow-ip "" clears), so a blue/green
  swap never re-enables HTTPS or drops the allowlist. Records without the
  fields behave exactly as before (app-domain host, automatic HTTPS, no
  gate). C06 identity (PreviewID/previewIDHex, route key, state path) and
  the blue/green machinery are unchanged.
- preview list --json rows gain url (http:// iff http_only, else https://);
  domain kept; empty list still []. Text list and deploy output print the
  real scheme.
- preview-exposure capability token (additive).
- contracts rev 6 (additive): preview-state schema gains optional
  record/list-row fields on both eras with the url-scheme invariant; two
  list-row fixtures generated from the real encoder; version-handshake
  fixture regenerated for the new token. No MI bump.

Tests: route written HTTP-only + gated, update inherits mode, field-by-
field overrides, default/legacy records unchanged, State round-trip incl.
pre-field and slug-era records, invalid input refused before mutation,
flag parsing through the real cobra command, list JSON url scheme, caddy
HTTPOnly rendering. Real Caddy (caddy:2-alpine via podman) accepted the
rendered blocks: adapt OK with no tls app, :80 only; non-allowed source
403, allowed source proxied.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…efore the worker starts

admit's running case parked the item in the pending slot and spawned the
worker; a second delivery arriving before the worker goroutine took it saw
workerLive && pending != nil and superseded it. The webhook had already told
the sender that delivery was running; it never ran. Found as a ~3% flake of
TestAdmission_NoGoroutinePileup under -race (GitHub CI on the mirror sync PR;
6/200 locally). The running item now goes to the worker directly.

Pinned deterministically: TestAdmission_RunningDeliveryIsNeverSupersedable
fails 5/5 on the old code, passes 50/50 under -race; all admission tests
300/300 under -race after the fix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…st box1)

ResolveServer's flag branch took --host verbatim, so every --app/--host
command (log, status, logs, rollback --app, ...) dialed the literal name
instead of the registered entry. Found by the R02 docs lane against
`teploy log`. A --host naming a servers.yml entry now resolves to the
entry's host/user; --user/--key still win; unregistered values and a
missing/unreadable servers.yml keep the raw-host behavior.

Pins: config ResolveServer named/override/raw cases; teploy log --app
--host box1 connects to the entry's user@host.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A connect-only probe passed against any published port: Docker's
userland proxy accepts on the host side and only then dials the
container, so a container with nothing listening still passed the tcp
gate (and auto's 404/3xx fallback, and heal's). The probe now connects
and holds up to 1s: a byte or the connection staying open is ready; the
proxy's immediate close (dead backend), a refused connect, or a bad
host is not. One command builder (deploy.TCPProbeCommand) shared by the
deploy gate and heal.

Proven live on colima: a sleep-only container published on :18096 with
health.mode tcp - old probe rc=0, new probe rc=1, the deploy gate now
times out; the busybox-httpd quickstart fixture still passes its tcp
gate. Pin (health_tcp_backend_test.go) runs the real command against
accept-then-close / silent-live / banner-live / closed listeners; red
on the old probe on Linux, skipped on bash<4 (macOS bash 3.2 reports a
read timeout as EOF - fail closed). internal/preview's probeTCP has the
same shape and is NOT changed here (L1 lane owns internal/preview).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… hint

Found proving the tcp-gate fix live: a container with nothing listening
was diagnosed as 'the app is listening on port 46107' - Docker's
embedded resolver (127.0.0.11:<random>) inside every container on a
user-defined network. It is now skipped (its presence still proves the
listener tool ran), so the correct 'nothing is listening' finding fires.
The slow-boot hint named a nonexistent key (health: { timeout: 90s });
the grammar is timeout_seconds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…pus rev 7)

A52 creates web/worker containers from the immutable image ID, so docker
ps reports a bare 12-hex short ID as the container image. Ship's wave-9
lesson: ID-created containers never matched the artifact tag.

Not display-only. Surfaces fixed:
- teploy status (text + --json), app status --json and server status
  --json: docker.ResolveImageTags batches one `docker image inspect`
  over the ID-form images (no call when none) and reports image = first
  repo tag, image_id = full sha256, image_tags = all tags. Untagged,
  removed or uninspectable images keep the ID (best effort; reporting
  never fails on it).
- Rollback recorded ImageRef from the target container's docker ps Image,
  OVERWRITING the target release record's requested ref: after a
  rollback of an ID-created container the state carried an unpullable
  short ID (DR bundle restore resolves the image from ImageRef). The
  release record's ref now wins; without one the container image is
  resolved to its tag.
Unchanged by design: prune's best-effort rmi and the predecessor
snapshot keep the raw docker ps value (they act on identity, not names).

Corpus rev 7 (additive, no MI bump): optional image_id/image_tags on the
app-list and server-status container objects; no fixture changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ship wave-9: `teploy env set` did not feed secret-backed vars; `secret
set` did. Root cause is precedence, which is by design: the server .env
(written by env set) is the FIRST --env-file at deploy; teploy.yml env:
plus decrypted secrets and resolved secret: references ride the later
attempt env file and win. The defect was env set reporting success for a
value that could never reach the container.

env set now checks before writing: a key in the app's secret store, or a
teploy.yml env: key holding a secret: (OpenBao) reference, is refused
(nothing written) with the remedy (secret set / secret rm / change it in
OpenBao); a plain teploy.yml env: key that shadows it gets a warning.
README states the precedence.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-gate refusal (X05)

examples/quickstart/app is the maintained deployable CLI fixture (X05
needed one in-repo; observe's fixtures/x05 has no Dockerfile). make
quickstart now runs deploy -> verify -> redeploy -> teploy health ->
status (asserts the image is reported by TAG, the ID-created-container
fix) -> rollback (asserts v1 served again) -> a qs3 container that runs
but never listens, asserting the tcp readiness gate refuses it and v1
keeps serving (the docker-proxy fix, end to end).

Fixture fixes found running it: the sh-wrapped httpd ignored SIGTERM as
PID 1, so every retirement waited out the 10s kill timeout (Exited 137;
deploys took ~11.5s) - a TERM trap makes stops immediate (deploys ~1.5s,
Exited 0); a real 200 /health so teploy health exercises HTTP, not the
404->TCP fallback. status' IMAGE column widened for tag names.

Verified green on the local colima fixture.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nding)

probeTCP was the connect-only /dev/tcp check L4 replaced in the deploy gate:
docker-proxy accepts the connect even when the backend is dead, so a preview
could be declared ready with nothing serving. It now runs
deploy.TCPProbeCommand, the same live-listener probe the deploy gate uses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@im-tyler
im-tyler merged commit 0060326 into main Sep 24, 2026
1 check passed
@im-tyler
im-tyler deleted the sync/orch-004743c branch September 24, 2026 21:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant