Cerberus Integration #345
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
| name: Cerberus Integration | |
| # Phase 2-3: Build Once, Test Many - Use registry image instead of building | |
| # This workflow now waits for docker-build.yml to complete and pulls the built image | |
| on: | |
| workflow_run: | |
| workflows: ["Docker Build, Publish & Test"] | |
| types: [completed] | |
| branches: [main, development, 'feature/**'] # Explicit branch filter prevents unexpected triggers | |
| # Allow manual trigger for debugging | |
| workflow_dispatch: | |
| inputs: | |
| image_tag: | |
| description: 'Docker image tag to test (e.g., pr-123-abc1234, latest)' | |
| required: false | |
| type: string | |
| # Prevent race conditions when PR is updated mid-test | |
| # Cancels old test runs when new build completes with different SHA | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.ref }}-${{ github.event.workflow_run.head_sha || github.sha }} | |
| cancel-in-progress: true | |
| jobs: | |
| cerberus-integration: | |
| name: Cerberus Security Stack Integration | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # Only run if docker-build.yml succeeded, or if manually triggered | |
| if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| # Determine the correct image tag based on trigger context | |
| # For PRs: pr-{number}-{sha}, For branches: {sanitized-branch}-{sha} | |
| - name: Determine image tag | |
| id: determine-tag | |
| env: | |
| EVENT: ${{ github.event.workflow_run.event }} | |
| REF: ${{ github.event.workflow_run.head_branch }} | |
| SHA: ${{ github.event.workflow_run.head_sha }} | |
| MANUAL_TAG: ${{ inputs.image_tag }} | |
| run: | | |
| # Manual trigger uses provided tag | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then | |
| if [[ -n "$MANUAL_TAG" ]]; then | |
| echo "tag=${MANUAL_TAG}" >> $GITHUB_OUTPUT | |
| else | |
| # Default to latest if no tag provided | |
| echo "tag=latest" >> $GITHUB_OUTPUT | |
| fi | |
| echo "source_type=manual" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| # Extract 7-character short SHA | |
| SHORT_SHA=$(echo "$SHA" | cut -c1-7) | |
| if [[ "$EVENT" == "pull_request" ]]; then | |
| # Use native pull_requests array (no API calls needed) | |
| PR_NUM=$(echo '${{ toJson(github.event.workflow_run.pull_requests) }}' | jq -r '.[0].number') | |
| if [[ -z "$PR_NUM" || "$PR_NUM" == "null" ]]; then | |
| echo "❌ ERROR: Could not determine PR number" | |
| echo "Event: $EVENT" | |
| echo "Ref: $REF" | |
| echo "SHA: $SHA" | |
| echo "Pull Requests JSON: ${{ toJson(github.event.workflow_run.pull_requests) }}" | |
| exit 1 | |
| fi | |
| # Immutable tag with SHA suffix prevents race conditions | |
| echo "tag=pr-${PR_NUM}-${SHORT_SHA}" >> $GITHUB_OUTPUT | |
| echo "source_type=pr" >> $GITHUB_OUTPUT | |
| else | |
| # Branch push: sanitize branch name and append SHA | |
| # Sanitization: lowercase, replace / with -, remove special chars | |
| SANITIZED=$(echo "$REF" | \ | |
| tr '[:upper:]' '[:lower:]' | \ | |
| tr '/' '-' | \ | |
| sed 's/[^a-z0-9-._]/-/g' | \ | |
| sed 's/^-//; s/-$//' | \ | |
| sed 's/--*/-/g' | \ | |
| cut -c1-121) # Leave room for -SHORT_SHA (7 chars) | |
| echo "tag=${SANITIZED}-${SHORT_SHA}" >> $GITHUB_OUTPUT | |
| echo "source_type=branch" >> $GITHUB_OUTPUT | |
| fi | |
| echo "sha=${SHORT_SHA}" >> $GITHUB_OUTPUT | |
| echo "Determined image tag: $(cat $GITHUB_OUTPUT | grep tag=)" | |
| # Pull image from registry with retry logic (dual-source strategy) | |
| # Try registry first (fast), fallback to artifact if registry fails | |
| - name: Pull Docker image from registry | |
| id: pull_image | |
| uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 | |
| with: | |
| timeout_minutes: 5 | |
| max_attempts: 3 | |
| retry_wait_seconds: 10 | |
| command: | | |
| IMAGE_NAME="ghcr.io/${{ github.repository_owner }}/charon:${{ steps.determine-tag.outputs.tag }}" | |
| echo "Pulling image: $IMAGE_NAME" | |
| docker pull "$IMAGE_NAME" | |
| docker tag "$IMAGE_NAME" charon:local | |
| echo "✅ Successfully pulled from registry" | |
| continue-on-error: true | |
| # Fallback: Download artifact if registry pull failed | |
| - name: Fallback to artifact download | |
| if: steps.pull_image.outcome == 'failure' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| SHA: ${{ steps.determine-tag.outputs.sha }} | |
| run: | | |
| echo "⚠️ Registry pull failed, falling back to artifact..." | |
| # Determine artifact name based on source type | |
| if [[ "${{ steps.determine-tag.outputs.source_type }}" == "pr" ]]; then | |
| PR_NUM=$(echo '${{ toJson(github.event.workflow_run.pull_requests) }}' | jq -r '.[0].number') | |
| ARTIFACT_NAME="pr-image-${PR_NUM}" | |
| else | |
| ARTIFACT_NAME="push-image" | |
| fi | |
| echo "Downloading artifact: $ARTIFACT_NAME" | |
| gh run download ${{ github.event.workflow_run.id }} \ | |
| --name "$ARTIFACT_NAME" \ | |
| --dir /tmp/docker-image || { | |
| echo "❌ ERROR: Artifact download failed!" | |
| echo "Available artifacts:" | |
| gh run view ${{ github.event.workflow_run.id }} --json artifacts --jq '.artifacts[].name' | |
| exit 1 | |
| } | |
| docker load < /tmp/docker-image/charon-image.tar | |
| docker tag $(docker images --format "{{.Repository}}:{{.Tag}}" | head -1) charon:local | |
| echo "✅ Successfully loaded from artifact" | |
| # Validate image freshness by checking SHA label | |
| - name: Validate image SHA | |
| env: | |
| SHA: ${{ steps.determine-tag.outputs.sha }} | |
| run: | | |
| LABEL_SHA=$(docker inspect charon:local --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' | cut -c1-7) | |
| echo "Expected SHA: $SHA" | |
| echo "Image SHA: $LABEL_SHA" | |
| if [[ "$LABEL_SHA" != "$SHA" ]]; then | |
| echo "⚠️ WARNING: Image SHA mismatch!" | |
| echo "Image may be stale. Proceeding with caution..." | |
| else | |
| echo "✅ Image SHA matches expected commit" | |
| fi | |
| - name: Run Cerberus integration tests | |
| id: cerberus-test | |
| run: | | |
| chmod +x scripts/cerberus_integration.sh | |
| scripts/cerberus_integration.sh 2>&1 | tee cerberus-test-output.txt | |
| exit ${PIPESTATUS[0]} | |
| - name: Dump Debug Info on Failure | |
| if: failure() | |
| run: | | |
| echo "## 🔍 Debug Information" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Container Status" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| docker ps -a --filter "name=charon" --filter "name=cerberus" --filter "name=backend" >> $GITHUB_STEP_SUMMARY 2>&1 || true | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Security Status API" >> $GITHUB_STEP_SUMMARY | |
| echo '```json' >> $GITHUB_STEP_SUMMARY | |
| curl -s http://localhost:8480/api/v1/security/status 2>/dev/null | head -100 >> $GITHUB_STEP_SUMMARY || echo "Could not retrieve security status" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Caddy Admin Config" >> $GITHUB_STEP_SUMMARY | |
| echo '```json' >> $GITHUB_STEP_SUMMARY | |
| curl -s http://localhost:2319/config 2>/dev/null | head -200 >> $GITHUB_STEP_SUMMARY || echo "Could not retrieve Caddy config" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Charon Container Logs (last 100 lines)" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| docker logs charon-cerberus-test 2>&1 | tail -100 >> $GITHUB_STEP_SUMMARY || echo "No container logs available" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| - name: Cerberus Integration Summary | |
| if: always() | |
| run: | | |
| echo "## 🔱 Cerberus Integration Test Results" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ steps.cerberus-test.outcome }}" == "success" ]; then | |
| echo "✅ **All Cerberus tests passed**" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Test Results:" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| grep -E "✓|PASS|TC-[0-9]|=== ALL" cerberus-test-output.txt || echo "See logs for details" | |
| grep -E "✓|PASS|TC-[0-9]|=== ALL" cerberus-test-output.txt >> $GITHUB_STEP_SUMMARY || echo "See logs for details" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Features Tested:" >> $GITHUB_STEP_SUMMARY | |
| echo "- WAF (Coraza) payload inspection" >> $GITHUB_STEP_SUMMARY | |
| echo "- Rate limiting enforcement" >> $GITHUB_STEP_SUMMARY | |
| echo "- Security handler ordering" >> $GITHUB_STEP_SUMMARY | |
| echo "- Legitimate traffic flow" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ **Cerberus tests failed**" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Failure Details:" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| grep -E "✗|FAIL|Error|failed" cerberus-test-output.txt | head -30 >> $GITHUB_STEP_SUMMARY || echo "See logs for details" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| fi | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| docker rm -f charon-cerberus-test || true | |
| docker rm -f cerberus-backend || true | |
| docker volume rm charon_cerberus_test_data caddy_cerberus_test_data caddy_cerberus_test_config 2>/dev/null || true | |
| docker network rm containers_default || true |