From dddb59fe43ca8fe50682876105d7ef55187ddbfa Mon Sep 17 00:00:00 2001 From: Marcin Zieba Date: Fri, 21 Aug 2026 14:18:29 +0200 Subject: [PATCH 1/5] ci: run the live NetBox lane against 4.3/4.4/4.5 latest patches plus main The weekly live lane only tested NetBox main, so the released-version GraphQL filter regression behind #120 (broken PowerOutletTemplateFilter on every released 4.3.x/4.4.x, fixed upstream only in 4.5.0) was invisible to CI. Turn the lane into a matrix over the latest patch of each supported line (v4.3.7, v4.4.10, v4.5.10) plus main, and run it on pull requests that change the workflow itself. The token bootstrap now serves both credential schemes without version sniffing: the v2 nbt_ credential when the Token model exposes it (NetBox >= 4.5) and the classic plaintext key otherwise, with the curl auth scheme derived from the token prefix the same way the importer and the integration tests already do. Rename the workflow to test-netbox.yaml to match its widened scope and raise the README compatibility floor from the stale 3.2+ claim to NetBox 4.3+, which is what the matrix actually proves. --- ...test-netbox-main.yaml => test-netbox.yaml} | 34 ++++++++++++++----- README.md | 6 ++-- 2 files changed, 28 insertions(+), 12 deletions(-) rename .github/workflows/{test-netbox-main.yaml => test-netbox.yaml} (82%) diff --git a/.github/workflows/test-netbox-main.yaml b/.github/workflows/test-netbox.yaml similarity index 82% rename from .github/workflows/test-netbox-main.yaml rename to .github/workflows/test-netbox.yaml index 5f483cc9..3a9e9701 100644 --- a/.github/workflows/test-netbox-main.yaml +++ b/.github/workflows/test-netbox.yaml @@ -1,10 +1,13 @@ # SPDX-License-Identifier: MIT -name: Weekly test against NetBox main +name: Test against live NetBox on: schedule: - cron: '0 6 * * 1' # Every Monday at 06:00 UTC workflow_dispatch: + pull_request: + paths: + - '.github/workflows/test-netbox.yaml' permissions: contents: read @@ -17,6 +20,12 @@ jobs: integration-test: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Latest patch release of every supported NetBox line, plus main. + netbox_ref: [main, v4.5.10, v4.4.10, v4.3.7] + services: postgres: image: postgres:16 @@ -54,13 +63,13 @@ jobs: with: enable-cache: true - - name: Checkout NetBox main + - name: Checkout NetBox ${{ matrix.netbox_ref }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: netbox-community/netbox path: netbox - ref: main + ref: ${{ matrix.netbox_ref }} - name: Install NetBox dependencies run: pip install -r netbox/requirements.txt @@ -92,7 +101,7 @@ jobs: # (June 2026) enforces object-level 'view' permission when serving # /media/devicetype-images/ and /media/image-attachments/ via MediaView, and that # view's TokenConditionalLoginRequiredMixin only authenticates the API token when - # LOGIN_REQUIRED is True. With it True, the integration test's Bearer-token session + # LOGIN_REQUIRED is True. With it True, the integration test's token-authenticated session # resolves to the admin superuser, restrict() grants access, and the image is # served (a genuinely missing file still 404s through django.views.static.serve). # NOTE: this also gates the REST API, so the readiness curl below must send the token. @@ -115,10 +124,17 @@ jobs: user = get_user_model().objects.get(username='admin') t = Token(user=user) t.save() - # v2 token credential: nbt_. (plaintext only available right after save) - print(f'nbt_{t.key}.{t.token}') + # v2 credential nbt_<key>.<plaintext> (NetBox >= 4.5, plaintext only available + # right after save) or the classic plaintext key on 4.3/4.4. + token = getattr(t, 'token', None) + print(f'nbt_{t.key}.{token}' if token else t.key) " 2>/dev/null | tail -1) echo "NETBOX_TOKEN=$TOKEN" >> "$GITHUB_ENV" + # The importer picks the scheme the same way: Bearer for nbt_, else Token. + case "$TOKEN" in + nbt_*) echo "NETBOX_AUTH_SCHEME=Bearer" >> "$GITHUB_ENV" ;; + *) echo "NETBOX_AUTH_SCHEME=Token" >> "$GITHUB_ENV" ;; + esac echo "Created API token" - name: Start NetBox dev server @@ -132,14 +148,14 @@ jobs: # anonymous curl would 403 and never report ready. # Wait up to 60 s for NetBox to respond for i in $(seq 1 30); do - if curl -sf -H "Authorization: Bearer $NETBOX_TOKEN" http://localhost:8000/api/ > /dev/null 2>&1; then + if curl -sf -H "Authorization: $NETBOX_AUTH_SCHEME $NETBOX_TOKEN" http://localhost:8000/api/ > /dev/null 2>&1; then echo "NetBox is ready (attempt $i)" break fi echo "Waiting for NetBox... ($i/30)" sleep 2 done - curl -sf -H "Authorization: Bearer $NETBOX_TOKEN" http://localhost:8000/api/ > /dev/null || { echo "NetBox did not start"; exit 1; } + curl -sf -H "Authorization: $NETBOX_AUTH_SCHEME $NETBOX_TOKEN" http://localhost:8000/api/ > /dev/null || { echo "NetBox did not start"; exit 1; } - name: Install importer dependencies working-directory: importer @@ -175,6 +191,6 @@ jobs: - name: Print NetBox version on failure if: failure() run: | - curl -s -H "Authorization: Bearer $NETBOX_TOKEN" http://localhost:8000/api/status/ | python3 -m json.tool || true + curl -s -H "Authorization: $NETBOX_AUTH_SCHEME $NETBOX_TOKEN" http://localhost:8000/api/status/ | python3 -m json.tool || true echo "--- NetBox server log ---" cat /tmp/netbox.log 2>/dev/null | tail -50 || true diff --git a/README.md b/README.md index cec7a8fd..06ebddc3 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # NetBox Device Type Import [![Tests](https://github.com/marcinpsk/Device-Type-Library-Import/actions/workflows/tests.yml/badge.svg)](https://github.com/marcinpsk/Device-Type-Library-Import/actions/workflows/tests.yml) -[![NetBox main](https://github.com/marcinpsk/Device-Type-Library-Import/actions/workflows/test-netbox-main.yaml/badge.svg)](https://github.com/marcinpsk/Device-Type-Library-Import/actions/workflows/test-netbox-main.yaml) -[![NetBox](https://img.shields.io/badge/NetBox-3.2%2B_through_4.5%2B-blue)](https://netbox.dev) +[![Live NetBox](https://github.com/marcinpsk/Device-Type-Library-Import/actions/workflows/test-netbox.yaml/badge.svg)](https://github.com/marcinpsk/Device-Type-Library-Import/actions/workflows/test-netbox.yaml) +[![NetBox](https://img.shields.io/badge/NetBox-4.3%2B-blue)](https://netbox.dev) [![Python](https://img.shields.io/badge/python-3.12%2B-blue)](https://www.python.org) [![Container image](https://img.shields.io/badge/ghcr.io-device--type--library--import-2496ED?logo=docker&logoColor=white)](https://github.com/marcinpsk/Device-Type-Library-Import/pkgs/container/device-type-library-import) This library is intended to be your friend and help you import all the device-types defined within the [NetBox Device Type Library Repository](https://github.com/netbox-community/devicetype-library). -> **Tested working with NetBox 3.2+ through 4.5+** (weekly CI run against NetBox `main`) +> **Requires NetBox 4.3 or later.** A weekly CI run exercises the full import pipeline against the latest 4.3, 4.4, and 4.5 patch releases plus NetBox `main`. ## Description From 47c27e152ac1dbe813987fa3605613b485f3912a Mon Sep 17 00:00:00 2001 From: Marcin Zieba <marcinpsk@gmail.com> Date: Fri, 21 Aug 2026 14:28:26 +0200 Subject: [PATCH 2/5] test(integration): accept the pre-4.5 front-port rear-port linkage shape NetBox 4.5 replaced FrontPortTemplate.rear_port + rear_port_position with the PortMapping M2M, and the REST serializer changed shape with it. The integration assertions only knew the M2M rear_ports list, so the 4.3/4.4 matrix lanes failed on a correct import. Normalize both shapes through one helper keyed on which field the payload carries. --- tests/integration/test_import.py | 34 ++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/tests/integration/test_import.py b/tests/integration/test_import.py index a1a4bb17..acb110f7 100644 --- a/tests/integration/test_import.py +++ b/tests/integration/test_import.py @@ -289,6 +289,20 @@ def _test_images(fd: dict, fd_id: int) -> None: # ────────────────────────────────────────────────────────────────────────────── +def front_port_mappings(fp: dict) -> list: + """Normalize a front-port template's rear-port linkage across NetBox versions. + + NetBox >= 4.5 serves an M2M ``rear_ports`` list of PortMapping entries; + older releases serve a scalar ``rear_port`` object plus ``rear_port_position``. + """ + if "rear_ports" in fp: + return fp.get("rear_ports") or [] + rear_port = fp.get("rear_port") + if not rear_port: + return [] + return [{"rear_port": rear_port["id"], "rear_port_position": fp.get("rear_port_position")}] + + def test_front_port_multiposition() -> None: print("\n=== Scenario E: Front-port multi-position linkage ===") fd = get_one("/dcim/device-types/", slug="testvendor-full-device") @@ -299,17 +313,17 @@ def test_front_port_multiposition() -> None: for name, expected_pos in [("FP1", 1), ("FP2", 2)]: fp = fps[name] - mapping = fp.get("rear_ports", []) + mapping = front_port_mappings(fp) if not mapping: - fail(f"{name}: rear_ports is empty — M2M linkage not created") + fail(f"{name}: rear-port linkage is empty — mapping not created") pos = mapping[0].get("rear_port_position") if pos != expected_pos: fail(f"{name}: rear_port_position = {pos!r}, expected {expected_pos}") ok(f"{name}: rear_port_position = {pos}") # Both front ports should point to the same rear port - rp1_id = fps["FP1"]["rear_ports"][0]["rear_port"] - rp2_id = fps["FP2"]["rear_ports"][0]["rear_port"] + rp1_id = front_port_mappings(fps["FP1"])[0]["rear_port"] + rp2_id = front_port_mappings(fps["FP2"])[0]["rear_port"] if rp1_id != rp2_id: fail(f"FP1 and FP2 point to different rear ports ({rp1_id} vs {rp2_id}), expected same RP1") ok("FP1 and FP2 both map to the same rear port (RP1)") @@ -317,10 +331,10 @@ def test_front_port_multiposition() -> None: # Also check patch-panel front ports pp = get_one("/dcim/device-types/", slug="testvendor-patch-panel-4") pp_fps = api("/dcim/front-port-templates/", device_type_id=pp["id"])["results"] - broken = [fp["name"] for fp in pp_fps if not fp.get("rear_ports")] + broken = [fp["name"] for fp in pp_fps if not front_port_mappings(fp)] if broken: - fail(f"patch-panel-4: {len(broken)} front ports have empty rear_ports: {broken}") - ok(f"patch-panel-4: all {len(pp_fps)} front ports have rear_ports mapping") + fail(f"patch-panel-4: {len(broken)} front ports have empty rear-port linkage: {broken}") + ok(f"patch-panel-4: all {len(pp_fps)} front ports have a rear-port mapping") # ────────────────────────────────────────────────────────────────────────────── @@ -354,10 +368,10 @@ def test_module_types() -> None: # Front port linkage for module type mt_fps = api("/dcim/front-port-templates/", module_type_id=mt_id)["results"] - broken = [fp["name"] for fp in mt_fps if not fp.get("rear_ports")] + broken = [fp["name"] for fp in mt_fps if not front_port_mappings(fp)] if broken: - fail(f"full-module: {len(broken)} front ports have empty rear_ports: {broken}") - ok("full-module: front port rear_ports mapping set correctly") + fail(f"full-module: {len(broken)} front ports have empty rear-port linkage: {broken}") + ok("full-module: front port rear-port mapping set correctly") # ────────────────────────────────────────────────────────────────────────────── From 0384d5f545336a1d621dea3aac85345eaf3971fd Mon Sep 17 00:00:00 2001 From: Marcin Zieba <marcinpsk@gmail.com> Date: Fri, 21 Aug 2026 14:29:01 +0200 Subject: [PATCH 3/5] ci: full matrix on PRs, weekly schedule on NetBox main only Released tags are immutable, so re-testing them weekly adds nothing: the schedule keeps tracking main alone. Pull requests and manual dispatches run the full supported matrix, so importer changes are gated against every supported released line. --- .github/workflows/test-netbox.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-netbox.yaml b/.github/workflows/test-netbox.yaml index 3a9e9701..05369dad 100644 --- a/.github/workflows/test-netbox.yaml +++ b/.github/workflows/test-netbox.yaml @@ -6,8 +6,6 @@ on: - cron: '0 6 * * 1' # Every Monday at 06:00 UTC workflow_dispatch: pull_request: - paths: - - '.github/workflows/test-netbox.yaml' permissions: contents: read @@ -23,8 +21,10 @@ jobs: strategy: fail-fast: false matrix: - # Latest patch release of every supported NetBox line, plus main. - netbox_ref: [main, v4.5.10, v4.4.10, v4.3.7] + # Weekly runs track the moving target (main) only; released tags are + # immutable, so the full supported matrix (latest patch of each line) + # runs on pull requests and on demand instead. + netbox_ref: ${{ github.event_name == 'schedule' && fromJSON('["main"]') || fromJSON('["main", "v4.5.10", "v4.4.10", "v4.3.7"]') }} services: postgres: From d9f4332c1c3cb1d9ae23cca33fdd5ab1e2768e80 Mon Sep 17 00:00:00 2001 From: Marcin Zieba <marcinpsk@gmail.com> Date: Fri, 21 Aug 2026 15:21:16 +0200 Subject: [PATCH 4/5] docs(readme): match the CI description to the split schedule The weekly schedule tests NetBox main only; the full released-version matrix runs on pull requests and manual dispatches. The README claimed the weekly run covered the whole matrix. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 06ebddc3..447a98e9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This library is intended to be your friend and help you import all the device-types defined within the [NetBox Device Type Library Repository](https://github.com/netbox-community/devicetype-library). -> **Requires NetBox 4.3 or later.** A weekly CI run exercises the full import pipeline against the latest 4.3, 4.4, and 4.5 patch releases plus NetBox `main`. +> **Requires NetBox 4.3 or later.** Every pull request and manual CI run exercises the full import pipeline against the latest 4.3, 4.4, and 4.5 patch releases plus NetBox `main`; a weekly scheduled run tracks NetBox `main`. ## Description From b31fb3cfc6f77f7d64933f01696153fecd310d97 Mon Sep 17 00:00:00 2001 From: Marcin Zieba <marcinpsk@gmail.com> Date: Fri, 21 Aug 2026 15:31:32 +0200 Subject: [PATCH 5/5] ci: disable the uv Actions cache in the live NetBox lane CodeQL flags the job as cache-poisonable: it executes third-party code (the NetBox checkout) in the privileged scheduled/dispatched context of the default branch, and setup-uv wrote the shared Actions cache from that same job. With no cache writes the vector is gone; uv re-resolves in seconds. --- .github/workflows/test-netbox.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-netbox.yaml b/.github/workflows/test-netbox.yaml index 05369dad..a53d21a1 100644 --- a/.github/workflows/test-netbox.yaml +++ b/.github/workflows/test-netbox.yaml @@ -61,7 +61,10 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - enable-cache: true + # No Actions cache in this job: it executes third-party code (the + # NetBox checkout), which must not be able to poison the default + # branch cache from the scheduled/dispatched privileged context. + enable-cache: false - name: Checkout NetBox ${{ matrix.netbox_ref }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1