Contributing
This guide covers how to set up a development environment for infrahub-sync and contribute to the project. For the release runbook, see RELEASING.md at the repository root — that's maintainer-only.
Prerequisites
- Python 3.11–3.13 for the full development profile (3.12 recommended). Python 3.10 runs everything except the Sync service.
- uv for dependency management
- Git
Setting up your development environment
Clone the repository
For V3 development, clone the feature/v3-develop branch. The default main branch
contains V2 and does not define the prefect and service extras required below.
To install the released private candidate, follow Install Infrahub Sync.
git clone --branch feature/v3-develop https://github.com/opsmill/infrahub-sync.git
cd infrahub-sync
Before installing dependencies, check the branch:
git branch --show-current
The expected output is feature/v3-develop.
Install uv
If you don't have uv installed, you can install it with:
curl -LsSf https://astral.sh/uv/install.sh | sh
Or see the uv installation guide for other options.
Install dependencies
uv sync --extra dev --extra prefect --extra service
The prefect and service extras are not optional for development. Without them the type
checker cannot resolve the imports in infrahub_sync/orchestration/ and
infrahub_sync/service/, and the tests that cover them skip themselves.
On Python 3.10 the Sync service is unavailable, so install the direct Prefect profile instead and exclude the service from type checking:
uv sync --python 3.10 --extra dev --extra prefect
Verify your setup
uv run infrahub-sync --help
uv run infrahub-sync configs --help
Install the Git hooks
prek.toml defines the commit hooks: Ruff formatting and lint for Python, rumdl for Markdown
and MDX, and checks for whitespace, YAML, TOML, large files, and private keys. Install them with:
uv run --frozen --extra dev prek install --force
Run the same command in an existing checkout. This project used pre-commit before, and
uv sync removes that package, so the .git/hooks/pre-commit file it generated stops working
and blocks every commit. --force replaces that file. It also overwrites any other script at
that path, so copy your own hook elsewhere first if you keep one there.
Find your way around the tree
Repository tour maps the current modules — the command-line client, configuration admission, the service and its worker, plans, product storage, adapters, the cache and the engine — and separates them from the material that is development-only or historical. Start there before changing code you have not touched before.
Development workflow
Before committing any changes, run the following commands in order:
# Run `rumdl fmt .`, then Ruff formatting and safe fixes.
uv run invoke format
# Run `rumdl check .`, then Ruff, Pylint, yamllint, and ty; stop at the first failure.
uv run invoke lint
invoke lint stops after the first gate that fails. Pylint does not pass on a clean
checkout: its leg compares the run against a recorded baseline and fails only on a new
diagnostic code or a count above the recorded maximum.
See Quality gates for the complete lint and formatter workflow.
Run uv run invoke check-310 before pushing as well: it builds its environments under
.preview/check-310-venv, leaves your active .venv alone, and reproduces the three CI legs your
environment cannot reach — ty under the Python 3.10 excludes, and the unit tests on
Python 3.10 both with the prefect extra and on a base install that has no service
dependencies. If Python 3.10 is not installed, the task skips loudly and exits successfully,
leaving all three legs unqualified. To install it, run uv python install 3.10. See
Testing tiers.
Validate the CLI
After making changes, verify the CLI still works:
uv run infrahub-sync --help
uv run infrahub-sync configs --help
uv run infrahub-sync runs plan --help
Running tests
The offline gate is what a change has to keep green. It is a single command, and it deselects every marker that needs a running stack, a live service or a Docker daemon:
uv run invoke tests.tests-unit
See Testing tiers for what each other tier needs, what it writes, and why a skipped check is not a pass. See Testing for the repository-wide rules about individual tests.
Running the full stack locally
To run the Sync HTTP API, its Prefect worker, and a disposable Infrahub against your checkout, see the local development stack. That page also holds the service
development loop: the API has no auto-reload, so a code change needs preview.down followed
by preview.up.
The suite that exercises that stack is opt-in under the preview marker, and it skips
rather than fails when the stack is not running:
uv run invoke preview.up # start the stack
uv run invoke preview.smoke # seed, then run the preview smoke suite
uv run invoke preview.down --volumes
CI tiers
Continuous integration runs in two tiers on a pull request. The fast tier runs on every push: lint, the uv check, the unit tests, and an image build for linux/amd64 with its release identity, deployment bundle, bill of materials, vulnerability scan and smoke test. It is what a review iteration waits for.
The full qualification tier adds the linux/arm64 build and smoke, the warm-builder freshness check, the Compose lifecycle suite and the clean-host matrix. Add the qualify label to a pull request to run it, and leave the label on: every later push re-runs it while it is there. It also escalates by itself when the diff touches a path only those stages cover — the Dockerfile, deploy/compose/, tests/compose/, tests/image/, tasks/image.py, tasks/compose.py, tasks/release.py, the qualification kit, or the workflow files that declare the tiers — so a change to the lifecycle qualifies whether or not anyone remembers the label.
Nothing is skipped permanently. The Full qualification check always runs and fails until the full tier has covered the head, so a pull request cannot merge on the fast tier alone.
A merge also runs the full tier by itself. A push to feature/v3-develop — which is what merging a pull request produces — is not a pull-request event, so it qualifies in full without a label. That covers the merged tree, which no pull-request run builds as itself: what a pull request tests is its head merged with the base at that moment, and the commit that lands can differ from it.
Code standards
Python style
- Python 3.10–3.13 compatible
- Type hints on new or changed code
- Ruff-formatted and lint-clean
- Clean under
ty; do not add[[tool.ty.overrides]]blocks to mask an error - Public functions and classes require documentation strings
- Raise specific exceptions; avoid broad
except Exception:
Line length
- Maximum line length: 120 characters (configured in
pyproject.toml)
Documentation
If you make user-facing changes (CLI flags, configuration options, new adapters), update the documentation.
Two documentation lines
The site publishes two versions. The current line is V3: its pages are docs/docs/** and its
navigation is docs/sidebars.ts. The V2 line is a frozen snapshot: its pages are
docs/versioned_docs/version-2.x/**, its navigation is
docs/versioned_sidebars/version-2.x-sidebars.json, and docs/versions.json lists it.
The snapshot was taken from the V2 documentation on the product's main branch and adapted
only where a versioned copy requires it: component imports resolve through the site alias, and
the product and example links it carries are pinned to the exact commit the snapshot came from
rather than to a moving branch.
The V2 team keeps maintaining main exactly as it does today. Nothing imports those changes
here: there is no automatic import, and the snapshot does not track main. A later V2
documentation change reaches the snapshot only when someone decides it should and edits the
affected file narrowly, by hand.
Rules for a documentation change
- Name the line or lines the change affects before you edit. A statement about product behavior belongs on the line whose product behaves that way.
- Edit the files of each affected line explicitly. A current-line change touches
docs/docs/**and, when navigation changes,docs/sidebars.ts. A V2 change touchesdocs/versioned_docs/version-2.x/**and its versioned sidebar. Neither edit implies the other. - Preserve the snapshot's provenance. Never bulk-refresh the V2 tree from
main, and never rerun the freeze over it. Both would silently replace reviewed content, discard the adaptations above, and lose the record of what was corrected and why. - Build and inspect both versions. A change to either line, to a shared component, or to the site configuration is verified by building the site and reading the affected pages on both versions, including the version menu and the route each page resolves to.
- Keep V3-only material on the current line. The Sync API service, the configuration registry, the package envelope, and the Compose deployment do not exist in V2. Do not describe them in the snapshot, and do not link the snapshot to pages that describe them.
- Keep shared components and assets compatible with both lines.
docs/src/**anddocs/static/**render inside both versions. A change that assumes current-line content, or that removes something the snapshot still imports, breaks the V2 build. - Put a release note on the line it describes. Notes for releases on the V2 line belong in the snapshot's release notes; notes for the current product belong on the current line.
- Pin the snapshot's outbound product links immutably. A link from the snapshot into the repository or the examples names the commit the snapshot came from, not a branch. Do not add a hard-coded root-relative or hosted-only cross-version link in the snapshot: the site is served under more than one base path, and such a link breaks under at least one of them.
- Adapt a paired correction to each line; do not copy bytes between them. The same fact is often written differently on the two lines, and the current wording frequently names commands, options, or surfaces that V2 never had. Rewrite the correction to fit the page you are editing.
Which line a correction belongs to
Documentation follows product behavior that actually ships.
- A product behavior or supported-version statement that changes on V2 is corrected on the V2 line.
- A change that affects both lines is corrected on both, worded for each.
- A historical instruction that is dangerous or materially false may receive a narrow correction in the snapshot, recorded with what was changed and why.
None of this makes V2 a maintained product line. It carries no promise of product fixes, security fixes, dependency updates, or changes ported back from the current line; correcting a page describes what that release did, and changes nothing about what is shipped for it.
Generate command-line documentation
uv run invoke docs.generate
Build documentation site
First-time setup (requires Node.js):
cd docs && pnpm install --frozen-lockfile
Build the site:
uv run invoke docs.docusaurus
Lint markdown files
Markdown structure is checked with rumdl, configured in
pyproject.toml:
uv run invoke docs.format-rumdl # run `rumdl fmt .`
uv run invoke docs.rumdl # run `rumdl check .`
Prose style is checked with Vale, configured in .vale.ini. Run it on
the files you changed:
vale docs/docs/contributing.mdx
Changelog entries
Release notes are written by contributors rather than generated from pull request titles, so every pull request into main must add a news fragment under changelog/. CI fails the pull request if it does not.
Create one with towncrier, naming it after the issue or pull request number:
uv run --extra dev towncrier create -c "Short description of what changed." 123.fixed.md
The file must be a direct child of changelog/ named <id>.<type>.md, where the type is one of security, removed, deprecated, added, changed, fixed, or housekeeping. Use + as the identifier when the change has no issue number, for example +short-slug.housekeeping.md.
Nested paths and unknown types are ignored by towncrier, so the check rejects them rather than let your entry disappear at release time.
If a change genuinely needs no entry — a dependency bump or a typo fix — a maintainer can label the pull request ci/skip-changelog.
Do not edit CHANGELOG.md or the version in pyproject.toml by hand. Both are generated when a release is prepared; see RELEASING.md.
Adding a new adapter
Adding an adapter is the single procedure. It covers the
connector itself, the capability declaration that makes a package naming it admissible, the two
conformance tests that guard that declaration, the example and its registry envelope, the
documentation page, and the read-only diff validation to run before any write.
It also records the current limit for an adapter that does not ship in this repository, which is worth reading before you start.
Invoke tasks
View all available tasks:
uv run invoke --list
Common tasks:
| Task | Description |
|---|---|
linter.format-ruff | Format Python code with ruff |
linter.lint-ruff | Lint Python code with ruff |
linter.lint-pylint | Lint Python code with pylint |
linter.lint-yaml | Lint YAML files with yamllint |
linter.lint-ty | Type-check with ty |
docs.format-rumdl | Format Markdown and MDX with rumdl |
docs.rumdl | Lint Markdown and MDX with rumdl |
docs.generate | Generate CLI documentation |
docs.docusaurus | Build documentation website |
format | Run rumdl formatting, then Ruff formatting and safe fixes |
lint | Run rumdl, Ruff, Pylint, yamllint, and ty in order |