--- # dance-lessons-coach Unified CI/CD Workflow # Single, optimized workflow that replaces all previous workflows # Fast execution with minimal repetition and maximum artifact sharing name: CI/CD Pipeline on: workflow_dispatch: {} push: branches: - main - 'ci/**' - 'feature/**' - 'fix/**' - 'refactor/**' paths-ignore: - 'README.md' - 'doc/**' - 'adr/**' - '.gitea/**' - 'documentation/**' - '*.md' - '.vibe/**' - 'features/**' pull_request: branches: - main types: [opened, synchronize, reopened, labeled] # Only run PR CI if the commit doesn't already have passing branch CI if: | github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened') paths-ignore: - 'README.md' - 'doc/**' - 'adr/**' - '.gitea/**' - 'documentation/**' - '*.md' - '.vibe/**' - 'features/**' # cancel any previously-started runs of this workflow on the same branch concurrency: group: ${{ github.ref }}-${{ github.workflow }} cancel-in-progress: true # Arcodange-specific environment variables env: GITEA_INTERNAL: "https://gitea.arcodange.lab/" GITEA_EXTERNAL: "https://gitea.arcodange.fr/" GITEA_ORG: "arcodange" GITEA_REPO: "dance-lessons-coach" CI_REGISTRY: "gitea.arcodange.lab" jobs: build-cache: name: Build Docker Cache runs-on: ubuntu-latest-ca if: "!contains(github.event.head_commit.message, '[skip ci]') && github.actor != 'ci-bot'" outputs: deps_hash: ${{ steps.calculate_hash.outputs.deps_hash }} cache_hit: ${{ steps.check_cache.outputs.cache_hit }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Calculate dependency hash id: calculate_hash run: | # Calculate hash of go.mod + go.sum + Dockerfile.build (inline, no script needed) DEPS_HASH=$(sha256sum go.mod go.sum docker/Dockerfile.build | sha256sum | cut -d' ' -f1 | head -c 12) echo "Dependency hash: $DEPS_HASH" echo "deps_hash=$DEPS_HASH" >> $GITHUB_OUTPUT - name: Check for existing cache (optimized with fallback) id: check_cache run: | # Check if image exists in registry using optimized approach with fallback IMAGE_NAME="${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}-build-cache:${{ steps.calculate_hash.outputs.deps_hash }}" # Fast check using docker manifest inspect (lighter than pull) echo "🔍 Checking cache: $IMAGE_NAME" # Try manifest inspect first (fastest method, but experimental) if docker manifest inspect "$IMAGE_NAME" >/dev/null 2>&1; then echo "✅ Cache hit - using existing build cache (manifest inspect)" echo "cache_hit=true" >> $GITHUB_OUTPUT else # Fallback to docker pull if manifest inspect fails (more reliable) echo "âš ī¸ Manifest inspect failed, falling back to docker pull..." if docker pull "$IMAGE_NAME" >/dev/null 2>&1; then echo "✅ Cache hit - using existing build cache (fallback: docker pull)" echo "cache_hit=true" >> $GITHUB_OUTPUT else echo "âš ī¸ Cache miss - will build new cache image" echo "cache_hit=false" >> $GITHUB_OUTPUT fi fi - name: Login to Gitea Container Registry if: steps.check_cache.outputs.cache_hit == 'false' uses: docker/login-action@v3 with: registry: ${{ env.CI_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.PACKAGES_TOKEN }} - name: Build and push Docker cache image if: steps.check_cache.outputs.cache_hit == 'false' run: | IMAGE_NAME="${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}-build-cache:${{ steps.calculate_hash.outputs.deps_hash }}" echo "Building cache image: $IMAGE_NAME" # Build the image using traditional docker build docker build \ --file docker/Dockerfile.build \ --tag "$IMAGE_NAME" \ . # Push the image docker push "$IMAGE_NAME" echo "✅ Build cache image pushed successfully" ci-pipeline: name: CI Pipeline needs: build-cache runs-on: ubuntu-latest-ca # Skip conditions: standard skip ci + actor check + respect skip_ci input if: "!contains(github.event.head_commit.message, '[skip ci]') && github.actor != 'ci-bot' && (!github.event.inputs.skip_ci || github.event.inputs.skip_ci == 'false')" container: image: ${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}-build-cache:${{ needs.build-cache.outputs.deps_hash }} services: postgres: image: postgres:15 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: dance_lessons_coach_bdd_test steps: - name: Checkout code uses: actions/checkout@v4 - name: Set database environment variables run: | echo "DLC_DATABASE_HOST=postgres" >> $GITHUB_ENV echo "DLC_DATABASE_PORT=5432" >> $GITHUB_ENV echo "DLC_DATABASE_USER=$POSTGRES_USER" >> $GITHUB_ENV echo "DLC_DATABASE_PASSWORD=$POSTGRES_PASSWORD" >> $GITHUB_ENV echo "DLC_DATABASE_NAME=$POSTGRES_DB" >> $GITHUB_ENV echo "DLC_DATABASE_SSL_MODE=disable" >> $GITHUB_ENV - name: Restore Swagger Docs Cache id: cache-swagger-restore uses: actions/cache/restore@v5 with: path: | pkg/server/docs/docs.go pkg/server/docs/swagger.json pkg/server/docs/swagger.yaml key: swagger-docs-${{ hashFiles('cmd/server/main.go', 'pkg/greet/*.go', 'pkg/server/*.go', 'go.mod') }} restore-keys: | swagger-docs- - name: Generate Swagger Docs if: steps.cache-swagger-restore.outputs.cache-hit != 'true' run: go generate ./pkg/server - name: Save Swagger Docs Cache if: steps.cache-swagger-restore.outputs.cache-hit != 'true' id: cache-swagger-save uses: actions/cache/save@v5 with: path: | pkg/server/docs/docs.go pkg/server/docs/swagger.json pkg/server/docs/swagger.yaml key: ${{ steps.cache-swagger-restore.outputs.cache-primary-key }} - name: Build all packages run: go build ./... - name: Wait for PostgreSQL to be ready run: | echo "Waiting for PostgreSQL to be ready..." for i in {1..30}; do if pg_isready -h postgres -p 5432 -U postgres -d dance_lessons_coach_bdd_test; then echo "✅ PostgreSQL is ready!" break fi echo "Waiting for PostgreSQL... ($i/30)" sleep 2 done # Verify PostgreSQL is accessible if ! pg_isready -h postgres -p 5432 -U postgres -d dance_lessons_coach_bdd_test; then echo "❌ PostgreSQL failed to start" exit 1 fi - name: Run BDD tests with strict validation and coverage run: | echo "Running BDD tests with strict validation and coverage..." # Use the run-bdd-tests.sh script which fails on undefined/pending steps # In CI environment, PostgreSQL is already running as a service export DLC_DATABASE_HOST=postgres export DLC_DATABASE_PORT=5432 export DLC_DATABASE_USER=postgres export DLC_DATABASE_PASSWORD=postgres export DLC_DATABASE_NAME=dance_lessons_coach_bdd_test export DLC_DATABASE_SSL_MODE=disable # T12: per-package isolated Postgres schema with migrations (re-enables what # PR #26 attempted but couldn't deliver because the empty schemas had no tables). # The fix: testserver Start() now builds a per-package isolated repo via # user.NewPostgresRepositoryFromDSN which DOES run AutoMigrate against the new # schema. Packages then run in parallel (~2.85x speedup observed locally). export BDD_SCHEMA_ISOLATION=true ./scripts/run-bdd-tests.sh # Generate BDD coverage report go tool cover -func=coverage.out > bdd_coverage.txt # Extract BDD coverage percentage and set as environment variable BDD_COVERAGE=$(grep "total:" bdd_coverage.txt | grep -oP '\d+\.\d+' | head -1) echo "BDD Coverage: ${BDD_COVERAGE}%" echo "DLC_BDD_COVERAGE=${BDD_COVERAGE}%" >> $GITHUB_ENV - name: Run unit tests with coverage run: | echo "Running unit tests with PostgreSQL service..." # Run unit tests excluding BDD tests (already run above) go test ./pkg/... ./cmd/... -coverprofile=unit_coverage.out -v # Generate unit coverage report go tool cover -func=unit_coverage.out > unit_coverage.txt # Extract unit test coverage percentage and set as environment variable UNIT_COVERAGE=$(grep "total:" unit_coverage.txt | grep -oP '\d+\.\d+' | head -1) echo "Unit Coverage: ${UNIT_COVERAGE}%" echo "DLC_UNIT_COVERAGE=${UNIT_COVERAGE}%" >> $GITHUB_ENV - name: Run go fmt run: go fmt ./... - name: Run swag fmt run: swag fmt - name: Build binaries run: ./scripts/build.sh # NOTE: Artifact upload disabled - actions/upload-artifact@v4 not available on Gitea # TODO: Replace with Gitea-specific upload action when available # - name: Upload Swagger documentation # uses: actions/upload-artifact@v4 # with: # name: swagger-docs # path: pkg/server/docs/swagger.json # retention-days: 1 # Badge and version updates - multiple commits, single push # All documentation updates happen in one step with single push at the end - name: Update badges and version (multiple commits, single push) if: always() && github.actor != 'ci-bot' run: | echo "đŸŽ¯ Updating badges and version..." echo "BDD Coverage: ${DLC_BDD_COVERAGE:-Not set}" echo "Unit Coverage: ${DLC_UNIT_COVERAGE:-Not set}" # Configure git git config user.name "CI Bot" git config user.email "ci@arcodange.fr" # Extract coverage values (remove % sign) BDD_COV=${DLC_BDD_COVERAGE%"%"} UNIT_COV=${DLC_UNIT_COVERAGE%"%"} # Update BDD coverage badge if value is set (use --no-push to avoid race conditions) if [ -n "$BDD_COV" ]; then echo "📊 Updating BDD coverage badge to ${BDD_COV}%" ./scripts/ci-update-coverage-badge.sh "$BDD_COV" "bdd" --no-push fi # Update Unit coverage badge if value is set (use --no-push to avoid race conditions) if [ -n "$UNIT_COV" ]; then echo "📊 Updating Unit coverage badge to ${UNIT_COV}%" ./scripts/ci-update-coverage-badge.sh "$UNIT_COV" "unit" --no-push fi # Check for version bump on main branch if [ "${{ github.ref }}" = "refs/heads/main" ]; then echo "🔖 Checking for version bump..." # Always read from git log: ${{ github.event.head_commit.message }} expression # is interpolated literally into the shell script, so any backtick, unbalanced # quote, or special char in a commit body breaks the next line of the script # (observed on PR #32-#35: 'syntax error: unexpected newline'). git log is safe. COMMIT_MSG=$(git log -1 --pretty=%B) ./scripts/ci-version-bump.sh "$COMMIT_MSG" --no-push fi # Single push for all commits (this is the ONLY push in the entire workflow) if [ -n "$(git status --porcelain)" ]; then echo "💾 Changes detected, pushing all commits..." git push echo "🎉 Successfully pushed all updates" else echo "â„šī¸ No changes to push" fi # Trigger Docker push workflow on main branch trigger-docker-push: name: Trigger Docker Push needs: [build-cache, ci-pipeline] runs-on: ubuntu-latest-ca if: "!contains(github.event.head_commit.message, '[skip ci]') && github.actor != 'ci-bot' && github.ref == 'refs/heads/main'" steps: - name: Trigger Docker Push Workflow run: | echo "🚀 Triggering Docker Push workflow..." curl -X POST \ -H "Authorization: token ${{ secrets.GITEA_TOKEN || secrets.PACKAGES_TOKEN }}" \ -H "Content-Type: application/json" \ "${{ env.GITEA_INTERNAL }}api/v1/repos/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}/actions/workflows/docker-push.yaml/dispatches" \ -d '{"ref":"${{ github.ref }}"}' echo "✅ Docker Push workflow triggered successfully!"