--- # 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 (inline, no script needed) DEPS_HASH=$(sha256sum go.mod go.sum | sha256sum | cut -d' ' -f1 | head -c 12) echo "Dependency hash: $DEPS_HASH" echo "deps_hash=$DEPS_HASH" >> $GITHUB_OUTPUT - name: Check for existing cache id: check_cache run: | # Check if image exists in registry IMAGE_NAME="${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}-build-cache:${{ steps.calculate_hash.outputs.deps_hash }}" # Try to pull the image to see if it exists if docker pull "$IMAGE_NAME" >/dev/null 2>&1; then echo "✅ Cache hit - using existing build cache" echo "cache_hit=true" >> $GITHUB_OUTPUT else echo "⚠️ Cache miss - will build new cache image" echo "cache_hit=false" >> $GITHUB_OUTPUT 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 if: "!contains(github.event.head_commit.message, '[skip ci]') && github.actor != 'ci-bot'" steps: - name: Checkout code uses: actions/checkout@v4 # - name: Install Docker Compose # run: sudo apt-get update && sudo apt-get install -y docker-compose-plugin - name: Start PostgreSQL with Docker Compose run: docker compose -f docker-compose.yml up -d postgres - name: Wait for PostgreSQL to be ready run: | echo "Waiting for PostgreSQL to be ready..." for i in {1..30}; do if docker exec dance-lessons-coach-postgres pg_isready -U postgres; then echo "✅ PostgreSQL is ready!" break fi echo "Waiting for PostgreSQL... ($i/30)" sleep 2 done # Verify PostgreSQL is accessible if ! docker exec dance-lessons-coach-postgres pg_isready -U postgres; then echo "❌ PostgreSQL failed to start" exit 1 fi - name: Set up build environment run: | IMAGE_NAME="${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}-build-cache:${{ needs.build-cache.outputs.deps_hash }}" echo "Build cache image: $IMAGE_NAME" echo "CACHE_IMAGE=$IMAGE_NAME" >> $GITHUB_ENV echo "DEPS_HASH=${{ needs.build-cache.outputs.deps_hash }}" >> $GITHUB_ENV # Cache is guaranteed to be available since build-cache job succeeded echo "✅ Using Docker build cache (guaranteed by build-cache job)" echo "CACHE_AVAILABLE=true" >> $GITHUB_ENV - name: Export DEPS_HASH for Docker Compose run: echo "DEPS_HASH=${{ needs.build-cache.outputs.deps_hash }}" >> $GITHUB_ENV - name: Generate Swagger Docs using Docker Compose run: cd /workspace/arcodange/dance-lessons-coach && docker compose -f docker-compose.build.yml run --rm -w /workspace build-cache sh -c "go generate ./pkg/server/" - name: Build all packages using Docker Compose run: cd /workspace/arcodange/dance-lessons-coach && docker compose -f docker-compose.build.yml run --rm -w /workspace build-cache sh -c "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 localhost -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 localhost -p 5432 -U postgres -d dance_lessons_coach_bdd_test; then echo "❌ PostgreSQL failed to start" exit 1 fi - name: Run tests with coverage using Docker Compose run: | echo "Running in Docker Compose container with PostgreSQL..." docker compose -f docker-compose.build.yml exec \ -e PGHOST=dance-lessons-coach-postgres \ -e PGPORT=5432 \ -e PGUSER=postgres \ -e PGPASSWORD=postgres \ -e PGDATABASE=dance_lessons_coach_bdd_test \ -w /workspace \ build-cache \ sh -c "go test ./... -coverprofile=coverage.out -v && go tool cover -func=coverage.out > coverage.txt" # Extract coverage percentage COVERAGE=$(grep "total:" coverage.txt | grep -oP '\d+\.\d+' | head -1) echo "Coverage: ${COVERAGE}%" # Update coverage badge using script export PACKAGES_TOKEN="${{ secrets.PACKAGES_TOKEN }}" export GITHUB_REF_NAME="${{ github.ref_name }}" ./scripts/ci-update-coverage-badge.sh "$COVERAGE" - name: Run go fmt run: go fmt ./... - name: Run swag fmt run: swag fmt - name: Build binaries run: docker compose -f docker-compose.build.yml exec -w /workspace build-cache sh -c "./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 # Docker build and push (main branch only) - name: Login to Gitea Container Registry if: github.ref == 'refs/heads/main' uses: docker/login-action@v3 with: registry: ${{ env.CI_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.PACKAGES_TOKEN }} - name: Build and push Docker image if: github.ref == 'refs/heads/main' run: | source VERSION IMAGE_VERSION="$MAJOR.$MINOR.$PATCH${PRERELEASE:+-$PRERELEASE}" # Generate Dockerfile.prod with correct dependency hash DEPS_HASH="${{ needs.build-cache.outputs.deps_hash }}" echo "Using dependency hash: $DEPS_HASH" # Create Dockerfile.prod with the correct cache image tag cat > docker/Dockerfile.prod << EOF # dance-lessons-coach Production Docker Image # Generated by CI/CD pipeline with dependency hash: $DEPS_HASH # Use the build cache image as base FROM gitea.arcodange.lab/arcodange/dance-lessons-coach-build-cache:$DEPS_HASH AS builder # Final minimal image FROM alpine:3.18 WORKDIR /app # Install minimal dependencies RUN apk add --no-cache ca-certificates tzdata # Copy binary from builder COPY --from=builder /workspace/dance-lessons-coach /app/dance-lessons-coach # Copy configuration COPY config.yaml /app/config.yaml # Set permissions RUN chmod +x /app/dance-lessons-coach # Set timezone ENV TZ=UTC # Expose port EXPOSE 8080 # Health check HEALTHCHECK --interval=30s --timeout=3s \ CMD wget -q --spider http://localhost:8080/api/health || exit 1 # Entry point ENTRYPOINT ["/app/dance-lessons-coach"] EOF TAGS="$IMAGE_VERSION latest ${{ github.sha }}" echo "Building Docker image with tags: $TAGS" # Build the production image docker build -t dance-lessons-coach -f docker/Dockerfile.prod . for TAG in $TAGS; do IMAGE_NAME="${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}:$TAG" echo "Tagging and pushing: $IMAGE_NAME" docker tag dance-lessons-coach "$IMAGE_NAME" docker push "$IMAGE_NAME" done - name: Show published images if: github.ref == 'refs/heads/main' run: | source VERSION IMAGE_VERSION="$MAJOR.$MINOR.$PATCH${PRERELEASE:+-$PRERELEASE}" echo "📦 Published Docker images:" echo " - ${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}:$IMAGE_VERSION" echo " - ${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}:latest" echo " - ${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}:${{ github.sha }}"