Skip to content

Contributing to NetBox SSL

Thank you for considering a contribution! This guide covers everything you need to know to propose changes, whether a bug fix, a new feature, or documentation.

Before you start

  • Read the Code of Conduct
  • Check the issue tracker to see if someone else is already working on the same thing
  • For non-trivial changes, open an issue first to discuss the approach — saves time for everyone

Development environment

The recommended setup uses Nix + direnv + uv for reproducible local development, with Docker Compose for the NetBox runtime.

The repo includes a flake.nix that pins Python 3.12, uv, PostgreSQL client, Redis, Docker Compose, and pre-commit. With Nix and direnv installed:

git clone https://github.com/ctrl-alt-automate/netbox-ssl.git
cd netbox-ssl
direnv allow

direnv loads the Nix environment automatically each time you cd into the directory.

Alternative: pip + venv

git clone https://github.com/ctrl-alt-automate/netbox-ssl.git
cd netbox-ssl
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev,docs]"

Docker NetBox

The plugin needs a running NetBox to test UI + API changes:

docker-compose up -d
# NetBox is now available at http://localhost:8000 (admin/admin)
# The plugin is mounted into the container for hot-reload

Running tests

The project has four test flavours, distinguished by pytest markers:

# Unit tests — fast, no NetBox required
pytest tests/ -m unit -v

# API integration tests — require running NetBox + token
NETBOX_TOKEN="nbt_xxx.yyy" pytest tests/ -m api -v

# Browser tests — require running NetBox + Playwright
pytest tests/ -m browser -v

# Load tests — require running NetBox, not run in CI
cd tests/load && NETBOX_TOKEN="nbt_..." locust -f locustfile.py --host=http://localhost:8000

The unified runner wraps all of these:

python scripts/run_tests.py --quick      # unit + parser only
python scripts/run_tests.py --coverage   # with coverage report

Unit-test coverage on netbox_ssl/utils/ must stay at or above 70% — CI enforces this via .coveragerc's fail_under = 70. Higher layers (models, views, scripts) are covered by Docker-based integration tests that also run in CI.

Code style

We use Ruff for linting and formatting:

ruff check netbox_ssl/ tests/      # lint
ruff format netbox_ssl/ tests/     # format

CI runs both. If ruff format --check fails, run ruff format locally and commit the result.

Additional style conventions:

  • Python: PEP 8, 120-character line length, type annotations on public functions
  • Tests: pytest-style, markers for categorisation (@pytest.mark.unit etc.)
  • Docstrings: Google style when helpful; keep them short
  • Comments: only when the why is non-obvious; prefer good names over comments

Commit conventions

We follow Conventional Commits:

<type>: <short description>

<optional body with more detail, why the change matters,
relevant issue numbers, etc.>

Types: feat, fix, docs, test, refactor, chore, ci, perf.

Commits are written in English. Signed commits are appreciated but not required.

Pull request process

  1. Branch from dev, not main. main tracks the latest release; dev is where unreleased work accumulates.
  2. Make focused commits. Each commit should be one logical change that could be reverted independently.
  3. Keep the diff focused. Don't reformat unrelated code in the same PR.
  4. Update tests. New features need new tests. Bug fixes need regression tests.
  5. Update docs. User-facing changes need docs/ updates. Behaviour changes need a CHANGELOG.md entry.
  6. Open the PR against dev. Fill in the PR template.
  7. Address review feedback. Use fixup! commits during review; the maintainer will squash-merge when ready.

The automated checks on PR are:

  • Ruff (lint + format)
  • Unit tests on Python 3.10, 3.11, 3.12
  • Package check (wheel inclusion)
  • Integration tests on NetBox 4.4, 4.5, and 4.6
  • MkDocs strict build (on docs-touching PRs)
  • Gemini code review (automatic, informational)

All must pass before merge.

Definition of Done

The PR template carries a conditional "Definition of Done" checklist. Each item guards a bug class that reached a release because the lenient local NetBox version hid it — NetBox 4.5/4.6 tolerate things that 4.4 rejects hard, and a local run never trips them. Tick the blocks that apply to your change.

Added or changed a model

Every concrete NetBoxModel needs a registered REST serializer (plus a viewset and a router.register(...) line). On save in a request context, NetBox's change-logging calls serialize_for_event → get_serializer_for_model; a model with no serializer raises SerializerNotFound — a hard 500 on NetBox 4.4 (#149). "Skip the API as YAGNI" is never valid for a NetBoxModel. The test_netboxmodel_completeness gate enforces this and the next item, on every NetBox version in the matrix.

Run makemigrations --check and confirm the new migration carries custom_field_data + tags whenever the model inherits NetBoxModel. A migration frozen at models.Model (before the mixin was added) lacks those columns and 500s at runtime (#118, recurred in v1.0.1).

Added or changed an API serializer or filterset

Run manage.py spectacular --validate against NetBox 4.6 and confirm a warning-free schema. Feeding a ChoiceSet.CHOICES 3-tuple to a MultipleChoiceFilter (instead of the ChoiceSet class) crashes /api/schema/ with a 500 (#111).

Added a test file

Confirm it collects under the host lane: pytest tests/ -p no:django --collect-only. A utils module imported at test-module scope that pulls in netbox_ssl.models, or an optional dependency imported at the top of a test, crashes collection there (#148, #149). And never let a test file skip at module level on an import error — that silently hides the very failure the test exists to catch (#143).

Added or changed a NetBox Script

Pass ObjectVar(model=...) the model class, not a dotted string — NetBox calls model.objects.all(), so a string raises AttributeError at import and the script never registers (#143). Make sure the script is reachable from the SCRIPTS_ROOT wrapper; plugin-bundled scripts are not auto-discovered.

Changed user-facing copy or support metadata

Keep the NetBox support matrix identical across README.md, COMPATIBILITY.md, and the pyproject.toml classifiers. A stale matrix has shipped more than once (caught in pre-flight at v1.2.0).

Review SLA

Maintainers review PRs on a best-effort basis — typically within 5 business days. For urgent security issues, see SECURITY.md.

Issue labels

  • good-first-issue — approachable for new contributors
  • help-wanted — maintainers welcome external contributions
  • bug / enhancement / documentation — issue type
  • needs-triage — not yet categorised by a maintainer

Release process (for maintainers)

Before cutting, run the pre-cut rehearsal and fix anything it flags:

python scripts/release_preflight.py X.Y.Z

It inspects files only (no container, no network) and asserts the release-critical invariants that have each broken a past release: version sync (pyproject.tomlnetbox_ssl/__init__.py), a dated CHANGELOG section for the target version, the NetBox support matrix (PluginConfig min/max_version ↔ README badge + table ↔ COMPATIBILITY.md), the publish-gate poll budget (#141/#142), and the gh-pages serialization guard (#110). Exit code is non-zero if any check fails.

  1. Create release/vX.Y.Z branch from dev
  2. Bump version in pyproject.toml and netbox_ssl/__init__.py
  3. Add CHANGELOG [X.Y.Z] section with Added/Changed/Deprecated/Security
  4. Update COMPATIBILITY.md if NetBox support matrix changes
  5. Open PR release/vX.Y.Z → dev, admin-merge after CI + Gemini review
  6. Open PR dev → main, admin-merge after CI
  7. Tag vX.Y.Z on main (annotated: git tag -a vX.Y.Z)
  8. Push tag → triggers publish.yml (PyPI) and docs.yml (GH Pages)
  9. Close the milestone's issues with link to the release

Docs deploy ordering (gh-pages)

The dev → main merge (step 6) and the tag push (step 8) both trigger docs.yml, and both mike deploy --push to the same gh-pages branch. The workflow declares a concurrency: { group: gh-pages-deploy } group so these runs serialize instead of racing (a non-fast-forward push rejection killed the release-critical tag deploy before this was added — issue #110). As extra insurance, allow the main-push Docs Deploy to finish before pushing the tag; if a deploy is ever rejected, re-run the failed run once the competing run completes.

See versioning.md for the semver policy.

Questions?

Open a GitHub Discussion or ping the maintainers on an existing issue. We're happy to help.