Skip to content

Dependency Pinning Manifest

Ratified posture for how the apothem declares, 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:

  1. Move new runtime deps to [project.dependencies] with exact pins (==).
  2. Commit a requirements.lock covering the runtime closure.
  3. Add a CI step verifying the lock matches pyproject.toml.

Dev tooling stays on range pins under either posture.