Skip to content

feat(orcad): deploy, activate and roll back a versioned orcad install - #16384

Closed
nwparker wants to merge 1 commit into
nwparker/orcad-item34-opsfrom
nwparker/orcad-item67-ship
Closed

nwparker wants to merge 1 commit into
nwparker/orcad-item34-opsfrom
nwparker/orcad-item67-ship

Conversation

@nwparker

@nwparker nwparker commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1622 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1622
Prod 20 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2465 $\color{#cf222e}{\Huge{\mathbf{−}}}$​222 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2243

Plan items 6 and 7, stacked on #16375. The last work before the swap.

Reuse, per the design's §02 table — not a second pipeline

Component What happened
Toolchain diagnosis (5 pure fns) reused directly
uploadRelayDirectory, SFTP write + lock helpers reused directly — the genuinely generic pieces
Versioned dir / GC / namespace parameterized — computeRemoteRelayDir hardcoded relay-${version} and RELAY_VERSION_DIR_REGEX only matched ^relay-…$, so it was blind to orcad dirs
Launch + readiness forked — the relay launches detached with an ORCA-RELAY sentinel; orcad publishes orca_server_ready

The GC trap — §06 falsifier 1, answered

Each model GCs only its own namespace, permanently. orcad removes orcad-<v>/; the relay removes relay-<v>/; neither ever removes the other's.

That is not a migration compromise. The two models serve different users on the same machine (SSH target vs paired peer), so there is no moment at which one is entitled to clean up after the other — a pass that deleted the sibling's tree would reach across the execution boundary the design exists to keep intact.

On top of ownership, orcad pins three idle-looking but load-bearing directories: the active version, the rollback target, and whichever version the live terminal daemon was forked from.

Activation is health-gated on the daemon, not on "listening"

A host answers readiness from its own process, so listening stays true while the terminal daemon that owns every terminal is dead — a green host that cannot run a single command. The gate therefore refuses on daemon_absent, daemon_degraded, and distinguishes pty-spawn coverage (a real PTY created and torn down inside the daemon) from handshake (the daemon answered but its spawn probe was a no-op).

Verified load-bearing: short-circuiting evaluateOrcadActivation to always activate fails 10 of its 11 tests, including:

FAIL  refuses when the candidate never published readiness
FAIL  refuses a readiness payload with no health, rather than reading silence as healthy
FAIL  refuses when a different build answered — a stale process holding the port

(My first injection attempt was a no-op and everything passed. That proved nothing about the tests, so I injected properly rather than concluding they were weak.)

The update must not kill the daemon

Item 4 bought the property that work survives an orcad restart — I verified a daemon outlives an orcad stop while holding a live PTY. An update restarts orcad, so the stop path uses SIGTERM, then a second SIGTERM meaning "your deadline elapsed", and never a kill: a kill skips orcad's own release of the daemon. Activation treats running terminals as a reason to defer, not proceed.

Proof

1,857 tests pass across src/main/ssh + src/main/orcad. Ratchet 0; build-orcad green and now stamping a version+hash (0.1.0+6f924cee5e71); smoke:orcad-terminal passes; typecheck and lint clean.

Trade-offs

Zero user-facing. Nothing ships or runs this yet — it is the deploy path the swap will use.

Plan items 6 and 7 from docs/design/shipping-orcad.html.

Install reuses the relay's transaction verbatim — per-version lock, staged
SFTP write, .install-complete sentinel, stale-lock recovery — under a
parameterized namespace, so orcad-<v>/ sits beside relay-<v>/ permanently
(§06). Parameterizing GC is the trap that creates: each model now collects
only its own directories, enforced twice (prefix-scoped remote listing plus
a local ownership re-check), and a client picks its model from how the host
is registered, never from what it finds on disk.

Activation is separate from installation, because a versioned directory
selects nothing. A candidate is launched, publishes orca_server_ready, and
only becomes active if its cross-process health payload passes: right build
hash, listening, daemon live, PTY self-test green. A rejected candidate is
stopped and the incumbent restarted, so a careful deploy cannot cause the
outage it was being careful about.

Update and rollback are shaped by the daemon. An update restarts orcad, the
daemon outlives it, and the surviving daemon was forked from the outgoing
bundle — so live terminals defer the update rather than proceed, and GC pins
the active version, the rollback target and the live daemon's bundle. Orca's
persisted state carries no schema version, so rollback restores a
pre-activation snapshot rather than trusting backward-readability; the point
past which it is unsafe is the first terminal created after activation,
which the snapshot cannot describe and the surviving daemon still owns.

Running the generated shell for real found two bugs the text assertions
missed: tar members re-quoted inside a shell variable captured nothing, and
kill -0 reports a zombie as alive.
@OrcaWin

OrcaWin commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator

Closing as superseded: versioned orcad deploy, activation and rollback landed in #16398 and now live under src/main/ssh/orcad-remote-* / orcad-activation-* on main.

@OrcaWin OrcaWin closed this Oct 11, 2026
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.

2 participants