Skip to content

Commit 00ad75b

Browse files
docs: add new-netbox-endpoint reference skill (.claude/skills/) (#408)
* docs: add .claude/skills/new-netbox-endpoint reference skill New project-scoped Claude Code skill codifying the repeatable workflow for adding a NetBox REST API endpoint as PowerShell cmdlets (Get/New/Set/Remove + tests). What it covers: - All 4 cmdlet templates with parameter-set layout, pipeline input, Write-Verbose logging, SupportsShouldProcess, and -Raw switch - Query filter parameter placement in Get templates - Test pattern (mock at InvokeNetboxRequest, not Invoke-RestMethod) + the Pester-default / UriBuilder / JSON depth gotchas - MANDATORY AssertNBMutualExclusiveParam on every Get cmdlet (PR #397/#400) - ValidateSet parity workflow against scripts/Verify-ValidateSetParity.ps1 - Recurring pitfalls from past releases: - PS 5.1 non-ASCII parse bug (PR #398, #404) - [ValidateRange] + [Nullable[T]] conflict (PR #398) - [AllowNull] + [ValidateSet] on [string] — use empty-string sentinel + [AllowEmptyString] (PR #401) - Default-value stripping in Pester mocks - Pagination .next origin validation (PR #404) - Build artefact drift on deploy.ps1 runs - Parameter naming (snake_case → PascalCase via underscore; [object[]]$Tags for new cmdlets; [bool] not [switch] for API bool fields) - Verification checklist before PR Iterated on feedback from a subagent reviewer dispatch: - Added Write-Verbose to New/Set/Remove templates (matches actual repo convention in all EventRules/* and peer functions) - Added query-filter placeholder block to the Get template - Made the Brief/Fields/Omit mutex check explicitly MANDATORY (not optional) per PR #397/#400 The skill itself is auto-discovered by Claude Code at `.claude/skills/new-netbox-endpoint/SKILL.md`. Human contributors can read it as a regular markdown doc. Updated `.gitignore` to un-ignore `.claude/skills/**` while keeping `.claude/commands/` and `settings.local.json` ignored (existing behavior for local-only slash commands and personal settings). No runtime code changes. CLAUDE.md refresh landed as a local workstation change (CLAUDE.md is intentionally outside the git repo per `.gitignore:26`, so it doesn't participate in this PR). * docs(skill): apply subagent review fixes to new-netbox-endpoint Three valid findings from the Explore-agent dispatch that didn't make it into the initial commit: 1. Add `Write-Verbose` to GET/NEW/SET/REMOVE templates (matches the actual convention in Functions/Extras/EventRules/* and peer functions; was missing from my first draft). 2. Add a query-filter placeholder block + explanation to the GET template. New contributors need to see where to put `-Label`, `-Status`, etc. and how they flow through BuildURIComponents. 3. Tighten the Brief/Fields/Omit mutex comment from "enforce mutual exclusion" to "MANDATORY on every Get cmdlet since PR #397/#400" so agents don't treat it as optional. Also tightened the frontmatter description per writing-skills guidance (should describe triggering conditions only, never summarise the skill's workflow). * docs(skill): address Gemini review — Tags pitfall row + JSON depth 10 Round 1 Gemini on PR #408, two valid findings applied: - Test template now uses `ConvertTo-Json -Compress -Depth 10`. The Pitfalls table already notes the default depth of 2 can mangle nested objects; the test example should demonstrate the fix rather than silently inherit the default. - Added a Tags row to the Pitfalls table explaining the [string[]] / [uint64[]] → [object[]] evolution. The Parameter naming section had a `see Pitfalls` pointer to a row that didn't exist; now it does. The third finding (remove the switch/foreach from the Get template and rely on BuildURIComponents' id / id__in branching) was rejected with evidence — see Gemini reply on the PR. That pattern would collapse the ByID and Query parameter sets into one, which is a functional change: ByID hits the detail endpoint (/api/.../42/) returning richer data; Query hits the list endpoint with an id__in filter. The switch+foreach is the actual repo convention, verified against Functions/DCIM/Devices/Get-NBDCIMDevice.ps1 and Functions/Extras/EventRules/Get-NBEventRule.ps1. * docs(skill): Gemini round 2 — ByID passes Brief/Fields/Omit, Offset uint32 Two valid findings from round 2: 1. ByID block ignored projection params (HIGH — real bug in my template). NetBox supports ?brief=1 / ?fields= / ?omit= on detail endpoints (/api/<x>/<id>/), and real Get functions (Get-NBDCIMDevice, Get-NBEventRule) all pass them through. My minimal template skipped BuildURIComponents in the ByID branch, so a user following the skill would produce cmdlets where -Brief did nothing on single-ID fetches. Fixed. 2. Offset type uint16 overflows on large NetBox datasets (MEDIUM). uint16 tops out at 65 535; real-world IPAM addresses and Circuits easily exceed that. Changed the template to uint32. Existing Get functions still use uint16 — leaving those as-is for now (would be a ~120-function sweep) and documenting the skill as the forward- going convention, matching the pattern used for [object[]]$Tags. The other two round-2 findings were rejected with evidence in the PR comment: (a) adding Id to the Query parameter set would swap detail representations for list representations on single-ID fetches; and (b) moving AssertNBMutualExclusiveParam to begin{} contradicts the actual repo convention established in PR #397/#400 where all 122 Get functions run it in process{}. * docs(skill): add null-clearing enum pattern example to Set template Gemini round 3 MEDIUM finding: Pitfalls table mentions the [AllowEmptyString] + '' sentinel + translate-to-$null pattern from PR #401, but the Set template doesn't demonstrate it. A user following the template literally would have to assemble the pieces from the pitfalls row. Added a dedicated "SET — null-clearing for enum string parameters" subsection after the basic Set template showing the complete pattern (Set-NBDCIMInterface-style) with: - [AllowEmptyString()] + [ValidateSet(..., '')] - $PSBoundParameters translation loop BEFORE BuildURIComponents - Inline comment explaining why the translation order matters Kept the basic Set template lean since null-clearing is opt-in for specific parameters that need it — not all Set cmdlets do. The [Nullable[bool]] suggestion in round 3 was rejected: the entire repo uses [bool] on Set-* boolean API fields (verified across Set-NBEventRule, Set-NBIPAMAddressRange, Set-NBDCIMDevice, etc.) because BuildURIComponents iterates $PSBoundParameters which only contains user-bound values — unbound [bool] defaults never leak into the PATCH body. See PR comment for the empirical reasoning.
1 parent 63f3aea commit 00ad75b

2 files changed

Lines changed: 363 additions & 1 deletion

File tree

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
---
2+
name: new-netbox-endpoint
3+
description: Use when adding or porting a PowerShell cmdlet that wraps a NetBox REST API endpoint in PowerNetbox — creating a Get-NB, New-NB, Set-NB, or Remove-NB function for a new or missing resource, touching a ValidateSet on an existing one, or writing the matching Pester tests.
4+
---
5+
6+
# Adding a new NetBox endpoint to PowerNetbox
7+
8+
## Overview
9+
10+
Each NetBox endpoint maps to up to four cmdlets: `Get-NB*`, `New-NB*`,
11+
`Set-NB*`, `Remove-NB*`. Each cmdlet is one file under
12+
`Functions/<Module>/<Resource>/`, and all four share a consistent shape
13+
driven by a central `InvokeNetboxRequest` + `BuildURIComponents` helpers.
14+
The job of this skill is to make the fifth, fiftieth, and five-hundredth
15+
endpoint look identical.
16+
17+
## When this applies
18+
19+
- NetBox ships a new API endpoint (e.g. new minor release adds `/api/dcim/<x>/`).
20+
- An issue asks for a missing resource (pattern seen on #356, #362-#364, #383).
21+
- A drift report from `scripts/Verify-ValidateSetParity.ps1` flags a new
22+
enum you want to expose.
23+
- You are porting a cmdlet from community code (e.g. NetboxPS) to PowerNetbox conventions.
24+
25+
## Workflow (in order)
26+
27+
1. **Scope** — Is the endpoint read-only, read-write, or read-only-and-ephemeral (job-style)?
28+
That decides how many of the four cmdlets you need. Get-only is fine; you
29+
don't need `New`/`Set`/`Remove` if NetBox doesn't expose POST/PATCH/DELETE.
30+
2. **Research** — skim the NetBox OpenAPI spec at
31+
`https://<host>/api/schema/` (or for dev: use
32+
`/.claude/commands/netbox-api.md` shorthand). Note every required field,
33+
every ChoiceSet, every pagination quirk.
34+
3. **ValidateSet parity** — for every `ValidateSet` attribute you add,
35+
run `scripts/Verify-ValidateSetParity.ps1 -Function <Name>` against the
36+
NetBox version you target. Drift is the single most common bug class on
37+
this codebase (#360, #365, #385, #389, #392).
38+
4. **Implement** — one file per cmdlet, copy from the templates below.
39+
5. **Test** — one `Context "<cmdlet>"` per cmdlet in
40+
`Tests/<Module>.Tests.ps1`. Mock at the `InvokeNetboxRequest` level,
41+
not `Invoke-RestMethod`, unless the cmdlet bypasses it (file uploads, SVG).
42+
6. **Build + verify**`./deploy.ps1 -Environment dev -SkipVersion`,
43+
then `Invoke-Pester ./Tests/<Module>.Tests.ps1`.
44+
7. **Revert build artefacts before commit**
45+
`git checkout PowerNetbox.psd1` (deploy.ps1 updates its date).
46+
47+
## Cmdlet templates
48+
49+
All four templates share: `[CmdletBinding()]`, explicit `process {}` for
50+
pipeline support, `-Raw` switch, `Write-Verbose` for any operational
51+
logging (never `Write-Host` in non-interactive paths).
52+
53+
### GET
54+
55+
Add query filter parameters (`-Label`, `-Parent`, `-Status`, etc.) inside
56+
the `ParameterSet = 'Query'` group — they flow through `BuildURIComponents`
57+
automatically and appear as `?name=value` query params. Match the NetBox
58+
API field name one-to-one (e.g. API field `mark_utilized` → PS param
59+
`Mark_Utilized`; snake→PascalCase via underscore). Type-check each:
60+
numeric IDs as `[uint64]`, booleans as `[bool]` (never `[switch]`), arrays
61+
as `[string[]]` or `[uint64[]]` depending on whether the API filter
62+
expects names or IDs.
63+
64+
```powershell
65+
function Get-NB[Module][Resource] {
66+
[CmdletBinding(DefaultParameterSetName = 'Query')]
67+
param (
68+
[switch]$All,
69+
[ValidateRange(1, 1000)]
70+
[int]$PageSize = 100,
71+
[Parameter(ParameterSetName = 'ByID', ValueFromPipelineByPropertyName)]
72+
[uint64[]]$Id,
73+
[Parameter(ParameterSetName = 'Query')]
74+
[string]$Name,
75+
# [Parameter(ParameterSetName = 'Query')]
76+
# [string]$Status, # <-- filter params go here
77+
# [Parameter(ParameterSetName = 'Query')]
78+
# [uint64]$Site_Id,
79+
[uint16]$Limit,
80+
[uint32]$Offset, # uint32, not uint16 — NetBox datasets (IPAM, Circuits) exceed 65 535 items
81+
[switch]$Brief,
82+
[string[]]$Fields,
83+
[string[]]$Omit,
84+
[switch]$Raw
85+
)
86+
process {
87+
# MANDATORY on every Get cmdlet since PR #397/#400.
88+
# User picks exactly one projection — Brief, Fields, OR Omit.
89+
AssertNBMutualExclusiveParam `
90+
-BoundParameters $PSBoundParameters `
91+
-Parameters 'Brief', 'Fields', 'Omit'
92+
93+
Write-Verbose "Retrieving <Resource>"
94+
switch ($PSCmdlet.ParameterSetName) {
95+
'ByID' {
96+
# Pass projection params (Brief / Fields / Omit) through on
97+
# detail endpoints too — NetBox supports ?brief=1 / ?fields=
98+
# / ?omit= on /api/<module>/<resource>/<id>/ (not just on
99+
# list endpoints). Call BuildURIComponents with 'Id', 'Raw',
100+
# 'All', 'PageSize' skipped so only Brief/Fields/Omit and any
101+
# other meaningful flags end up as query params.
102+
foreach ($i in $Id) {
103+
$Segments = [System.Collections.ArrayList]::new(@('<module>', '<resource>', $i))
104+
$URIComponents = BuildURIComponents -URISegments $Segments.Clone() `
105+
-ParametersDictionary $PSBoundParameters `
106+
-SkipParameterByName 'Id', 'Raw', 'All', 'PageSize'
107+
$URI = BuildNewURI -Segments $URIComponents.Segments -Parameters $URIComponents.Parameters
108+
InvokeNetboxRequest -URI $URI -Raw:$Raw
109+
}
110+
}
111+
default {
112+
$Segments = [System.Collections.ArrayList]::new(@('<module>', '<resource>'))
113+
$URIComponents = BuildURIComponents -URISegments $Segments.Clone() `
114+
-ParametersDictionary $PSBoundParameters `
115+
-SkipParameterByName 'Raw', 'All', 'PageSize'
116+
InvokeNetboxRequest `
117+
-URI (BuildNewURI -Segments $URIComponents.Segments -Parameters $URIComponents.Parameters) `
118+
-Raw:$Raw -All:$All -PageSize $PageSize
119+
}
120+
}
121+
}
122+
}
123+
```
124+
125+
### NEW
126+
127+
```powershell
128+
function New-NB[Module][Resource] {
129+
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')]
130+
param (
131+
[Parameter(Mandatory)]
132+
[string]$Name,
133+
# Prefer [object[]] for Tags — it accepts both IDs (uint64) and
134+
# strings. Older cmdlets use [string[]] or [uint64[]] and are
135+
# kept as-is for back-compat, but new code should use [object[]].
136+
[object[]]$Tags,
137+
[switch]$Raw
138+
)
139+
process {
140+
Write-Verbose "Creating <Resource>: $Name"
141+
$Segments = [System.Collections.ArrayList]::new(@('<module>', '<resource>'))
142+
$URIComponents = BuildURIComponents -URISegments $Segments.Clone() `
143+
-ParametersDictionary $PSBoundParameters -SkipParameterByName 'Raw'
144+
if ($PSCmdlet.ShouldProcess($Name, 'Create <Resource>')) {
145+
InvokeNetboxRequest `
146+
-URI (BuildNewURI -Segments $URIComponents.Segments) `
147+
-Method POST -Body $URIComponents.Parameters -Raw:$Raw
148+
}
149+
}
150+
}
151+
```
152+
153+
### SET (PATCH)
154+
155+
```powershell
156+
function Set-NB[Module][Resource] {
157+
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
158+
param (
159+
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
160+
[uint64]$Id,
161+
[string]$Name,
162+
[switch]$Raw
163+
)
164+
process {
165+
Write-Verbose "Updating <Resource> ID $Id"
166+
$Segments = [System.Collections.ArrayList]::new(@('<module>', '<resource>', $Id))
167+
$URIComponents = BuildURIComponents -URISegments $Segments.Clone() `
168+
-ParametersDictionary $PSBoundParameters -SkipParameterByName 'Id', 'Raw'
169+
if ($PSCmdlet.ShouldProcess($Id, 'Update <Resource>')) {
170+
InvokeNetboxRequest `
171+
-URI (BuildNewURI -Segments $URIComponents.Segments) `
172+
-Method PATCH -Body $URIComponents.Parameters -Raw:$Raw
173+
}
174+
}
175+
}
176+
```
177+
178+
#### SET — null-clearing for enum string parameters
179+
180+
When a `Set-*` function's parameter has `[ValidateSet]` and the user needs
181+
to be able to *clear* that field server-side (send JSON `null`), the
182+
`[AllowNull()] [ValidateSet] [string]` combination doesn't work —
183+
PowerShell coerces `$null` to `""` at bind time and then ValidateSet
184+
rejects the empty string (see Pitfalls row 3).
185+
186+
Pattern used on `Set-NBDCIMInterface` (`Duplex`, `POE_Mode`, `POE_Type`,
187+
`RF_Role`, `Mode`) from PR #401 — add `''` to the ValidateSet as a
188+
caller-visible sentinel, use `[AllowEmptyString()]`, then translate `''`
189+
`$null` in `process {}` **before** `BuildURIComponents` so the PATCH
190+
body carries a literal JSON `null`:
191+
192+
```powershell
193+
function Set-NB[Module][Resource] {
194+
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
195+
param (
196+
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
197+
[uint64]$Id,
198+
199+
[AllowEmptyString()]
200+
[ValidateSet('full', 'half', 'auto', '', IgnoreCase = $true)]
201+
[string]$Duplex, # pass '' to clear server-side
202+
203+
[switch]$Raw
204+
)
205+
process {
206+
# Translate '' → $null for the clearable enum params BEFORE
207+
# BuildURIComponents, so the PATCH body becomes {"duplex": null}
208+
# rather than {"duplex": ""} (which NetBox rejects).
209+
$clearable = @('Duplex')
210+
foreach ($p in $clearable) {
211+
if ($PSBoundParameters.ContainsKey($p) -and $PSBoundParameters[$p] -eq '') {
212+
$PSBoundParameters[$p] = $null
213+
}
214+
}
215+
216+
$Segments = [System.Collections.ArrayList]::new(@('<module>', '<resource>', $Id))
217+
$URIComponents = BuildURIComponents -URISegments $Segments.Clone() `
218+
-ParametersDictionary $PSBoundParameters -SkipParameterByName 'Id', 'Raw'
219+
if ($PSCmdlet.ShouldProcess($Id, 'Update <Resource>')) {
220+
InvokeNetboxRequest `
221+
-URI (BuildNewURI -Segments $URIComponents.Segments) `
222+
-Method PATCH -Body $URIComponents.Parameters -Raw:$Raw
223+
}
224+
}
225+
}
226+
```
227+
228+
Numeric parameters needing null-clearing use `[Nullable[T]]` instead —
229+
see Pitfalls row 2 for the `[ValidateRange]` + `[Nullable[int]]` conflict
230+
and PR #398 for the rollout across 9 numeric Interface parameters.
231+
232+
### REMOVE
233+
234+
```powershell
235+
function Remove-NB[Module][Resource] {
236+
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
237+
[OutputType([void])]
238+
param (
239+
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
240+
[uint64]$Id,
241+
[switch]$Raw
242+
)
243+
process {
244+
Write-Verbose "Deleting <Resource> ID $Id"
245+
if ($PSCmdlet.ShouldProcess($Id, 'Delete <Resource>')) {
246+
InvokeNetboxRequest `
247+
-URI (BuildNewURI -Segments @('<module>', '<resource>', $Id)) `
248+
-Method DELETE -Raw:$Raw
249+
}
250+
}
251+
}
252+
```
253+
254+
## Test pattern
255+
256+
Most test files mock at the **module API surface** — that is,
257+
`InvokeNetboxRequest` — not the lower-level `Invoke-RestMethod`. Nice
258+
side effect: your test doesn't care about auth headers, retry logic,
259+
or cross-platform HTTP differences.
260+
261+
```powershell
262+
Describe "<Module> tests" -Tag '<Module>' {
263+
BeforeAll {
264+
Mock -CommandName 'CheckNetboxIsConnected' -ModuleName 'PowerNetbox' -MockWith { return $true }
265+
Mock -CommandName 'InvokeNetboxRequest' -ModuleName 'PowerNetbox' -MockWith {
266+
return [ordered]@{
267+
'Method' = if ($Method) { $Method } else { 'GET' } # defaults don't apply to mocks
268+
'Uri' = $URI.Uri.AbsoluteUri # AbsoluteUri encodes spaces as %20
269+
'Body' = if ($Body) { $Body | ConvertTo-Json -Compress -Depth 10 } else { $null }
270+
}
271+
}
272+
InModuleScope -ModuleName 'PowerNetbox' {
273+
$script:NetboxConfig.Hostname = 'netbox.domain.com'
274+
$script:NetboxConfig.HostScheme = 'https'
275+
$script:NetboxConfig.HostPort = 443
276+
}
277+
}
278+
279+
Context "Get-NB<Module><Resource>" {
280+
It "Should request the list endpoint" {
281+
$r = Get-NB<Module><Resource>
282+
$r.Method | Should -Be 'GET'
283+
$r.Uri | Should -Be 'https://netbox.domain.com/api/<module>/<resource>/'
284+
}
285+
}
286+
}
287+
```
288+
289+
Mock at `Invoke-RestMethod` level only when the cmdlet bypasses
290+
`InvokeNetboxRequest`: `New-NBImageAttachment` (multipart form),
291+
`Export-NBRackElevation` SVG mode, `ErrorHandling.Tests.ps1`,
292+
`Setup.Tests.ps1`, `CrossPlatform.Tests.ps1`, `Branching.Tests.ps1`.
293+
294+
## ValidateSet parity — run it
295+
296+
Every time you add or edit a `ValidateSet`:
297+
298+
```pwsh
299+
./scripts/Verify-ValidateSetParity.ps1 -NetboxVersion v<x.y.z>
300+
# Or scope to the cmdlet you touched:
301+
./scripts/Verify-ValidateSetParity.ps1 -Function New-NBDCIMInterface
302+
```
303+
304+
If your parameter deliberately has values NetBox's choices.py doesn't have
305+
(e.g. the `''` empty-string sentinel for PATCH null-clearing, or local
306+
meta-values like `'Both'` for rack elevation), add an exemption to
307+
`scripts/validateset-parity-exclusions.txt` with a comment explaining why.
308+
309+
## Pitfalls (things that have bitten us more than once)
310+
311+
| Pitfall | Cause | Fix |
312+
|---|---|---|
313+
| Windows PS 5.1 CI fails with "Missing closing '}'" at some line far from the real issue | Non-ASCII char (em-dash U+2014, arrows, curly quotes) in a `.ps1` file without a UTF-8 BOM — PS 5.1 parses as Windows-1252 | Stay ASCII in `.ps1` identifiers and test titles. Markdown `.md` files are unaffected. (Recurring: PR #398, PR #404) |
314+
| `[ValidateRange]` + `[Nullable[int]]` throws `ValidationMetadataException` on `$null` | ValidateRange binds before Nullable wrapping | Drop `[ValidateRange]` on `Set-*` versions that need null-clearing; rely on server-side validation (PR #398) |
315+
| `[AllowNull()] [ValidateSet(...)] [string]$X` rejects `$null` | PS coerces `$null``""` before ValidateSet runs | Use `[AllowEmptyString()] [ValidateSet('a','b','',...)]` and translate `''``$null` in `process{}` before `BuildURIComponents` (PR #401) |
316+
| Pester mock gets `$null` for default-valued params | Default values do not apply inside `MockWith` | Add explicit defaults inside the mock (see test template above) |
317+
| `ConvertTo-Json` mangles nested objects at the default depth | `ConvertTo-Json` defaults to `-Depth 2` | Pass `-Depth 10` explicitly when needed |
318+
| PSCustomObject iterated like a hashtable throws | PSCustomObject is not a hashtable | Use `$obj.PSObject.Properties` to iterate |
319+
| Build artefact noise in git diff | `deploy.ps1` updates `PowerNetbox.psd1` date on every build | `git checkout PowerNetbox.psd1` before staging |
320+
| `-Tags` parameter accepts only names or only IDs, not both | Older cmdlets type `Tags` as `[string[]]` (names) or `[uint64[]]` (IDs) — mutually exclusive | New cmdlets: `[object[]]$Tags` so callers can pass a mix. Legacy cmdlets left as-is for back-compat; add `[object[]]` only on new code |
321+
| Bulk operations pipeline runs unboundedly | No client throttle by default | `Send-NBBulkRequest` has `MaxItems = 10000` cap; pass `-BatchSize` to size each POST |
322+
| Pagination `.next` follow to wrong host | Server-controlled URL could be attacker-controlled | `InvokeNetboxRequest` validates origin against original URI via `GetLeftPart(Authority)` (PR #404) |
323+
324+
## Parameter naming conventions
325+
326+
- **Snake-case** in the cmdlet body: parameters map 1:1 to NetBox API fields.
327+
`Device_Type` (PS) → `device_type` (API) — done automatically by `BuildURIComponents`.
328+
- `Id` (not `ID`) for PowerShell consistency with pipeline binding.
329+
- `Tags` should be `[object[]]` — see Pitfalls. Older cmdlets use
330+
`[string[]]` or `[uint64[]]`; left as-is for compat, but new code uses `[object[]]`.
331+
- `-Raw` switch on every cmdlet (returns the full response object rather than
332+
the `.results` array).
333+
- Boolean API fields: use `[bool]`, not `[switch]`. Switches mean "omitted
334+
vs supplied" but NetBox treats missing vs false differently.
335+
336+
## Verification before PR
337+
338+
- [ ] `./deploy.ps1 -Environment dev -SkipVersion` — build succeeds, no duplicate function names
339+
- [ ] `Invoke-Pester ./Tests/<Module>.Tests.ps1` — all green
340+
- [ ] `Invoke-ScriptAnalyzer -Path ./Functions/<Module>/<new file>.ps1` — clean
341+
- [ ] `./scripts/Verify-ValidateSetParity.ps1` — no new drift findings
342+
- [ ] `./scripts/Verify-FilterExclusion.ps1` — if you touched any `Get-NB*.ps1`, see `docs/guides/` if unfamiliar
343+
- [ ] `grep -nP "[^\x00-\x7F]" Functions/... Tests/...` — no non-ASCII in `.ps1` files
344+
- [ ] `git checkout PowerNetbox.psd1` — revert build-artefact drift before committing
345+
346+
## Pointers
347+
348+
| Topic | File |
349+
|---|---|
350+
| Central HTTP helper | `Functions/Helpers/InvokeNetboxRequest.ps1` |
351+
| Body + URL parameter assembly | `Functions/Helpers/BuildURIComponents.ps1` |
352+
| URL builder | `Functions/Helpers/BuildNewURI.ps1` |
353+
| Auth header construction (v1 vs v2) | `Functions/Helpers/Get-NBRequestHeaders.ps1` |
354+
| Mutex helper for Brief / Fields / Omit | `Functions/Helpers/AssertNBMutualExclusiveParam.ps1` |
355+
| Bulk operations | `Functions/Helpers/Send-NBBulkRequest.ps1` |
356+
| ValidateSet parity script | `scripts/Verify-ValidateSetParity.ps1` |
357+
| ValidateSet parity exceptions | `scripts/validateset-parity-exclusions.txt` |
358+
| Filter exclusion auditor | `scripts/Verify-FilterExclusion.ps1` |
359+
| Exemption file for filter auditor | `scripts/filter-exclusion-exemptions.txt` |
360+
| NetBox best-practices reference | `../netbox-best-practices/` (parent project dir) |

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ concatenated.ps1
2020
*.apikey
2121

2222
# Development files (keep in dev branch only)
23-
.claude/
23+
.claude/*
24+
!.claude/skills/
25+
!.claude/skills/**
2426
CLAUDE.md
2527
Connect-DevNetbox.ps1
2628
.vscode/

0 commit comments

Comments
 (0)