Container CI #35
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
| # ============================================================================= | |
| # Container CI - World-class container testing after CI passes | |
| # ============================================================================= | |
| name: Container CI | |
| on: | |
| # Triggered after CI workflow completes successfully | |
| workflow_run: | |
| workflows: ["CI"] | |
| types: [completed] | |
| branches: [main, develop] | |
| # Manual trigger for testing specific containers | |
| workflow_dispatch: | |
| inputs: | |
| containers: | |
| description: 'Containers to test (comma-separated, or "all")' | |
| required: false | |
| default: "all" | |
| concurrency: | |
| group: container-ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_PREFIX: ${{ github.repository_owner }}/chronos | |
| jobs: | |
| # =========================================================================== | |
| # Check CI status and get change detection from CI workflow | |
| # =========================================================================== | |
| check-ci: | |
| name: Check CI Status | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'workflow_run' | |
| outputs: | |
| should_run: ${{ steps.check.outputs.should_run }} | |
| steps: | |
| - name: Check CI workflow conclusion | |
| id: check | |
| run: | | |
| if [ "${{ github.event.workflow_run.conclusion }}" != "success" ]; then | |
| echo "CI workflow failed with: ${{ github.event.workflow_run.conclusion }}" | |
| echo "should_run=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "should_run=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| # =========================================================================== | |
| # Determine which containers to build based on CI change detection | |
| # =========================================================================== | |
| changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| needs: check-ci | |
| if: | | |
| always() && | |
| (github.event_name == 'workflow_dispatch' || needs.check-ci.outputs.should_run == 'true') | |
| outputs: | |
| core: ${{ steps.set-matrix.outputs.core }} | |
| query-service: ${{ steps.set-matrix.outputs.query-service }} | |
| control-plane: ${{ steps.set-matrix.outputs.control-plane }} | |
| matrix: ${{ steps.set-matrix.outputs.matrix }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set Matrix | |
| id: set-matrix | |
| run: | | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then | |
| if [[ "${{ github.event.inputs.containers }}" == "all" ]]; then | |
| echo 'matrix=["core","query-service","control-plane"]' >> "$GITHUB_OUTPUT" | |
| echo "core=true" >> "$GITHUB_OUTPUT" | |
| echo "query-service=true" >> "$GITHUB_OUTPUT" | |
| echo "control-plane=true" >> "$GITHUB_OUTPUT" | |
| else | |
| # Convert comma-separated to JSON array | |
| containers=$(echo '${{ github.event.inputs.containers }}' | tr ',' '\n' | jq -R . | jq -sc .) | |
| echo "matrix=$containers" >> "$GITHUB_OUTPUT" | |
| # Set individual outputs | |
| echo '${{ github.event.inputs.containers }}' | tr ',' '\n' | while read -r c; do | |
| echo "$c=true" >> "$GITHUB_OUTPUT" | |
| done | |
| fi | |
| else | |
| # Use paths-filter for workflow_run trigger to detect changes | |
| # This is more efficient than duplicating CI's detection | |
| matrix='[]' | |
| # Check for changes using git diff against base | |
| git fetch origin "${{ github.event.workflow_run.head_branch || 'main' }}" --depth=2 | |
| # Detect changes per service | |
| CORE_CHANGED="false" | |
| QS_CHANGED="false" | |
| CP_CHANGED="false" | |
| if git diff --name-only HEAD~1 | grep -q "apps/core/"; then | |
| CORE_CHANGED="true" | |
| matrix=$(echo "$matrix" | jq -c '. + ["core"]') | |
| fi | |
| if git diff --name-only HEAD~1 | grep -q "apps/query-service/"; then | |
| QS_CHANGED="true" | |
| matrix=$(echo "$matrix" | jq -c '. + ["query-service"]') | |
| fi | |
| if git diff --name-only HEAD~1 | grep -q "apps/control-plane/"; then | |
| CP_CHANGED="true" | |
| matrix=$(echo "$matrix" | jq -c '. + ["control-plane"]') | |
| fi | |
| # If no changes detected, skip container builds | |
| if [[ "$matrix" == "[]" ]]; then | |
| echo "No container changes detected" | |
| fi | |
| echo "matrix=$matrix" >> "$GITHUB_OUTPUT" | |
| echo "core=$CORE_CHANGED" >> "$GITHUB_OUTPUT" | |
| echo "query-service=$QS_CHANGED" >> "$GITHUB_OUTPUT" | |
| echo "control-plane=$CP_CHANGED" >> "$GITHUB_OUTPUT" | |
| fi | |
| # =========================================================================== | |
| # Stage 1: Build containers (no push) - validates Dockerfiles compile | |
| # =========================================================================== | |
| build: | |
| name: Build ${{ matrix.container }} | |
| needs: changes | |
| runs-on: ubuntu-latest | |
| if: needs.changes.outputs.matrix != '[]' && needs.changes.outputs.matrix != '' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| container: ${{ fromJson(needs.changes.outputs.matrix) }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Set build context and dockerfile | |
| id: config | |
| run: | | |
| case "${{ matrix.container }}" in | |
| core) | |
| echo "context=apps/core" >> "$GITHUB_OUTPUT" | |
| echo "dockerfile=apps/core/Dockerfile" >> "$GITHUB_OUTPUT" | |
| echo "image=chronos-core" >> "$GITHUB_OUTPUT" | |
| echo "port=3900" >> "$GITHUB_OUTPUT" | |
| ;; | |
| query-service) | |
| echo "context=apps/query-service" >> "$GITHUB_OUTPUT" | |
| echo "dockerfile=apps/query-service/Dockerfile" >> "$GITHUB_OUTPUT" | |
| echo "image=chronos-query-service" >> "$GITHUB_OUTPUT" | |
| echo "port=4000" >> "$GITHUB_OUTPUT" | |
| ;; | |
| control-plane) | |
| echo "context=apps/control-plane" >> "$GITHUB_OUTPUT" | |
| echo "dockerfile=apps/control-plane/Dockerfile" >> "$GITHUB_OUTPUT" | |
| echo "image=chronos-control-plane" >> "$GITHUB_OUTPUT" | |
| echo "port=8080" >> "$GITHUB_OUTPUT" | |
| ;; | |
| esac | |
| - name: Build image (validation only) | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: ${{ steps.config.outputs.context }} | |
| file: ${{ steps.config.outputs.dockerfile }} | |
| push: false | |
| load: true | |
| tags: ${{ steps.config.outputs.image }}:test | |
| cache-from: type=gha,scope=${{ matrix.container }} | |
| cache-to: type=gha,mode=max,scope=${{ matrix.container }} | |
| build-args: | | |
| VERSION=${{ github.sha }} | |
| REVISION=${{ github.sha }} | |
| BUILDTIME=${{ github.event.workflow_run.head_commit.timestamp || github.event.head_commit.timestamp }} | |
| - name: Check image size | |
| run: | | |
| SIZE=$(docker images --format "{{.Size}}" ${{ steps.config.outputs.image }}:test) | |
| echo "### 📦 Image Size: $SIZE" >> "$GITHUB_STEP_SUMMARY" | |
| # Convert to MB for comparison | |
| SIZE_MB=$(docker images --format "{{.Size}}" ${{ steps.config.outputs.image }}:test | \ | |
| awk '{ | |
| if (index($0, "GB") > 0) { gsub("GB", "", $0); print $0 * 1024 } | |
| else if (index($0, "MB") > 0) { gsub("MB", "", $0); print $0 } | |
| else if (index($0, "KB") > 0) { gsub("KB", "", $0); print $0 / 1024 } | |
| else { print 0 } | |
| }') | |
| if (( $(echo "$SIZE_MB > 500" | bc -l) )); then | |
| echo "⚠️ Warning: Image size ($SIZE) exceeds 500MB threshold" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Test container startup | |
| run: | | |
| # Set minimal env vars for services that need them | |
| ENV_ARGS="" | |
| if [ "${{ matrix.container }}" == "query-service" ]; then | |
| # Query service needs these to start (but can run without database) | |
| ENV_ARGS="-e SECRET_KEY_BASE=$(openssl rand -hex 64)" | |
| fi | |
| # Start container | |
| CONTAINER_ID=$(docker run -d $ENV_ARGS -p ${{ steps.config.outputs.port }}:${{ steps.config.outputs.port }} \ | |
| ${{ steps.config.outputs.image }}:test) | |
| # Wait for container to be ready | |
| MAX_WAIT=60 | |
| WAITED=0 | |
| while [ $WAITED -lt $MAX_WAIT ]; do | |
| STATUS=$(docker inspect --format='{{.State.Status}}' $CONTAINER_ID) | |
| if [ "$STATUS" = "exited" ]; then | |
| EXIT_CODE=$(docker inspect --format='{{.State.ExitCode}}' $CONTAINER_ID) | |
| echo "❌ Container exited with code $EXIT_CODE" >> "$GITHUB_STEP_SUMMARY" | |
| docker logs $CONTAINER_ID | |
| docker rm -f $CONTAINER_ID | |
| exit 1 | |
| fi | |
| # Check if container has healthcheck | |
| HEALTH=$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' $CONTAINER_ID) | |
| if [ "$HEALTH" = "healthy" ] || [ "$HEALTH" = "no-healthcheck" ]; then | |
| if [ "$STATUS" = "running" ]; then | |
| echo "✅ Container started successfully in ${WAITED}s" >> "$GITHUB_STEP_SUMMARY" | |
| docker rm -f $CONTAINER_ID | |
| exit 0 | |
| fi | |
| fi | |
| sleep 1 | |
| WAITED=$((WAITED + 1)) | |
| done | |
| echo "❌ Container failed to start within ${MAX_WAIT}s" >> "$GITHUB_STEP_SUMMARY" | |
| docker logs $CONTAINER_ID | |
| docker rm -f $CONTAINER_ID | |
| exit 1 | |
| # =========================================================================== | |
| # Docker Compose Integration Test | |
| # =========================================================================== | |
| integration: | |
| name: Integration Test | |
| needs: [changes, build] | |
| runs-on: ubuntu-latest | |
| if: | | |
| always() && | |
| needs.build.result == 'success' && | |
| needs.changes.outputs.matrix != '[]' && | |
| needs.changes.outputs.matrix != '' && | |
| (contains(fromJson(needs.changes.outputs.matrix), 'core') || | |
| contains(fromJson(needs.changes.outputs.matrix), 'query-service')) | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Build all services | |
| run: | | |
| docker compose build core query-service | |
| - name: Start services | |
| run: | | |
| docker compose up -d core | |
| # Wait for core to be healthy | |
| timeout 60 bash -c 'until docker compose exec -T core wget --spider -q http://localhost:3900/health; do sleep 2; done' | |
| echo "✅ Core service is healthy" >> "$GITHUB_STEP_SUMMARY" | |
| - name: Run integration tests | |
| run: | | |
| # Basic health check | |
| curl -sf http://localhost:3900/health || exit 1 | |
| # Test event creation | |
| RESPONSE=$(curl -sf -X POST http://localhost:3900/api/v1/events \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"stream_id":"test-stream","event_type":"TestEvent","data":{"message":"hello"}}') | |
| if echo "$RESPONSE" | jq -e '.event_id' > /dev/null; then | |
| echo "✅ Event creation test passed" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "❌ Event creation test failed" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| - name: Cleanup | |
| if: always() | |
| run: docker compose down -v | |
| # =========================================================================== | |
| # Summary Report | |
| # =========================================================================== | |
| summary: | |
| name: Test Summary | |
| needs: [changes, build, integration] | |
| runs-on: ubuntu-latest | |
| if: always() | |
| steps: | |
| - name: Generate Summary | |
| run: | | |
| echo "# 🐳 Container CI Summary" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| # Check if there were any containers to build | |
| if [ "${{ needs.changes.outputs.matrix }}" == "[]" ] || [ -z "${{ needs.changes.outputs.matrix }}" ]; then | |
| echo "**No container changes detected - skipped container builds**" >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| fi | |
| echo "| Container | Build | Startup |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|-----------|-------|---------|" >> "$GITHUB_STEP_SUMMARY" | |
| # This is a simplified summary - actual results come from build job | |
| if [ "${{ needs.build.result }}" == "success" ]; then | |
| echo "| All | ✅ | ✅ |" >> "$GITHUB_STEP_SUMMARY" | |
| elif [ "${{ needs.build.result }}" == "skipped" ]; then | |
| echo "| All | ⏭️ | ⏭️ |" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "| All | ❌ | - |" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| if [ "${{ needs.integration.result }}" == "success" ]; then | |
| echo "**Integration Tests:** ✅ Passed" >> "$GITHUB_STEP_SUMMARY" | |
| elif [ "${{ needs.integration.result }}" == "skipped" ]; then | |
| echo "**Integration Tests:** ⏭️ Skipped" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "**Integration Tests:** ❌ Failed" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Check overall status | |
| if: needs.build.result == 'failure' | |
| run: exit 1 |