Separate concerns properly: - Badge updates: run on all branches to keep documentation accurate - Docker builds: only on main branch for production deployments This follows best practices where documentation should be updated regardless of branch, but deployments only happen from main.
353 lines
13 KiB
YAML
353 lines
13 KiB
YAML
---
|
||
# 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
|
||
if: "!contains(github.event.head_commit.message, '[skip ci]') && github.actor != 'ci-bot'"
|
||
|
||
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" >> $GITHUB_ENV
|
||
echo "DLC_DATABASE_PASSWORD=postgres" >> $GITHUB_ENV
|
||
echo "DLC_DATABASE_NAME=dance_lessons_coach_bdd_test" >> $GITHUB_ENV
|
||
echo "DLC_DATABASE_SSL_MODE=disable" >> $GITHUB_ENV
|
||
|
||
- name: Generate Swagger Docs
|
||
run: go generate ./pkg/server
|
||
|
||
- 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
|
||
./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
|
||
|
||
# Single badge update step using environment variables (KISS approach)
|
||
# Badge updates should run on all branches to keep documentation accurate
|
||
- name: Update all badges (single commit)
|
||
if: always() && github.actor != 'ci-bot'
|
||
run: |
|
||
echo "🎯 Updating all badges using environment variables..."
|
||
echo "BDD Coverage: ${DLC_BDD_COVERAGE:-Not set}"
|
||
echo "Unit Coverage: ${DLC_UNIT_COVERAGE:-Not set}"
|
||
|
||
# Extract coverage values (remove % sign)
|
||
BDD_COV=${DLC_BDD_COVERAGE%"%"}
|
||
UNIT_COV=${DLC_UNIT_COVERAGE%"%"}
|
||
|
||
# Update badges only if values are set
|
||
if [ -n "$BDD_COV" ] || [ -n "$UNIT_COV" ]; then
|
||
./scripts/update-all-badges.sh "$BDD_COV" "$UNIT_COV"
|
||
|
||
# Configure git
|
||
git config user.name "CI Bot"
|
||
git config user.email "ci@arcodange.fr"
|
||
git add README.md
|
||
|
||
# Commit only if there are changes
|
||
if git commit -m "🤖 chore: update coverage badges [skip ci]"; then
|
||
echo "✅ Badges updated, committing changes..."
|
||
git push
|
||
echo "🎉 Successfully pushed badge updates"
|
||
else
|
||
echo "ℹ️ No badge changes to commit"
|
||
fi
|
||
else
|
||
echo "⚠️ No coverage data available to update badges"
|
||
fi
|
||
|
||
# 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 }}"
|