name: Integration Tests # Real integration tests against a live Proxmox VE instance. # By default, provisions a throwaway nested PVE VM via Terraform, runs tests, # then destroys it. Can also run against a pre-existing PVE with skip_provision. # # Required repository secrets (for provisioning): # PVE_ENDPOINT - Parent PVE API URL (e.g. https://pve.example.com:8006) # PVE_API_TOKEN - Parent PVE API token (for Terraform + uploads) # PVE_TARGET_NODE - Parent PVE node name # # Optional secrets (for skip_provision mode): # PVETEST_HOST - Hostname or IP of a pre-existing nested PVE # PVETEST_APITOKEN - API token for the pre-existing nested PVE # # WARNING: Integration tests create and destroy real VMs, upload files, # and modify network configuration. Never run against production. on: push: branches: [ main ] pull_request: branches: [ main ] workflow_dispatch: inputs: pve_version: description: 'PVE version to test (8 or 9)' required: false type: choice options: - '9' - '8' default: '9' skip_provision: description: 'Skip provisioning (use existing PVE from PVETEST_HOST secret)' required: false type: boolean default: false env: INFRA_DIR: tests/infrastructure SCRIPTS_DIR: tests/infrastructure/scripts PVE_PASSWORD: "Testpass123!" jobs: integration: runs-on: [self-hosted, proxmox, integration] timeout-minutes: 45 steps: - uses: actions/checkout@v5 # ── Pre-flight cleanup ───────────────────────────────────────────── - name: Pre-flight cleanup if: inputs.skip_provision != true shell: bash id: iso env: PVE_API_TOKEN: ${{ secrets.PVE_API_TOKEN }} run: | VERSION="${{ inputs.pve_version || '9' }}" if [ "$VERSION" = "9" ]; then echo "base=/opt/pve-isos/proxmox-ve_9.1-1.iso" >> "$GITHUB_OUTPUT" echo "filename=proxmox-ve_9.1-1-auto.iso" >> "$GITHUB_OUTPUT" else echo "base=/opt/pve-isos/proxmox-ve_8.4-1.iso" >> "$GITHUB_OUTPUT" echo "filename=proxmox-ve_8.4-1-auto.iso" >> "$GITHUB_OUTPUT" fi bash ${SCRIPTS_DIR}/preflight-cleanup.sh \ "${{ secrets.PVE_ENDPOINT }}" \ "${PVE_API_TOKEN}" \ 99900 \ "auto.iso" \ "${INFRA_DIR}" # ── Provision nested PVE ─────────────────────────────────────────── - name: Generate answer file if: inputs.skip_provision != true shell: bash run: | sed "s/\${root_password}/${PVE_PASSWORD}/" \ ${INFRA_DIR}/answer.toml.tftpl > ${RUNNER_TEMP}/answer.toml echo "Generated answer file:" cat ${RUNNER_TEMP}/answer.toml - name: Prepare auto-install ISO if: inputs.skip_provision != true shell: bash run: | bash ${SCRIPTS_DIR}/prepare-auto-iso.sh \ "${{ steps.iso.outputs.base }}" \ ${RUNNER_TEMP}/answer.toml \ ${SCRIPTS_DIR}/first-boot.sh \ "${{ runner.temp }}/${{ steps.iso.outputs.filename }}" - name: Terraform init if: inputs.skip_provision != true shell: bash working-directory: ${{ env.INFRA_DIR }} run: terraform init -input=false - name: Terraform apply if: inputs.skip_provision != true id: terraform shell: bash working-directory: ${{ env.INFRA_DIR }} env: TF_VAR_proxmox_endpoint: ${{ secrets.PVE_ENDPOINT }} TF_VAR_proxmox_api_token: ${{ secrets.PVE_API_TOKEN }} TF_VAR_target_node: ${{ secrets.PVE_TARGET_NODE }} TF_VAR_iso_local_path: ${{ runner.temp }}/${{ steps.iso.outputs.filename }} TF_VAR_test_vm_password: ${{ env.PVE_PASSWORD }} run: | terraform apply -auto-approve -input=false echo "vm_id=$(terraform output -raw pve_test_vm_id)" >> "$GITHUB_OUTPUT" - name: Wait for install and create API token if: inputs.skip_provision != true id: provision shell: bash env: PVE_API_TOKEN: ${{ secrets.PVE_API_TOKEN }} run: | OUTPUT=$(bash ${SCRIPTS_DIR}/create-api-token.sh \ "${{ secrets.PVE_ENDPOINT }}" \ "${PVE_API_TOKEN}" \ "${{ steps.terraform.outputs.vm_id }}" \ "${PVE_PASSWORD}" \ 900) VM_IP=$(echo "$OUTPUT" | grep "^IP=" | cut -d= -f2) VM_TOKEN=$(echo "$OUTPUT" | grep "^TOKEN=" | cut -d= -f2-) # Mask sensitive values before they appear in logs echo "::add-mask::${VM_IP}" echo "::add-mask::${VM_TOKEN}" echo "Nested PVE ready at ${VM_IP}" echo "host=${VM_IP}" >> "$GITHUB_OUTPUT" echo "token=${VM_TOKEN}" >> "$GITHUB_OUTPUT" # ── Resolve test target ──────────────────────────────────────────── - name: Set test target id: target shell: bash run: | if [ "${{ inputs.skip_provision }}" = "true" ]; then echo "host=${{ secrets.PVETEST_HOST }}" >> "$GITHUB_OUTPUT" echo "port=${{ secrets.PVETEST_PORT || '8006' }}" >> "$GITHUB_OUTPUT" echo "token=${{ secrets.PVETEST_APITOKEN }}" >> "$GITHUB_OUTPUT" echo "node=${{ secrets.PVETEST_NODE || 'pve' }}" >> "$GITHUB_OUTPUT" echo "storage=${{ secrets.PVETEST_STORAGE || 'local' }}" >> "$GITHUB_OUTPUT" else echo "host=${{ steps.provision.outputs.host }}" >> "$GITHUB_OUTPUT" echo "port=8006" >> "$GITHUB_OUTPUT" echo "token=${{ steps.provision.outputs.token }}" >> "$GITHUB_OUTPUT" echo "node=pve" >> "$GITHUB_OUTPUT" echo "storage=local" >> "$GITHUB_OUTPUT" fi - name: Verify PVE API reachable shell: bash run: | HOST="${{ steps.target.outputs.host }}" PORT="${{ steps.target.outputs.port }}" TOKEN="${{ steps.target.outputs.token }}" echo "Testing connectivity to PVE API at ${HOST}:${PORT}..." if curl -sk --connect-timeout 10 \ -H "Authorization: PVEAPIToken=${TOKEN}" \ "https://${HOST}:${PORT}/api2/json/nodes" | grep -q '"node"'; then echo "PVE API is responsive" else echo "::error::Cannot reach PVE API at ${HOST}:${PORT}" exit 1 fi # ── Build and test ───────────────────────────────────────────────── - name: Build module run: dotnet publish src/PSProxmoxVE/PSProxmoxVE.csproj --configuration Release --framework net9.0 --output ./publish/net9.0 - name: Clean publish output for PS module loading run: rm -f ./publish/net9.0/*.deps.json ./publish/net9.0/*.runtimeconfig.json - name: Install Pester 5 shell: pwsh run: Install-Module -Name Pester -MinimumVersion 5.0 -Force -Scope CurrentUser - name: Copy module to module path shell: pwsh run: | $modulePath = "$HOME/.local/share/powershell/Modules/PSProxmoxVE" New-Item -ItemType Directory -Path $modulePath -Force | Out-Null Copy-Item -Path ./publish/net9.0/* -Destination $modulePath -Recurse -Force - name: Create test ISO shell: bash run: dd if=/dev/urandom of=${RUNNER_TEMP}/pvetest.iso bs=1M count=1 2>/dev/null - name: Run integration tests shell: pwsh env: DOTNET_ROOT: '' DOTNET_MULTILEVEL_LOOKUP: '' PVETEST_HOST: ${{ steps.target.outputs.host }} PVETEST_PORT: ${{ steps.target.outputs.port }} PVETEST_APITOKEN: ${{ steps.target.outputs.token }} PVETEST_NODE: ${{ steps.target.outputs.node }} PVETEST_STORAGE: ${{ steps.target.outputs.storage }} PVETEST_ISO_PATH: ${{ runner.temp }}/pvetest.iso run: | Import-Module Pester -MinimumVersion 5.0 $config = New-PesterConfiguration $config.Run.Path = "tests/PSProxmoxVE.Tests/Integration" $config.Filter.Tag = "Integration" $config.Output.Verbosity = "Detailed" $config.TestResult.Enabled = $true $config.TestResult.OutputFormat = "NUnitXml" $config.TestResult.OutputPath = "TestResults/integration-results.xml" Invoke-Pester -Configuration $config - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: name: integration-test-results path: TestResults/ # ── Teardown ─────────────────────────────────────────────────────── - name: Terraform destroy if: always() && inputs.skip_provision != true shell: bash working-directory: ${{ env.INFRA_DIR }} env: TF_VAR_proxmox_endpoint: ${{ secrets.PVE_ENDPOINT }} TF_VAR_proxmox_api_token: ${{ secrets.PVE_API_TOKEN }} TF_VAR_target_node: ${{ secrets.PVE_TARGET_NODE }} TF_VAR_iso_local_path: ${{ runner.temp }}/${{ steps.iso.outputs.filename }} TF_VAR_test_vm_password: ${{ env.PVE_PASSWORD }} run: | terraform init -input=false terraform destroy -auto-approve -input=false || true rm -f terraform.tfstate terraform.tfstate.backup .terraform.tfstate.lock.info - name: Final cleanup if: always() && inputs.skip_provision != true shell: bash env: PVE_API_TOKEN: ${{ secrets.PVE_API_TOKEN }} run: | bash ${SCRIPTS_DIR}/preflight-cleanup.sh \ "${{ secrets.PVE_ENDPOINT }}" \ "${PVE_API_TOKEN}" \ 99900 \ "${{ steps.iso.outputs.filename }}" \ "${INFRA_DIR}" || true