name: CI/CD Pipeline (Fully Isolated DinD) on: push: branches: [ main, develop ] pull_request: branches: [ main ] env: REGISTRY: ${{ secrets.CI_HOST }}:443 IMAGE_NAME: ${{ secrets.APP_NAME || 'sharenet' }} jobs: # Job 1: Testing - Uses DinD with multiple containers for comprehensive testing test: name: Run Tests (DinD) runs-on: ci if: ${{ startsWith(gitea.ref, 'refs/heads/main') }} steps: - name: Setup DinD Environment run: | # Check if DinD container exists (running or not) if docker ps -a --format "table {{.Names}}" | grep -q "^ci-dind$"; then echo "DinD container exists, checking status..." # Check if it's running if docker ps --format "table {{.Names}}" | grep -q "^ci-dind$"; then echo "DinD container is running, reusing existing setup" # Verify DinD is still working docker exec ci-dind docker version else echo "DinD container exists but is not running, starting it..." docker start ci-dind # Wait for DinD container to be fully ready echo "Waiting for DinD container to be ready..." timeout 30 bash -c 'until docker exec ci-dind docker version > /dev/null 2>&1; do echo "Waiting for Docker daemon inside DinD..."; sleep 5; done' echo "DinD container is ready" fi else echo "Starting new DinD container..." # Start DinD container for isolated CI operations docker run -d \ --name ci-dind \ --privileged \ -p 2375:2375 \ -e DOCKER_TLS_CERTDIR="" \ docker:dind # Wait for DinD to be ready echo "Waiting for DinD container to be ready..." timeout 15 bash -c 'until docker exec ci-dind docker version > /dev/null 2>&1; do echo "Waiting for Docker daemon inside DinD..."; sleep 5; done' echo "DinD container is ready" # Login to Docker Registry (using HTTPS port 443) echo "${{ secrets.REGISTRY_PASSWORD }}" | docker exec -i ci-dind docker login ${{ secrets.CI_HOST }}:443 -u ${{ secrets.REGISTRY_USER }} --password-stdin echo "DinD container setup complete" fi - name: Checkout code to workspace run: | # Use the pre-configured workspace directory (created in CI guide Step 7.3) # Clone the repository to workspace rm -rf /tmp/ci-workspace /tmp/ci-workspace/.* 2>/dev/null || true git clone "${{ gitea.server_url }}/${{ gitea.repository }}" /tmp/ci-workspace cd /tmp/ci-workspace git checkout "${{ gitea.sha }}" # Copy workspace to DinD container docker exec ci-dind rm -rf /workspace/* /workspace/.* 2>/dev/null || true docker cp /tmp/ci-workspace/. ci-dind:/workspace/ - name: Check and prepare Docker Registry base images run: | # Set environment variables export CI_HOST="${{ secrets.CI_HOST }}" export APP_NAME="${{ secrets.APP_NAME || 'sharenet' }}" export REGISTRY_USER="${{ secrets.REGISTRY_USER }}" export REGISTRY_PASSWORD="${{ secrets.REGISTRY_PASSWORD }}" # Login to Docker Registry echo "Logging into Docker Registry..." echo "$REGISTRY_PASSWORD" | docker exec -i ci-dind docker login "$CI_HOST:443" -u "$REGISTRY_USER" --password-stdin # Check if base images exist in Docker Registry, pull from Docker Hub if not BASE_IMAGES=("rust:1.75-slim" "node:20-slim" "postgres:15-alpine") for image in "${BASE_IMAGES[@]}"; do image_name=$(echo "$image" | cut -d: -f1) image_tag=$(echo "$image" | cut -d: -f2) registry_image="$CI_HOST:443/$APP_NAME/$image_name:$image_tag" echo "Checking if $registry_image exists in Docker Registry..." # Try to pull from Docker Registry first if docker exec ci-dind docker pull "$registry_image" 2>/dev/null; then echo "✓ Found $registry_image in Docker Registry" else echo "✗ $registry_image not found in Docker Registry, pulling from Docker Hub..." # Pull from Docker Hub if docker exec ci-dind docker pull "$image"; then echo "✓ Successfully pulled $image from Docker Hub" # Tag for Docker Registry docker exec ci-dind docker tag "$image" "$registry_image" # Push to Docker Registry if docker exec ci-dind docker push "$registry_image"; then echo "✓ Successfully pushed $registry_image to Docker Registry" else echo "✗ Failed to push $registry_image to Docker Registry" exit 1 fi else echo "✗ Failed to pull $image from Docker Hub" exit 1 fi fi done echo "All base images are ready in Docker Registry!" - name: Start testing environment run: | # Start testing environment using dedicated compose file inside DinD echo "Starting testing environment..." # Set environment variables for docker-compose export CI_HOST="${{ secrets.CI_HOST }}" export APP_NAME="${{ secrets.APP_NAME || 'sharenet' }}" # Start the testing environment with build args docker exec ci-dind sh -c "export CI_HOST='$CI_HOST' && export APP_NAME='$APP_NAME' && docker compose -f /workspace/docker-compose.test.yml up -d --build" # Wait for all services to be ready with better error handling echo "Waiting for testing environment to be ready..." MAX_WAIT=180 WAIT_COUNT=0 while [ $WAIT_COUNT -lt $MAX_WAIT ]; do # Check if all containers are running RUNNING_CONTAINERS=$(docker exec ci-dind docker compose -f /workspace/docker-compose.test.yml ps -q | wc -l) EXPECTED_CONTAINERS=3 # postgres, rust-toolchain, node-toolchain if [ "$RUNNING_CONTAINERS" -eq "$EXPECTED_CONTAINERS" ]; then echo "All containers are running" break else echo "Waiting for containers to start... ($RUNNING_CONTAINERS/$EXPECTED_CONTAINERS running)" sleep 2 WAIT_COUNT=$((WAIT_COUNT + 2)) fi done if [ $WAIT_COUNT -ge $MAX_WAIT ]; then echo "ERROR: Timeout waiting for containers to start" echo "Container status:" docker exec ci-dind docker compose -f /workspace/docker-compose.test.yml ps echo "Container logs:" docker exec ci-dind docker compose -f /workspace/docker-compose.test.yml logs exit 1 fi # Additional wait for PostgreSQL to be healthy echo "Waiting for PostgreSQL to be healthy..." timeout 60 bash -c 'until docker exec ci-dind docker exec ci-cd-test-postgres pg_isready -h localhost -p 5432 -U postgres; do sleep 1; done' # Verify all containers are running echo "Final container status:" docker exec ci-dind docker compose -f /workspace/docker-compose.test.yml ps - name: Install SQLx CLI in Rust container run: | docker exec ci-dind docker exec ci-cd-test-rust cargo install sqlx-cli --no-default-features --features postgres - name: Validate migration files env: DATABASE_URL: postgres://postgres:password@localhost:5432/sharenet_test run: | # Wait for PostgreSQL to be ready echo "Waiting for PostgreSQL to be ready..." timeout 60 bash -c 'until docker exec ci-dind docker exec ci-cd-test-postgres pg_isready -h localhost -p 5432 -U postgres; do sleep 1; done' # Create test database if it doesn't exist docker exec ci-dind docker exec ci-cd-test-rust sqlx database create --database-url "$DATABASE_URL" || true # Run initial migrations to set up the database docker exec ci-dind docker exec ci-cd-test-rust sqlx migrate run --database-url "$DATABASE_URL" || true # Validate migration files docker exec ci-dind docker exec ci-cd-test-rust ./scripts/validate_migrations.sh --verbose - name: Run backend tests working-directory: ./backend env: DATABASE_URL: postgres://postgres:password@localhost:5432/sharenet_test run: | # Run tests with increased parallelism for Rust docker exec ci-dind docker exec ci-cd-test-rust cargo test --all --jobs 4 docker exec ci-dind docker exec ci-cd-test-rust cargo clippy --all -- -D warnings docker exec ci-dind docker exec ci-cd-test-rust cargo fmt --all -- --check - name: Install frontend dependencies run: | docker exec ci-dind docker exec ci-cd-test-node npm ci - name: Run frontend tests run: | docker exec ci-dind docker exec ci-cd-test-node npm run lint docker exec ci-dind docker exec ci-cd-test-node npm run type-check docker exec ci-dind docker exec ci-cd-test-node npm run build - name: Cleanup Testing Environment if: always() run: | # Stop and remove all testing containers (but keep DinD running) docker exec ci-dind docker compose -f /workspace/docker-compose.test.yml down # Job 2: Building - Build and push Docker images using same DinD build-and-push: name: Build and Push Docker Images (DinD) needs: [test] runs-on: ci if: ${{ startsWith(gitea.ref, 'refs/heads/main') }} steps: - name: Set up Docker Buildx in DinD run: | # Set up Docker Buildx inside the existing DinD container docker exec ci-dind docker buildx create --use --name ci-builder || true docker exec ci-dind docker buildx inspect --bootstrap # Ensure code is available in DinD (reuse from test job) docker exec ci-dind sh -c "cd /workspace && git fetch && git reset --hard origin/${{ gitea.ref_name }}" # Verify we have the correct repository docker exec ci-dind sh -c "cd /workspace && git remote -v" - name: Build and push backend image run: | # Build and push backend image using DinD docker exec ci-dind docker buildx build \ --platform linux/amd64 \ --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/backend:${{ gitea.sha }} \ --push \ --cache-from type=gha \ --cache-to type=gha,mode=max \ -f /workspace/backend/Dockerfile \ /workspace/backend - name: Build and push frontend image run: | # Build and push frontend image using DinD docker exec ci-dind docker buildx build \ --platform linux/amd64 \ --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/frontend:${{ gitea.sha }} \ --push \ --cache-from type=gha \ --cache-to type=gha,mode=max \ -f /workspace/frontend/Dockerfile \ /workspace/frontend - name: Cleanup Testing Environment if: always() run: | # Clean up test containers but keep DinD running for reuse docker exec ci-dind docker system prune -f || true # Check if DinD needs restart due to resource accumulation DISK_USAGE=$(docker exec ci-dind df -h /var/lib/docker 2>/dev/null | tail -1 | awk '{print $5}' | sed 's/%//' || echo "0") echo "DinD disk usage: ${DISK_USAGE}%" # Restart DinD if disk usage is high (>80%) if [ "$DISK_USAGE" -gt 80 ]; then echo "WARNING: High disk usage (${DISK_USAGE}%), restarting DinD container..." docker restart ci-dind echo "DinD container restarted" else echo "Disk usage acceptable (${DISK_USAGE}%), keeping DinD running" fi # Job 3: Deployment - Runs directly on production runner (no DinD needed) deploy: name: Deploy to Production needs: build-and-push runs-on: prod if: ${{ startsWith(gitea.ref, 'refs/heads/main') }} steps: - name: Setup deployment directory run: | # Create deployment directory if it doesn't exist sudo mkdir -p /opt/${{ secrets.APP_NAME || 'sharenet' }} sudo chown ${{ secrets.PROD_SERVICE_USER || 'prod-service' }}:${{ secrets.PROD_SERVICE_USER || 'prod-service' }} /opt/${{ secrets.APP_NAME || 'sharenet' }} sudo chmod 755 /opt/${{ secrets.APP_NAME || 'sharenet' }} - name: Checkout code to deployment directory uses: actions/checkout@v4 with: path: /opt/${{ secrets.APP_NAME || 'sharenet' }} - name: Set proper ownership run: | # Ensure proper ownership of all files sudo chown -R ${{ secrets.PROD_SERVICE_USER || 'prod-service' }}:${{ secrets.PROD_SERVICE_USER || 'prod-service' }} /opt/${{ secrets.APP_NAME || 'sharenet' }} # Change to deployment directory for all subsequent operations cd /opt/${{ secrets.APP_NAME || 'sharenet' }} - name: Create environment file for deployment run: | # Create environment file for this deployment echo "IMAGE_TAG=${{ gitea.sha }}" > .env echo "REGISTRY=${{ secrets.CI_HOST }}:443" >> .env echo "IMAGE_NAME=${{ secrets.APP_NAME || 'sharenet' }}" >> .env echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD || 'your_secure_password_here' }}" >> .env echo "POSTGRES_USER=${{ secrets.POSTGRES_USER || 'sharenet' }}" >> .env echo "POSTGRES_DB=${{ secrets.POSTGRES_DB || 'sharenet' }}" >> .env echo "DATABASE_URL=postgresql://${{ secrets.POSTGRES_USER || 'sharenet' }}:${{ secrets.POSTGRES_PASSWORD || 'your_secure_password_here' }}@postgres:5432/${{ secrets.POSTGRES_DB || 'sharenet' }}" >> .env echo "NODE_ENV=production" >> .env echo "RUST_LOG=info" >> .env - name: Make scripts executable run: chmod +x scripts/*.sh - name: Configure Docker for Docker Registry access run: | # Configure Docker to access Docker Registry on CI Linode (using HTTPS) # Since we're using Caddy with automatic HTTPS, no certificate configuration is needed # Wait for Docker to be ready timeout 30 bash -c 'until docker info; do sleep 1; done' - name: Validate migration files run: | echo "Validating migration files before deployment..." ./scripts/validate_migrations.sh --verbose || { echo "ERROR: Migration validation failed. Deployment aborted." exit 1 } - name: Pull and deploy application run: | # Pull latest images from Docker Registry echo "Pulling latest images from Docker Registry..." docker compose -f docker-compose.prod.yml pull # Deploy the application stack echo "Deploying application stack..." docker compose -f docker-compose.prod.yml up -d # Wait for all services to be healthy echo "Waiting for all services to be healthy..." timeout 120 bash -c 'until docker compose -f docker-compose.prod.yml ps | grep -q "healthy" && docker compose -f docker-compose.prod.yml ps | grep -q "Up"; do sleep 2; done' # Verify deployment echo "Verifying deployment..." docker compose -f docker-compose.prod.yml ps