Sentry: bump getsentry/taskbroker from 26.5.0 to 26.5.1 in /services/sentry #119
Workflow file for this run
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
| # BeeCompose CI/CD Pipeline | |
| # Docker Compose Testing with Parallel CVE Scanning and Service Testing | |
| # | |
| # This workflow validates all Docker Compose configurations and scans images for vulnerabilities. | |
| # Both CVE scanning and service testing run in parallel (matrix strategy) for faster pipeline execution. | |
| # | |
| # Pipeline stages: | |
| # 1. Lint - Docker Compose linting with DCLint (syntax + best practices) | |
| # 2. Validate OCI - Check OCI compatibility for artifact publishing | |
| # 3. Discover - Find all services and validate basic syntax | |
| # 4. Extract Images - Collect Docker images for CVE scanning | |
| # 5. CVE Scan - Security vulnerability scanning with Trivy (parallel per image) | |
| # 6. CVE Summary - Aggregate scan results | |
| # 7. Test Services - Parallel Docker Compose testing (max 8 concurrent) | |
| # 8. Test Summary - Aggregate test results | |
| # 9. Summary - Final pipeline results | |
| name: CI/CD Pipeline | |
| on: | |
| push: | |
| branches: [main, master, develop] | |
| paths: | |
| - 'services/**' | |
| - 'scripts/**' | |
| - '.github/workflows/**' | |
| - '.github/scripts/**' | |
| - '.dclintrc.yaml' | |
| pull_request: | |
| branches: [main, master] | |
| paths: | |
| - 'services/**' | |
| - 'scripts/**' | |
| - '.github/workflows/**' | |
| - '.github/scripts/**' | |
| - '.dclintrc.yaml' | |
| workflow_dispatch: | |
| inputs: | |
| skip_cve_scan: | |
| description: 'Skip CVE vulnerability scanning' | |
| required: false | |
| default: 'false' | |
| type: boolean | |
| specific_service: | |
| description: 'Test specific service only (leave empty for all)' | |
| required: false | |
| default: '' | |
| type: string | |
| fail_on_high: | |
| description: 'Fail on HIGH severity CVEs (not just CRITICAL)' | |
| required: false | |
| default: 'false' | |
| type: boolean | |
| env: | |
| # Test configuration | |
| TEST_TIMEOUT_MINUTES: 5 | |
| HEALTH_CHECK_RETRIES: 30 | |
| HEALTH_CHECK_INTERVAL: 10 | |
| # CVE scanning configuration | |
| # Note: CVE scan generates reports but does not fail the pipeline | |
| # This allows tracking vulnerabilities while not blocking deployments | |
| CVE_FAIL_ON_CRITICAL: false | |
| CVE_FAIL_ON_HIGH: false | |
| # Prevent concurrent CI runs on the same branch | |
| concurrency: | |
| group: ci-cd-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Default permissions (restrictive) - minimal access by default | |
| permissions: | |
| contents: read | |
| jobs: | |
| # ============================================================================ | |
| # Job 1: Lint Docker Compose Files | |
| # ============================================================================ | |
| lint: | |
| name: Lint Docker Compose | |
| runs-on: ubuntu-latest | |
| outputs: | |
| lint_passed: ${{ steps.lint.outputs.passed }} | |
| error_count: ${{ steps.lint.outputs.error_count }} | |
| warning_count: ${{ steps.lint.outputs.warning_count }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Discover compose files to lint | |
| id: find-files | |
| run: | | |
| set -euo pipefail | |
| echo "=== Finding Docker Compose files ===" | |
| # Find all docker-compose.yml files, excluding nested services | |
| FILES=() | |
| while IFS= read -r file; do | |
| # Get the directory depth (services/name/docker-compose.yml = depth 3) | |
| DEPTH=$(echo "$file" | tr '/' '\n' | wc -l | tr -d ' ') | |
| if [[ "$DEPTH" -eq 3 ]]; then | |
| FILES+=("$file") | |
| fi | |
| done < <(find services -name "docker-compose.yml" -type f | sort) | |
| FILE_COUNT=${#FILES[@]} | |
| echo "Found $FILE_COUNT Docker Compose files" | |
| # Save file list for linting | |
| printf '%s\n' "${FILES[@]}" > /tmp/compose-files.txt | |
| echo "file_count=$FILE_COUNT" >> $GITHUB_OUTPUT | |
| - name: Run DCLint | |
| id: lint | |
| run: | | |
| set -uo pipefail | |
| echo "=== Running Docker Compose Linter (DCLint) ===" | |
| # Create results directory | |
| mkdir -p lint-results | |
| # Run DCLint using Docker image | |
| # Mount workspace and run linter recursively on services directory | |
| # Note: dclint takes paths directly, not a 'lint' subcommand | |
| docker run --rm \ | |
| -v "${{ github.workspace }}:/app" \ | |
| zavoloklom/dclint:latest \ | |
| /app/services \ | |
| -r \ | |
| -c /app/.dclintrc.yaml \ | |
| -f stylish \ | |
| -o /app/lint-results/dclint-report.txt \ | |
| 2>&1 | tee lint-results/dclint-output.txt || LINT_EXIT_CODE=$? | |
| LINT_EXIT_CODE=${LINT_EXIT_CODE:-0} | |
| # Parse results for summary | |
| ERROR_COUNT=0 | |
| WARNING_COUNT=0 | |
| if [[ -f lint-results/dclint-report.txt ]] && [[ -s lint-results/dclint-report.txt ]]; then | |
| # Count errors and warnings from output (match severity indicators) | |
| ERROR_COUNT=$(grep -cE '\berror\b' lint-results/dclint-report.txt 2>/dev/null || true) | |
| WARNING_COUNT=$(grep -cE '\bwarning\b' lint-results/dclint-report.txt 2>/dev/null || true) | |
| # Ensure we have valid integers | |
| ERROR_COUNT=${ERROR_COUNT:-0} | |
| WARNING_COUNT=${WARNING_COUNT:-0} | |
| # Strip any whitespace | |
| ERROR_COUNT=$(echo "$ERROR_COUNT" | tr -d '[:space:]') | |
| WARNING_COUNT=$(echo "$WARNING_COUNT" | tr -d '[:space:]') | |
| fi | |
| # Validate counts are integers, default to 0 if not | |
| [[ "$ERROR_COUNT" =~ ^[0-9]+$ ]] || ERROR_COUNT=0 | |
| [[ "$WARNING_COUNT" =~ ^[0-9]+$ ]] || WARNING_COUNT=0 | |
| echo "error_count=${ERROR_COUNT}" >> $GITHUB_OUTPUT | |
| echo "warning_count=${WARNING_COUNT}" >> $GITHUB_OUTPUT | |
| # Display results | |
| echo "" | |
| echo "=== DCLint Results ===" | |
| if [[ -f lint-results/dclint-report.txt ]]; then | |
| cat lint-results/dclint-report.txt | |
| elif [[ -f lint-results/dclint-output.txt ]]; then | |
| cat lint-results/dclint-output.txt | |
| fi | |
| # Determine pass/fail | |
| if [[ $LINT_EXIT_CODE -eq 0 ]]; then | |
| echo "" | |
| echo "All Docker Compose files passed linting!" | |
| echo "passed=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "" | |
| echo "::error::Docker Compose linting failed with $ERROR_COUNT error(s) and $WARNING_COUNT warning(s)" | |
| echo "passed=false" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| - name: Generate JSON report for CI integration | |
| if: always() | |
| run: | | |
| # Generate JSON format report for potential downstream processing | |
| docker run --rm \ | |
| -v "${{ github.workspace }}:/app" \ | |
| zavoloklom/dclint:latest \ | |
| /app/services \ | |
| -r \ | |
| -c /app/.dclintrc.yaml \ | |
| -f json \ | |
| -o /app/lint-results/dclint-report.json \ | |
| 2>/dev/null || true | |
| - name: Upload lint results | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: dclint-results | |
| path: lint-results/ | |
| retention-days: 30 | |
| - name: Add lint summary to GitHub Actions | |
| if: always() | |
| run: | | |
| echo "## Docker Compose Lint Results" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [[ "${{ steps.lint.outputs.passed }}" == "true" ]]; then | |
| echo "✅ **All files passed linting**" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ **Linting failed**" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- Errors: ${{ steps.lint.outputs.error_count }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- Warnings: ${{ steps.lint.outputs.warning_count }}" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "See the \`dclint-results\` artifact for detailed report." >> $GITHUB_STEP_SUMMARY | |
| # ============================================================================ | |
| # Job 1b: Test bc CLI Helper | |
| # ============================================================================ | |
| test-cli: | |
| name: Test CLI Helper | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Install shellcheck | |
| run: sudo apt-get install -y shellcheck | |
| - name: Run bc CLI tests | |
| run: ./.github/scripts/test-bc-cli.sh | |
| - name: Add CLI test summary | |
| if: always() | |
| run: | | |
| echo "## bc CLI Tests" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "CLI helper script validated successfully." >> $GITHUB_STEP_SUMMARY | |
| # ============================================================================ | |
| # Job 1c: Service Coverage Check | |
| # ============================================================================ | |
| # Ensures all services in services/ are documented in README.md and scripts/bc | |
| service-coverage: | |
| name: Service Coverage | |
| runs-on: ubuntu-latest | |
| outputs: | |
| coverage_passed: ${{ steps.coverage.outputs.passed }} | |
| service_count: ${{ steps.coverage.outputs.service_count }} | |
| readme_count: ${{ steps.coverage.outputs.readme_count }} | |
| bc_count: ${{ steps.coverage.outputs.bc_count }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Check service coverage | |
| id: coverage | |
| run: | | |
| set -euo pipefail | |
| echo "=== Checking Service Coverage ===" | |
| # Count services in each location | |
| SERVICE_COUNT=$(find services -maxdepth 1 -mindepth 1 -type d | wc -l | tr -d ' ') | |
| README_COUNT=$(grep -cE "^\| \[.*\]\(services/.*/README\.md\)" README.md 2>/dev/null || echo 0) | |
| BC_COUNT=$(grep -cE "^\s*echo \"\s+[a-z0-9-]+\s+-" scripts/bc 2>/dev/null || echo 0) | |
| echo "Service directories: $SERVICE_COUNT" | |
| echo "README.md entries: $README_COUNT" | |
| echo "scripts/bc entries: $BC_COUNT" | |
| # Output counts | |
| echo "service_count=$SERVICE_COUNT" >> $GITHUB_OUTPUT | |
| echo "readme_count=$README_COUNT" >> $GITHUB_OUTPUT | |
| echo "bc_count=$BC_COUNT" >> $GITHUB_OUTPUT | |
| # Run the full coverage check script | |
| if ./.github/scripts/check-service-coverage.sh; then | |
| echo "passed=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "passed=false" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| - name: Add coverage summary | |
| if: always() | |
| run: | | |
| echo "## Service Coverage Check" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [[ "${{ steps.coverage.outputs.passed }}" == "true" ]]; then | |
| echo "✅ **All services properly documented**" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ **Service coverage incomplete**" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Location | Count |" >> $GITHUB_STEP_SUMMARY | |
| echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Service Directories | ${{ steps.coverage.outputs.service_count }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| README.md Table | ${{ steps.coverage.outputs.readme_count }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| scripts/bc List | ${{ steps.coverage.outputs.bc_count }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "All services must be documented in README.md and scripts/bc." >> $GITHUB_STEP_SUMMARY | |
| # ============================================================================ | |
| # Job 2: Validate OCI Compatibility | |
| # ============================================================================ | |
| validate-oci: | |
| name: Validate OCI Compatibility | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| outputs: | |
| oci_compatible: ${{ steps.validate.outputs.compatible }} | |
| bind_mount_count: ${{ steps.validate.outputs.bind_mount_count }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Check OCI compatibility | |
| id: validate | |
| run: | | |
| set -uo pipefail | |
| echo "=== Validating OCI Compatibility ===" | |
| ERRORS=0 | |
| WARNINGS=0 | |
| BIND_MOUNTS=0 | |
| # Create results file | |
| echo "# OCI Compatibility Report" > /tmp/oci-report.md | |
| echo "" >> /tmp/oci-report.md | |
| echo "| Service | Status | Issues |" >> /tmp/oci-report.md | |
| echo "|---------|--------|--------|" >> /tmp/oci-report.md | |
| for COMPOSE in services/*/docker-compose.yml; do | |
| SERVICE=$(dirname "$COMPOSE" | xargs basename) | |
| ISSUES="" | |
| STATUS="✅ Pass" | |
| # Load env files for variable substitution | |
| ENV_FILE="services/${SERVICE}/.env" | |
| EXAMPLE_FILE="services/${SERVICE}/.env.example" | |
| set +u | |
| if [[ -f "$ENV_FILE" ]]; then | |
| set -a; source "$ENV_FILE" 2>/dev/null || true; set +a | |
| fi | |
| if [[ -f "$EXAMPLE_FILE" ]]; then | |
| set -a; source "$EXAMPLE_FILE" 2>/dev/null || true; set +a | |
| fi | |
| set -u | |
| # Check for bind mounts (except docker.sock which is allowed for traefik) | |
| # Use awk to detect bind mounts and check if source is docker.sock | |
| CONFIG=$(docker compose -f "$COMPOSE" config 2>/dev/null) | |
| # Find bind mounts that are NOT docker.sock | |
| # The config format has 'type: bind' followed by 'source: <path>' on next lines | |
| BINDS=$(echo "$CONFIG" | awk ' | |
| /type: bind/ { in_bind=1; next } | |
| in_bind && /source:/ { | |
| if ($2 !~ /docker\.sock/) { | |
| print $0 | |
| count++ | |
| } | |
| in_bind=0 | |
| } | |
| /^[^ ]/ { in_bind=0 } | |
| END { exit (count > 0 ? 0 : 1) } | |
| ' 2>/dev/null) || true | |
| if [[ -n "$BINDS" ]]; then | |
| BIND_COUNT=$(echo "$BINDS" | wc -l | tr -d ' ') | |
| BIND_MOUNTS=$((BIND_MOUNTS + BIND_COUNT)) | |
| ISSUES="${ISSUES}bind mounts ($BIND_COUNT); " | |
| STATUS="❌ Fail" | |
| ERRORS=$((ERRORS + 1)) | |
| echo "::error file=$COMPOSE::Contains $BIND_COUNT bind mount(s) - not OCI compatible" | |
| fi | |
| # Check for build directives | |
| if grep -qE '^\s+build:' "$COMPOSE" 2>/dev/null; then | |
| ISSUES="${ISSUES}build directive; " | |
| STATUS="⚠️ Warn" | |
| WARNINGS=$((WARNINGS + 1)) | |
| echo "::warning file=$COMPOSE::Contains build directive - images must be pre-built for OCI" | |
| fi | |
| # Check for local config file references (not in volumes section) | |
| if grep -qE '^\s+-\s+\./[^/]+\.(yml|yaml|conf|env|toml):' "$COMPOSE" 2>/dev/null; then | |
| ISSUES="${ISSUES}local config files; " | |
| if [[ "$STATUS" != "❌ Fail" ]]; then | |
| STATUS="⚠️ Warn" | |
| fi | |
| WARNINGS=$((WARNINGS + 1)) | |
| echo "::warning file=$COMPOSE::References local config files" | |
| fi | |
| # Default issues text | |
| if [[ -z "$ISSUES" ]]; then | |
| ISSUES="None" | |
| fi | |
| echo "| $SERVICE | $STATUS | $ISSUES |" >> /tmp/oci-report.md | |
| done | |
| # Summary | |
| echo "" >> /tmp/oci-report.md | |
| echo "## Summary" >> /tmp/oci-report.md | |
| echo "" >> /tmp/oci-report.md | |
| echo "- **Errors:** $ERRORS" >> /tmp/oci-report.md | |
| echo "- **Warnings:** $WARNINGS" >> /tmp/oci-report.md | |
| echo "- **Total bind mounts:** $BIND_MOUNTS" >> /tmp/oci-report.md | |
| # Output results | |
| echo "bind_mount_count=$BIND_MOUNTS" >> $GITHUB_OUTPUT | |
| if [[ $ERRORS -eq 0 ]]; then | |
| echo "compatible=true" >> $GITHUB_OUTPUT | |
| echo "" | |
| echo "All services are OCI compatible!" | |
| else | |
| echo "compatible=false" >> $GITHUB_OUTPUT | |
| echo "" | |
| echo "::error::$ERRORS service(s) have OCI compatibility issues" | |
| exit 1 | |
| fi | |
| echo "" | |
| cat /tmp/oci-report.md | |
| - name: Add OCI summary to GitHub Actions | |
| if: always() | |
| run: | | |
| echo "## OCI Compatibility Check" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [[ "${{ steps.validate.outputs.compatible }}" == "true" ]]; then | |
| echo "✅ **All services are OCI compatible**" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ **Some services have OCI compatibility issues**" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Bind mounts detected: ${{ steps.validate.outputs.bind_mount_count }}" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Services can be deployed from OCI artifacts:" >> $GITHUB_STEP_SUMMARY | |
| echo '```bash' >> $GITHUB_STEP_SUMMARY | |
| echo 'docker compose -f oci://ghcr.io/beevelop/<service>:<version> up -d' >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| # ============================================================================ | |
| # Job 3: Discover and Validate Services | |
| # ============================================================================ | |
| discover: | |
| name: Discover Services | |
| runs-on: ubuntu-latest | |
| needs: [lint, validate-oci] | |
| outputs: | |
| services: ${{ steps.discover.outputs.services }} | |
| service_count: ${{ steps.discover.outputs.service_count }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Discover Docker Compose services | |
| id: discover | |
| run: | | |
| set -euo pipefail | |
| echo "=== Discovering Docker Compose services ===" | |
| # Extract service directories using proper line-by-line reading | |
| SERVICES=() | |
| SKIPPED_SERVICES=() | |
| while IFS= read -r file; do | |
| SERVICE_DIR=$(dirname "$file") | |
| SERVICE_NAME=$(basename "$SERVICE_DIR") | |
| # Skip nested services (like traefik/whoami) - only include depth=2 (services/name) | |
| DEPTH=$(echo "$SERVICE_DIR" | tr '/' '\n' | wc -l | tr -d ' ') | |
| if [[ "$DEPTH" -eq 2 ]]; then | |
| # Skip services with .ci-skip marker file | |
| if [[ -f "${SERVICE_DIR}/.ci-skip" ]]; then | |
| echo "Skipping ${SERVICE_NAME} (has .ci-skip marker)" | |
| SKIPPED_SERVICES+=("$SERVICE_NAME") | |
| else | |
| SERVICES+=("$SERVICE_NAME") | |
| fi | |
| fi | |
| done < <(find services -name "docker-compose.yml" -type f | sort) | |
| if [[ ${#SKIPPED_SERVICES[@]} -gt 0 ]]; then | |
| echo "" | |
| echo "=== Skipped ${#SKIPPED_SERVICES[@]} services with .ci-skip marker ===" | |
| printf '%s\n' "${SKIPPED_SERVICES[@]}" | |
| fi | |
| # Filter for specific service if requested | |
| if [[ -n "${{ github.event.inputs.specific_service }}" ]]; then | |
| SPECIFIC="${{ github.event.inputs.specific_service }}" | |
| if printf '%s\n' "${SERVICES[@]}" | grep -q "^${SPECIFIC}$"; then | |
| SERVICES=("$SPECIFIC") | |
| echo "Filtering to specific service: $SPECIFIC" | |
| else | |
| echo "ERROR: Service '$SPECIFIC' not found!" | |
| echo "Available services: ${SERVICES[*]}" | |
| exit 1 | |
| fi | |
| fi | |
| # Convert to JSON array | |
| SERVICES_JSON=$(printf '%s\n' "${SERVICES[@]}" | jq -R . | jq -s -c .) | |
| SERVICE_COUNT=${#SERVICES[@]} | |
| echo "services=$SERVICES_JSON" >> $GITHUB_OUTPUT | |
| echo "service_count=$SERVICE_COUNT" >> $GITHUB_OUTPUT | |
| echo "" | |
| echo "=== Discovered $SERVICE_COUNT services ===" | |
| echo "$SERVICES_JSON" | jq -r '.[]' | |
| - name: Validate compose file syntax | |
| run: | | |
| set -euo pipefail | |
| echo "=== Validating Docker Compose syntax ===" | |
| ERRORS=0 | |
| for SERVICE in $(echo '${{ steps.discover.outputs.services }}' | jq -r '.[]'); do | |
| COMPOSE_FILE="services/${SERVICE}/docker-compose.yml" | |
| echo -n "Validating ${SERVICE}... " | |
| if docker compose -f "$COMPOSE_FILE" config > /dev/null 2>&1; then | |
| echo "OK" | |
| else | |
| echo "FAILED" | |
| echo "--- Error details for ${SERVICE} ---" | |
| docker compose -f "$COMPOSE_FILE" config 2>&1 || true | |
| echo "---" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| done | |
| if [[ $ERRORS -gt 0 ]]; then | |
| echo "" | |
| echo "ERROR: $ERRORS compose file(s) have syntax errors!" | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "All compose files validated successfully!" | |
| # ============================================================================ | |
| # Job 4: Extract All Docker Images | |
| # ============================================================================ | |
| extract-images: | |
| name: Extract Docker Images | |
| runs-on: ubuntu-latest | |
| needs: discover | |
| outputs: | |
| images: ${{ steps.extract.outputs.images }} | |
| image_count: ${{ steps.extract.outputs.image_count }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Extract unique images from all compose files | |
| id: extract | |
| run: | | |
| set -uo pipefail | |
| echo "=== Extracting Docker images from compose files ===" | |
| # Create temp file for all images | |
| > /tmp/all_images.txt | |
| for SERVICE in $(echo '${{ needs.discover.outputs.services }}' | jq -r '.[]'); do | |
| COMPOSE_FILE="services/${SERVICE}/docker-compose.yml" | |
| ENV_FILE="services/${SERVICE}/.env" | |
| EXAMPLE_FILE="services/${SERVICE}/.env.example" | |
| echo "Processing ${SERVICE}..." | |
| # Load environment variables if they exist | |
| # Disable nounset temporarily as env files may contain $ chars in values | |
| # (e.g., bcrypt hashes like $2y$10$...) | |
| set +u | |
| if [[ -f "$ENV_FILE" ]]; then | |
| set -a | |
| source "$ENV_FILE" 2>/dev/null || true | |
| set +a | |
| fi | |
| # Also load example env for any missing vars | |
| if [[ -f "$EXAMPLE_FILE" ]]; then | |
| set -a | |
| source "$EXAMPLE_FILE" 2>/dev/null || true | |
| set +a | |
| fi | |
| set -u | |
| # Extract images using docker compose config | |
| # Use sed to trim leading/trailing whitespace from image names | |
| IMAGES=$(docker compose -f "$COMPOSE_FILE" config 2>/dev/null | \ | |
| grep -E '^\s*image:' | \ | |
| sed 's/.*image://' | \ | |
| sed 's/^[[:space:]]*//' | \ | |
| sed 's/[[:space:]]*$//' | \ | |
| tr -d '"' || true) | |
| if [[ -n "$IMAGES" ]]; then | |
| echo "$IMAGES" >> /tmp/all_images.txt | |
| fi | |
| done | |
| # Get unique images, remove empty lines, and trim any remaining whitespace | |
| UNIQUE_IMAGES=$(cat /tmp/all_images.txt | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//' | sort -u | grep -v '^$' || true) | |
| IMAGE_COUNT=$(echo "$UNIQUE_IMAGES" | grep -c . || echo "0") | |
| # Convert to JSON array | |
| IMAGES_JSON=$(echo "$UNIQUE_IMAGES" | jq -R . | jq -s -c .) | |
| echo "images=$IMAGES_JSON" >> $GITHUB_OUTPUT | |
| echo "image_count=$IMAGE_COUNT" >> $GITHUB_OUTPUT | |
| echo "" | |
| echo "=== Found $IMAGE_COUNT unique Docker images ===" | |
| echo "$UNIQUE_IMAGES" | |
| # ============================================================================ | |
| # Job 5: CVE Vulnerability Scanning (Parallel) | |
| # ============================================================================ | |
| # Images are scanned in parallel using matrix strategy for faster pipeline execution. | |
| # Each image runs in its own job, and results are aggregated in cve-scan-summary. | |
| cve-scan: | |
| name: CVE Scan - ${{ matrix.image }} | |
| runs-on: ubuntu-latest | |
| needs: extract-images | |
| if: ${{ github.event.inputs.skip_cve_scan != 'true' && needs.extract-images.outputs.image_count != '0' }} | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 10 | |
| matrix: | |
| image: ${{ fromJson(needs.extract-images.outputs.images) }} | |
| steps: | |
| - name: Install Trivy | |
| uses: aquasecurity/trivy-action@master | |
| with: | |
| scan-type: 'image' | |
| image-ref: ${{ matrix.image }} | |
| format: 'json' | |
| output: 'trivy-results.json' | |
| severity: 'CRITICAL,HIGH,MEDIUM,LOW' | |
| timeout: '10m' | |
| continue-on-error: true | |
| - name: Parse scan results | |
| id: parse | |
| run: | | |
| set -uo pipefail | |
| IMAGE="${{ matrix.image }}" | |
| SAFE_NAME=$(echo "$IMAGE" | tr '/:' '__') | |
| echo "=== Scan results for: $IMAGE ===" | |
| if [[ -f trivy-results.json ]] && [[ -s trivy-results.json ]]; then | |
| # Parse vulnerability counts | |
| CRITICAL=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' trivy-results.json 2>/dev/null || echo 0) | |
| HIGH=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="HIGH")] | length' trivy-results.json 2>/dev/null || echo 0) | |
| MEDIUM=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="MEDIUM")] | length' trivy-results.json 2>/dev/null || echo 0) | |
| LOW=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="LOW")] | length' trivy-results.json 2>/dev/null || echo 0) | |
| # Determine status | |
| if [[ $CRITICAL -gt 0 ]]; then | |
| STATUS="CRITICAL" | |
| elif [[ $HIGH -gt 0 ]]; then | |
| STATUS="HIGH" | |
| else | |
| STATUS="PASS" | |
| fi | |
| echo "Critical: $CRITICAL, High: $HIGH, Medium: $MEDIUM, Low: $LOW" | |
| echo "Status: $STATUS" | |
| # Export results | |
| echo "critical=$CRITICAL" >> $GITHUB_OUTPUT | |
| echo "high=$HIGH" >> $GITHUB_OUTPUT | |
| echo "medium=$MEDIUM" >> $GITHUB_OUTPUT | |
| echo "low=$LOW" >> $GITHUB_OUTPUT | |
| echo "status=$STATUS" >> $GITHUB_OUTPUT | |
| echo "scanned=true" >> $GITHUB_OUTPUT | |
| # Rename for artifact upload | |
| mv trivy-results.json "${SAFE_NAME}.json" | |
| else | |
| echo "WARNING: Scan failed or produced no results" | |
| echo "critical=0" >> $GITHUB_OUTPUT | |
| echo "high=0" >> $GITHUB_OUTPUT | |
| echo "medium=0" >> $GITHUB_OUTPUT | |
| echo "low=0" >> $GITHUB_OUTPUT | |
| echo "status=SKIPPED" >> $GITHUB_OUTPUT | |
| echo "scanned=false" >> $GITHUB_OUTPUT | |
| # Create empty result file | |
| echo '{"skipped": true, "image": "${{ matrix.image }}"}' > "${SAFE_NAME}.json" | |
| fi | |
| echo "safe_name=${SAFE_NAME}" >> $GITHUB_OUTPUT | |
| - name: Upload individual scan result | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: cve-scan-${{ steps.parse.outputs.safe_name }} | |
| path: ${{ steps.parse.outputs.safe_name }}.json | |
| retention-days: 30 | |
| # ============================================================================ | |
| # Job 5b: Aggregate CVE Scan Results | |
| # ============================================================================ | |
| cve-scan-summary: | |
| name: CVE Scan Summary | |
| runs-on: ubuntu-latest | |
| needs: [extract-images, cve-scan] | |
| if: ${{ always() && needs.extract-images.result == 'success' && github.event.inputs.skip_cve_scan != 'true' }} | |
| outputs: | |
| critical_count: ${{ steps.summarize.outputs.critical_count }} | |
| high_count: ${{ steps.summarize.outputs.high_count }} | |
| scan_passed: ${{ steps.summarize.outputs.scan_passed }} | |
| steps: | |
| - name: Download all scan artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: cve-scan-* | |
| path: scan-results/ | |
| merge-multiple: true | |
| - name: Aggregate results | |
| id: summarize | |
| run: | | |
| set -uo pipefail | |
| echo "=== Aggregating CVE scan results ===" | |
| TOTAL_CRITICAL=0 | |
| TOTAL_HIGH=0 | |
| TOTAL_MEDIUM=0 | |
| TOTAL_LOW=0 | |
| SCANNED=0 | |
| SKIPPED=0 | |
| # Create summary file | |
| echo "# CVE Scan Results" > summary.md | |
| echo "" >> summary.md | |
| echo "| Image | Critical | High | Medium | Low | Status |" >> summary.md | |
| echo "|-------|----------|------|--------|-----|--------|" >> summary.md | |
| # Process each result file | |
| for RESULT_FILE in scan-results/*.json; do | |
| if [[ ! -f "$RESULT_FILE" ]]; then | |
| continue | |
| fi | |
| # Extract image name from filename | |
| FILENAME=$(basename "$RESULT_FILE" .json) | |
| IMAGE_NAME=$(echo "$FILENAME" | tr '__' ':/') | |
| # Check if skipped | |
| if jq -e '.skipped' "$RESULT_FILE" > /dev/null 2>&1; then | |
| echo "| \`$IMAGE_NAME\` | - | - | - | - | SKIPPED |" >> summary.md | |
| SKIPPED=$((SKIPPED + 1)) | |
| continue | |
| fi | |
| # Parse counts | |
| CRITICAL=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' "$RESULT_FILE" 2>/dev/null || echo 0) | |
| HIGH=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="HIGH")] | length' "$RESULT_FILE" 2>/dev/null || echo 0) | |
| MEDIUM=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="MEDIUM")] | length' "$RESULT_FILE" 2>/dev/null || echo 0) | |
| LOW=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="LOW")] | length' "$RESULT_FILE" 2>/dev/null || echo 0) | |
| # Update totals | |
| TOTAL_CRITICAL=$((TOTAL_CRITICAL + CRITICAL)) | |
| TOTAL_HIGH=$((TOTAL_HIGH + HIGH)) | |
| TOTAL_MEDIUM=$((TOTAL_MEDIUM + MEDIUM)) | |
| TOTAL_LOW=$((TOTAL_LOW + LOW)) | |
| SCANNED=$((SCANNED + 1)) | |
| # Determine status | |
| if [[ $CRITICAL -gt 0 ]]; then | |
| STATUS="CRITICAL" | |
| elif [[ $HIGH -gt 0 ]]; then | |
| STATUS="HIGH" | |
| else | |
| STATUS="PASS" | |
| fi | |
| echo "| \`$IMAGE_NAME\` | $CRITICAL | $HIGH | $MEDIUM | $LOW | $STATUS |" >> summary.md | |
| done | |
| IMAGE_COUNT=${{ needs.extract-images.outputs.image_count }} | |
| # Write totals to summary | |
| echo "" >> summary.md | |
| echo "## Summary" >> summary.md | |
| echo "" >> summary.md | |
| echo "- **Images Scanned:** $SCANNED / $IMAGE_COUNT" >> summary.md | |
| echo "- **Images Skipped:** $SKIPPED" >> summary.md | |
| echo "- **Total Critical:** $TOTAL_CRITICAL" >> summary.md | |
| echo "- **Total High:** $TOTAL_HIGH" >> summary.md | |
| echo "- **Total Medium:** $TOTAL_MEDIUM" >> summary.md | |
| echo "- **Total Low:** $TOTAL_LOW" >> summary.md | |
| echo "" | |
| echo "=== CVE Scan Complete ===" | |
| cat summary.md | |
| # Export results | |
| echo "critical_count=$TOTAL_CRITICAL" >> $GITHUB_OUTPUT | |
| echo "high_count=$TOTAL_HIGH" >> $GITHUB_OUTPUT | |
| # Report vulnerabilities but don't fail the pipeline | |
| if [[ $TOTAL_CRITICAL -gt 0 ]]; then | |
| echo "::warning::Found $TOTAL_CRITICAL CRITICAL vulnerabilities - see CVE scan report for details" | |
| fi | |
| if [[ $TOTAL_HIGH -gt 0 ]]; then | |
| echo "::warning::Found $TOTAL_HIGH HIGH vulnerabilities - see CVE scan report for details" | |
| fi | |
| # Always pass - CVE scan is for reporting only | |
| echo "scan_passed=true" >> $GITHUB_OUTPUT | |
| echo "" | |
| echo "CVE scan completed - report generated (non-blocking)" | |
| - name: Upload aggregated results | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: cve-scan-results | |
| path: | | |
| summary.md | |
| scan-results/ | |
| retention-days: 30 | |
| - name: Add CVE summary to GitHub Actions | |
| run: | | |
| echo "## CVE Vulnerability Scan Results" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| cat summary.md >> $GITHUB_STEP_SUMMARY | |
| # ============================================================================ | |
| # Job 6: Parallel Docker Compose Testing | |
| # ============================================================================ | |
| # Services are tested in parallel using matrix strategy for faster pipeline execution. | |
| # Each service runs in its own isolated job with unique project naming to prevent conflicts. | |
| test-services: | |
| name: Test - ${{ matrix.service }} | |
| runs-on: ubuntu-latest | |
| needs: [discover, cve-scan-summary] | |
| # Run even if CVE scan is skipped, but not if it failed | |
| if: ${{ always() && needs.discover.result == 'success' && (needs.cve-scan-summary.result == 'success' || needs.cve-scan-summary.result == 'skipped') }} | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 8 | |
| matrix: | |
| service: ${{ fromJson(needs.discover.outputs.services) }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Test service | |
| id: test | |
| run: | | |
| set -uo pipefail | |
| SERVICE="${{ matrix.service }}" | |
| SERVICE_DIR="services/${SERVICE}" | |
| COMPOSE_FILE="${SERVICE_DIR}/docker-compose.yml" | |
| # Use unique project name to isolate parallel tests | |
| PROJECT_NAME="test-${SERVICE}-${{ github.run_id }}" | |
| echo "=== Testing: $SERVICE ===" | |
| echo "Project: $PROJECT_NAME" | |
| # Create results directory | |
| mkdir -p test-results | |
| TEST_START=$(date +%s) | |
| TEST_STATUS="PASSED" | |
| TEST_DETAILS="" | |
| # Load environment variables | |
| set +u | |
| if [[ -f "${SERVICE_DIR}/.env" ]]; then | |
| set -a | |
| source "${SERVICE_DIR}/.env" 2>/dev/null || true | |
| set +a | |
| fi | |
| if [[ -f "${SERVICE_DIR}/.env.example" ]]; then | |
| set -a | |
| source "${SERVICE_DIR}/.env.example" 2>/dev/null || true | |
| set +a | |
| fi | |
| set -u | |
| # Set test-specific environment variables | |
| export SERVICE_DOMAIN="${SERVICE}.test.local" | |
| # Create isolated traefik network for this test | |
| # Using project-specific name to avoid conflicts between parallel tests | |
| TRAEFIK_NETWORK="${PROJECT_NAME}_traefik" | |
| docker network create "$TRAEFIK_NETWORK" > /dev/null 2>&1 || true | |
| # Also create the standard traefik_default network (some services expect it) | |
| # Skip for traefik/traefik-tunnel services - they create their own network | |
| if [[ "$SERVICE" != "traefik" && "$SERVICE" != "traefik-tunnel" ]]; then | |
| docker network create traefik_default > /dev/null 2>&1 || true | |
| fi | |
| # Step 1: Pull images (quiet mode, suppress progress) | |
| echo "Pulling images..." | |
| if timeout 300 docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE" pull --quiet 2>&1 | grep -v "Pulling\|Pull complete\|Already exists\|Waiting\|Downloading\|Extracting" || true; then | |
| echo "Images ready" | |
| else | |
| echo "WARNING: Some images may have failed to pull" | |
| fi | |
| # Step 2: Start services (suppress verbose Creating/Started messages) | |
| echo "Starting services..." | |
| # Capture output and filter out the noisy status messages | |
| UP_OUTPUT=$(timeout 120 docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE" up -d --quiet-pull 2>&1) || UP_EXIT=$? | |
| UP_EXIT=${UP_EXIT:-0} | |
| # Only show errors or warnings, not the Creating/Created/Starting/Started noise | |
| echo "$UP_OUTPUT" | grep -vE "Creating|Created|Starting|Started|Pulling|Pull complete" | grep -v "^$" || true | |
| if [[ $UP_EXIT -eq 0 ]]; then | |
| echo "Services started" | |
| # Step 3: Wait for services to stabilize | |
| sleep 10 | |
| # Step 4: Check container health | |
| echo "Checking container status..." | |
| CONTAINERS=$(docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE" ps -q 2>/dev/null | wc -l) | |
| RUNNING=$(docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE" ps --status running -q 2>/dev/null | wc -l) | |
| echo "Containers: $RUNNING running of $CONTAINERS defined" | |
| if [[ "$RUNNING" -gt 0 ]]; then | |
| # Show container status (compact format) | |
| docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE" ps --format "table {{.Name}}\t{{.Status}}" | |
| # Check each container is running (not restarting) | |
| HEALTHY=true | |
| for CONTAINER in $(docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE" ps -q 2>/dev/null); do | |
| STATE=$(docker inspect --format='{{.State.Status}}' "$CONTAINER" 2>/dev/null || echo "unknown") | |
| RESTARTS=$(docker inspect --format='{{.RestartCount}}' "$CONTAINER" 2>/dev/null || echo "0") | |
| if [[ "$STATE" != "running" ]] || [[ "$RESTARTS" -gt 2 ]]; then | |
| CONTAINER_NAME=$(docker inspect --format='{{.Name}}' "$CONTAINER" 2>/dev/null | sed 's/^\///') | |
| echo "::warning::Container $CONTAINER_NAME - State: $STATE, Restarts: $RESTARTS" | |
| HEALTHY=false | |
| fi | |
| done | |
| if [[ "$HEALTHY" == "true" ]]; then | |
| TEST_DETAILS="All containers running" | |
| else | |
| TEST_STATUS="FAILED" | |
| TEST_DETAILS="Container health issues" | |
| fi | |
| else | |
| TEST_STATUS="FAILED" | |
| TEST_DETAILS="No containers running" | |
| fi | |
| else | |
| TEST_STATUS="FAILED" | |
| TEST_DETAILS="Failed to start services" | |
| fi | |
| # Calculate duration | |
| TEST_END=$(date +%s) | |
| TEST_DURATION=$((TEST_END - TEST_START)) | |
| echo "" | |
| echo "Result: $TEST_STATUS (${TEST_DURATION}s) - $TEST_DETAILS" | |
| # Capture logs for failed tests | |
| if [[ "$TEST_STATUS" == "FAILED" ]]; then | |
| echo "--- Container logs ---" | |
| docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE" logs --tail=50 2>&1 || true | |
| docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE" logs --tail=100 > "test-results/${SERVICE}-logs.txt" 2>&1 || true | |
| fi | |
| # Cleanup (suppress output) | |
| echo "Cleaning up..." | |
| docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE" down --volumes --remove-orphans > /dev/null 2>&1 || true | |
| docker network rm "$TRAEFIK_NETWORK" > /dev/null 2>&1 || true | |
| # Export results | |
| echo "status=$TEST_STATUS" >> $GITHUB_OUTPUT | |
| echo "duration=$TEST_DURATION" >> $GITHUB_OUTPUT | |
| echo "details=$TEST_DETAILS" >> $GITHUB_OUTPUT | |
| # Write result file for aggregation | |
| echo "${SERVICE}|${TEST_STATUS}|${TEST_DURATION}|${TEST_DETAILS}" > "test-results/${SERVICE}-result.txt" | |
| # Exit with failure if test failed | |
| if [[ "$TEST_STATUS" == "FAILED" ]]; then | |
| exit 1 | |
| fi | |
| - name: Upload test result | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: test-result-${{ matrix.service }} | |
| path: test-results/ | |
| retention-days: 30 | |
| # ============================================================================ | |
| # Job 6b: Aggregate Test Results | |
| # ============================================================================ | |
| test-services-summary: | |
| name: Test Summary | |
| runs-on: ubuntu-latest | |
| needs: [discover, test-services] | |
| if: always() | |
| outputs: | |
| passed: ${{ steps.summarize.outputs.passed }} | |
| failed: ${{ steps.summarize.outputs.failed }} | |
| all_passed: ${{ steps.summarize.outputs.all_passed }} | |
| steps: | |
| - name: Download all test results | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: test-result-* | |
| path: all-results/ | |
| merge-multiple: true | |
| - name: Aggregate results | |
| id: summarize | |
| run: | | |
| set -uo pipefail | |
| echo "=== Aggregating test results ===" | |
| PASSED=0 | |
| FAILED=0 | |
| FAILED_SERVICES="" | |
| # Create summary | |
| echo "# Docker Compose Test Results" > summary.md | |
| echo "" >> summary.md | |
| echo "| Service | Status | Duration | Details |" >> summary.md | |
| echo "|---------|--------|----------|---------|" >> summary.md | |
| # Process each result file | |
| for RESULT_FILE in all-results/*-result.txt; do | |
| if [[ ! -f "$RESULT_FILE" ]]; then | |
| continue | |
| fi | |
| # Parse result: SERVICE|STATUS|DURATION|DETAILS | |
| IFS='|' read -r SERVICE STATUS DURATION DETAILS < "$RESULT_FILE" | |
| if [[ "$STATUS" == "PASSED" ]]; then | |
| PASSED=$((PASSED + 1)) | |
| echo "| $SERVICE | PASS | ${DURATION}s | $DETAILS |" >> summary.md | |
| else | |
| FAILED=$((FAILED + 1)) | |
| FAILED_SERVICES="${FAILED_SERVICES} ${SERVICE}" | |
| echo "| $SERVICE | FAIL | ${DURATION}s | $DETAILS |" >> summary.md | |
| fi | |
| done | |
| SERVICE_COUNT=${{ needs.discover.outputs.service_count }} | |
| # Write summary | |
| echo "" >> summary.md | |
| echo "## Summary" >> summary.md | |
| echo "" >> summary.md | |
| echo "- **Total Services:** $SERVICE_COUNT" >> summary.md | |
| echo "- **Passed:** $PASSED" >> summary.md | |
| echo "- **Failed:** $FAILED" >> summary.md | |
| if [[ $FAILED -gt 0 ]]; then | |
| echo "" >> summary.md | |
| echo "### Failed Services" >> summary.md | |
| echo "" >> summary.md | |
| for SVC in $FAILED_SERVICES; do | |
| echo "- $SVC" >> summary.md | |
| done | |
| fi | |
| echo "" | |
| echo "=== Test Results ===" | |
| cat summary.md | |
| # Export results | |
| echo "passed=$PASSED" >> $GITHUB_OUTPUT | |
| echo "failed=$FAILED" >> $GITHUB_OUTPUT | |
| if [[ $FAILED -eq 0 ]]; then | |
| echo "all_passed=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "all_passed=false" >> $GITHUB_OUTPUT | |
| echo "::error::$FAILED service(s) failed testing:$FAILED_SERVICES" | |
| fi | |
| - name: Upload aggregated results | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: docker-compose-test-results | |
| path: | | |
| summary.md | |
| all-results/ | |
| retention-days: 30 | |
| - name: Add test summary to GitHub Actions | |
| run: | | |
| echo "## Docker Compose Test Results" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| cat summary.md >> $GITHUB_STEP_SUMMARY | |
| # ============================================================================ | |
| # Job 7: Final Summary | |
| # ============================================================================ | |
| summary: | |
| name: Pipeline Summary | |
| runs-on: ubuntu-latest | |
| needs: [lint, test-cli, service-coverage, validate-oci, discover, extract-images, cve-scan-summary, test-services-summary] | |
| if: always() | |
| steps: | |
| - name: Generate summary | |
| run: | | |
| echo "# CI/CD Pipeline Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "## Jobs Status" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Lint Docker Compose | ${{ needs.lint.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Test CLI Helper | ${{ needs.test-cli.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Service Coverage | ${{ needs.service-coverage.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Validate OCI | ${{ needs.validate-oci.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Discover Services | ${{ needs.discover.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Extract Images | ${{ needs.extract-images.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| CVE Scan | ${{ needs.cve-scan-summary.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Test Services | ${{ needs.test-services-summary.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "## Metrics" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Lint Errors:** ${{ needs.lint.outputs.error_count || 0 }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Lint Warnings:** ${{ needs.lint.outputs.warning_count || 0 }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Services Discovered:** ${{ needs.discover.outputs.service_count }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Service Coverage:** ${{ needs.service-coverage.outputs.readme_count }}/${{ needs.service-coverage.outputs.service_count }} README, ${{ needs.service-coverage.outputs.bc_count }}/${{ needs.service-coverage.outputs.service_count }} bc" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Unique Images:** ${{ needs.extract-images.outputs.image_count }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Tests Passed:** ${{ needs.test-services-summary.outputs.passed || 0 }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Tests Failed:** ${{ needs.test-services-summary.outputs.failed || 0 }}" >> $GITHUB_STEP_SUMMARY | |
| if [[ "${{ needs.cve-scan-summary.result }}" == "success" ]]; then | |
| echo "- **Critical CVEs:** ${{ needs.cve-scan-summary.outputs.critical_count || 0 }} (informational)" >> $GITHUB_STEP_SUMMARY | |
| echo "- **High CVEs:** ${{ needs.cve-scan-summary.outputs.high_count || 0 }} (informational)" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Determine overall status (CVE scan is informational, not blocking) | |
| if [[ "${{ needs.lint.result }}" == "success" && "${{ needs.service-coverage.result }}" == "success" && "${{ needs.validate-oci.result }}" == "success" && "${{ needs.test-services-summary.outputs.all_passed }}" == "true" ]]; then | |
| echo "## Result: PASSED" >> $GITHUB_STEP_SUMMARY | |
| if [[ "${{ needs.cve-scan-summary.outputs.critical_count }}" -gt 0 ]] 2>/dev/null; then | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "> **Note:** CVE vulnerabilities detected - review the scan report artifact for details." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| else | |
| echo "## Result: FAILED" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| - name: Check overall result | |
| run: | | |
| if [[ "${{ needs.lint.result }}" == "failure" ]]; then | |
| echo "Pipeline failed: Docker Compose linting failed" | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.test-cli.result }}" == "failure" ]]; then | |
| echo "Pipeline failed: CLI helper tests failed" | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.service-coverage.result }}" == "failure" ]]; then | |
| echo "Pipeline failed: Service coverage check failed - ensure all services are in README.md and scripts/bc" | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.validate-oci.result }}" == "failure" ]]; then | |
| echo "Pipeline failed: OCI compatibility validation failed" | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.test-services-summary.outputs.all_passed }}" != "true" ]]; then | |
| echo "Pipeline failed: ${{ needs.test-services-summary.outputs.failed }} service test(s) failed" | |
| exit 1 | |
| fi | |
| # CVE scan is informational only - don't fail the pipeline | |
| if [[ "${{ needs.cve-scan-summary.result }}" == "failure" ]]; then | |
| echo "::warning::CVE scan job failed - check logs for details" | |
| fi | |
| echo "Pipeline completed successfully!" |