name: Integration Tests # Real integration tests against a live Proxmox VE instance. # # Architecture: # build → GitHub-hosted, dotnet publish → upload artifact # container-image → GitHub-hosted, build+push Docker image to GHCR # provision → self-hosted, provisions ALL nested PVE VMs via run-integration.sh # test → self-hosted, runs Pester tests via run-integration.sh # cleanup → self-hosted, tears down all VMs via run-integration.sh (always runs) # cleanup-images → GitHub-hosted, prunes old GHCR container images # # The provision/test/cleanup logic lives in tests/infrastructure/scripts/run-integration.sh # which is the single source of truth shared between CI and local dev containers. # # 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_TEST_PASSWORD - Root password for nested PVE instances (used for both provision and tests) # # Required repository variables: # PVE_TARGET_NODE - Parent PVE node name (variable, not secret, to avoid log masking) # # Optional secrets (for skip_provision mode): # PVETEST_HOST - Hostname or IP of a pre-existing nested PVE # # WARNING: Integration tests create and destroy real resources on the target # node. Never run against production. concurrency: group: integration-tests cancel-in-progress: false on: push: branches: [ main ] # NOTE: pull_request trigger intentionally removed. Integration tests # run on a self-hosted runner with access to secrets and a live PVE # node. Fork PRs must not execute code on that runner. Integration # tests run automatically on push to main after merge. workflow_dispatch: inputs: skip_provision: description: 'Skip provisioning (use existing PVE from PVETEST_HOST secret)' required: false type: boolean default: false permissions: contents: read packages: write env: SCRIPTS_DIR: tests/infrastructure/scripts TEST_IMAGE: ghcr.io/goodolclint/psproxmoxve-integration CACHE_DIR: /opt/pve-integration jobs: # ── Build module artifact (GitHub-hosted) ──────────────────────────── build: if: github.actor != 'dependabot[bot]' runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v6 - name: Setup .NET uses: actions/setup-dotnet@v5 with: dotnet-version: '10.0.x' - name: Build module run: dotnet publish src/PSProxmoxVE/PSProxmoxVE.csproj --configuration Release --framework netstandard2.0 --output ./publish/netstandard2.0 - name: Clean publish output run: rm -f ./publish/netstandard2.0/*.deps.json - name: Upload module artifact uses: actions/upload-artifact@v7 with: name: module-integration path: ./publish/netstandard2.0/ # ── Build test container image (GitHub-hosted) ─────────────────────── container-image: if: github.actor != 'dependabot[bot]' runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@v6 - name: Log in to GHCR uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v7 with: context: . file: tests/Dockerfile.test target: dev-infra push: true tags: ${{ env.TEST_IMAGE }}:${{ github.sha }},${{ env.TEST_IMAGE }}:latest # ── Provision all nested PVE VMs + storage VM (self-hosted) ───────── provision: if: inputs.skip_provision != true needs: [build, container-image] runs-on: [self-hosted, proxmox, integration] timeout-minutes: 45 container: image: ghcr.io/goodolclint/psproxmoxve-integration:${{ github.sha }} credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} volumes: - /opt/pve-integration:/opt/pve-integration # WARNING: The Docker socket is mounted to allow storage containers to be # managed from within the job container. This grants root-equivalent access # to the runner's Docker daemon. Only use on dedicated, isolated self-hosted # runners — never on shared runners. - /var/run/docker.sock:/var/run/docker.sock steps: - uses: actions/checkout@v6 - name: Provision PVE instances shell: bash env: PVE_ENDPOINT: ${{ secrets.PVE_ENDPOINT }} PVE_API_TOKEN: ${{ secrets.PVE_API_TOKEN }} PVE_TARGET_NODE: ${{ vars.PVE_TARGET_NODE }} PVE_PASSWORD: ${{ secrets.PVE_TEST_PASSWORD }} run: bash ${SCRIPTS_DIR}/run-integration.sh provision # ── Integration tests (self-hosted, per PVE version) ──────────────── test: needs: [build, provision] runs-on: [self-hosted, proxmox, integration] timeout-minutes: 20 container: image: ghcr.io/goodolclint/psproxmoxve-integration:${{ github.sha }} credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} volumes: - /opt/pve-integration:/opt/pve-integration strategy: fail-fast: false max-parallel: 2 matrix: pve_version: ['9', '8'] steps: - uses: actions/checkout@v6 - name: Download module artifact uses: actions/download-artifact@v8 with: name: module-integration path: ./publish/netstandard2.0/ - name: Run integration tests (PVE ${{ matrix.pve_version }}) shell: bash env: PVE_PASSWORD: ${{ secrets.PVE_TEST_PASSWORD }} MODULE_ARTIFACT: ./publish/netstandard2.0 SKIP_PROVISION: ${{ inputs.skip_provision || 'false' }} PVETEST_HOST: ${{ secrets.PVETEST_HOST }} PVETEST_PASSWORD: ${{ secrets.PVE_TEST_PASSWORD }} run: bash ${SCRIPTS_DIR}/run-integration.sh test ${{ matrix.pve_version }} - name: Upload test results if: always() uses: actions/upload-artifact@v7 with: name: integration-test-results-pve${{ matrix.pve_version }} path: TestResults/ # ── Cleanup: destroy all VMs (always runs) ────────────────────────── cleanup: needs: [provision, test] if: always() && needs.provision.result != 'skipped' && github.actor != 'dependabot[bot]' runs-on: [self-hosted, proxmox, integration] timeout-minutes: 15 container: image: ghcr.io/goodolclint/psproxmoxve-integration:${{ github.sha }} credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} volumes: - /opt/pve-integration:/opt/pve-integration # WARNING: The Docker socket is mounted to allow storage containers to be # stopped from within the job container. This grants root-equivalent access # to the runner's Docker daemon. Only use on dedicated, isolated self-hosted # runners — never on shared runners. - /var/run/docker.sock:/var/run/docker.sock steps: - uses: actions/checkout@v6 - name: Cleanup PVE instances shell: bash env: PVE_ENDPOINT: ${{ secrets.PVE_ENDPOINT }} PVE_API_TOKEN: ${{ secrets.PVE_API_TOKEN }} PVE_TARGET_NODE: ${{ vars.PVE_TARGET_NODE }} PVE_PASSWORD: ${{ secrets.PVE_TEST_PASSWORD }} # Always use force-cleanup in CI — handles corrupted state from # cancelled runs, timeouts, or partial provisioning failures. run: bash ${SCRIPTS_DIR}/run-integration.sh force-cleanup # ── Clean up old container images from GHCR ────────────────────────── cleanup-images: needs: test if: always() && github.actor != 'dependabot[bot]' runs-on: ubuntu-latest timeout-minutes: 5 env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: - name: Delete old container image versions uses: actions/delete-package-versions@v5 with: package-name: psproxmoxve-integration package-type: container min-versions-to-keep: 3 delete-only-untagged-versions: false token: ${{ secrets.GITHUB_TOKEN }}