Dependency Pinning Manifest¶
Ratified posture for how the
apothemdeclares, pins, and locks its dependencies. Audience: contributors and downstream consumers who need a reproducible install.
Posture¶
The project is a configuration ecosystem, not a runtime library. Its sole Python dependencies are development-time tooling — formatters, linters, type checkers, test runners. There are no installed-and-imported runtime dependencies that ship with the package.
This shapes the pinning policy:
| Class | Declaration | Lock policy |
|---|---|---|
| Runtime dependencies | None | n/a — no runtime closure to lock |
| Development tooling | Range pins in pyproject.toml [project.optional-dependencies] dev |
Lock generated on demand by the contributor |
| GitHub Actions | Major-version tags in workflow uses: |
Auto-rewritten to commit SHAs by Dependabot |
Declarations¶
pyproject.toml declares dev tooling with conservative range pins:
[project.optional-dependencies]
dev = [
"ruff>=0.6.0",
"mypy>=1.11.0",
"pytest>=8.0.0",
"pytest-cov>=5.0.0",
]
The >= form lets contributors install patch and minor updates without re-locking. CI runs against the latest matching versions on every push, surfacing breakage promptly.
Locking¶
Lock files are generated on demand by the contributor — they are not committed to the tree because the project ships no runtime closure that needs reproducible-by-default installation.
When a contributor wants a reproducible local environment they may run one of:
# Astral uv (recommended — fastest resolver):
uv pip install -e ".[dev]"
uv pip freeze > requirements.dev.lock
# pip-tools:
pip-compile --extra=dev -o requirements.dev.lock pyproject.toml
# Plain pip:
pip install -e ".[dev]"
pip freeze > requirements.dev.lock
The resulting requirements.dev.lock is local to the contributor's machine. CI does not consume it; CI installs from pyproject.toml directly so the test matrix exercises the latest tooling versions.
Rationale¶
- Why range pins instead of exact pins on dev tooling? Dev tooling that breaks on minor updates breaks contributor flow regardless of the pin. Exact pins delay the breakage but do not prevent it; range pins surface it sooner.
- Why no committed lock file? The project has no runtime dependency closure. Committing a dev-only lock would suggest a determinism the project does not actually guarantee for end users.
- Why SHA-pinning on GitHub Actions instead of tag-pinning? Tags are mutable; SHAs are immutable. Action-supply-chain attacks have moved tags between commits in the past. Dependabot rewrites tag references to specific SHAs on first run and bumps them weekly thereafter.
Future revisions¶
If the project grows runtime dependencies — for example, adopting pyyaml for plugin manifest parsing, or httpx for outbound calls — the runtime closure becomes lockable. At that point, this manifest revises to:
- Move new runtime deps to
[project.dependencies]with exact pins (==). - Commit a
requirements.lockcovering the runtime closure. - Add a CI step verifying the lock matches
pyproject.toml.
Dev tooling stays on range pins under either posture.