Status: Accepted
Date: 2026-03-22
Deciders: Scott, Development Team
After adding a new Diagnostics page to the Orpheus UI, make update on the Jetson appeared to succeed but the page never appeared. Investigation revealed that every component's update target was broken: it ran git pull + make install (local venv) + make restart, but never deployed the updated source code to the production paths under /opt/orpheus/. The systemd services were restarting with stale code.
This affected all 11 deployable components:
- 8 agents (
/opt/orpheus/agents/{name}/) - orpheus-ui (
/opt/orpheus/ui/) - orpheus-dashboard (
/opt/orpheus/dashboard/) - orpheus-gps (
/opt/orpheus/services/orpheus-gps/)
Each component's install-service.sh script had the correct rsync/copy logic for initial deployment, but there was no lightweight mechanism to re-deploy source code without re-running the full install script.
When we first added deploy targets, each of the 11 Makefiles contained its own copy of the deploy logic (~12 lines each). Reviewing the broader Makefile landscape revealed the same problem across other target categories:
- Deploy logic — identical rsync + copy + pip-install duplicated 11 times
- Python venv setup —
check-deps,check-python-version, venv creation, and tool paths (PYTHON,PIP,PYTEST,RUFF) duplicated in every Python component - Lint/format targets — identical
ruff check,ruff format,ruff format --checktargets in every Python component - Systemd management —
start,stop,restart,status,logs,install-servicetargets copy-pasted across all deployable components
Individual component Makefiles ranged from 200–330 lines, with the majority being boilerplate.
Introduce four shared Makefile includes under make/:
| Include | Variables | Targets Provided |
|---|---|---|
common_python.mk |
PYTHON_SYSTEM, PYTHON_REQUIRED_VERSION, VENV, SRC_DIR, TEST_DIR → derives PYTHON, PIP, PYTEST, RUFF |
check-python-version, check-deps, $(VENV)/bin/activate |
common_lint.mk |
(requires common_python.mk first) |
lint, format, check |
common_deploy.mk |
SERVICE_NAME, DEPLOY_ROOT, DEPLOY_SRC_DIR, DEPLOY_EXTRA_FILES |
deploy, _deploy-check |
common_service.mk |
SERVICE_NAME |
install-service, uninstall-service, start, stop, restart, status, logs, plus service-* aliases |
Each component Makefile now sets its identity variables and includes the relevant shared files:
SERVICE_NAME := orpheus-agent-audio-motion
DEPLOY_ROOT := /opt/orpheus/agents/$(SERVICE_NAME)
include ../../make/common_python.mk
include ../../make/common_lint.mk
include ../../make/common_service.mk
include ../../make/common_deploy.mkComponents that don't fit the standard pattern include only what applies:
- orpheus-dashboard skips
common_lint.mk(uses prettier instead of ruff) - orpheus_ui root skips
common_python.mkandcommon_lint.mk(delegates to backend/frontend sub-Makefiles) - orpheus_ui/backend uses
common_python.mkandcommon_lint.mkat depth../../../make/ - orpheus_ui/frontend is Node.js-based and uses no shared includes
The shared deploy target:
- Validates the deploy root exists (fails with a helpful error if
install-servicehasn't been run) - Rsyncs source with
--deleteto remove stale files - Copies configurable extra files (
pyproject.toml,requirements.txt, optionallysetup.py) - Runs
pip installin the deploy venv if one exists - Sets ownership to
orpheus:orpheusviachown -R(not rsync--chown, which is unavailable on macOS)
A shell-based test suite at tests/make/test_common_deploy.sh validates:
- Deploy rsync behavior (source files, stale file cleanup, custom source dirs)
- Deploy failure when
DEPLOY_ROOTis missing common_python.mkdefault variables and override behaviorcommon_service.mktarget definitions- All 12 component Makefiles parse without errors
- All 11 deployable Makefiles have a
deploytarget
This test runs in CI when files under make/ or tests/make/ change.
- Single source of truth for deploy, venv, lint, and service management logic
- Component Makefiles reduced from 200–330 lines to 80–200 lines (component-specific targets only)
make updatenow actually deploys code to production paths- Adding a new component is a few lines of identity variables + includes
- Cross-cutting changes (e.g., adding a new rsync flag) happen in one file
- Shell tests catch regressions in shared logic before they reach production
- Components gain a dependency on
../../make/*.mk— standalone use outside the monorepo would need adjustment - The
includepath assumes a fixed directory depth (2 levels from repo root for most, 3 fororpheus_ui/backend) - Shared includes add a layer of indirection — contributors must read the
.mkfiles to understand available targets
make install-serviceremains the mechanism for first-time provisioning (creates venv, user, systemd unit);make deployonly handles code sync- Bluetooth autoconnect and MQTT are unaffected — they use different deployment patterns
- The GNU Make
?=operator ensures component-level overrides always take precedence over shared defaults