Send-PveFile replaces Send-PveIso with a -ContentType parameter
(iso, vztmpl, import). This is a breaking rename — the module has
not been released yet.
Test infrastructure refactored:
- prepare-test-vm.sh replaced with prepare-test-environment.sh
(only SSH ops: enable content types, upload cloud-init snippet,
curl download cloud image)
- VM provisioning moved into integration tests as "Linux VM —
Provisioning" context: Send-PveFile, New-PveVm, Import-PveVmDisk,
Set-PveVmConfig, Set-PveCloudInitConfig, Start-PveVm,
Test-PveVmGuestAgent — all tested as part of the test suite
- PVETEST_LINUX_VMID env var removed (VM created by tests)
- PVETEST_CLOUD_IMAGE_PATH + PVETEST_PASSWORD added
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>
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>
- 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>
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>
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>
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>
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>
- 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>
- first-boot.sh: Remove .sources files (PVE 9 DEB822 format) not just
.list, and detect suite (trixie vs bookworm) from debian.sources
- create-api-token.sh: Accept full endpoint URL (fixes missing port),
use /access/domains for API check (no auth needed on PVE 9),
avoid pipefail on curl|python3 pipe
- preflight-cleanup.sh: New script that runs at start and end of every
provisioning run — cleans orphaned VMs, ISOs, and stale terraform state
- Workflow: Add pre-flight and final cleanup steps so failed runs can't
leave the runner/host in a broken state
All changes validated manually on the self-hosted runner.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Terraform destroy now handles cancelled runs gracefully:
- Re-runs terraform init (in case it didn't complete)
- Force-unlocks stale state locks from killed apply steps
- Cleans up state files so next run starts fresh
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use proxmox_virtual_environment_file resource to upload the auto-install
ISO via the API. terraform destroy now cleans up both the VM and ISO.
- Remove manual upload-to-pve.sh and cleanup-pve-storage.sh scripts.
- Commit .terraform.lock.hcl for reproducible CI builds.
- Replace iso_file_id variable with iso_local_path and iso_storage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Workflow now provisions a throwaway nested PVE VM via Terraform, runs
integration tests against it, then destroys it. Uses proxmox-auto-install-assistant
to bake the answer file and a first-boot script (installs qemu-guest-agent)
directly into the ISO. IP is discovered via the QEMU guest agent on the
parent PVE, eliminating the need for static IP configuration.
Supports both PVE 8.x and 9.x via workflow_dispatch version selector.
Skip provisioning with skip_provision=true to test against a pre-existing PVE.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The self-hosted runner has dotnet 9.0 installed globally. Drop the
setup-dotnet step (which tried to install to /usr/share/dotnet without
permission) and build/publish against the project's actual target
framework net9.0 instead of the nonexistent net10.0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Connectivity check: pass API token and check /nodes instead of /version
(PVE 9 requires authentication for all API endpoints including /version)
- Runner labels updated to proxmox,integration externally
- PVETEST secrets configured for integration test target
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Integration tests now run on self-hosted runners with proxmox/integration
labels on every push to main. Adds PVE API connectivity check before
test execution and uses Pester configuration object for cleaner setup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pester tests that start the mock PVE server, run real cmdlets against it,
and validate end-to-end behavior: auth flows, node/VM/storage/network/
user operations, pipeline support, and deserialization. Runs on every
push via GitHub-hosted runners.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build workflow validates both net48 and net10.0 targets with code
coverage. Unit test workflow runs Pester across 6 OS/PS combinations.
Integration test workflow is manual trigger only with secrets check.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>