Previous approach (/code-review:code-review --comment) was based on
incorrect documentation research. Reviewing the actual official
examples at anthropics/claude-code-action/examples/pr-review-*.yml
reveals the correct pattern:
1. Use a custom prompt with explicit review instructions (not a
plugin slash command)
2. Use claude_args with --allowedTools to enable the MCP inline
comment tool and gh pr CLI commands — this is what lets Claude
actually post to the PR
3. Enable track_progress: true for visual progress tracking
Without --allowedTools, Claude has no way to post anything because
the tools for PR commenting aren't allowed by default.
Also removed the plugins and plugin_marketplaces inputs since
they're not needed — the review runs via prompt instructions and
the allowed tools alone.
The custom prompt is tailored to PSProxmoxVE with focus areas
specific to the module: DECISIONS.md compliance, cmdlet
conventions, API correctness against the PVE OpenAPI spec,
test coverage, and security.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dependabot PRs were being rejected with:
Workflow initiated by non-human actor: dependabot (type: Bot)
Add allowed_bots: 'dependabot[bot]' to permit dependency update reviews.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Publish-Module -Path requires the directory name to match the module
name. Changed artifact download path from ./publish/netstandard2.0/
to ./publish/PSProxmoxVE/ so Publish-Module can find the manifest.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI cleanup should always succeed regardless of Terraform state
(cancelled runs, timeouts, partial failures). force-cleanup uses
direct API calls + docker rm instead of terraform destroy.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Terraform state was stored in the container's working directory
(fresh checkout), so it was lost between CI jobs. The cleanup job
couldn't destroy resources because it had no state.
Now stores state at /opt/pve-integration/work/terraform.tfstate via
-state flag on all terraform commands. This persists across the
provision → test → cleanup job chain in GitHub Actions.
Also:
- Force cleanup now removes state from both local and shared paths
- Added -reconfigure to terraform init (avoids backend mismatch errors)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevents overlapping integration test runs on the self-hosted runner.
Uses cancel-in-progress: false so each run completes its full
lifecycle (including cleanup) before the next one starts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix storage_ip derivation to use default route first, not Swarm RemoteManagers
- Make iSCSI container entrypoint idempotent (check before create for target/LUN/bind)
- Pin erichough/nfs-server from 'latest' to '2.2.1' for reproducibility
- Replace curl|sh Docker install with apt repo in Dockerfile.test
- Add docker.sock security warnings in docker-compose.test.yml and CI workflow
- Update stale 'storage VM' references in comments and .env.test.example"
Co-authored-by: GoodOlClint <151449+GoodOlClint@users.noreply.github.com>
Agent-Logs-Url: https://github.com/GoodOlClint/PSProxmoxVE/sessions/534c0a63-2988-496c-b0ae-e291fcfa050b
Provision two PVE nodes per version (a/b) for future cluster testing,
plus Docker-based iSCSI target and NFS server for shared storage tests.
Multi-node changes:
- Each PVE version gets two nodes: 9a/9b and 8a/8b (4 VMs total)
- Parameterized answer.toml FQDN for unique hostnames per node
- Per-node auto-install ISOs with unique answer files
- Node name discovered from FQDN and included in test config
- API token creation handles pre-existing tokens (delete + recreate)
- New test env vars: PVETEST_HOST_B, PVETEST_APITOKEN_B
- Removed preflight cleanup from provision (use explicit cleanup instead)
Docker storage services:
- New docker-compose.storage.yml with iSCSI (tgt) and NFS containers
- Host networking so PVE nodes can reach storage services
- Docker socket mounted into dev-infra container for host Docker access
- Docker CLI added to dev-infra Dockerfile stage
- New test env vars: PVETEST_STORAGE_VM_IP, PVETEST_ISCSI_IQN, PVETEST_NFS_EXPORT
Other fixes:
- first-boot.sh installs open-iscsi on PVE nodes
- preflight-cleanup.sh handles empty ISO filename gracefully
- TMPDIR set to work dir to avoid /tmp overflow during ISO uploads
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace tests/infrastructure/Dockerfile with tests/Dockerfile.test
(single multi-stage Dockerfile for both CI and local dev)
- CI container-image job now builds from Dockerfile.test target dev-infra
- Add ARM support: PowerShell installed via dotnet tool on arm64,
APT package on amd64
- Replace tests/dev.sh (bash) with tests/dev.ps1 (PowerShell) for
cross-platform support (Windows, macOS, Linux)
- Add -DockerHost parameter for running x86 containers on a remote
Docker host from ARM Macs (rsyncs repo, uses SSH Docker transport)
- Add -NoCleanup switch to keep nested PVE VMs after integration tests
- integration command now provisions nested PVE VMs instead of testing
against a pre-existing PVE directly
- Share /opt/pve-isos host path between CI and local dev (was separate
Docker named volume)
- Delete tools/Invoke-Tests.ps1 (unused, overlapped with run-integration.sh)
- Add .gitignore entries for Terraform state/artifacts
- Update all documentation references
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The node name "pve" was being masked in all CI logs because GitHub
Actions auto-masks secret values. Since the node name is not sensitive,
use vars.PVE_TARGET_NODE instead of secrets.PVE_TARGET_NODE.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Provision, test, and cleanup jobs now call run-integration.sh instead
of inlining ~170 lines of shell. Logic changes only need to be made
in one place, shared between CI and local dev.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CI workflow: PVE_PASSWORD now reads from secrets.PVE_TEST_PASSWORD
- variables.tf: removed default password, requires TF_VAR env var
- Integration README: examples use <your-test-password> placeholder
- create-api-token.sh: example IP changed to pve.example.com
- .gitignore: added .env/.env.* exclusion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Runner VM now has 2 runner instances registered, so both test matrix
entries can execute simultaneously against their respective pre-
provisioned nested PVE VMs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GitHub Actions redacts any value that matches a secret or masked string
in job outputs, making them empty in downstream jobs. This affected
host IPs, tokens, and even port 8006 (substring of PVE_ENDPOINT).
The provision job now writes connection details to
/opt/pve-isos/test-config.json on the shared volume. Test jobs read
this file directly with jq — no job outputs needed for sensitive values.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
::add-mask:: on the VM IP addresses caused GitHub Actions to redact
them in job outputs, making needs.provision.outputs.pve*_host empty
in the test jobs. The IPs are internal lab addresses, not sensitive.
Token masking is retained.
Also removes terraform debug output from prior commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
${{ runner.temp }} may resolve differently inside containers. Use
${RUNNER_TEMP} shell variable with unquoted heredoc instead. Added
debug output to diagnose ISO path issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restructures the integration test workflow from a monolithic sequential
job into separate provision → test → cleanup stages:
- provision: creates ALL nested PVE VMs in a single terraform apply
(parallel), waits for APIs, creates tokens, passes outputs to tests
- test: matrix [pve9, pve8] consumes provision outputs, no provisioning
- cleanup: always runs, API-only teardown for all VMs
Terraform refactored to for_each with pve_instances map variable,
enabling parallel ISO upload and VM creation.
New caching scripts reduce redundant downloads:
- ensure-base-iso.sh: downloads PVE ISOs to /opt/pve-isos if missing
- ensure-cloud-images.sh: caches cloud image + OVA with 7-day TTL
- prepare-auto-iso.sh: --cache-dir flag with hash-based skip
Runner no longer needs manual ISO provisioning (zero-touch setup).
cleanup-images bumped to min-versions-to-keep: 3 to survive overlapping
runs. All jobs gated against dependabot.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The dependabot actor check was only on the build job. The container-image
and cleanup-images jobs still ran — cleanup-images with if: always()
deleted GHCR container images while the main branch integration run was
still using them, causing PVE 8 tests to fail with "image not found".
Now all four integration jobs (build, container-image, integration,
cleanup-images) are gated with github.actor != 'dependabot[bot]'.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
unit-tests.yml: The publishable project targets netstandard2.0 only
(D009/F047), so the net48 build matrix entry fails. PS 5.1 loads
netstandard2.0 assemblies on .NET Framework 4.8, so the PS 5.1 Pester
tests now use the netstandard2.0 artifact.
integration-tests.yml: Skip entire workflow for dependabot PRs since
integration tests on the self-hosted runner are expensive and dependency
bumps don't warrant full end-to-end testing. Also updates dotnet SDK
from 9.0.x to 10.0.x for consistency.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
F058 (critical): Replace while(true) infinite-loop task polling with
TaskService.WaitForTask in 5 container snapshot and storage cmdlets.
F073+F047 (high): Migrate net9.0 → net10.0 across both source .csproj
files, build.yml, publish.yml, and test helper.
F071 (medium): Add Uri.EscapeDataString() to all inline URL path
segments in ~16 cmdlets that bypass service classes (D003).
F062+F063 (medium): Add ConfirmImpact.High to Restart-PveContainer
and Suspend-PveContainer (D006).
F075 (medium): Generate markdown help docs for 89 cmdlets that were
missing documentation (170 total, up from 81).
F072 (low): Remove unused System.Text.Json dependency from Core.csproj.
F074 (low): Raise publish smoke-test threshold from 60 to 150.
F065 (low): Add .github/ISSUE_TEMPLATE/config.yml.
F066 (low): Add CODEOWNERS.
Also fix _TestHelper.ps1 net9.0 → net10.0 framework reference.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>