promotion package
Reviewed against commit 8a2cb42ad on 2026-10-23.
Reference: generated package page orchestrata_ultimate.promotion (23 modules, 4,089 lines of code); key modules runner, promoter, stages_verify, identity. The overview describes the three environments and how a promotion fits among them: orchestrata overview.
The story
orchestrata_ultimate.promotion exists because promote_env.py used to record a receipt — gate satisfied, target reachable, timestamp written — while moving no code, restarting nothing, and verifying nothing afterward. That made PROMOTED a claim with no referent: measured 2026-08-10, the ledger read PROMOTED while prod ran a tree 76 commits behind, and both statements were "true" under that definition. This package makes promotion mean deployment: pin a commit, put it on the target, restart what runs it, PROVE the result, and record the sha so "what is in prod?" has an answer.
Verification is the load-bearing part — a deploy that does not verify is a hopeful restart. Every stage is idempotent (re-running a completed deploy is a no-op) and supports execute=False, returning what it WOULD do without touching anything: that is the operator's --dry-run before restarting a live system, and what lets the tests assert real decisions without bouncing prod.
Modules
| Module | Purpose |
|---|---|
models.py |
StageStatus (StrEnum), StageResult, DeployReport — the result vocabulary every stage returns |
projects.py |
Which workspace directories are PROJECTS vs worktrees, decided exactly via git (.git directory vs gitdir: file) — no name pattern-matching, so it stays correct as worktrees come and go |
runner.py |
Runs the stages in order; the ordering guarantee is the whole product — first FAIL stops the run, and a failure at or after the restart rolls the target back to the sha it was serving before |
stages.py |
Thin re-export of the stage functions, which now live in the stages_* modules (see the inventory below). The order runner.deploy runs them in at 8a2cb42ad is resolve_commit → probe_endpoints → advance_tree → sync_deps → install_assets → record_version → restart_services → verify, then (each skippable with skip_graphs / skip_ingest) bootstrap_graphs → ingest_projects; every stage is idempotent and execute=False-aware. Two stages were added after this page was first written: record_version (writes ROOT/VERSION for the sha that landed) and ingest_projects (builds the AST graph and Project nodes for every workspace project). |
Module inventory (8a2cb42ad)
The package has 23 modules and 4,089 lines of code. Beyond the four above:
| Module | Purpose |
|---|---|
promoter.py, promoter_targets.py, env_profile.py |
The stateful Promoter that gates an environment along dev, test, prod and keeps the JSON ledger; the target-specific promotion bodies lifted out of it; and the environment-profile helpers for scripts/promote_env.py. |
stages_resolve.py |
resolve_commit (pin the commit; refuse a dirty or unshareable one) and probe_endpoints (is the infrastructure the profile names there?). |
stages_tree.py |
advance_tree and sync_deps (installs only when the lock moved). |
stages_assets.py |
install_assets (unit files onto the host, executable bits on hooks) and record_version. |
stages_units.py |
Daemon-unit discovery (daemon_units), parse_systemd_timespan and restart_services. |
stages_verify.py |
The verify stage described below. |
stages_graphs.py |
bootstrap_graphs (bring the operational KG, orchkg, to its schema) and ingest_projects. |
stages_exec.py, stages_context.py, unit_deps.py |
Subprocess plumbing, the DeployContext every stage runs against, and Requires=/BindsTo= parsing for the services stage. |
identity.py, identity_core.py, identity_scope.py, identity_checks.py, identity_skips.py, identity_urls.py |
The deployment identity verifier (U-T6): a command-line tool and the last check of verify, split into core engine, scope and discovery, ExecStart/path checks, skip rules and URL judging. |
The verify stage: identity, then a settle hold (2026-10-07)
stages_verify.verify polls is-active for every long-running unit and the kernel port until both read healthy (patience settle, default 90 s, measured on the kernel's 9-s bind), then runs the identity verifier (promotion/identity.py: cwd, argv0 and interpreter of every unit bound to the deployed tree, start time not before the recorded VERSION; tool-tree template instances such as orchestrata-ast-daemon@… are judged against /opt/orchestrata-tools, ADR-0067, built as plan item P6a). Between the poll and identity sits the settle hold (plan item P6b, commits 7ac5e7f79 and 341678e58): the stage reads RestartUSec, NRestarts and ActiveState for every unit, sleeps the longest RestartSec + 1 s once, re-reads, and refuses any unit that restarted (NRestarts not 0) or is no longer active. The reason is measured: on the first green prod promote (1fed577b4) the conversation consumer exited 1 one second after start and was restarted every 10 s; one is-active poll landed inside an active window and the promote went green. With the hold the same shape is refused in 11 s, every time (NRestarts rises in the same sample as the return to active). The guarantee is bounded and says so: a unit whose first crash comes later than poll time + longest RestartSec + 1 s is not seen by this stage (a minimum-uptime floor is the open follow-up P6d). A RestartSec longer than the patience is refused without sleeping. ship.sh's own post-promote check 5b (--plain listing, dependency names read from the unit file, PROD_ROOT/ prefix, commit 325c721c6) is the independent second reading of the same units.
Related pages
- Deploying orchestrata (procedure)
- factory package — pre-dispatch / pre-promotion admission gates
- Architecture decisions
Class map (generated)
Generated from the live source AST by scripts/gen-module-class-diagrams.py — do not hand-edit. Abstract classes have a green border; members listed are those defined on the class itself (inherited members excluded). Solid empty-triangle arrows = in-package inheritance; dashed arrows into dashed boxes = external/stdlib/cross-package bases; filled diamond = composition (constructed in __init__); open diamond = aggregation (annotated/injected reference); diamond edge labels name the owning attribute.