Skip to content
Apothem
Reference

Dependency Pinning Manifest

Dependency declaration policy covering Python runtime ranges, dev tooling, release-tool lock files, and ratified version floors for each build surface.

Ratified posture for how Apothem declares, pins, and locks dependencies. Audience: contributors and downstream consumers who need to understand the reproducibility model.

Posture

Apothem is a Python CLI with a small runtime dependency set plus a larger development, audit, release, and documentation toolchain. Runtime dependencies use conservative lower bounds in pyproject.toml; the website commits site/package-lock.json; GitHub Actions are pinned by immutable SHA with version comments and are bumped by Dependabot.

ClassDeclarationLock policy
Runtime Python dependencies[project.dependencies] in pyproject.tomlLower-bound ranges for users; no Python runtime lock ships with the package. CI installs a hash-locked closure of them (see CI dependency locks)
Dev / security tooling[project.optional-dependencies] extrasLower-bound ranges, except ruff and mypy, which the dev extra pins exactly to the versions CI runs (see CI tool locks for ruff); CI installs the dev extra from a hash-locked closure (see CI dependency locks)
Release toolingscripts/release/requirements-build-linux.txt for release workflows; the release extra in [project.optional-dependencies] for local buildsHash-locked for release workflows; generated from the release tooling closure. The release extra is a lower-bound range
CI pipscripts/ci/requirements-ci-pip.txtHash-locked: one pip==X.Y.Z pin with the hashes of its wheel and sdist, installed with --require-hashes -r by every workflow that upgrades pip. Dependabot proposes the upgrades and rewrites the hashes; scripts/dev/check_pip_pin_drift.py holds the pin at pip 26.2 or later
CI dependency locksscripts/ci/requirements-ci-dev.txt, -runtime.txt, -test.txt and -prereqs.txtHash-locked universal closures compiled with uv pip compile from pyproject.toml (or, for -prereqs.txt, the engine's host prerequisites) as of a lock date, less a 7-day cooldown, recorded in each file, installed with --require-hashes -r (the package itself with --no-deps -e .). CI tests the locked versions until scripts/dev/refresh_ci_locks.py regenerates them; pip-audit.yml audits the newest allowed versions weekly and audits the locks. tests/scripts/test_ci_locks_fresh.py fails when a lock no longer satisfies its inputs
CI tool locksscripts/ci/requirements-ci-ruff.txt, -bandit.txt, -pip-audit.txt, -pip-licenses.txt and -pre-commit.txtOne hash-locked closure per tool the workflows run, compiled by the same script from an exact spec in scripts/dev/ci_lock_table.py, with the same lock date and cooldown, and installed with --require-hashes -r. No workflow spells a tool version. pre-commit needs Python 3.11 or later, so its lock does too, and the pip-audit lock is compiled for 3.11 as well because the newest filelock that supports 3.10 has unfixed advisories. No lock but the CI pip pins pip
Documentation sitesite/package.jsonsite/package-lock.json committed and consumed by npm ci
VS Code packagertools/vsce/package.jsontools/vsce/package-lock.json committed; release and publish jobs run npm ci --ignore-scripts
Channel-smoke harness CLIstools/channel-smoke/package.jsontools/channel-smoke/package-lock.json committed and consumed by npm ci
GitHub ActionsSHA-pinned uses: refs with version commentsDependabot proposes weekly updates; exceptions carry action-pinning-exempt: markers

Release ratification

SurfaceRatified stateRationale
Runtime depsCurrent lower bounds live in pyproject.toml [project.dependencies]Current stable floors ratified before release; no known runtime CVE blocker
Dev toolingCurrent lower bounds live in pyproject.toml dev extra; ruff and mypy are the exact pins thereLatest stable floors ratified before release; ruff and mypy are each pinned to one version for the hooks, CI and contributors, the mypy hook pins the dev lock's versions of the libraries it lists, and tests/scripts/test_ci_tool_locks.py fails when they differ
Security toolingCurrent lower bounds live in pyproject.toml security extraCurrent scanner floors ratified before release
Release toolingHash-locked closure lives in scripts/release/requirements-build-linux.txtRelease workflow installs with --require-hashes; local scripts require build to already exist instead of silently installing moving dependencies
CI pipHash-locked pin lives in scripts/ci/requirements-ci-pip.txtpip 26.2 or later closes CVE-2026-13346; workflows install it with --require-hashes, so the download is checked against its published hash
CI dependency locksHash-locked closures live in scripts/ci/requirements-ci-{dev,runtime,test,prereqs}.txtWorkflows install them with --require-hashes; each records its lock date, cooldown and cutoff, and the freshness test keeps them in step with pyproject.toml
CI tool locksHash-locked closures live in scripts/ci/requirements-ci-{ruff,bandit,pip-audit,pip-licenses,pre-commit}.txtWorkflows install them with --require-hashes and carry no tool version; the specs live in scripts/dev/ci_lock_table.py, and the lock date, cooldown and cutoff match the dependency locks
Docs toolingCurrent ranges live in site/package.json; exact resolution lives in site/package-lock.jsonnpm outdated returns no stale docs dependency after lockfile refresh
GitHub ActionsExisting SHA pins retained for the current release; the SLSA reusable workflow remains the documented tag-pinned exception, marked with an action-pinning-exempt: comment in release.ymlPublic CI rerun is green; broad action-major upgrades are intentionally left to dependency-automation PRs to avoid release-surface churn
npm packagingpublish-npm.yml workflow runs on the v*.*.* tag push, with workflow_dispatch as the fallbackThe single public npm package is @ahmed-g-gad/apothem; publication waits for the operator's approval of the release environment, given after scripts/release/check-release-ready.sh passes

Locking

Contributors' own Python lock files are generated on demand and are not committed: Apothem's runtime closure is small and the CLI supports multiple Python minor versions. CI is the exception. It installs the hash-locked files in scripts/ci/: four dependency locks (requirements-ci-dev.txt, -runtime.txt, -test.txt and -prereqs.txt) and one lock per tool the workflows run (-ruff.txt, -bandit.txt, -pip-audit.txt, -pip-licenses.txt and -pre-commit.txt). python scripts/dev/refresh_ci_locks.py compiles all nine as of a lock date, less a 7-day cooldown that matches Dependabot's, so CI tests the releases that were at least a week old on the lock date and refreshing the files moves them forward. Each file records the lock date, the cooldown and the resulting cutoff, and --check reproduces the cutoff. An urgent security fix that is younger than the cooldown needs a refresh run with --cooldown-days 0, or a cooldown exception for its package in scripts/dev/ci_lock_table.py, which lifts the cooldown for that package alone. The weekly pip-audit.yml run audits the newest versions pyproject.toml allows as well as the nine locks, asking the OSV service for advisories. Contributors keep installing with pip install -e ".[dev]".

# Astral uv:
uv pip install -e ".[dev,security,release]"
uv pip freeze > requirements.dev.lock

# pip-tools:
pip-compile --extra=dev --extra=security --extra=release -o requirements.dev.lock pyproject.toml

# Plain pip:
pip install -e ".[dev,security,release]"
pip freeze > requirements.dev.lock

The website is different: site/package-lock.json is committed and CI uses npm ci, so the rendered documentation site has a reproducible Node closure.

Rationale

  • Why range pins for Python? The CLI supports a broad Python matrix, so pyproject.toml keeps conservative floors. CI installs hash-locked closures of those ranges, which the maintainer refreshes with scripts/dev/refresh_ci_locks.py (weekly is the intended cadence), and the weekly pip-audit.yml run resolves the newest allowed versions to reveal problems with current tooling.
  • Why a committed Node lock? The website is a deployed static artifact; the lockfile gives deterministic npm ci builds for that public surface.
  • Why SHA-pinned Actions? Tags are mutable; SHAs are immutable. Dependabot proposes the SHA bumps and rewrites each version comment, while the release gate records any intentional hold.
  • What the update bot does not touch. Dependabot version updates (.github/dependabot.yml) cover the GitHub Actions pins, the website lockfile, the vsce packager lockfile (tools/vsce), the harness-CLI lockfile the channel smoke installs (tools/channel-smoke), the hash-locked pip that CI installs (scripts/ci/requirements-ci-pip.txt) and the pre-commit hooks. The other nine files in scripts/ci are closures regenerated whole with scripts/dev/refresh_ci_locks.py, none of them pins pip, and Dependabot's version updates for that folder are limited to pip. The runtime floors in pyproject.toml, the hash-locked release requirements, the vendored closure in src/apothem/_vendor/vendor.txt and the pre-commit hooks pinned in lockstep with pyproject.toml move by hand, because a single-pin bump would break the files they agree with. The ruff and mypy hooks are two of them: tests/scripts/test_ci_tool_locks.py fails until the exact ruff pin in pyproject.toml, the hook rev, the ruff lock and the dev lock carry one version, and until the exact mypy pin, the mypy hook rev and the libraries that hook lists match the dev lock, so a Dependabot security pull request for either tool is adapted by hand. The website's @types/node major follows its Node floor (engines and .nvmrc, Node 22), so Dependabot proposes only its minor and patch updates; raise that major together with the floor. Version updates leave npm overrides blocks alone. Dependabot security updates still cover every manifest, so a security pull request can propose one of these pins, or move an override; adapt it by hand or close it.
  • Merging a Dependabot pull request. The repository's squash default takes the PR title. Some Dependabot titles, mostly grouped ones that name a directory, run past the 72-character squash subject; the PR Title check names them, and a retitle fixes them. A pull request that changes a tool lockfile runs no ci.yml job, so the npm audit workflow audits and installs it, packages the VS Code extension for the vsce lockfile and runs the vsce release tests, and the channel smoke workflow runs for the channel-smoke lockfile. The Markdown Lint workflow runs the markdownlint hook on a pre-commit hooks pull request; the other hooks are not exercised in CI, so run pre-commit run --all-files on its branch before merging it.

On this page