- Add missing XML doc comments to all public types and members in
PSProxmoxVE.Core (Authentication, Exceptions, Models, Client)
- Remove CS1591 suppression from PSProxmoxVE.Core.csproj
- Build succeeds with 0 warnings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Container snapshots (4 cmdlets):
- Get-PveContainerSnapshot, New-PveContainerSnapshot
- Remove-PveContainerSnapshot, Restore-PveContainerSnapshot
- Service methods in ContainerService
- Pester unit tests
SDN subnet management (3 cmdlets):
- Get-PveSdnSubnet, New-PveSdnSubnet, Remove-PveSdnSubnet
- PveSdnSubnet model and NetworkService methods
- Pester unit tests
Also updates manifest (73 cmdlets), README cmdlet reference, and CHANGELOG.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add HelpMessage to every [Parameter] attribute
- Add [ValidateRange(100, 999999999)] to all VmId parameters
- Add ConfirmImpact.High to Stop-PveVm and Reset-PveVm
- Add ShouldProcess to Invoke-PveVmGuestExec
- Add WriteVerbose before every API call
- Add WriteWarning on -SkipCertificateCheck usage in Connect-PveServer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PVE 9 may not immediately reflect the template flag in the VM list
after New-PveTemplate returns. Add a 3-second delay before verifying.
PVE 8 passed without the delay.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The script uses PSProxmoxVE cmdlets but the module was only copied to
the module path after it ran. Move the install step before the test VM
deployment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each push creates a ~770MB container image tagged with the commit SHA.
Add a cleanup job that runs after integration tests complete, keeping
only the most recent version and deleting all others.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ConfirmImpact.High causes an interactive prompt in CI without
-Confirm:\$false, leading to a null reference error.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The cmdlet calls the /cloudinit/dump endpoint which returns YAML
content, not a task UPID. This gets passed to WaitForTask causing a
501 error. PVE regenerates cloud-init automatically on VM start, so
the explicit regenerate call is not needed for test VM setup.
Bug to fix: RegenerateCloudInitImage should PUT to /cloudinit instead
of GET from /cloudinit/dump.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Debian .qcow2 extension was rejected by PVE's download-url API.
Ubuntu's noble-server-cloudimg-amd64.img has a .img extension that
PVE accepts, and cloud-init + qemu-guest-agent work the same way.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace raw qm/SSH commands with module cmdlets where possible:
- Invoke-PveStorageDownload for cloud image download
- New-PveVm for VM creation
- Set-PveCloudInitConfig for cloud-init user/password/IP
- Invoke-PveCloudInitRegenerate to apply config
- Start-PveVm to boot the VM
SSH/SCP retained only for operations without API support:
- Snippet upload (no snippet API)
- pvesm set for snippets content type
- qm importdisk (no importdisk cmdlet)
- qm set for --scsi0, --cicustom, --agent, --boot (not in Set-PveVmConfig)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace virt-customize with a cloud-init custom user-data snippet that
installs qemu-guest-agent on first boot. The snippet is SCP'd to the
nested PVE's /var/lib/vz/snippets/ and referenced via --cicustom.
This eliminates the need for libguestfs-tools entirely — no install
on nested PVE, no extra Docker image, no large dependencies.
Timeout increased to 300s to allow for cloud-init package installation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- prepare-test-vm.sh: install libguestfs-tools if not present on the
fresh nested PVE before running virt-customize
- Mask PVE_PASSWORD in logs via ::add-mask:: to prevent leaking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move virt-customize to run on the nested PVE host (which already has
libguestfs) instead of inside the CI container. This removes
libguestfs-tools, linux-image-generic, and qemu-utils from the
Dockerfile, shrinking the image from ~1.2GB to ~770MB.
- prepare-test-vm.sh: downloads Debian cloud image on nested PVE,
runs virt-customize there to install qemu-guest-agent, then creates
VM with imported disk + cloud-init
- Remove build-test-image.sh (no longer needed)
- Remove /opt/pve-images volume mount
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Inside a container job, ${{ runner.temp }} resolves to the host path
(/opt/actions-runner/_work/_temp) which doesn't exist in the container.
The RUNNER_TEMP environment variable is correctly set to the container-
mapped path (/__w/_temp). Move all references to shell run: blocks
where the env var expands properly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
proxmox-auto-install-assistant requires xorriso for ISO manipulation.
Expand the verification step to check all tools the scripts depend on.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build the Docker image as a job within the integration workflow rather
than a separate workflow. This ensures the container always matches the
current commit's Dockerfile. Tag with github.sha so the integration job
pulls the exact image, not a stale latest.
- container-image job: builds and pushes to GHCR with sha + latest tags
- integration job: uses credentials: to pull private image on self-hosted
- Delete standalone build-test-container.yml
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GHCR requires lowercase repository names. Use repository_owner with
bash lowercase expansion for the container build, and hardcode the
lowercase image name in the integration workflow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Infrastructure:
- build-test-image.sh: Downloads Alpine cloud image, uses virt-customize
to install qemu-guest-agent, caches at /opt/pve-images/
- prepare-test-vm.sh: SCPs image to nested PVE, creates VM via
qm importdisk with cloud-init and agent enabled, waits for agent
Workflow:
- Build Alpine image step (cached on runner, only built once)
- Deploy test VM step after provisioning, passes LINUX_VMID to tests
Tests:
- Guest Agent VM lifecycle: verify running, ACPI restart, ACPI stop
- Templates: convert Linux VM to template, clone from template
(New-PveTemplate, New-PveVmFromTemplate)
- Fix Get-PveTask: use .Upid property instead of whole PveTask object
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PVE 8 (Bookworm) may not have /etc/apt/sources.list.d/debian.sources,
causing the grep fallback to use 'trixie' which is invalid for PVE 8.
Use /etc/os-release VERSION_CODENAME which works on both PVE 8 and 9.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New-PveRole: Privileges is a comma-separated string, not an array
- Set-PvePermission: uses UgId+Type, not Users
- Get-PveVmConfig: no Name property — test config get/set together
- Restart-PveVm: replace with Reset-PveVm (ACPI timeout on no-OS VM)
- Remove resize disk test (Set-PveVmConfig lacks Settings param)
- Copy-PveVm: must provide NewVmId explicitly
- Get-PveTask: requires Upid — test via Start-PveVm task chain
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Workflow changes:
- Convert to matrix strategy testing PVE 8.4 and 9.1 sequentially
- Each version gets unique VMID (99908/99909) and VM name
- Pass PVETEST_PVE_VERSION env var for version-specific assertions
- Version-specific artifact names for test results
New integration tests:
- User CRUD: create, list, update (Set-PveUser), remove
- API Token CRUD: create, list, remove (on dedicated test user)
- Role CRUD: create with privileges, list, remove
- Permissions: list and set (Get/Set-PvePermission)
- VM config: get and set config, resize disk
- VM lifecycle: restart, explicit remove
- Snapshots: full lifecycle (create, list, restore, remove)
- Storage: list content (Get-PveStorageContent)
- Tasks: list recent tasks (Get-PveTask)
- Version assertion: verify PVE major version matches expected
Coverage improved from 15 to ~30 cmdlets tested against live API.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PS 5.1 cannot parse the ?? operator (PS 7.0+), causing a discovery
error even though integration tests are excluded by tag. Replace with
if/else that works on all PS versions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PS 5.1 prompts interactively for mandatory parameters instead of
throwing, causing CI to hang indefinitely. Replace invocation-based
tests with attribute-based IsMandatory checks where duplicates exist,
or convert to attribute checks where they don't.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The net9.0 DLL references System.Runtime 9.0.0.0 which is unavailable
on GitHub runners where pwsh ships with .NET 8.x. Adding netstandard2.0
as a target produces a DLL compatible with any .NET version (PS 5.1 on
.NET Framework through PS 7.x on .NET 8/9).
- Add netstandard2.0 to TargetFrameworks in Core and module csproj
- Update #if NET48 conditionals to include NETSTANDARD2_0
- Switch CI workflows from net9.0 to netstandard2.0 for Pester tests
- Update _TestHelper.ps1 framework search order
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Separate dotnet publish into its own job that uploads artifacts. Pester
test jobs download pre-built artifacts and never install setup-dotnet,
eliminating the DOTNET_ROOT / System.Runtime resolution conflict that
caused all PS 7.x CI failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous approach of env: DOTNET_ROOT='' at the step level wasn't
sufficient — the .NET runtime may have already cached the original
DOTNET_ROOT before pwsh started. Now:
- Remove *.deps.json AND *.runtimeconfig.json from publish output
- Unset DOTNET_ROOT via $env:DOTNET_ROOT = $null inside the pwsh script
- _TestHelper.ps1 also unsets DOTNET_ROOT and cleans deps/runtimeconfig
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverts to GitHub-hosted runner matrix (Windows PS 5.1/7.5, Ubuntu,
macOS) with the two critical fixes that resolve System.Runtime 9.0.0.0
assembly loading on PS 7.x:
1. Remove PSProxmoxVE.deps.json from publish output (prevents .NET
dependency resolution conflicts with PS's bundled runtime)
2. Unset DOTNET_ROOT/DOTNET_MULTILEVEL_LOOKUP in Pester steps (prevents
setup-dotnet from overriding PS's assembly resolution)
Both fixes validated in Docker (705/705 tests pass).
Adds timeout-minutes: 15 to all jobs to prevent runaway billing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All CI now runs on the self-hosted runner using Docker containers,
eliminating GitHub-hosted runner minute consumption:
- Unit tests: PS 7.5 container (mcr.microsoft.com/powershell:7.5-ubuntu-24.04)
with .NET SDK 9.0 installed at build time
- Build/xUnit: .NET SDK 9.0 container (mcr.microsoft.com/dotnet/sdk:9.0)
- Integration tests: unchanged (runs directly on self-hosted runner)
Drops net48/PS 5.1 CI matrix (Windows-only, can't run in Linux
containers). Local Docker testing available via tests/Dockerfile.test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Loading via the module manifest (.psd1) ensures PowerShell sets up
proper assembly resolution context. Loading via raw DLL path bypasses
PS's module loading infrastructure and fails on CI runners where the
.NET SDK's assembly resolution conflicts with PS's bundled runtime.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The setup-dotnet action sets DOTNET_ROOT which overrides PS 7.x's
bundled .NET runtime for assembly resolution, causing System.Runtime
9.0.0.0 FileNotFoundException. Unsetting it for Pester steps lets PS
use its own runtime.
Also adds timeout-minutes: 15 to prevent runaway jobs from consuming
Actions minutes (PS 5.1 job was hanging for 5+ hours).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The deps.json file causes .NET's AssemblyLoadContext to conflict with
PowerShell's own runtime assembly resolution, resulting in
FileNotFoundException for System.Runtime 9.0.0.0 on all PS 7.x
platforms. Binary PS modules should not include deps.json.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CS8601: Add null-forgiving operator on guarded dictionary assignments
in cmdlets where IsNullOrEmpty check precedes the assignment
- CS8602: Add Assert.NotNull after JObject["data"] in xUnit model tests
(JToken indexer returns nullable on net48)
- CS8604: Add null-forgiving on guarded arguments in NodeService,
WaitPveTaskCmdlet, GetPveTemplateCmdlet, and auth test parameters
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Coverage for 16 cmdlets that had no unit tests:
- Nodes: Get-PveNode, Get-PveNodeStatus
- VM config: Get-PveVmConfig, Set-PveVmConfig, Resize-PveVmDisk
- VM ops: Copy-PveVm, Move-PveVm
- Container config: Copy-PveContainer, Get-PveContainerConfig,
Set-PveContainerConfig
- Storage: Invoke-PveStorageDownload
- Roles: New-PveRole, Remove-PveRole
- Tasks: Get-PveTask, Wait-PveTask
Each test covers: command existence, parameter metadata, pipeline
support, ShouldProcess/WhatIf, and no-session error handling.
Total Pester unit tests: 705 (was 431)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CS8604: Use cmdlet's own Node property instead of nullable task.Node
in WaitForTask calls across all VM cmdlets
- CS8618: Make PveSession.ServerVersion nullable (PveVersion?) since it
is set post-construction by PveAuthenticator
- CS8603: Add null-forgiving operator where IsNullOrWhiteSpace guards
guarantee non-null in PveHttpClient
- CS1573: Add XML param documentation to all public service methods
- xUnit1012: Make test parameters nullable (string?) for null test cases
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create shared _TestHelper.ps1 for reliable module loading in both
local dev and CI (fixes System.Runtime 9.0.0.0 FileNotFoundException
on PS 7.x by trying Import-Module by name first, then local paths)
- Use (Get-Module).ModuleBase for manifest path resolution
- Remove PSProxmoxVE.MockServer project and MockIntegration tests
- Remove MockIntegration from ExcludeTag filters
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Real integration tests against a provisioned PVE instance replace the
mock server tests. The MockServer project remains for now but the
workflow is no longer needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Change test projects from net10.0 to net9.0 to match source projects
- Update build workflow from net8.0 to net9.0
- Upgrade checkout to v5, setup-dotnet to v5 across all workflows
- Remove outdated PS 7.2 test matrix entries
- Restructure integration tests: create test keeps VM alive so start/stop,
snapshot, and cloud-init tests can use it instead of skipping. AfterAll
handles cleanup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Change FQDN from pve-test.local to pve.test.local so the PVE node
name is "pve" (matching what tests expect)
- Mask the dynamically created API token and VM IP with ::add-mask::
before they appear in workflow logs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GitHub Actions env: blocks don't do shell expansion — ${RUNNER_TEMP}
is literal there. Use ${{ runner.temp }} for env: blocks and keep
${RUNNER_TEMP} for run: blocks where the shell resolves it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
/tmp on the self-hosted runner has sticky bit — files created by
previous runs (or different users) can't be overwritten. RUNNER_TEMP
is per-run and owned by the runner user.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The proxmox-auto-install-assistant defaults --tmp to the source ISO's
directory (/opt/pve-isos/), which the runner user can't write to.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>