Skip to content

Container CI

Container CI #35

Workflow file for this run

# =============================================================================
# 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