Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
586acca
feat(transactions): open every write alias of an operation and join t…
marcinpsk Sep 30, 2026
a4d5bba
refactor: run every plugin write through the write scope
marcinpsk Sep 30, 2026
dde7e57
feat(family): name each kept channel when its reconciliation fails
marcinpsk Sep 30, 2026
fe8c641
test(guard): keep transaction and connection state in transactions.py
marcinpsk Sep 30, 2026
10108bf
fix(rule-cache): key the rule snapshot by its read alias
marcinpsk Sep 30, 2026
f29ea13
docs(family): correct the alias note of the template-name block cache
marcinpsk Sep 30, 2026
42f52ab
ci: install release, publish and docs tools from the lock file
marcinpsk Sep 30, 2026
101d1a6
ci: pass --no-build to uv run in the release, publish and docs jobs
marcinpsk Sep 30, 2026
2161ef2
fix(rule-selection): hold the rule cache in one snapshot type
marcinpsk Sep 30, 2026
8cc2c2f
refactor(tests): split the channel rows of the branch write tests
marcinpsk Sep 30, 2026
adfdd59
test(guard): count a None alias as no alias in a named exception
marcinpsk Sep 30, 2026
2e46e98
feat(triggers): run the rename triggers on the connection of the save
marcinpsk Sep 30, 2026
620407e
refactor(tests): share the bay and interface helpers of the branch cases
marcinpsk Sep 30, 2026
6ff7c58
test(branch): check the ObjectChange of every rename in a branch install
marcinpsk Sep 30, 2026
d436135
test(branch): move a module in a branch
marcinpsk Sep 30, 2026
7443728
docs: state the alias and the commit that the rename triggers use
marcinpsk Sep 30, 2026
05535b8
docs(configuration): put a script install in a transaction on the int…
marcinpsk Oct 1, 2026
6ecfadc
feat(transactions): run plugin writes on both connections of a netbox…
marcinpsk Oct 1, 2026
dd57d72
Merge branch 'main' into develop
marcinpsk Oct 1, 2026
591212d
feat(triggers): run the rename triggers on the connection of the save…
marcinpsk Oct 1, 2026
cb9f40b
refactor(branching): ask in one module whether netbox-branching is in…
marcinpsk Oct 1, 2026
5598d35
feat(jobs): run Apply Rules and Convert jobs in the branch they were …
marcinpsk Oct 1, 2026
2b972b7
feat(api): refuse a background rule request in a netbox-branching branch
marcinpsk Oct 1, 2026
d4b21f6
ci: enforce the coverage gate on the combined data of two legs
marcinpsk Oct 1, 2026
14e9da1
ci: enforce the coverage gate on the combined data of two legs (#154)
marcinpsk Oct 1, 2026
d6b5c47
feat(jobs): run plugin jobs in the branch they were enqueued from (#153)
marcinpsk Oct 1, 2026
86f16f1
build: make the coverage gate exact and run it only on the combined data
marcinpsk Oct 1, 2026
d085034
build: make the coverage gate exact and run it only on the combined d…
marcinpsk Oct 1, 2026
d98ce48
feat(triggers): do nothing while netbox-branching merges, reverts or …
marcinpsk Oct 1, 2026
148fa23
docs(configuration): describe netbox-branching support and its limits
marcinpsk Oct 1, 2026
a22ef3d
docs: name the transaction and branching modules in the agent instruc…
marcinpsk Oct 1, 2026
204eb91
test(branching): pin each replay call site of netbox-branching
marcinpsk Oct 1, 2026
c12a9e4
refactor(tests): define the shared test fixtures once, outside the te…
marcinpsk Oct 1, 2026
a27508d
test(boundaries): refuse an import of a test module inside the test p…
marcinpsk Oct 1, 2026
12c10b4
test(branching): count every replay reference with its receiver and s…
marcinpsk Oct 1, 2026
22a7430
test(branching): fingerprint each reviewed scope of netbox-branching
marcinpsk Oct 1, 2026
2af2bb0
test(boundaries): read the whole test package, subpackages and __init…
marcinpsk Oct 1, 2026
1e66e42
test(branching): pin the reviewed netbox-branching release
marcinpsk Oct 1, 2026
6414a1c
test(boundaries): refuse a wildcard import from the test package
marcinpsk Oct 1, 2026
e7e5b58
fix(models): check the write alias on every rule save
marcinpsk Oct 1, 2026
660895e
fix(models): leave a replayed rule save to netbox-branching
marcinpsk Oct 1, 2026
a8a668f
feat(triggers): do nothing while netbox-branching merges, reverts or …
marcinpsk Oct 1, 2026
74f09ad
Merge branch 'main' into develop
marcinpsk Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .devcontainer/scripts/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,8 @@ if ! command -v gh >/dev/null 2>&1; then
(type -p wget >/dev/null || apt-get install -y -qq wget) \
&& install -d -m 755 /etc/apt/keyrings \
&& out=$(mktemp) \
&& wget -qO "$out" https://cli.github.com/packages/githubcli-archive-keyring.gpg \
&& cat "$out" | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& wget -q --max-redirect=0 -O "$out" https://cli.github.com/packages/githubcli-archive-keyring.gpg \
&& install -m 644 "$out" /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
&& apt-get update -qq \
&& apt-get install -y -qq gh \
Expand Down
8 changes: 5 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ Requires NetBox ≥ 4.3.0 and Python ≥ 3.12. Licensed under Apache-2.0 (REUSE-
This follows the standard [NetBox plugin pattern](https://netboxlabs.com/docs/netbox/en/stable/plugins/development/):

- **`models.py`**: Defines `InterfaceNameRule`. A rule can select an exact module type or a regex pattern, add parent, device, and platform scopes, and describe flat or channelized breakout output.
- **`signals.py`**: Connects `pre_save` and `post_save` for `dcim.Module` and `dcim.Device` and passes each save to `rename_triggers.py`. The receivers hold no state and make no decision. It intentionally does not connect to `dcim.Interface` because NetBox creates module interfaces with `bulk_create()`. It also connects the optional LibreNMS prediction signal when that plugin is installed.
- **`rename_triggers.py`**: Owns the rename-trigger lifecycle. It reads the previous state before a save and lets a read error fail the save. After the save it decides whether the save is a rename trigger and schedules one reapply per module or device per transaction with `transaction.on_commit()`. The reapply compares the earliest previous state with the committed row, and it catches and logs failures at that boundary.
- **`signals.py`**: Connects `pre_save` and `post_save` for `dcim.Module`, `dcim.ModuleBay` and `dcim.Device` and passes each save to `rename_triggers.py` with the alias of the save, which must be the write alias. The receivers hold no state and make no decision. It intentionally does not connect to `dcim.Interface` because NetBox creates module interfaces with `bulk_create()`. It also connects the optional LibreNMS prediction signal when that plugin is installed.
- **`rename_triggers.py`**: Owns the rename-trigger lifecycle. It reads the previous state before a save and lets a read error fail the save. After the save it decides whether the save is a rename trigger and schedules one reapply per module or device per transaction with `transaction.on_commit()` on the connection of the save, so the plan runs after that connection commits. The reapply compares the earliest previous state with the committed row, and it catches and logs failures at that boundary.
- **`transactions.py`**: The one owner of database connections and transaction state. Each plugin write runs in a write scope on `default`, and in a netbox-branching branch also on the branch connection.
- **`branching.py`**: The one module that imports netbox-branching. It checks its version at startup, gives a job the identity of its branch, and marks each merge, revert and sync so that the rename triggers do nothing while it replays changes.
- **`rule_selection.py`**: Loads and fingerprints enabled rules, separates exact and regex candidates, applies scope priority, and pins one cached snapshot across batch work.
- **`name_template.py`**: Owns the name-template language, its template-variable catalogue, and evaluation.
- **`naming.py`**: Builds template-variable values from the module-bay hierarchy.
Expand Down Expand Up @@ -78,7 +80,7 @@ netbox-test netbox_interface_name_rules/tests/test_views.py::TestClassName::test
TEST_DB_NAME=test_netbox_interface_name_rules TEST_REDIS_HOST=redis pytest netbox_interface_name_rules
```

`pyproject.toml` adds `-n auto` and coverage options. Do not pass your own `-n`.
`pyproject.toml` adds `-n auto` and coverage options. Do not pass your own `-n`. A local run prints coverage but does not fail on it: the 97% gate runs in CI on the combined data of two legs.

## REUSE/SPDX compliance

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/coverage-badge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:

steps:
- name: Download coverage report
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: coverage-report
run-id: ${{ github.event.workflow_run.id }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/mkdocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
python-version: '3.12'

- name: Install dependencies
run: uv pip install --system --group docs
run: uv sync --locked --no-build --only-group docs

- name: Save coverage report from gh-pages
run: |
Expand All @@ -59,7 +59,7 @@ jobs:
fi

- name: Deploy documentation
run: mkdocs gh-deploy --force --strict
run: uv run --no-sync --no-build mkdocs gh-deploy --force --strict

- name: Restore coverage report
if: always()
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-pypi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ jobs:
with:
python-version: "3.x"
- name: Install pypa/build
run: uv pip install --system --group packaging
run: uv sync --locked --no-build --only-group packaging
- name: Build a binary wheel and a source tarball
run: python3 -m build
run: uv run --no-sync --no-build python -m build
- name: Store the distribution packages
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ jobs:
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0

- name: Install dependencies
run: uv sync --group dev
run: uv sync --locked --no-build --only-group release --only-group packaging

- name: Run semantic-release
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: uv run semantic-release version --changelog --push --vcs-release
run: uv run --no-sync --no-build semantic-release version --changelog --push --vcs-release
72 changes: 65 additions & 7 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,16 @@ jobs:
netbox-version: "v4.3.7"
- python-version: "3.13"
netbox-version: "v4.5.3"
# The one cell that reports coverage. Every coverage step below reads this flag, so the
# release that owns the report is named here and nowhere else.
coverage: true
# A coverage leg: the value names its data artifact, which the combine job downloads.
coverage: "netbox-v4.5.3"
- python-version: "3.14"
netbox-version: "v4.7.0"
# The same release with netbox-branching, so the branch tests run against a real provisioned branch.
- python-version: "3.14"
netbox-version: "v4.7.0"
netbox-branching: "1.2.1"
# The second coverage leg: only this cell runs the netbox-branching code paths.
coverage: "netbox-v4.7.0-branching"
# Early warning for upcoming NetBox changes. Non-blocking: unreleased NetBox breaks
# for reasons that are not ours, so this must not gate a PR.
- python-version: "3.13"
Expand Down Expand Up @@ -164,13 +165,72 @@ jobs:
# instead of baseline drift. Bumping the top version means editing the matrix above, this
# condition, and tests/query_counts.json together. The branch leg records too: netbox-branching adds queries.
UPDATE_QUERY_COUNTS: ${{ (matrix.netbox-version != 'v4.7.0' || matrix.netbox-branching) && '1' || '' }}
# `addopts` turns coverage on for every invocation, but only one cell reports it.
# `addopts` collects coverage without the gate; the combine job enforces `fail_under`.
COVERAGE_OPTION: ${{ !matrix.coverage && '--no-cov' || '' }}
run: |
pytest -n auto netbox_interface_name_rules -o pythonpath=../netbox/netbox $COVERAGE_OPTION

- name: Generate coverage report
- name: Upload coverage data
if: matrix.coverage
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-data-${{ matrix.coverage }}
path: netbox-InterfaceNameRules-plugin/.coverage
include-hidden-files: true
if-no-files-found: error
retention-days: 1

# The 97% gate applies to the union of the coverage legs, because some code runs only with netbox-branching.
coverage:
needs: test-netbox
runs-on: ubuntu-latest
permissions:
contents: read

steps:
# The same path as in the test job, so the absolute paths in the data files resolve here.
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: netbox-InterfaceNameRules-plugin
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
Comment thread
marcinpsk marked this conversation as resolved.

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"

# One download per leg by exact name, so a missing leg fails this step.
- name: Download coverage data of NetBox v4.5.3
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: coverage-data-netbox-v4.5.3
path: coverage-data/netbox-v4.5.3

- name: Download coverage data of NetBox v4.7.0 with netbox-branching
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: coverage-data-netbox-v4.7.0-branching
path: coverage-data/netbox-v4.7.0-branching

- name: Install coverage
working-directory: netbox-InterfaceNameRules-plugin
run: |
uv pip install --system --only-binary=:all: --group ci-tests

# `coverage report` enforces `fail_under` from pyproject.toml on the combined data.
- name: Combine and check coverage
working-directory: netbox-InterfaceNameRules-plugin
env:
COVERAGE_RCFILE: pyproject.toml
run: |
coverage combine ../coverage-data/netbox-v4.5.3/.coverage ../coverage-data/netbox-v4.7.0-branching/.coverage
coverage report

- name: Generate coverage report
working-directory: netbox-InterfaceNameRules-plugin
env:
COVERAGE_RCFILE: pyproject.toml
Expand All @@ -181,15 +241,13 @@ jobs:
coverage xml -o ../coverage-report/coverage.xml

- name: Upload coverage report
if: matrix.coverage
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-report
path: coverage-report/
retention-days: 7

- name: Upload coverage to Codecov
if: matrix.coverage
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
files: coverage-report/coverage.xml
Expand Down
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ The complete path from a NetBox model save, through the committed callback, to t
_Avoid_: Signal handler performance

**Rename trigger**:
A saved change in NetBox after which the names a rule gives may be wrong, so the plugin must reapply its rules. The triggers are: a module is installed, a module's type changes, a module moves to another bay or device, an occupied module bay's position or name changes, and a device's virtual chassis or virtual-chassis position changes (the device joins a virtual chassis, leaves it, or gets a different position). A bay's name change is a trigger only when a template variable reads the name: a bay whose position is a template token takes its position from the trailing digits of its name. An edit of an empty bay is not a trigger. A module's type change also reaches each module nested in it whose rule changes, because a rule can be scoped to a parent module type. The triggers of one transaction cause one reapply plan, which reapplies each module and each device at most once. A device reapply also reapplies every module of its device that the plan did not already reapply for a module trigger.
A saved change in NetBox after which the names a rule gives may be wrong, so the plugin must reapply its rules. The triggers are: a module is installed, a module's type changes, a module moves to another bay or device, an occupied module bay's position or name changes, and a device's virtual chassis or virtual-chassis position changes (the device joins a virtual chassis, leaves it, or gets a different position). A bay's name change is a trigger only when a template variable reads the name: a bay whose position is a template token takes its position from the trailing digits of its name. An edit of an empty bay is not a trigger. A module's type change also reaches each module nested in it whose rule changes, because a rule can be scoped to a parent module type. A change that netbox-branching replays in a merge, a revert or a sync is not a rename trigger: the replayed changes already hold the names. The triggers of one transaction cause one reapply plan, which reapplies each module and each device at most once. A device reapply also reapplies every module of its device that the plan did not already reapply for a module trigger.
_Avoid_: Signal, event

**Reapply**:
Expand Down
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ with `test_`, and set `TEST_REDIS_HOST` to the Redis server that the tests can u
TEST_DB_NAME=test_netbox_interface_name_rules TEST_REDIS_HOST=localhost pytest netbox_interface_name_rules
```

A local run prints coverage but does not fail on it. The 97% gate runs in CI on the combined
coverage data of the NetBox v4.5.3 leg and the netbox-branching leg.

### Commits

We use [Conventional Commits](https://www.conventionalcommits.org/):
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ automatically apply renaming rules based on configurable templates.
- **Breakout support**: create multiple channel interfaces from a single port (e.g., QSFP+ 4x10G)
- **Scoping**: rules can be scoped to specific device types, parent module types, or be universal
- **Bulk import/export**: YAML-based rule management via the UI or API
- **netbox-branching**: renames run in the active branch, and a merge, revert or sync keeps the names that it replays (netbox-branching 1.2.x on NetBox 4.7). A channel that the plugin kept at its old name is the exception: see [Limits in a branch](https://marcinpsk.github.io/netbox-InterfaceNameRules-plugin/configuration/#limits-in-a-branch)

## Supported scenarios

Expand Down
Loading
Loading