Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 7 additions & 2 deletions .github/workflows/lint-format.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ jobs:
python-version: '3.12'

- name: Install dependencies
run: 'uv pip install --system --only-binary=:all: --group lint'
shell: bash
run: |
uv export --system-certs --locked --only-group lint --no-emit-project --format requirements-txt | \
uv pip install --system-certs --system --only-binary=:all: --require-hashes -r /dev/stdin

- name: Check the lockfile is current
run: uv lock --check
Expand All @@ -42,8 +45,10 @@ jobs:
run: pre-commit run --all-files zizmor

- name: Check the release configuration
shell: bash
run: |
uv pip install --system --only-binary=:all: --group release
uv export --system-certs --locked --only-group release --no-emit-project --format requirements-txt | \
uv pip install --system-certs --system --only-binary=:all: --require-hashes -r /dev/stdin
python .github/scripts/check_release_config.py
fixtures="$(mktemp -d)"
trap 'rm -rf "$fixtures"' EXIT
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/test-netbox-main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@ jobs:

- name: Install NetBox and plugin
working-directory: netbox-InterfaceNameRules-plugin
shell: bash
run: |
uv pip install --system -r ../netbox/requirements.txt
uv pip install --system --only-binary=:all: --group ci-tests
uv pip install --system -e .
uv export --system-certs --locked --group ci-tests --no-default-groups --no-emit-project --format requirements-txt | \
uv pip install --system-certs --system --only-binary=:all: --require-hashes -r /dev/stdin
uv pip install --system-certs --system --no-deps -e .

- name: Set up NetBox configuration
working-directory: netbox
Expand Down
24 changes: 14 additions & 10 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
# 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"
netbox-branching: true
# 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
Expand Down Expand Up @@ -100,10 +100,12 @@ jobs:

- name: Install NetBox and plugin
working-directory: netbox-InterfaceNameRules-plugin
shell: bash
run: |
uv pip install --system -r ../netbox/requirements.txt
uv pip install --system --only-binary=:all: --group ci-tests
uv pip install --system -e .
uv export --system-certs --locked --group ci-tests --no-default-groups --no-emit-project --format requirements-txt | \
uv pip install --system-certs --system --only-binary=:all: --require-hashes -r /dev/stdin
uv pip install --system-certs --system --no-deps -e .

- name: Set up NetBox configuration
working-directory: netbox
Expand Down Expand Up @@ -131,12 +133,12 @@ jobs:
# netbox-branching refuses to start without DynamicSchemaDict and its router, and it must be the last plugin.
- name: Install and configure netbox-branching
if: matrix.netbox-branching
working-directory: netbox
env:
NETBOX_BRANCHING_VERSION: ${{ matrix.netbox-branching }}
working-directory: netbox-InterfaceNameRules-plugin
shell: bash
run: |
uv pip install --system --only-binary=:all: "netboxlabs-netbox-branching==${NETBOX_BRANCHING_VERSION}"
cat >> netbox/netbox/configuration.py << 'EOF'
uv export --system-certs --locked --only-group ci-branching --no-emit-project --format requirements-txt | \
uv pip install --system-certs --system --only-binary=:all: --require-hashes -r /dev/stdin
cat >> ../netbox/netbox/netbox/configuration.py << 'EOF'
from netbox_branching.utilities import DynamicSchemaDict
DATABASES = DynamicSchemaDict({'default': DATABASE})
del DATABASE
Expand Down Expand Up @@ -196,7 +198,7 @@ jobs:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
Expand All @@ -218,8 +220,10 @@ jobs:

- name: Install coverage
working-directory: netbox-InterfaceNameRules-plugin
shell: bash
run: |
uv pip install --system --only-binary=:all: --group ci-tests
uv export --system-certs --locked --only-group ci-tests --no-emit-project --format requirements-txt | \
uv pip install --system-certs --system --only-binary=:all: --require-hashes -r /dev/stdin

# `coverage report` enforces `fail_under` from pyproject.toml on the combined data.
- name: Combine and check coverage
Expand Down
19 changes: 19 additions & 0 deletions netbox_interface_name_rules/tests/test_ci_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@ def _steps_using(job, action):
return [step for step in job["steps"] if step.get("uses", "").startswith(f"{action}@")]


class WorkflowActionPinsTest(unittest.TestCase):
"""Workflow jobs must use the same setup-uv action revision."""

def test_setup_uv_pins_match_across_all_workflow_jobs(self):
pins = {}
for path in sorted((_PROJECT_ROOT / ".github" / "workflows").iterdir()):
if path.suffix not in {".yaml", ".yml"}:
continue
workflow = yaml.safe_load(path.read_text(encoding="utf-8"))
for name, job in workflow["jobs"].items():
if "steps" not in job:
continue
for index, step in enumerate(_steps_using(job, "astral-sh/setup-uv")):
pins[f"{path.name}:{name}:{index}"] = step["uses"]

self.assertTrue(pins, "no setup-uv steps found")
self.assertEqual(len(set(pins.values())), 1, f"setup-uv pins differ across workflow jobs: {pins}")


class CoverageCombineWorkflowTest(unittest.TestCase):
"""Every coverage leg must reach the one job that enforces the gate."""

Expand Down
11 changes: 8 additions & 3 deletions netbox_interface_name_rules/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,8 @@ def test_a_glob_style_pattern_saves_and_warns_that_it_matches_nothing(self):

response = self._post("GLC-T*")

self.assertTrue(InterfaceNameRule.objects.filter(module_type_pattern="GLC-T*").exists())
rule = InterfaceNameRule.objects.get(module_type_pattern="GLC-T*")
self.assertRedirects(response, rule.get_absolute_url())
warnings = [str(m) for m in get_messages(response.wsgi_request) if m.level_tag == "warning"]
self.assertTrue(any("matches no module type" in m for m in warnings), warnings)

Expand Down Expand Up @@ -557,6 +558,7 @@ def test_editing_a_rule_into_a_zero_match_pattern_also_warns(self):

rule.refresh_from_db()
self.assertEqual(rule.module_type_pattern, "VIEW-T*")
self.assertRedirects(response, rule.get_absolute_url())
warnings = [str(m) for m in get_messages(response.wsgi_request) if m.level_tag == "warning"]
self.assertTrue(any("matches no module type" in m for m in warnings), warnings)

Expand Down Expand Up @@ -628,6 +630,7 @@ def test_a_rejected_submission_is_not_called_ineffective(self):
},
)

self.assertEqual(response.status_code, 200)
self.assertFalse(InterfaceNameRule.objects.filter(module_type_pattern="REJECT-T*").exists())
warnings = [str(m) for m in get_messages(response.wsgi_request) if m.level_tag == "warning"]
self.assertEqual([m for m in warnings if "matches no module type" in m], [], warnings)
Expand All @@ -650,7 +653,8 @@ def test_a_device_interface_rule_is_not_called_ineffective(self):
follow=True,
)

self.assertTrue(InterfaceNameRule.objects.filter(module_type_pattern="^Ethernet.*$").exists())
rule = InterfaceNameRule.objects.get(module_type_pattern="^Ethernet.*$")
self.assertRedirects(response, rule.get_absolute_url())
warnings = [str(m) for m in get_messages(response.wsgi_request) if m.level_tag == "warning"]
self.assertEqual([m for m in warnings if "matches no module type" in m], [], warnings)

Expand All @@ -661,7 +665,8 @@ def test_an_equivalent_regex_pattern_warns_about_nothing(self):

response = self._post("GLC-T.*")

self.assertTrue(InterfaceNameRule.objects.filter(module_type_pattern="GLC-T.*").exists())
rule = InterfaceNameRule.objects.get(module_type_pattern="GLC-T.*")
self.assertRedirects(response, rule.get_absolute_url())
warnings = [str(m) for m in get_messages(response.wsgi_request) if m.level_tag == "warning"]
self.assertEqual([m for m in warnings if "matches no module type" in m], [])

Expand Down
12 changes: 7 additions & 5 deletions netbox_interface_name_rules/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,28 +136,30 @@ def post(self, request, *args, **kwargs):
device_rule = _submitted(request, "applies_to_device_interfaces")
started = timezone.now()
response = super().post(request, *args, **kwargs)
if not regex_rule or device_rule:
return response
if regex_rule and not device_rule:
self._warn_if_pattern_matches_nothing(request, pattern, started)
return response

def _warn_if_pattern_matches_nothing(self, request, pattern, started):
# Only a rule written by this request saved; a rejected form leaves nothing to report on.
saved = self.queryset.model.objects.filter(
module_type_pattern=pattern, module_type_is_regex=True, last_updated__gte=started
)
if not saved.exists():
return response
return
from .rule_selection import module_types_matching_pattern

try:
matched = module_types_matching_pattern(pattern)
except ValidationError:
return response
return
if not matched:
messages.warning(
request,
f"Pattern {pattern!r} matches no module type, so this rule will not fire. "
"The field takes a regular expression, not a glob: '*' repeats the character "
"before it, so 'GLC-T*' does not match 'GLC-TE'. Use 'GLC-T.*'.",
)
return response


class InterfaceNameRuleCreateView(ZeroMatchPatternWarningMixin, generic.ObjectEditView):
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ devcontainer = [
"pynetbox",
"playwright",
]
ci-branching = [
"netboxlabs-netbox-branching==1.2.1",
]

[tool.pytest.ini_options]
pythonpath = ["/opt/netbox/netbox", "."]
Expand Down
13 changes: 13 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading