Deploying Orchestrata
python scripts/promote_env.py promote --to prod --yes
That is the whole thing. It advances the target tree, syncs dependencies, installs the systemd units, restarts the services, proves they serve, bootstraps both graphs, and ingests every workspace project's AST into the operational KG. If verification fails after the restart, it returns the target to the sha it was serving.
Always look before you leap:
python scripts/promote_env.py promote --to prod --dry-run
What promote used to mean, and why it changed
Until 2026-08-10 promote wrote a receipt — gate satisfied, target reachable,
timestamp — and stopped. Nothing moved. The consequence was measured that day:
the ledger read PROMOTED while prod served a tree 76 commits behind, and
no part of the system could tell, because the record contained no sha to
compare against anything.
The ledger now records the deployed sha, and status compares it to the
target's real HEAD:
python scripts/promote_env.py status
# prod validate=PASS@2026-08-10T13:07 promote=DEPLOYED@... sha=4a1cb40fd693
# drift: target matches ledger at 4a1cb40fd693
The stages
| Stage | Does | Fails when |
|---|---|---|
resolve |
Pins the commit | Source tree dirty, or the commit is on no remote |
infra |
TCP-probes Redis + graph | The profile is incomplete, or the fleet is not there |
tree |
fetch + checkout on the target |
Target is not a checkout, fetch/checkout fails |
deps |
uv sync --frozen when the lock moved |
sync fails. SKIP when the lock is unchanged |
assets |
Runs deploy/systemd/install.sh |
A hook is non-executable in the commit |
services |
Restarts the units | — |
verify |
Polls units + ports to a deadline | A unit is not active, or a port is not listening |
graphs |
bootstrap_graphs.py --all |
Bootstrap fails |
ingest |
serialize → ast-to-kg, per project |
Every project failed |
SKIP is success, not silence: re-running a completed deploy should print a
wall of SKIPs, which is how you see idempotence held.
verify exists because a restart is not evidence. A unit can sit active
while its socket refuses connections, and a port can answer because the OLD
process still holds it — so both are checked. This is the runtime twin of the
suite's kernel rule: a run that does not verify the kernel is false-green, and
so is a deploy whose only evidence is that systemd did not complain.
What it refuses
- A dirty source tree. The working copy is not any commit, so recording one would be a lie by construction.
- An unpushed commit. The sha would exist only on that machine — the deploy could not be reproduced, reviewed, or rolled forward from anywhere else. Prod running a commit nobody can fetch is how a machine becomes the only copy of its own state.
- An incomplete profile. A missing
REDIS_PORT, or a graph URI with no port, FAILS the stage. It never falls back to a literal (RUL-003, RUL-011): probing an invented port reports reachability about an endpoint nobody configured. - A non-executable hook. The mode is recorded in git and delivered by the
checkout, so a hook arriving without it is a defect in the commit. The stage
names it rather than
chmod-ing the target. - Prod without
--yes.
Worktrees are excluded, structurally
ingest walks the workspace and skips worktrees by testing whether .git is a
file (worktree) or a directory (repo) — never by name. Ingesting one
would duplicate every node of a repo already present, under a second path that
drifts the moment the branch does.
Measured on 2026-08-10: 31 projects, 2 worktrees excluded, 25 non-repo directories ignored.
Environment isolation
Every target-tree subprocess runs under the target profile's environment, not the deploying shell's. Deploys are run by a human whose shell is usually carrying the test overlay; a bootstrap or ingest that inherited it would write the TEST graph while reporting a prod deployment, silently, with no later way to tell which graph got the data.
Three lessons from the first real deployment
Running it found what reading it did not.
verifyslept 3 seconds and probed once. The kernel loads Prolog, connects Redis and the graph, and binds its port LAST — 9s after the unit started, 6s after verify had given up. It polls to a deadline now (90s default, returning the moment the system is up). A fixed sleep is a guess about a machine's speed.- The deploy disabled its own rollback.
assetschmod +x'd every.shunder.claude/hooks, including_payload.sh— a sourced library git records100644. That one mode change dirtied the target, so the rollback could not check out the previous sha, and prod stayed on the unverified tree. Executability is now checked, never imposed. - The dry run printed no sha.
resolveassigned it only when executing, so the plan readwould advance ca0c86e5 ->and closed withDEPLOYED prod @ -. A dry run is read instead of deploying; a blank in it is a defect.
Rollback
Automatic, from the services stage onward — a failure before it has changed
nothing that needs undoing. It checks out the previous sha with --force
(unexpected dirt on the target is not worth preserving over getting the system
back up), reloads systemd, restarts the units, and records whether the
previous sha actually came up. Rollback is verified, not assumed: if the old
sha will not serve either, the record says so rather than implying recovery.
Code
src/orchestrata_ultimate/promotion/—models,projects,stages,runnerscripts/promote_env.py— the CLI, the gate ordering, the ledgertests/promotion/test_promotion.py— real git repos, real bare remotes, real worktreesreports/promotion-state.json— the ledger
The package was named deploy until the lexicon gate caught it: deploy is a
work_type word in the intent vocabulary, and a subsystem holding that name
turns "deploy the gateway" into a reference to a package. The action keeps the
word.