End-to-End (E2E) pipeline testing verifies the complete journey from source commit through CI/CD pipeline, build, signing, registry push, and final deployment. This ensures no breakage at integration points and validates the entire supply chain.
Pipeline Flow Tested
The end-to-end pipeline testing workflow follows a structured sequence of stages, each with specific verification steps.
Stage 1: Source Code Commit — Developers commit code to the repository, which triggers a webhook to initiate the pipeline.
Stage 2: CI/CD Pipeline Start — The pipeline executes multiple checks in parallel: static application security testing (SAST) and dependency analysis, image build using hermetic build techniques, container tests, attestation generation (SBOM and SLSA provenance), image signing with Cosign or KMS, and vulnerability scanning.
Stage 3: Push to Registry — Once the build succeeds, the image is pushed to the registry with attestations attached, and registry-side scanning is automatically triggered.
Stage 4: Deploy to Staging — The image is pulled from the registry, signature verification occurs, Kubernetes pods are created, and health checks are run.
Stage 5: Integration Tests in Staging — Automated tests verify API endpoints, database operations, cache functionality, and messaging/queue semantics.
Stage 6: Approval Gate — Security team reviews vulnerability scan results and performance metrics before approving production deployment.
Stage 7: Deploy to Production — The signed image is pulled and deployed using progressive rollout (canary deployment) with continuous metrics monitoring and automatic rollback capability.
Stage 8: Post-Deployment Verification — Production smoke tests are run, metrics baselines are established, and logging and tracing are verified as operational.
Setting Up E2E Tests
Prerequisites
# Install e2e test frameworkcurl -sSL https://releases.cleanstart.dev/e2e-tests-latest.tar.gz | tar xzchmod +x ./e2e-tests # Verify./e2e-tests --version # Install dependenciespip install pytest pytest-asyncio pyyaml docker # Set environment variablesexport REGISTRY_URL="gcr.io/my-project"export STAGING_CLUSTER="gke-staging"export PROD_CLUSTER="gke-prod-us-central1"export DOCKER_CONFIG="$HOME/.docker"Create E2E Test Configuration
Create e2e-config.yaml:
version: "1.0"project: my-appimage_name: appenvironment: staging: cluster: gke-staging namespace: staging registry: gcr.io/my-project replicas: 2 production: cluster: gke-prod-us-central1 namespace: production registry: gcr.io/my-project replicas: 5 canary_percentage: 20 # Canary: 20% of traffic initially tests: baseline: enabled: true timeout: 300 # 5 minutes api: enabled: true timeout: 600 endpoints: - path: /health method: GET expected_status: 200 - path: /api/v1/items method: GET expected_status: 200 - path: /api/v1/items method: POST body: '{"name": "test"}' expected_status: 201 database: enabled: true timeout: 300 connection_string: postgresql://user:pass@postgres:5432/mydb performance: enabled: true timeout: 900 thresholds: p95_response_time_ms: 200 throughput_rps: 1000 error_rate_percent: 0.1Test Types and Examples
1. Pipeline Trigger Tests
# test_pipeline_trigger.pyimport pytestimport subprocessimport timefrom datetime import datetime def test_github_webhook_triggers_build(): """Verify GitHub webhook triggers CI/CD pipeline""" # Create test commit in feature branch subprocess.run(["git", "checkout", "-b", "e2e-test-trigger"], check=True) with open("test-file.txt", "w") as f: f.write(f"E2E test {datetime.now()}") subprocess.run(["git", "add", "test-file.txt"], check=True) subprocess.run(["git", "commit", "-m", "E2E: Trigger pipeline test"], check=True) subprocess.run(["git", "push", "origin", "e2e-test-trigger"], check=True) # Poll for build start (GitHub Actions API) max_retries = 60 # 5 minutes build_found = False for _ in range(max_retries): result = subprocess.run( ["gh", "run", "list", "--branch", "e2e-test-trigger", "--limit", "1", "--json", "status"], capture_output=True, text=True ) if "in_progress" in result.stdout or "completed" in result.stdout: build_found = True break time.sleep(5) assert build_found, "Build was not triggered by webhook" # Cleanup subprocess.run(["git", "checkout", "main"], check=True) subprocess.run(["git", "branch", "-D", "e2e-test-trigger"], check=True)2. Image Build and Push Tests
# test_image_build.pyimport subprocessimport dockerimport json def test_image_builds_successfully(): """Verify image builds without errors""" result = subprocess.run( ["docker", "build", "-t", "test-app:e2e", "."], capture_output=True, text=True ) assert result.returncode == 0, f"Build failed: {result.stderr}" client = docker.from_env() image = client.images.get("test-app:e2e") assert image is not None def test_image_signed_correctly(): """Verify image is signed with correct key""" image_uri = f"{REGISTRY_URL}/app:latest" result = subprocess.run( ["cosign", "verify", "--key", "cosign.pub", image_uri], capture_output=True, text=True ) assert result.returncode == 0, "Image signature verification failed" assert "Signature verified" in result.stdout def test_sbom_attached_to_image(): """Verify SBOM attestation is attached""" image_uri = f"{REGISTRY_URL}/app:latest" result = subprocess.run( ["cosign", "verify-attestation", "--type", "sbom", image_uri], capture_output=True, text=True ) assert result.returncode == 0, "SBOM attestation not found" # Parse SBOM attestation_json = json.loads(result.stdout) sbom_data = json.loads(attestation_json['payload']) assert "packages" in sbom_data, "SBOM missing packages" assert len(sbom_data["packages"]) > 0, "SBOM has no packages" def test_image_pushed_to_registry(): """Verify image is in registry and accessible""" image_uri = f"{REGISTRY_URL}/app:latest" result = subprocess.run( ["gcloud", "container", "images", "describe", image_uri], capture_output=True, text=True ) assert result.returncode == 0, f"Image not found in registry: {image_uri}"3. Staging Deployment Tests
# test_staging_deployment.pyimport subprocessimport timeimport requestsfrom kubernetes import client, config def test_staging_image_pull(): """Verify image can be pulled in staging cluster""" config.load_kube_config(context="gke-staging") v1 = client.CoreV1Api() # Get imagePullSecrets from namespace ns = v1.read_namespace("staging") assert ns is not None def test_staging_pod_deployment(): """Verify pods deploy successfully in staging""" # Trigger deployment result = subprocess.run( ["kubectl", "set", "image", "deployment/app", f"app={REGISTRY_URL}/app:latest", "-n", "staging"], capture_output=True, text=True ) assert result.returncode == 0 # Wait for rollout result = subprocess.run( ["kubectl", "rollout", "status", "deployment/app", "-n", "staging", "--timeout=5m"], capture_output=True, text=True ) assert result.returncode == 0, "Deployment rollout failed" def test_staging_health_check(): """Verify health endpoints in staging""" # Port forward to staging service pf_process = subprocess.Popen( ["kubectl", "port-forward", "svc/app", "8080:80", "-n", "staging"], stdout=subprocess.PIPE ) time.sleep(3) # Wait for port forward to establish try: # Test health endpoint response = requests.get("http://localhost:8080/health", timeout=10) assert response.status_code == 200 finally: pf_process.terminate() def test_staging_readiness_probe(): """Verify readiness probes pass in staging""" config.load_kube_config(context="gke-staging") v1 = client.CoreV1Api() pods = v1.list_namespaced_pod("staging", label_selector="app=app") for pod in pods.items: # Check all containers are ready for container_status in pod.status.container_statuses: assert container_status.ready, f"Container {container_status.name} not ready"4. API Integration Tests
# test_api_integration.pyimport requestsimport pytestimport json BASE_URL = "http://localhost:8000" # After port-forward def test_api_health_endpoint(): """Test health endpoint""" response = requests.get(f"{BASE_URL}/health") assert response.status_code == 200 assert response.json()["status"] == "ok" def test_api_list_items(): """Test GET /api/v1/items""" response = requests.get(f"{BASE_URL}/api/v1/items") assert response.status_code == 200 assert isinstance(response.json(), list) def test_api_create_item(): """Test POST /api/v1/items""" payload = {"name": "Test Item", "description": "E2E test"} response = requests.post(f"{BASE_URL}/api/v1/items", json=payload) assert response.status_code == 201 data = response.json() assert "id" in data assert data["name"] == "Test Item" def test_api_get_item(): """Test GET /api/v1/items/{id}""" # Create item first create_response = requests.post( f"{BASE_URL}/api/v1/items", json={"name": "Test"} ) item_id = create_response.json()["id"] # Get item response = requests.get(f"{BASE_URL}/api/v1/items/{item_id}") assert response.status_code == 200 assert response.json()["id"] == item_id def test_api_update_item(): """Test PATCH /api/v1/items/{id}""" # Create item create_response = requests.post( f"{BASE_URL}/api/v1/items", json={"name": "Original"} ) item_id = create_response.json()["id"] # Update item response = requests.patch( f"{BASE_URL}/api/v1/items/{item_id}", json={"name": "Updated"} ) assert response.status_code == 200 assert response.json()["name"] == "Updated" def test_api_delete_item(): """Test DELETE /api/v1/items/{id}""" # Create item create_response = requests.post( f"{BASE_URL}/api/v1/items", json={"name": "To Delete"} ) item_id = create_response.json()["id"] # Delete item response = requests.delete(f"{BASE_URL}/api/v1/items/{item_id}") assert response.status_code == 204 # Verify deleted response = requests.get(f"{BASE_URL}/api/v1/items/{item_id}") assert response.status_code == 4045. Performance E2E Tests
# test_e2e_performance.pyimport requestsimport timeimport statisticsfrom concurrent.futures import ThreadPoolExecutor BASE_URL = "http://localhost:8000" def test_sustained_throughput(): """Test sustained throughput of 1000 RPS""" duration = 60 # Test for 60 seconds response_times = [] errors = 0 def make_request(): try: start = time.time() response = requests.get(f"{BASE_URL}/health", timeout=5) elapsed = (time.time() - start) * 1000 # ms if response.status_code == 200: response_times.append(elapsed) return True else: return False except: return False start_time = time.time() requests_count = 0 with ThreadPoolExecutor(max_workers=100) as executor: while time.time() - start_time < duration: futures = [executor.submit(make_request) for _ in range(100)] for future in futures: if not future.result(): errors += 1 requests_count += 1 # Calculate metrics throughput = requests_count / duration p95_latency = statistics.quantiles(response_times, n=20)[18] if response_times else 0 p99_latency = statistics.quantiles(response_times, n=100)[98] if response_times else 0 error_rate = (errors / requests_count) * 100 if requests_count > 0 else 0 assert throughput >= 1000, f"Throughput {throughput:.0f} RPS < 1000 RPS target" assert p95_latency <= 200, f"P95 latency {p95_latency:.0f}ms > 200ms target" assert error_rate < 0.1, f"Error rate {error_rate:.2f}% > 0.1% target" def test_response_time_slo(): """Verify p95 and p99 response times meet SLO""" response_times = [] for _ in range(1000): start = time.time() response = requests.get(f"{BASE_URL}/api/v1/items") elapsed = (time.time() - start) * 1000 assert response.status_code == 200 response_times.append(elapsed) p95 = statistics.quantiles(response_times, n=20)[18] p99 = statistics.quantiles(response_times, n=100)[98] assert p95 <= 200, f"P95 {p95:.0f}ms > 200ms SLO" assert p99 <= 500, f"P99 {p99:.0f}ms > 500ms SLO"Running E2E Tests
Execute Complete E2E Test Suite
# Run all E2E tests./e2e-tests run \ --config e2e-config.yaml \ --image gcr.io/my-project/app:v1.2.3 \ --staging-cluster gke-staging \ --prod-cluster gke-prod-us-central1 \ --report e2e-results.html # Run specific test phase./e2e-tests run \ --config e2e-config.yaml \ --phase staging-deployment # Just staging tests # Run with detailed logging./e2e-tests run \ --config e2e-config.yaml \ --verbose \ --log-file e2e.logCI/CD Integration
# .github/workflows/e2e.ymlname: End-to-End Pipeline Tests on: push: branches: [main] jobs: e2e: runs-on: ubuntu-latest permissions: id-token: write contents: read steps: - uses: actions/checkout@v4 - name: Authenticate to GCP uses: google-github-actions/auth@v1 with: workload_identity_provider: ${{ secrets.WIF_PROVIDER }} service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }} - name: Set up GKE kubeconfig run: | gcloud container clusters get-credentials gke-staging --region us-central1 gcloud container clusters get-credentials gke-prod-us-central1 --region us-central1 - name: Install E2E test framework run: | curl -sSL https://releases.cleanstart.dev/e2e-tests-latest.tar.gz | tar xz chmod +x ./e2e-tests - name: Run E2E Pipeline Tests run: | ./e2e-tests run \ --config e2e-config.yaml \ --image gcr.io/${{ env.GCP_PROJECT }}/app:${{ github.sha }} \ --report e2e-results.html env: GCP_PROJECT: ${{ secrets.GCP_PROJECT_ID }} - name: Upload E2E Results uses: actions/upload-artifact@v3 with: name: e2e-results path: e2e-results.html - name: Comment on PR with Results if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const results = fs.readFileSync('e2e-results.html', 'utf8'); github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: `## E2E Pipeline Tests\n✅ All tests passed\n[View full report](${process.env.ARTIFACT_URL})` });Monitoring E2E Results
# View E2E test dashboard./e2e-tests dashboard \ --config e2e-config.yaml \ --days 30 # Analyze failure trends./e2e-tests analyze-failures \ --config e2e-config.yaml \ --days 7 # Compare E2E performance baselines./e2e-tests compare-baselines \ --baseline ./baseline-v1.0.0.json \ --current ./current-results.jsonTroubleshooting E2E Failures
Failure | Root Cause | Solution |
|---|---|---|
Image signature fails | Not signed | Run |
Pod deployment fails | Image not found | Check registry access, image exists |
API test fails | Service not ready | Increase deployment timeout |
Performance test fails | Resource exhausted | Increase cluster resources, reduce load |
Database test fails | Schema not migrated | Ensure initContainer runs first |
E2E Result Report Example
E2E PIPELINE TEST REPORT
Image: gcr.io/my-project/app:v1.2.3 Build: #1234 (GitHub Actions) Duration: 18 minutes Status: ALL TESTS PASSED
PHASE 1: PIPELINE TRIGGER GitHub webhook triggers build - PASS CI/CD pipeline starts successfully - PASS
PHASE 2: IMAGE BUILD & PUSH Image builds without errors - PASS Image signed with correct key - PASS SBOM attached to image - PASS Image pushed to registry - PASS Vulnerability scan completed - PASS
PHASE 3: STAGING DEPLOYMENT Image pulled in staging cluster - PASS Pods deployed successfully - PASS Health checks pass (2/2 pods healthy) - PASS Readiness probes pass - PASS
PHASE 4: API INTEGRATION TESTS Health endpoint: 200 OK - PASS List items: 200 OK - PASS Create item: 201 Created - PASS Get item: 200 OK - PASS Update item: 200 OK - PASS Delete item: 204 No Content - PASS
PHASE 5: PERFORMANCE TESTS Sustained throughput: 1240 RPS (target: 1000) - PASS P95 latency: 145ms (target: 200ms) - PASS P99 latency: 320ms (target: 500ms) - PASS Error rate: 0.05% (target: <0.1%) - PASS
PHASE 6: PRODUCTION DEPLOYMENT APPROVAL All security checks passed - PASS Performance baseline met - PASS Ready for production deployment - PASS
═══════════════════════════════════════════════════════════════ Result: IMAGE APPROVED FOR PRODUCTION ═══════════════════════════════════════════════════════════════
