# Install uv (if you don't have it)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install all dependencies (including dev tools like ruff)
uv sync
# Or with pip
pip install -e ".[dev]"VIP uses ruff for both linting and code formatting. The easiest way to run checks is with just:
just check # run both lint and format checks
just fix # auto-fix lint issues and reformat
# Or individually
just lint # ruff check
just format-check # ruff format --check
just lint-fix # ruff check --fix
just format # ruff formatWithout just, run ruff directly:
uv run ruff check src/ src/vip_tests/ # lint
uv run ruff format --check src/ src/vip_tests/ # format check
uv run ruff check --fix src/ src/vip_tests/ # auto-fix lint
uv run ruff format src/ src/vip_tests/ # reformatuv run mypy src/uv.lock is committed and must stay reproducible across machines. The uv
version is pinned so relocking always produces the same output:
-
pyproject.toml's[tool.uv] required-version = ">=0.11"rejects any uv older than 0.11 for every uv command in this repo. Older uv (e.g. the 0.6.x still shipped by some package managers) strips theupload-timewheel annotations and writes an older lockfile revision, which churns ~2000 lines on any relock. If uv refuses to run, upgrade it (uv self update, orbrew upgrade uv). -
just relockregenerates the lockfile with an exact pinned uv version (UV_VERSIONin thejustfile), fetched viauvx— so the output is identical even when your local uv is a different version. Always relock with this recipe rather than a bareuv lock:just relock
When bumping the pin, change both
UV_VERSIONin thejustfileand therequired-versionfloor inpyproject.tomltogether.
Never hand-edit conflict markers in uv.lock. Take either side wholesale, then
regenerate deterministically:
git checkout --theirs -- uv.lock # or --ours; the starting point doesn't matter
just relock # re-resolves from pyproject.toml with the pinned uv
git add uv.lockBecause the uv version is pinned, the regenerated lockfile is identical to what CI and other contributors produce, so the conflict resolves cleanly.
The wheel published to PyPI carries the version constraints declared in
pyproject.toml's [project.dependencies], so uv tool install posit-vip and
pip install posit-vip resolve against them. To keep a fresh install
predictable (see #399):
- Exact
==pins for the dependencies that shape aviprun's output —pytest,pytest-bdd,pytest-order,pytest-playwright,pytest-xdist, andplaywright. Each pin must equal the version resolved inuv.lock;selftests/test_dependency_pins.pyfails if they drift apart. - Next-major caps (e.g.
requests>=2.33.0,<3) on every other runtime dependency, so a breaking major release cannot land on install. Thereportandloadoptional groups are capped the same way; thedevgroup is left uncapped by this policy (aside fromruff's pre-existing narrow range).
Bumps flow through Dependabot's uv job (weekly, 7-day cooldown): it raises the
pin or cap in pyproject.toml and updates uv.lock in one PR, which CI gates
before merge. The next release then ships the tested set. To bump a pin by hand,
edit the constraint and run just relock in the same commit.
Releases run on a weekly train, not on every merge. release.yml runs on a
schedule (Thursday evenings) and on workflow_dispatch for out-of-band
releases. It computes the next version, exits cleanly if there are no commits
since the last release, bumps the version in pyproject.toml and
src/vip/__init__.py, relocks uv.lock, updates CHANGELOG.md, and pushes a
single chore(release): <version> commit plus a vX.Y.Z tag.
The tag push triggers two workflows:
publish.yml-- builds the wheel/sdist, asserts the tag matches thepyproject.tomlversion, attaches the locked constraints file to the GitHub release via draft-then-publish (so the asset lands before immutable releases lock it), publishes to PyPI with PEP 740 attestations, then smoke-tests the published package by installingposit-vip==<version>from PyPI and runningvip --version.docker.yml-- builds and pushes the container image toghcr.io/posit-dev/vip, then pulls the pushed tag back and runsvip versionas a sanity check.
VIP's version is calendar-based (YYYY.M.PATCH), matching the product train
it validates rather than semver. scripts/next_version.py implements the
one rule this reduces to: the first release of a calendar month is
YYYY.M.0; every later release that month bumps the patch instead. The
first Thursday of a month is always the first release of that month, so this
single rule covers both the monthly and the weekly cadence without any
day-of-month or nth-weekday logic. A version like 2026.7.3 says "the fourth
VIP release cut in July 2026" -- it is not a promise that this build targets
Posit Team 2026.07.x specifically, and it makes no statement about which
product versions VIP supports (see MINIMUM_SUPPORTED_POSIT_TEAM in
src/vip/version.py for that).
Conventional commit types (feat, fix, chore, ...) still group entries in
CHANGELOG.md and are still enforced by pr-title.yml, but they no longer
influence the version number. The release gate is simply whether any commit
landed since the last tag -- a week of nothing but chore(deps) bumps still
ships, rather than being silently skipped the way semantic-release would.
git-cliff (configured in cliff.toml) replaced python-semantic-release's
changelog generation. It is invoked as git-cliff --tag v<version> --unreleased --prepend CHANGELOG.md, which sets the tag for a version that
does not exist yet, so the changelog entry lands inside the release commit,
before the tag is created -- semantic-release's changelog step could not do
this, since its changelog is tag-driven and can only write an entry for a tag
that already exists.
workflow_dispatch's optional version input is the escape hatch for an
urgent out-of-band release. Leaving it blank behaves exactly like a scheduled
run, including the no-commits gate -- it's a "release now" button, not an
override. Supplying an explicit version bypasses that gate, on the grounds
that supplying a version is a deliberate act.
Whichever way the version was arrived at, the workflow then refuses to release
it unless it is strictly greater than the last release tag. This applies to
the computed version too, not only a dispatched one, because the rule keys
off today's calendar month: if a tag ever lands in a future month, every
scheduled run until the calendar catches up computes something lower than the
highest tag. One stray v2026.12.0 cut in July would otherwise poison
August, September, October and November in turn. A PyPI version cannot be
reused once published, so each of those would be unrecoverable.
scripts/next_version.py --verify <version> --last-tag <tag> is that check,
kept in the script rather than inline in the workflow so selftests/
can cover it.
- Non-destructive — tests create, verify, and clean up their own content. They never modify or delete existing customer content.
- Diagnostic — tests are sequenced so failures localize problems. Prerequisites run first; product tests follow.
- Loosely coupled — the suite avoids tight coupling to product client libraries. API calls use plain HTTP where practical.
- Duplication over coupling — code duplication with product-internal test suites is acceptable if it keeps VIP independent and version-flexible.