ADR 0014: Independent Component Versioning (VERSION-file single source of truth)¶
Status: Accepted
Date: 2026-06-21
Deciders: Scott, Development Team
Compatibility matrix: docs/version-compat-matrix.md
Context¶
Every component (platform/orpheus-common, services/orpheus_ui/backend,
services/orpheus-gps, and the nine agents/orpheus-agent-*) carried a
hardcoded version string, and orpheus-common carried it three times
(its pyproject.toml, its setup.py, and __init__.__version__). These
drift: the UI's setup.py still said 0.1.0 while its pyproject.toml said
0.2.2. There was no per-component release signal — a fix to orpheus-common
forced consumers to track repo HEAD, and a breaking change in one component had
no version to express it.
This is the prerequisite for publishing components to PyPI and for an automated per-component release pipeline (both downstream backlog items): a package can't be published or depended-on cleanly without a stable, independent version.
Decision¶
-
One
VERSIONfile per component (strict SemVerMAJOR.MINOR.PATCH) is the single source of truth, sitting next to that component'spyproject.toml. -
Build-backend stayspyproject.tomlreads it dynamically. Each component declaresdynamic = ["version"]in[project]andsetuptools.build_meta;requiresis raised tosetuptools>=61.0(the floor for file-based dynamic version). The two components that keep a legacysetup.pydrop theirversion=keyword so the pyproject dynamic value is the only source. -
__version__resolves from installed metadata, not a duplicated string:so the runtime version always equals what was built fromfrom importlib.metadata import PackageNotFoundError, version as _pkg_version try: __version__ = _pkg_version("<dist-name>") except (ImportError, PackageNotFoundError): __version__ = "0.0.0+unknown"VERSION. -
Dependents declare a minimum, open-ended
orpheus-commonfloor (orpheus-common>=0.2.0, no upper bound). In the monorepo this is satisfied by the editable path install in each component'srequirements.txt(-e ../../platform/orpheus-common), so it never triggers a PyPI lookup duringmake install; it becomes load-bearing once components are published. -
scripts/bump-version.sh <component-path> <major|minor|patch>bumps theVERSIONfile and prepends a datedCHANGELOG.mdstub.
Governance (proposed default — tune as the project grows)¶
- A maintainer bumps a component with
scripts/bump-version.shas part of the change that warrants it; the bump and its CHANGELOG stub land in the same PR. - SemVer intent: major = a breaking change to a component's public API
(for
orpheus-common, anything importers rely on); minor = additive, backwards-compatible; patch = fixes with no API change. - When
orpheus-commontakes a major bump, raise theorpheus-common>=floor in affected dependents and update the compatibility matrix in the same PR. Minor/patch bumps need no dependent changes (open upper bound).
This policy is deliberately lightweight for a solo/small-team repo; revisit if contributor count or the release cadence grows.
Consequences¶
- Unblocks the PyPI Publishing Pipeline and Automated Release Workflows backlog items (independent, buildable versions per component).
- No behavior change at runtime and no data/schema/config/MQTT impact — this is build-metadata only. The previous binary reads the same DB untouched.
- Verified:
make install(editable) builds every component with the version resolved fromVERSION, andpython -m buildproduces correctly versioned wheels. - Reversible:
git revertrestores the hardcoded versions; nothing is published or deployed by this change. - Cost: a new
setuptools>=61.0build-time floor (already met by the toolchain) and oneVERSIONfile per component to keep current via the bump script.