yang2sdk 2026.10.16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. yang2sdk-2026.10.16/.env.example +9 -0
  2. yang2sdk-2026.10.16/.env.lab-device.example +19 -0
  3. yang2sdk-2026.10.16/.env.notconf.example +5 -0
  4. yang2sdk-2026.10.16/.env.srl-lab.example +6 -0
  5. yang2sdk-2026.10.16/.github/pull_request_template.md +41 -0
  6. yang2sdk-2026.10.16/.github/workflows/ci-nightly.yaml +38 -0
  7. yang2sdk-2026.10.16/.github/workflows/ci-pr.yaml +80 -0
  8. yang2sdk-2026.10.16/.github/workflows/ci-srl.yaml +216 -0
  9. yang2sdk-2026.10.16/.github/workflows/release.yaml +94 -0
  10. yang2sdk-2026.10.16/.gitignore +28 -0
  11. yang2sdk-2026.10.16/.python-version +1 -0
  12. yang2sdk-2026.10.16/AGENTS.md +310 -0
  13. yang2sdk-2026.10.16/CHANGELOG.md +985 -0
  14. yang2sdk-2026.10.16/CONTRIBUTING.md +75 -0
  15. yang2sdk-2026.10.16/LICENSE +201 -0
  16. yang2sdk-2026.10.16/NOTICE +2 -0
  17. yang2sdk-2026.10.16/PKG-INFO +141 -0
  18. yang2sdk-2026.10.16/README.md +110 -0
  19. yang2sdk-2026.10.16/RFCs/6241.md +77 -0
  20. yang2sdk-2026.10.16/RFCs/6242.md +32 -0
  21. yang2sdk-2026.10.16/RFCs/7950.md +235 -0
  22. yang2sdk-2026.10.16/RFCs/7951.md +43 -0
  23. yang2sdk-2026.10.16/RFCs/8040.md +47 -0
  24. yang2sdk-2026.10.16/RFCs/8341.md +37 -0
  25. yang2sdk-2026.10.16/RFCs/8342.md +192 -0
  26. yang2sdk-2026.10.16/RFCs/8526.md +136 -0
  27. yang2sdk-2026.10.16/RFCs/8527.md +28 -0
  28. yang2sdk-2026.10.16/SECURITY.md +68 -0
  29. yang2sdk-2026.10.16/docs/README.md +14 -0
  30. yang2sdk-2026.10.16/docs/dev.md +160 -0
  31. yang2sdk-2026.10.16/docs/lab.md +93 -0
  32. yang2sdk-2026.10.16/docs/usage.md +137 -0
  33. yang2sdk-2026.10.16/pyproject.toml +109 -0
  34. yang2sdk-2026.10.16/src/yang2sdk/__init__.py +13 -0
  35. yang2sdk-2026.10.16/src/yang2sdk/cli/__init__.py +1 -0
  36. yang2sdk-2026.10.16/src/yang2sdk/cli/canonicalize_ast.py +228 -0
  37. yang2sdk-2026.10.16/src/yang2sdk/cli/closure.py +202 -0
  38. yang2sdk-2026.10.16/src/yang2sdk/cli/compiler.py +601 -0
  39. yang2sdk-2026.10.16/src/yang2sdk/cli/downloader.py +269 -0
  40. yang2sdk-2026.10.16/src/yang2sdk/cli/features.py +116 -0
  41. yang2sdk-2026.10.16/src/yang2sdk/cli/model_gaps.py +664 -0
  42. yang2sdk-2026.10.16/src/yang2sdk/cli/sdk_verify.py +1652 -0
  43. yang2sdk-2026.10.16/src/yang2sdk/plugin/__init__.py +1 -0
  44. yang2sdk-2026.10.16/src/yang2sdk/plugin/plugin_wrapper.py +9 -0
  45. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/__init__.py +6 -0
  46. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/core.py +531 -0
  47. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/ir.py +1481 -0
  48. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/__init__.py.jinja +1 -0
  49. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/data_models/__init__.py.jinja +32 -0
  50. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/data_models/_base.py.jinja +297 -0
  51. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/data_models/models.py.jinja +48 -0
  52. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/data_navigators/__init__.py.jinja +42 -0
  53. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/data_navigators/_base.py.jinja +243 -0
  54. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/data_navigators/navigators.py.jinja +247 -0
  55. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/netconf/session_manager.py.jinja +672 -0
  56. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/__init__.py.jinja +1 -0
  57. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/data_models/__init__.py.jinja +35 -0
  58. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/data_models/_base.py.jinja +142 -0
  59. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/data_models/models.py.jinja +105 -0
  60. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/data_navigators/__init__.py.jinja +66 -0
  61. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/data_navigators/_base.py.jinja +520 -0
  62. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/data_navigators/navigators.py.jinja +190 -0
  63. yang2sdk-2026.10.16/src/yang2sdk/plugin/src/templates/restconf/session_manager.py.jinja +252 -0
  64. yang2sdk-2026.10.16/src/yang2sdk/py.typed +0 -0
  65. yang2sdk-2026.10.16/tests/__init__.py +0 -0
  66. yang2sdk-2026.10.16/tests/conftest.py +548 -0
  67. yang2sdk-2026.10.16/tests/fixtures/golden/.gitkeep +0 -0
  68. yang2sdk-2026.10.16/tests/notconf/__init__.py +0 -0
  69. yang2sdk-2026.10.16/tests/notconf/compose.yaml +24 -0
  70. yang2sdk-2026.10.16/tests/notconf/matrix.json +117 -0
  71. yang2sdk-2026.10.16/tests/notconf/wait_healthy.py +82 -0
  72. yang2sdk-2026.10.16/tests/srl/.env.srl.example +8 -0
  73. yang2sdk-2026.10.16/tests/srl/PINNED.md +59 -0
  74. yang2sdk-2026.10.16/tests/srl/README.md +57 -0
  75. yang2sdk-2026.10.16/tests/srl/__init__.py +0 -0
  76. yang2sdk-2026.10.16/tests/srl/lab.py +112 -0
  77. yang2sdk-2026.10.16/tests/srl/srl-startup.cli +11 -0
  78. yang2sdk-2026.10.16/tests/srl/srl.clab.yml +14 -0
  79. yang2sdk-2026.10.16/tests/test_lab_device_netconf.py +283 -0
  80. yang2sdk-2026.10.16/tests/test_matrix.py +3995 -0
  81. yang2sdk-2026.10.16/tests/test_netconf_logging.py +358 -0
  82. yang2sdk-2026.10.16/tests/test_notconf_protocol.py +174 -0
  83. yang2sdk-2026.10.16/tests/test_sdk_generate.py +717 -0
  84. yang2sdk-2026.10.16/tests/test_sdk_verify_ci.py +97 -0
  85. yang2sdk-2026.10.16/tests/test_sdk_verify_debug.py +95 -0
  86. yang2sdk-2026.10.16/tests/test_srl_netconf.py +539 -0
  87. yang2sdk-2026.10.16/uv.lock +910 -0
@@ -0,0 +1,9 @@
1
+ DEVICE_NAME = 'device_name'
2
+ # OS version: names the generated package (<device>_<os-version>) and feeds
3
+ # --device-version. Also read from $DEVICE_VERSION by the compilers.
4
+ DEVICE_VERSION = '1.4.0'
5
+ DEVICE_IP = '192.168.137.42'
6
+ DEVICE_USER = 'username'
7
+ DEVICE_PASS = 'password'
8
+ RESTCONF_PORT = 443
9
+ NETCONF_PORT = 830
@@ -0,0 +1,19 @@
1
+ # Real lab device (LAB ONLY, shared equipment). Names only, dummy values.
2
+ # The generated SDK and the test-suite resolve these names; nothing here is
3
+ # ever printed, logged, or committed. `.env` itself is gitignored.
4
+ #
5
+ # Fallback: the legacy DEVICE_* names keep working for `yang-downloader`
6
+ # and `sdk-verify`, so an existing .env does not need to change.
7
+ LAB_DEVICE_IP=192.0.2.10
8
+ LAB_DEVICE_NETCONF_PORT=830
9
+ LAB_DEVICE_USER=netconf-operator
10
+ LAB_DEVICE_PASS=change-me
11
+
12
+ # Optional: pin the device host key so the secure default (verify=True)
13
+ # works end to end. Without it the client fails closed, which is correct.
14
+ #LAB_DEVICE_KNOWN_HOSTS=/path/to/known_hosts
15
+ # Explicit lab opt-out; logs a warning and skips host-key verification.
16
+ #LAB_DEVICE_ALLOW_UNVERIFIED=1
17
+
18
+ # Gates the write test. Writes are off unless this is exactly 1.
19
+ LAB_DEVICE_ALLOW_WRITE=0
@@ -0,0 +1,5 @@
1
+ # Simulator lab only. Defaults match notconf pre-built images (admin/admin).
2
+ # Never commit real device credentials here.
3
+ NOTCONF_USER=admin
4
+ NOTCONF_PASS=admin
5
+ NOTCONF_RUN_INTEGRATION=0
@@ -0,0 +1,6 @@
1
+ # SR Linux containerlab lab (LAB ONLY). Names only.
2
+ # Defaults match tests/srl/README.md; the fixture skips when nothing listens.
3
+ SRL_DEVICE_IP=127.0.0.1
4
+ SRL_DEVICE_NETCONF_PORT=1830
5
+ SRL_DEVICE_USER=admin
6
+ SRL_DEVICE_PASS=change-me
@@ -0,0 +1,41 @@
1
+ ## What
2
+
3
+ <!-- One or two sentences. What changes, and why now? -->
4
+
5
+ ## Why
6
+
7
+ <!-- The problem this solves. If it fixes a bug, link the issue. -->
8
+
9
+ ## Checklist
10
+
11
+ - [ ] The five gate commands pass (ruff, ruff format, ty, pyrefly, offline tests)
12
+ - [ ] `AGENTS.md` still describes the behaviour accurately, if the change touches
13
+ the generator contract, the parity table, or a safety default
14
+ - [ ] New IR fields have template rendering **and** a generated sample that imports
15
+ - [ ] Protocol changes cite an RFC section (`RFCs/*.md`)
16
+ - [ ] No credential, device IP, or `temp/` output committed
17
+
18
+ ## Evidence
19
+
20
+ Per `AGENTS.md`'s evidence rule — this is the part reviewers rely on most.
21
+
22
+ **Files changed:**
23
+
24
+ **Commands run:**
25
+
26
+ <!-- e.g. the five gate commands; `uv run yang2netconf temp/yang_modules/srl/*.yang --device srl` -->
27
+
28
+ **Verified:**
29
+
30
+ **Not verified / WIP:**
31
+
32
+ <!-- Be explicit. If you did not run the simulator matrix or a lab job, say so.
33
+ A "skip" is not a pass, and unverified work presented as verified is the
34
+ one failure mode this repo cannot recover from. -->
35
+
36
+ ## Safety
37
+
38
+ - [ ] No secure default weakened (`verify=True`, `auto_commit=False`,
39
+ `replace()` completeness guard, no embedded credentials)
40
+ - [ ] Any device quirk is documented and isolated to a downstream adapter, not
41
+ baked into the generic path
@@ -0,0 +1,38 @@
1
+ name: ci-nightly
2
+ on:
3
+ schedule:
4
+ - cron: "0 2 * * *"
5
+ workflow_dispatch:
6
+
7
+ jobs:
8
+ full-matrix:
9
+ runs-on: ubuntu-24.04
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ image:
14
+ - { package: notconf, tag: latest }
15
+ - { package: notconf-ietf, tag: latest }
16
+ - { package: notconf-sros, tag: "21.10" }
17
+ - { package: notconf-sros, tag: "22.2" }
18
+ - { package: notconf-junos, tag: 21.1R1 }
19
+ - { package: notconf-junos, tag: 23.4R1 }
20
+ - { package: notconf-cisco-xr, tag: "762" }
21
+ - { package: notconf-cisco-xr, tag: "771" }
22
+ - { package: notconf-cisco-xr, tag: "2411" }
23
+ - { package: notconf-cisco-xr, tag: "2531" }
24
+ - { package: notconf-cisco-nx, tag: 10.4-4 }
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: astral-sh/setup-uv@v5
28
+ - run: uv sync --locked --group dev --extra lab
29
+ - run: >-
30
+ uv run pytest tests/test_notconf_protocol.py tests/test_sdk_generate.py
31
+ tests/test_sdk_verify_ci.py -v
32
+ env:
33
+ NOTCONF_RUN_INTEGRATION: "1"
34
+ NOTCONF_IMAGE_PACKAGE: ${{ matrix.image.package }}
35
+ NOTCONF_IMAGE_TAG: ${{ matrix.image.tag }}
36
+ - name: Collect container logs
37
+ if: always()
38
+ run: docker ps -a --format '{{.Names}}' | grep -e yang2sdk- || true
@@ -0,0 +1,80 @@
1
+ name: ci-pr
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ jobs:
8
+ lint-typecheck:
9
+ runs-on: ubuntu-24.04
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: astral-sh/setup-uv@v5
13
+ - run: uv sync --locked --group dev --extra lab
14
+ # `uv run`, not `uvx`: the four gates are pinned exactly in the dev group
15
+ # so CI installs what uv.lock says. `uvx` would resolve the newest release
16
+ # on every run and could redden main with no commit to explain it.
17
+ - run: uv run ruff check .
18
+ - run: uv run ruff format --check .
19
+ - run: uv run ty check
20
+ - run: uv run pyrefly check
21
+
22
+ offline-tests:
23
+ needs: lint-typecheck
24
+ runs-on: ubuntu-24.04
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: astral-sh/setup-uv@v5
28
+ - run: uv sync --locked --group dev --extra lab
29
+ - run: uv run pytest tests/test_matrix.py -v
30
+
31
+ smoke:
32
+ needs: lint-typecheck
33
+ runs-on: ubuntu-24.04
34
+ strategy:
35
+ fail-fast: false
36
+ matrix:
37
+ image:
38
+ - { package: notconf, tag: latest }
39
+ - { package: notconf-ietf, tag: latest }
40
+ - { package: notconf-sros, tag: "22.2" }
41
+ - { package: notconf-junos, tag: 23.4R1 }
42
+ - { package: notconf-cisco-xr, tag: "2411" }
43
+ - { package: notconf-cisco-nx, tag: 10.4-4 }
44
+ steps:
45
+ - uses: actions/checkout@v4
46
+ - uses: astral-sh/setup-uv@v5
47
+ - run: uv sync --locked --group dev --extra lab
48
+ # test_sdk_verify_ci.py drives `sdk-verify` (every reachable node + every RPC
49
+ # Input serialised) against the same container the other two reuse, so it
50
+ # adds no container startup. It was a manual lab harness in no workflow,
51
+ # which is how three shipped defects survived: the RESTCONF
52
+ # ancestor-envelope unwrap, the NETCONF augment-closure gap, and the
53
+ # ietf-notification-capabilities model hole. Blocking, per image.
54
+ - run: >-
55
+ uv run pytest tests/test_notconf_protocol.py tests/test_sdk_generate.py
56
+ tests/test_sdk_verify_ci.py -v
57
+ env:
58
+ NOTCONF_RUN_INTEGRATION: "1"
59
+ NOTCONF_IMAGE_PACKAGE: ${{ matrix.image.package }}
60
+ NOTCONF_IMAGE_TAG: ${{ matrix.image.tag }}
61
+
62
+ # ci-gate: the single required check on main. It `needs` every other job in
63
+ # this workflow, so the branch ruleset only has to require this one stable
64
+ # name instead of one context per job/matrix shard. When you add a job to
65
+ # this workflow, add it to ci-gate's `needs` in the same PR (forgetting it
66
+ # fails open: the new job simply isn't gated).
67
+ ci-gate:
68
+ needs: [lint-typecheck, offline-tests, smoke]
69
+ if: always()
70
+ runs-on: ubuntu-latest
71
+ steps:
72
+ - name: Fail unless every ci-pr job succeeded
73
+ run: |
74
+ needs='${{ toJSON(needs) }}'
75
+ echo "$needs" | jq -c 'with_entries(.value = .value.result)'
76
+ if ! echo "$needs" | jq -e 'all(.[]; .result == "success")' > /dev/null; then
77
+ echo "::error::ci-gate: one or more ci-pr jobs did not succeed"
78
+ exit 1
79
+ fi
80
+ echo "all ci-pr jobs succeeded"
@@ -0,0 +1,216 @@
1
+ name: ci-srl
2
+ # SR Linux (Nokia) in containerlab — the repo's only real *commercial* NOS.
3
+ #
4
+ # Why this exists: every notconf image is a simulator built from a handful of
5
+ # IETF/vendor models, and the Groove G30 is a single lab device that is
6
+ # writable-running only. None of them exercise a full vendor model tree
7
+ # (hundreds of modules, deep augment closure, identityref leaves) through the
8
+ # generated client. SR Linux does, in a container, for free.
9
+ #
10
+ # NOT an NMDA device, contrary to an earlier revision of this file. Measured
11
+ # against SR Linux 25.10.1 from a raw <hello> (370 capabilities): its complete
12
+ # base-capability set is candidate, confirmed-commit, rollback-on-error,
13
+ # startup, url, validate, with-defaults, with-operational-defaults and
14
+ # yang-library. It ships the ietf-netconf-nmda *module* but does NOT advertise
15
+ # `:nmda:1.0` — precisely the false positive guarded in
16
+ # tests/test_matrix.py::test_nmda_is_detected_from_the_capability_uri_not_a_module_name.
17
+ # So this job must not be described as NMDA coverage.
18
+ #
19
+ # Cost: this repository is public, so standard GitHub-hosted runners are free.
20
+ # Do NOT move this job to a `*-large` runner — larger runners are billed even
21
+ # for public repositories.
22
+ #
23
+ # Severity is deliberately split:
24
+ # deploy / readiness / get-schema / generate -> BLOCKING. Zero coverage must
25
+ # never render as a green run, and each of these failing means the steps
26
+ # after it prove nothing.
27
+ # pytest / sdk-verify -> non-blocking
28
+ # (continue-on-error). Device boot timing and init races are real; a
29
+ # failure here is a signal to read, not a merge blocker.
30
+ # Every blocking step must also pass for pytest to run at all (the default
31
+ # `if: success()`), so a broken lab is never reported as "tests passed".
32
+ on:
33
+ pull_request:
34
+ workflow_dispatch:
35
+
36
+ concurrency:
37
+ group: ci-srl-${{ github.ref }}
38
+ cancel-in-progress: true
39
+
40
+ jobs:
41
+ srl-containerlab:
42
+ # Standard runner, not a larger one: 4 vCPU / 16 GB RAM is above SR Linux's
43
+ # documented 2 vCPU / 4 GB minimum, and it keeps the job free for a public repo.
44
+ runs-on: ubuntu-24.04
45
+ timeout-minutes: 45
46
+ # Deliberately no `needs:`. GitHub Actions cannot express dependencies
47
+ # across workflow files, so this cannot wait on ci-pr.yaml's lint-typecheck
48
+ # job — it runs in parallel with ci-pr instead, and does its own `uv sync`.
49
+ # Since the pytest step is non-blocking, ordering against the real gates
50
+ # would buy nothing anyway.
51
+
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+
55
+ - uses: astral-sh/setup-uv@v5
56
+
57
+ - name: Install containerlab (pinned)
58
+ # CLAB_VERSION is pinned for the same reason tests/srl/srl.clab.yml pins
59
+ # the SR Linux image: an unpinned containerlab release can change deploy
60
+ # or readiness behaviour overnight and turn a green pipeline red with no
61
+ # code change. Bump it deliberately, and note the bump in the PR.
62
+ # `install-containerlab` only — the runner already ships docker and has
63
+ # passwordless sudo, and "all" would try to install its own docker.
64
+ env:
65
+ CLAB_VERSION: "0.79.0"
66
+ run: |
67
+ curl -sL https://containerlab.dev/setup | sudo -E bash -s install-containerlab
68
+ containerlab version
69
+
70
+ - name: Install project
71
+ run: uv sync --locked --group dev --extra lab
72
+
73
+ - name: Deploy SR Linux lab
74
+ # `sudo -E` keeps $HOME so containerlab writes its lab state next to
75
+ # the topology file (tests/srl/clab-*/, already gitignored) instead of
76
+ # under /root. containerlab needs root for the privileged SR Linux node.
77
+ #
78
+ # Most likely failure mode on a new runner: SR Linux's DPDK-based XDP
79
+ # datapath needs the SSSE3 instruction set and containerlab *aborts*
80
+ # deploy without it. GitHub's AMD EPYC runners have SSSE3, but keep the
81
+ # full deploy output in the log so that diagnosis is one scroll away.
82
+ run: |
83
+ sudo -E containerlab deploy -t tests/srl/srl.clab.yml
84
+
85
+ - name: Wait for NETCONF readiness
86
+ # The lab is not usable the instant `deploy` returns: SR Linux takes
87
+ # minutes to bring up its management plane. tests/srl/lab.py polls the
88
+ # RFC 6241 Sec 8 hello with the lab's own credentials and exits
89
+ # non-zero on timeout, so a node that never boots fails here loudly
90
+ # rather than skipping every test below.
91
+ run: uv run python -m tests.srl.lab
92
+
93
+ - name: Download YANG from the node (get-schema)
94
+ # Same path the notconf suite uses: the shipped yang-downloader, so the
95
+ # integration suite exercises the code users actually run. It also
96
+ # writes features.json from the <hello> capability set, which
97
+ # yang2netconf picks up automatically as --features-file.
98
+ #
99
+ # Single source of truth for host/port/credentials: the committed
100
+ # tests/srl/.env.srl.example (published containerlab defaults, not
101
+ # secrets). It must be sourced *here* — every `run:` is a fresh shell,
102
+ # so exporting it in an earlier step would not survive. downloader.main()
103
+ # reads DEVICE_IP / NETCONF_PORT / DEVICE_USER / DEVICE_PASS /
104
+ # DEVICE_NAME from the environment and exits 2 if any is missing.
105
+ # DEVICE_NAME is what makes the output land in temp/yang_modules/srl.
106
+ #
107
+ # NOTE: downloader.main() discards download_all()'s failure count and
108
+ # still exits 0, so a partial extraction looks like success. Guard on
109
+ # the artefacts instead of trusting the exit code.
110
+ run: |
111
+ set -a
112
+ # shellcheck disable=SC1091
113
+ source tests/srl/.env.srl.example
114
+ set +a
115
+ echo "lab endpoint ${DEVICE_IP}:${NETCONF_PORT} as device '${DEVICE_NAME}'"
116
+ uv run yang-downloader
117
+ count=$(find temp/yang_modules/srl -name '*.yang' | wc -l)
118
+ echo "downloaded ${count} YANG modules"
119
+ if [ "$count" -lt 10 ]; then
120
+ echo "::error::get-schema returned only ${count} modules — refusing to compile"
121
+ exit 1
122
+ fi
123
+ test -f temp/yang_modules/srl/features.json \
124
+ || { echo "::error::features.json missing — <hello> capability parse failed"; exit 1; }
125
+
126
+ - name: Generate the NETCONF client from the node's own models
127
+ # Roots are the two the tests navigate to: <system> (system/name/host-name,
128
+ # management, logging) and the interfaces list.
129
+ #
130
+ # yang2netconf now resolves the augment closure itself (88c2681), so the
131
+ # explicit closure.py step that used to live here is redundant — passing
132
+ # the two root files is enough. Kept as a single command so this job
133
+ # exercises the documented user path rather than a special one.
134
+ run: |
135
+ uv run yang2netconf \
136
+ temp/yang_modules/srl/srl_nokia-system@*.yang \
137
+ temp/yang_modules/srl/srl_nokia-interfaces@*.yang \
138
+ --device srl \
139
+ --yang-dir temp/yang_modules/srl \
140
+ --output-dir temp/netconf_clients/srl
141
+ test -f temp/netconf_clients/srl/__init__.py
142
+
143
+ - name: SR Linux NETCONF suite
144
+ # NON-BLOCKING on purpose. Everything above must pass for this step to
145
+ # even run (default `if: success()`), so a broken lab is still a red X.
146
+ # A failure *here* is a device/timing or generator-regression signal that
147
+ # should be read, not a merge blocker.
148
+ #
149
+ # Expect one skip: test_hostname_mutate_restore. A whole-<system> merge
150
+ # is not expressible against this device for two verified reasons —
151
+ # SR Linux rejects identityref writes in the RFC 7951 string form it
152
+ # itself emits, and a write below the root omits the ancestor path the
153
+ # device requires. Both are documented at tests/test_srl_netconf.py.
154
+ # This job's first run earned that skip: it is not a pre-existing
155
+ # condition.
156
+ id: srl-tests
157
+ continue-on-error: true
158
+ env:
159
+ NOTCONF_RUN_INTEGRATION: "1"
160
+ run: uv run pytest tests/test_srl_netconf.py -v
161
+
162
+ - name: sdk-verify against the full vendor tree
163
+ # NON-BLOCKING, same severity as the pytest step above. This is the
164
+ # exhaustive walk — every reachable node at bounded depth, every RPC
165
+ # Input serialised — over a tree the notconf simulators cannot match
166
+ # (397 modules, deep augment closure, identityref leaves).
167
+ #
168
+ # `crud` is enabled here and only here. It is a writer: it holds the
169
+ # candidate lock, merges, reads back, discards, and proves restoration
170
+ # with a whole-tree digest, and it refuses to run without BOTH
171
+ # --write and a second acknowledgement (--allow-running-writes here,
172
+ # since SR Linux has :candidate). That is why it does not belong in the
173
+ # notconf smoke matrix.
174
+ #
175
+ # Measured locally against 25.10.1: 291 pass / 0 fail / 170 skip, with
176
+ # `verify-restore: config digest matches snapshot`.
177
+ id: srl-sdk-verify
178
+ continue-on-error: true
179
+ run: |
180
+ set -a
181
+ # shellcheck disable=SC1091
182
+ source tests/srl/.env.srl.example
183
+ set +a
184
+ uv run sdk-verify \
185
+ --device srl \
186
+ --protocol netconf \
187
+ --tiers read,rpc,crud \
188
+ --write \
189
+ --allow-running-writes \
190
+ --no-verify-tls \
191
+ --snapshot temp/verify/srl-netconf-crud-snapshot.json \
192
+ --json-out temp/verify/srl-netconf-report.json
193
+
194
+ - name: Container logs
195
+ # `failure()` alone would MISS the case that matters most: a failing
196
+ # pytest step does not put the job in a failed state, because it is
197
+ # continue-on-error. Check those steps' own outcomes too, or a
198
+ # generator regression would be reported with no device logs to
199
+ # explain it. Runs before destroy, while the containers still exist.
200
+ if: >-
201
+ failure() ||
202
+ steps.srl-tests.outcome == 'failure' ||
203
+ steps.srl-sdk-verify.outcome == 'failure'
204
+ run: |
205
+ docker ps -a --format '{{.Names}}' | grep -e clab- || true
206
+ for c in $(docker ps -a --format '{{.Names}}' | grep -e clab- || true); do
207
+ echo "===== ${c} ====="
208
+ docker logs --tail 200 "$c" || true
209
+ done
210
+
211
+ - name: Destroy the lab
212
+ # Always: an abandoned privileged container keeps its port binding and
213
+ # its ~2 GB of disk until the runner is reclaimed.
214
+ if: always()
215
+ run: |
216
+ sudo -E containerlab destroy -t tests/srl/srl.clab.yml --cleanup || true
@@ -0,0 +1,94 @@
1
+ name: release
2
+
3
+ # Publishes to PyPI when a `v*` tag is pushed. Nothing here runs on a PR, so
4
+ # this workflow is deliberately NOT part of `ci-pr`'s `ci-gate` needs list
5
+ # (AGENTS.md: `main` requires only that one stable check name).
6
+ #
7
+ # Tagging without this workflow shipped nothing — the repo had no publish job
8
+ # at all, which is why v0.1.0 needed one written from scratch.
9
+ #
10
+ # One-time setup (PyPI side, not automatable from this repo):
11
+ # * Preferred: configure "Trusted Publishing" for this repo on PyPI. No
12
+ # secret is needed at all, and the `PYPI_TOKEN` environment reference
13
+ # below stays empty.
14
+ # * Otherwise create a PyPI API token scoped to this project and add it as
15
+ # the repository secret `PYPI_TOKEN`.
16
+ # Either way the `environment: pypi` gate below should be created in the
17
+ # repo settings so publishing requires an approver.
18
+ on:
19
+ push:
20
+ tags: ["v*"]
21
+
22
+ permissions:
23
+ contents: read
24
+
25
+ jobs:
26
+ build:
27
+ runs-on: ubuntu-24.04
28
+ outputs:
29
+ version: ${{ steps.version.outputs.value }}
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+ - uses: astral-sh/setup-uv@v5
33
+
34
+ # GitHub records a `run` step's outputs ONLY from `name=value` lines on
35
+ # stdout (or the deprecated `::set-output::`). A bare `print()` writes
36
+ # nothing to $GITHUB_OUTPUT, so `steps.version.outputs.value` below was
37
+ # always empty and the guard failed for EVERY tag -- including one that
38
+ # matched -- which is why the first release run (38891426993) failed with
39
+ # "pyproject.toml says " and an empty version rather than the real
40
+ # mismatch. Write the output explicitly, and fail here if it is empty
41
+ # instead of letting the comparison below treat "" as a version.
42
+ #
43
+ # `python3`, not `uv run`: tomllib is stdlib on the runner's 3.12, so the
44
+ # guard needs no synced environment, and the step then runs offline in
45
+ # `tests/test_matrix.py::test_release_tag_guard_*` without nesting uv.
46
+ - name: Read pyproject version
47
+ id: version
48
+ run: |
49
+ v="$(python3 -c 'import tomllib,pathlib;print(tomllib.loads(pathlib.Path("pyproject.toml").read_text()).get("project",{}).get("version",""))')"
50
+ if [ -z "$v" ]; then
51
+ echo "could not read [project].version from pyproject.toml" >&2
52
+ exit 1
53
+ fi
54
+ echo "value=$v" >> "$GITHUB_OUTPUT"
55
+
56
+ - name: Fail if the tag does not match pyproject
57
+ run: |
58
+ set -euo pipefail
59
+ want="${GITHUB_REF_NAME#v}"
60
+ if [ "$want" != "${{ steps.version.outputs.value }}" ]; then
61
+ echo "tag $GITHUB_REF_NAME implies $want but pyproject.toml says ${{ steps.version.outputs.value }}" >&2
62
+ exit 1
63
+ fi
64
+
65
+ - run: uv build --out-dir dist
66
+
67
+ # --strict: fail on anything PyPI would reject, so a rejected upload is
68
+ # caught here rather than after the tag is already public.
69
+ - run: uv run --with twine twine check --strict dist/*
70
+
71
+ - uses: actions/upload-artifact@v4
72
+ with:
73
+ name: dist
74
+ path: dist/*
75
+
76
+ publish:
77
+ needs: build
78
+ runs-on: ubuntu-24.04
79
+ environment: pypi
80
+ permissions:
81
+ # id-token is what Trusted Publishing needs; harmless when a token is used.
82
+ id-token: write
83
+ steps:
84
+ - uses: actions/download-artifact@v4
85
+ with:
86
+ name: dist
87
+ path: dist
88
+ # With no `password` this uses Trusted Publishing (OIDC). With
89
+ # PYPI_TOKEN set in the environment it falls back to that token. The
90
+ # value is passed via `env`, never interpolated into the `with:` block,
91
+ # so it cannot be echoed into a log.
92
+ - uses: pypa/gh-action-pypi-publish@release/v1
93
+ env:
94
+ PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
@@ -0,0 +1,28 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+ *.log
9
+
10
+ # Virtual environments
11
+ .venv
12
+ .ruff_cache
13
+ .pytest_cache/
14
+
15
+ # env files
16
+ *.env
17
+
18
+ # containerlab runtime state (LAB ONLY) — lives next to the topology file
19
+ tests/srl/clab-*/
20
+
21
+ # Temporary files
22
+ digest.txt
23
+ # Simulator snapshot payloads (run-local; strict golden comparison is follow-up)
24
+ tests/fixtures/golden/*.json
25
+ # Ephemeral workspace (yang_modules/, *_clients/, yang_tree/, verify/,
26
+ # model_gaps/). Never committed; every writer creates its own output dir
27
+ # on demand, so `rm -rf temp/` is always recoverable (re-download/recompile).
28
+ temp/
@@ -0,0 +1 @@
1
+ 3.12