ci: pin release deps + Dependabot + build provenance attestation (#406) #491
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Integration Tests | |
| # ================= | |
| # Test PowerNetbox against real Netbox instances via Docker | |
| # | |
| # This workflow: | |
| # - Spins up Netbox via Docker Compose | |
| # - Runs integration tests against the live API | |
| # - Tests multiple Netbox versions in parallel (matrix) | |
| # - Only runs on ubuntu (Docker required) | |
| # | |
| # Triggers: | |
| # - Push to dev/main/beta branches (post-merge validation) | |
| # - Manual dispatch with custom Netbox version | |
| # - Weekly scheduled run (catch upstream changes) | |
| # | |
| # NOTE: Intentionally not triggered on pull_request. Integration tests | |
| # take ~5 min (mostly Docker/Netbox startup) and are not required checks. | |
| # Unit tests (2350+) provide sufficient pre-merge coverage. Integration | |
| # runs post-merge on push to catch any regressions immediately. | |
| name: Integration Tests | |
| on: | |
| push: | |
| branches: [dev, main, beta] | |
| # Allow manual trigger with custom version | |
| workflow_dispatch: | |
| inputs: | |
| netbox_version: | |
| description: 'Netbox Docker image tag (e.g., v4.5.8-4.0.2)' | |
| required: false | |
| default: 'v4.5.8-4.0.2' | |
| type: string | |
| # Run weekly to catch upstream Netbox changes | |
| schedule: | |
| - cron: '0 6 * * 1' # Every Monday at 6 AM UTC | |
| env: | |
| # Default API token (matches docker-compose.ci.yml) | |
| NETBOX_TOKEN: "0123456789abcdef0123456789abcdef01234567" | |
| # Docker Compose project name (for predictable container names) | |
| COMPOSE_PROJECT_NAME: "powernetbox" | |
| permissions: | |
| contents: read | |
| jobs: | |
| # ============================================================ | |
| # Integration Tests - Matrix of Netbox versions | |
| # ============================================================ | |
| integration: | |
| name: Netbox ${{ matrix.netbox_short }} | |
| runs-on: ubuntu-latest | |
| # Don't fail entire workflow if experimental version fails | |
| continue-on-error: ${{ matrix.experimental }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # ---------------------------------------------------------- | |
| # Minimum supported version | |
| # ---------------------------------------------------------- | |
| - netbox: "v4.3.7-3.3.0" | |
| netbox_short: "4.3.7" | |
| experimental: false | |
| # ---------------------------------------------------------- | |
| # Stable version - MUST pass (production target) | |
| # ---------------------------------------------------------- | |
| - netbox: "v4.4.10-3.4.2" | |
| netbox_short: "4.4.10" | |
| experimental: false | |
| # ---------------------------------------------------------- | |
| # Latest version - Netbox 4.5.8 (netbox-docker 4.0.2) | |
| # ---------------------------------------------------------- | |
| - netbox: "v4.5.8-4.0.2" | |
| netbox_short: "4.5.8" | |
| experimental: false | |
| env: | |
| NETBOX_VERSION: ${{ github.event.inputs.netbox_version || matrix.netbox }} | |
| NETBOX_HOST: "localhost:8000" | |
| steps: | |
| # ---------------------------------------------------------- | |
| # Setup | |
| # ---------------------------------------------------------- | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 | |
| # ---------------------------------------------------------- | |
| # Start Netbox Stack | |
| # ---------------------------------------------------------- | |
| - name: Pull Docker images | |
| run: | | |
| echo "Pulling images for Netbox ${{ env.NETBOX_VERSION }}..." | |
| docker compose -f docker-compose.ci.yml pull | |
| - name: Start Netbox stack | |
| run: | | |
| echo "Starting Netbox stack..." | |
| docker compose -f docker-compose.ci.yml up -d | |
| echo "" | |
| echo "Container status:" | |
| docker compose -f docker-compose.ci.yml ps | |
| - name: Wait for Netbox to be ready | |
| run: | | |
| echo "Waiting for Netbox to be ready (this may take 5-6 minutes for Netbox 4.5+ migrations)..." | |
| echo "" | |
| MAX_WAIT=420 # 7 minutes max (4.5.0 has many migrations) | |
| ELAPSED=0 | |
| INTERVAL=10 | |
| while [ $ELAPSED -lt $MAX_WAIT ]; do | |
| # Check container health status | |
| STATUS=$(docker inspect --format='{{.State.Health.Status}}' powernetbox-netbox-1 2>/dev/null || echo "starting") | |
| echo "[${ELAPSED}s] Netbox container status: $STATUS" | |
| if [ "$STATUS" = "healthy" ]; then | |
| echo "" | |
| echo "Netbox is healthy! Verifying web server..." | |
| # Double-check web server is responding (use /login/ - no auth required) | |
| if curl -sf http://localhost:8000/login/ > /dev/null 2>&1; then | |
| echo "Netbox web server is responding!" | |
| exit 0 | |
| fi | |
| fi | |
| if [ "$STATUS" = "unhealthy" ]; then | |
| echo "" | |
| echo "ERROR: Container became unhealthy!" | |
| echo "" | |
| echo "=== Container Logs ===" | |
| docker compose -f docker-compose.ci.yml logs netbox --tail 100 | |
| exit 1 | |
| fi | |
| sleep $INTERVAL | |
| ELAPSED=$((ELAPSED + INTERVAL)) | |
| done | |
| echo "" | |
| echo "ERROR: Timeout waiting for Netbox to be ready after ${MAX_WAIT}s" | |
| echo "" | |
| echo "=== Container Status ===" | |
| docker compose -f docker-compose.ci.yml ps | |
| echo "" | |
| echo "=== Netbox Logs ===" | |
| docker compose -f docker-compose.ci.yml logs netbox --tail 100 | |
| exit 1 | |
| - name: Display Netbox version info | |
| run: | | |
| echo "=== Netbox Status API ===" | |
| curl -s http://localhost:8000/api/status/ | jq . | |
| # Extract version for later use | |
| VERSION=$(curl -s http://localhost:8000/api/status/ | jq -r '.["netbox-version"]') | |
| echo "" | |
| echo "Running Netbox version: $VERSION" | |
| echo "NETBOX_RUNNING_VERSION=$VERSION" >> $GITHUB_ENV | |
| # ---------------------------------------------------------- | |
| # Install PowerShell & Dependencies | |
| # ---------------------------------------------------------- | |
| - name: Install PowerShell dependencies | |
| shell: pwsh | |
| run: | | |
| Write-Host "Installing Pester..." -ForegroundColor Cyan | |
| Set-PSRepository PSGallery -InstallationPolicy Trusted | |
| Install-Module Pester -Force -Scope CurrentUser -MinimumVersion 5.0.0 | |
| $pesterVersion = (Get-Module Pester -ListAvailable | Select-Object -First 1).Version | |
| Write-Host "Pester version: $pesterVersion" -ForegroundColor Green | |
| - name: Build PowerNetbox module | |
| shell: pwsh | |
| run: | | |
| Write-Host "Building module..." -ForegroundColor Cyan | |
| ./deploy.ps1 -Environment dev -SkipVersion | |
| Write-Host "Importing module..." -ForegroundColor Cyan | |
| Import-Module ./PowerNetbox/PowerNetbox.psd1 -Force | |
| $cmdCount = (Get-Command -Module PowerNetbox).Count | |
| Write-Host "Module loaded with $cmdCount commands" -ForegroundColor Green | |
| # ---------------------------------------------------------- | |
| # v2 Token Tests (Netbox 4.5+) | |
| # ---------------------------------------------------------- | |
| - name: Test v2 token format support | |
| if: startsWith(matrix.netbox_short, '4.5') | |
| shell: pwsh | |
| env: | |
| NETBOX_HOST: ${{ env.NETBOX_HOST }} | |
| NETBOX_TOKEN: ${{ env.NETBOX_TOKEN }} | |
| NETBOX_RUNNING_VERSION: ${{ env.NETBOX_RUNNING_VERSION }} | |
| run: | | |
| Write-Host "============================================" -ForegroundColor Cyan | |
| Write-Host " Testing v2 Token Support (Netbox 4.5+)" -ForegroundColor Cyan | |
| Write-Host "============================================" -ForegroundColor Cyan | |
| Write-Host "" | |
| Import-Module ./PowerNetbox/PowerNetbox.psd1 -Force | |
| # Check if Netbox is responding correctly | |
| # The Docker image for 4.5-beta may have issues | |
| if ($env:NETBOX_RUNNING_VERSION -eq 'null' -or [string]::IsNullOrEmpty($env:NETBOX_RUNNING_VERSION)) { | |
| Write-Host "WARNING: Netbox version detection returned null" -ForegroundColor Yellow | |
| Write-Host " This indicates the Docker image may not be fully functional." -ForegroundColor Yellow | |
| Write-Host " Skipping token auth tests (known 4.5-beta Docker image issue)." -ForegroundColor Yellow | |
| Write-Host "" | |
| # Only run regex pattern tests (no API calls) | |
| Write-Host "Running v2 token format detection tests only..." -ForegroundColor Cyan | |
| } | |
| # v2 token format: nbt_<KEY>.<SECRET> | |
| $v2Pattern = '^nbt_[A-Za-z0-9]+\.[A-Za-z0-9]+$' | |
| $isV2Token = $env:NETBOX_TOKEN -match $v2Pattern | |
| $netboxWorking = $env:NETBOX_RUNNING_VERSION -ne 'null' -and -not [string]::IsNullOrEmpty($env:NETBOX_RUNNING_VERSION) | |
| # Test 1: Bearer header (only works with v2 tokens and working Netbox) | |
| if (-not $netboxWorking) { | |
| Write-Host "Test 1: SKIPPED - Netbox not responding correctly" -ForegroundColor Yellow | |
| } elseif ($isV2Token) { | |
| Write-Host "Test 1: Testing Bearer header with v2 token..." -ForegroundColor Yellow | |
| $headers = @{ | |
| 'Authorization' = "Bearer $env:NETBOX_TOKEN" | |
| 'Accept' = 'application/json' | |
| } | |
| try { | |
| $response = Invoke-RestMethod -Uri "http://$env:NETBOX_HOST/api/status/" -Headers $headers | |
| Write-Host " PASS: Bearer header works with v2 token" -ForegroundColor Green | |
| Write-Host " Version: $($response.'netbox-version')" -ForegroundColor Gray | |
| } catch { | |
| Write-Host " FAIL: Bearer header failed: $($_.Exception.Message)" -ForegroundColor Red | |
| exit 1 | |
| } | |
| } else { | |
| Write-Host "Test 1: SKIPPED - Bearer auth requires v2 token (nbt_xxx.yyy format)" -ForegroundColor Yellow | |
| Write-Host " Current token is v1 format (40-char hex)" -ForegroundColor Gray | |
| } | |
| # Test 2: Token header should work with both v1 and v2 tokens (backwards compat) | |
| if (-not $netboxWorking) { | |
| Write-Host "Test 2: SKIPPED - Netbox not responding correctly" -ForegroundColor Yellow | |
| } else { | |
| Write-Host "Test 2: Testing Token header (backwards compat)..." -ForegroundColor Yellow | |
| $headers = @{ | |
| 'Authorization' = "Token $env:NETBOX_TOKEN" | |
| 'Accept' = 'application/json' | |
| } | |
| try { | |
| $response = Invoke-RestMethod -Uri "http://$env:NETBOX_HOST/api/status/" -Headers $headers | |
| Write-Host " PASS: Token header works on Netbox 4.5+" -ForegroundColor Green | |
| Write-Host " Version: $($response.'netbox-version')" -ForegroundColor Gray | |
| } catch { | |
| Write-Host " FAIL: Token header failed: $($_.Exception.Message)" -ForegroundColor Red | |
| exit 1 | |
| } | |
| } | |
| # Test 3: v2 token format detection (regex pattern matching - no API needed) | |
| Write-Host "Test 3: Testing v2 token format detection..." -ForegroundColor Yellow | |
| $v2Token = "nbt_ExampleKey123.ExampleSecretValue456789" | |
| $v1Token = "0123456789abcdef0123456789abcdef01234567" | |
| if ($v2Token -match $v2Pattern) { | |
| Write-Host " PASS: v2 token format correctly identified" -ForegroundColor Green | |
| } else { | |
| Write-Host " FAIL: v2 token format not recognized" -ForegroundColor Red | |
| exit 1 | |
| } | |
| if ($v1Token -notmatch $v2Pattern) { | |
| Write-Host " PASS: v1 token correctly not matched as v2" -ForegroundColor Green | |
| } else { | |
| Write-Host " FAIL: v1 token incorrectly matched as v2" -ForegroundColor Red | |
| exit 1 | |
| } | |
| Write-Host "" | |
| Write-Host "All v2 token tests passed!" -ForegroundColor Green | |
| # ---------------------------------------------------------- | |
| # Setup API Token (v1 or v2 depending on Netbox version) | |
| # ---------------------------------------------------------- | |
| - name: Setup API Token | |
| id: token | |
| run: | | |
| set +e # Don't exit on error - we handle errors ourselves | |
| echo "Testing v1 token authentication..." | |
| V1_TOKEN="0123456789abcdef0123456789abcdef01234567" | |
| # Test if v1 token works | |
| RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Token $V1_TOKEN" http://localhost:8000/api/status/) | |
| if [ "$RESPONSE" = "200" ]; then | |
| echo "V1 token works - using legacy token" | |
| echo "token=$V1_TOKEN" >> $GITHUB_OUTPUT | |
| echo "token_type=v1" >> $GITHUB_OUTPUT | |
| else | |
| echo "V1 token failed (HTTP $RESPONSE) - creating v2 token for Netbox 4.5.0+" | |
| # Create a v2 token using Netbox's Token model | |
| echo "Running Django shell to create token..." | |
| DJANGO_OUTPUT=$(docker exec powernetbox-netbox-1 /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py shell -c " | |
| import sys | |
| from users.models import Token | |
| from django.contrib.auth import get_user_model | |
| try: | |
| User = get_user_model() | |
| user = User.objects.filter(username='admin').first() | |
| print(f'DEBUG: Found user: {user}', file=sys.stderr) | |
| if not user: | |
| user = User.objects.create_superuser('admin', 'admin@example.com', 'admin') | |
| print(f'DEBUG: Created superuser', file=sys.stderr) | |
| # Delete existing tokens | |
| deleted = Token.objects.filter(user=user).delete() | |
| print(f'DEBUG: Deleted tokens: {deleted}', file=sys.stderr) | |
| # Create token instance | |
| token = Token(user=user) | |
| # Generate the plaintext secret | |
| secret = Token.generate() # Static method | |
| print(f'DEBUG: Generated secret: {secret[:10]}...', file=sys.stderr) | |
| # Set via the 'token' property - this automatically: | |
| # 1. Generates the key (token.key) | |
| # 2. Calls update_digest() to compute HMAC | |
| token.token = secret | |
| print(f'DEBUG: Set token property, key={token.key}', file=sys.stderr) | |
| # Save the token | |
| token.save() | |
| print(f'DEBUG: Token saved, id={token.id}, hmac_digest={token.hmac_digest[:10] if token.hmac_digest else None}...', file=sys.stderr) | |
| # Output the full v2 token: nbt_{key}.{secret} | |
| full_token = f'nbt_{token.key}.{secret}' | |
| print(full_token) | |
| print(f'DEBUG: Created v2 token successfully', file=sys.stderr) | |
| except Exception as e: | |
| print(f'ERROR: {type(e).__name__}: {e}', file=sys.stderr) | |
| import traceback | |
| traceback.print_exc() | |
| " 2>&1) | |
| echo "Django output:" | |
| echo "$DJANGO_OUTPUT" | |
| # Extract the token line | |
| TOKEN_OUTPUT=$(echo "$DJANGO_OUTPUT" | grep -E '^nbt_' || true) | |
| if [ -n "$TOKEN_OUTPUT" ] && [[ "$TOKEN_OUTPUT" == nbt_* ]]; then | |
| echo "Created v2 token: ${TOKEN_OUTPUT:0:20}..." | |
| echo "::add-mask::$TOKEN_OUTPUT" | |
| echo "token=$TOKEN_OUTPUT" >> $GITHUB_OUTPUT | |
| echo "token_type=v2" >> $GITHUB_OUTPUT | |
| else | |
| echo "WARNING: Failed to create v2 token. Using v1 fallback (tests may be skipped)." | |
| echo "token=$V1_TOKEN" >> $GITHUB_OUTPUT | |
| echo "token_type=v1-fallback" >> $GITHUB_OUTPUT | |
| fi | |
| fi | |
| # ---------------------------------------------------------- | |
| # Verify API Auth Works | |
| # ---------------------------------------------------------- | |
| - name: Verify API authentication | |
| id: verify_api | |
| shell: pwsh | |
| env: | |
| NETBOX_HOST: ${{ env.NETBOX_HOST }} | |
| NETBOX_TOKEN: ${{ steps.token.outputs.token }} | |
| TOKEN_TYPE: ${{ steps.token.outputs.token_type }} | |
| run: | | |
| Write-Host "Verifying API authentication..." -ForegroundColor Cyan | |
| Write-Host "Token type: $env:TOKEN_TYPE" -ForegroundColor Gray | |
| # Determine auth header based on token type | |
| $authHeader = if ($env:NETBOX_TOKEN -match '^nbt_') { | |
| "Bearer $env:NETBOX_TOKEN" | |
| } else { | |
| "Token $env:NETBOX_TOKEN" | |
| } | |
| $headers = @{ | |
| 'Authorization' = $authHeader | |
| 'Accept' = 'application/json' | |
| } | |
| try { | |
| $response = Invoke-RestMethod -Uri "http://$env:NETBOX_HOST/api/dcim/sites/?limit=1" -Headers $headers | |
| Write-Host "API authentication successful!" -ForegroundColor Green | |
| echo "api_working=true" >> $env:GITHUB_OUTPUT | |
| } catch { | |
| Write-Host "WARNING: API authentication failed: $($_.Exception.Message)" -ForegroundColor Yellow | |
| Write-Host "This may be a Docker image issue. Integration tests will be skipped." -ForegroundColor Yellow | |
| echo "api_working=false" >> $env:GITHUB_OUTPUT | |
| } | |
| # ---------------------------------------------------------- | |
| # Run Integration Tests | |
| # ---------------------------------------------------------- | |
| - name: Run Integration tests | |
| id: integration_tests | |
| if: steps.verify_api.outputs.api_working == 'true' | |
| shell: pwsh | |
| env: | |
| NETBOX_HOST: ${{ env.NETBOX_HOST }} | |
| NETBOX_TOKEN: ${{ steps.token.outputs.token }} | |
| NETBOX_VERSION: ${{ matrix.netbox_short }} | |
| run: | | |
| Write-Host "============================================" -ForegroundColor Cyan | |
| Write-Host " Running Integration Tests" -ForegroundColor Cyan | |
| Write-Host " Netbox: $env:NETBOX_VERSION" -ForegroundColor Cyan | |
| Write-Host " Host: $env:NETBOX_HOST" -ForegroundColor Cyan | |
| Write-Host "============================================" -ForegroundColor Cyan | |
| Write-Host "" | |
| $config = New-PesterConfiguration | |
| $config.Run.Path = './Tests/Integration.Tests.ps1' | |
| $config.Run.Exit = $true | |
| $config.Filter.Tag = @('Integration') | |
| $config.TestResult.Enabled = $true | |
| $config.TestResult.OutputPath = 'IntegrationTestResults.xml' | |
| $config.TestResult.OutputFormat = 'NUnitXml' | |
| $config.Output.Verbosity = 'Detailed' | |
| Invoke-Pester -Configuration $config | |
| - name: Skip Integration tests (API not working) | |
| if: steps.verify_api.outputs.api_working != 'true' | |
| run: | | |
| echo "::warning::Skipping integration tests - API authentication not working (likely Docker image issue)" | |
| echo "This is expected for some beta/experimental Netbox versions." | |
| # ---------------------------------------------------------- | |
| # Artifacts & Cleanup | |
| # ---------------------------------------------------------- | |
| - name: Upload test results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: integration-results-${{ matrix.netbox_short }} | |
| path: IntegrationTestResults.xml | |
| retention-days: 30 | |
| - name: Collect debug logs on failure | |
| if: failure() | |
| run: | | |
| echo "=== Collecting debug information ===" > debug-logs.txt | |
| echo "" >> debug-logs.txt | |
| echo "=== Docker Compose Status ===" >> debug-logs.txt | |
| docker compose -f docker-compose.ci.yml ps >> debug-logs.txt 2>&1 | |
| echo "" >> debug-logs.txt | |
| echo "=== Netbox Logs (last 200 lines) ===" >> debug-logs.txt | |
| docker compose -f docker-compose.ci.yml logs netbox --tail 200 >> debug-logs.txt 2>&1 | |
| echo "" >> debug-logs.txt | |
| echo "=== PostgreSQL Logs ===" >> debug-logs.txt | |
| docker compose -f docker-compose.ci.yml logs postgres --tail 50 >> debug-logs.txt 2>&1 | |
| echo "" >> debug-logs.txt | |
| echo "=== Redis Logs ===" >> debug-logs.txt | |
| docker compose -f docker-compose.ci.yml logs redis --tail 50 >> debug-logs.txt 2>&1 | |
| - name: Upload debug logs on failure | |
| uses: actions/upload-artifact@v4 | |
| if: failure() | |
| with: | |
| name: debug-logs-${{ matrix.netbox_short }} | |
| path: debug-logs.txt | |
| retention-days: 7 | |
| - name: Cleanup Docker resources | |
| if: always() | |
| run: | | |
| echo "Stopping containers..." | |
| docker compose -f docker-compose.ci.yml down -v --remove-orphans | |
| echo "Pruning unused Docker resources..." | |
| docker system prune -f --volumes | |
| # ============================================================ | |
| # Summary Job - Report overall status | |
| # ============================================================ | |
| summary: | |
| name: Integration Summary | |
| runs-on: ubuntu-latest | |
| needs: integration | |
| if: always() | |
| steps: | |
| - name: Check integration results | |
| run: | | |
| echo "Integration test result: ${{ needs.integration.result }}" | |
| if [ "${{ needs.integration.result }}" = "failure" ]; then | |
| echo "::error::One or more integration tests failed!" | |
| exit 1 | |
| elif [ "${{ needs.integration.result }}" = "cancelled" ]; then | |
| echo "::warning::Integration tests were cancelled" | |
| exit 0 | |
| else | |
| echo "::notice::All integration tests passed!" | |
| fi |