name: CI/CD Pipeline (Forgejo Container Registry) on: push: branches: [ main, develop ] pull_request: branches: [ main ] env: REGISTRY_HOST: ${{ secrets.REGISTRY_HOST }} OWNER_REPO: ${{ gitea.repository }} 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" # Install Cosign in DinD container (pinned version) echo "Installing Cosign..." docker exec ci-dind sh -c "COSIGN_VERSION=v2.2.4 && wget -O /usr/local/bin/cosign https://github.com/sigstore/cosign/releases/download/\${COSIGN_VERSION}/cosign-linux-amd64 && chmod +x /usr/local/bin/cosign" echo "DinD container setup complete" fi - name: Checkout code to workspace run: | # Use the pre-configured workspace directory (created in CI guide Step 6.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 base images run: | # Set environment variables export REGISTRY_HOST="${{ secrets.REGISTRY_HOST }}" export OWNER_REPO="${{ gitea.repository }}" export REGISTRY_USERNAME="${{ secrets.REGISTRY_USERNAME }}" export REGISTRY_TOKEN="${{ secrets.REGISTRY_TOKEN }}" # Login to Forgejo Container Registry echo "Logging into Forgejo Container Registry..." echo "$REGISTRY_TOKEN" | docker exec -i ci-dind docker login "$REGISTRY_HOST" -u "$REGISTRY_USERNAME" --password-stdin # Check if base images exist in Forgejo Container 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="$REGISTRY_HOST/$OWNER_REPO/$image_name:$image_tag" echo "Checking if $registry_image exists in Forgejo Container Registry..." # Try to pull from Forgejo Container Registry first if docker exec ci-dind docker pull "$registry_image" 2>/dev/null; then echo "✓ Found $registry_image in Forgejo Container Registry" else echo "✗ $registry_image not found in Forgejo Container 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 Forgejo Container Registry docker exec ci-dind docker tag "$image" "$registry_image" # Push to Forgejo Container Registry if docker exec ci-dind docker push "$registry_image"; then echo "✓ Successfully pushed $registry_image to Forgejo Container Registry" # Sign the image with Cosign (optional) if [ -n "${{ secrets.COSIGN_PRIVATE_KEY }}" ]; then echo "Signing image with Cosign..." echo "${{ secrets.COSIGN_PRIVATE_KEY }}" | docker exec -i ci-dind sh -c "cat > /tmp/cosign.key && chmod 600 /tmp/cosign.key" if docker exec ci-dind sh -c "COSIGN_PASSWORD='${{ secrets.COSIGN_PASSWORD }}' cosign sign -y --key /tmp/cosign.key $registry_image"; then echo "✓ Successfully signed $registry_image with Cosign" else echo "✗ Failed to sign $registry_image with Cosign" exit 1 fi docker exec ci-dind rm -f /tmp/cosign.key else echo "Skipping Cosign signing (no private key provided)" fi else echo "✗ Failed to push $registry_image to Forgejo Container 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 Forgejo Container Registry!" - name: Start testing environment run: | # Start testing environment using Kubernetes pod inside DinD echo "Starting testing environment..." # Set environment variables export CI_HOST="${{ secrets.CI_HOST }}" export APP_NAME="${{ secrets.APP_NAME || 'sharenet' }}" # Create workspace directory and start pod docker exec ci-dind sh -c "mkdir -p /tmp/ci-workspace && cp -r /workspace/* /tmp/ci-workspace/" docker exec ci-dind sh -c "podman play kube /workspace/ci-pod.yaml" # 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 pod is running and ready POD_STATUS=$(docker exec ci-dind podman pod ps --filter name=ci-cd-test-pod --format "{{.Status}}" 2>/dev/null || echo "") if [ "$POD_STATUS" = "Running" ]; then echo "Pod is running" break else echo "Waiting for pod to start... (Status: $POD_STATUS)" sleep 2 WAIT_COUNT=$((WAIT_COUNT + 2)) fi done if [ $WAIT_COUNT -ge $MAX_WAIT ]; then echo "ERROR: Timeout waiting for pod to start" echo "Pod status:" docker exec ci-dind podman pod ps echo "Pod logs:" docker exec ci-dind podman logs ci-cd-test-pod-postgres || true 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 podman exec ci-cd-test-pod-postgres pg_isready -h localhost -p 5432 -U postgres; do sleep 1; done' # Verify pod is running echo "Final pod status:" docker exec ci-dind podman pod ps - name: Install SQLx CLI in Rust container run: | docker exec ci-dind podman exec ci-cd-test-pod-rust-toolchain 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 podman exec ci-cd-test-pod-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 podman exec ci-cd-test-pod-rust-toolchain sqlx database create --database-url "$DATABASE_URL" || true # Run initial migrations to set up the database docker exec ci-dind podman exec ci-cd-test-pod-rust-toolchain sqlx migrate run --database-url "$DATABASE_URL" || true # Validate migration files docker exec ci-dind podman exec ci-cd-test-pod-rust-toolchain ./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 podman exec ci-cd-test-pod-rust-toolchain cargo test --all --jobs 4 docker exec ci-dind podman exec ci-cd-test-pod-rust-toolchain cargo clippy --all -- -D warnings docker exec ci-dind podman exec ci-cd-test-pod-rust-toolchain cargo fmt --all -- --check - name: Install frontend dependencies run: | docker exec ci-dind podman exec ci-cd-test-pod-node-toolchain npm ci - name: Run frontend tests run: | docker exec ci-dind podman exec ci-cd-test-pod-node-toolchain npm run lint docker exec ci-dind podman exec ci-cd-test-pod-node-toolchain npm run type-check docker exec ci-dind podman exec ci-cd-test-pod-node-toolchain npm run build - name: Cleanup Testing Environment if: always() run: | # Stop and remove testing pod (but keep DinD running) docker exec ci-dind podman pod stop ci-cd-test-pod || true docker exec ci-dind podman pod rm ci-cd-test-pod || true # 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: Login to Forgejo registry run: | docker exec ci-dind docker login "${{ env.REGISTRY_HOST }}" \ -u "${{ secrets.REGISTRY_USERNAME }}" \ -p "${{ secrets.REGISTRY_TOKEN }}" - name: Build and push backend image env: IMAGE: ${{ env.REGISTRY_HOST }}/${{ env.OWNER_REPO }}/backend TAG: ${{ gitea.sha }} run: | # Build and push backend image using DinD docker exec ci-dind docker buildx build \ --platform linux/amd64 \ --tag "${IMAGE}:${TAG}" \ --push \ --cache-from type=gha \ --cache-to type=gha,mode=max \ -f /workspace/backend/Dockerfile \ /workspace/backend # Sign the backend image with Cosign (optional) if [ -n "${{ secrets.COSIGN_PRIVATE_KEY }}" ]; then echo "Signing backend image with Cosign..." echo "${{ secrets.COSIGN_PRIVATE_KEY }}" | docker exec -i ci-dind sh -c "cat > /tmp/cosign.key && chmod 600 /tmp/cosign.key" DIGEST=$(docker exec ci-dind docker image inspect "${IMAGE}:${TAG}" --format '{{index .RepoDigests 0}}' | cut -d'@' -f2) docker exec ci-dind sh -c "COSIGN_PASSWORD='${{ secrets.COSIGN_PASSWORD }}' cosign sign -y --key /tmp/cosign.key ${IMAGE}@${DIGEST}" docker exec ci-dind rm -f /tmp/cosign.key else echo "Skipping Cosign signing (no private key provided)" fi - name: Build and push frontend image env: IMAGE: ${{ env.REGISTRY_HOST }}/${{ env.OWNER_REPO }}/frontend TAG: ${{ gitea.sha }} run: | # Build and push frontend image using DinD docker exec ci-dind docker buildx build \ --platform linux/amd64 \ --tag "${IMAGE}:${TAG}" \ --push \ --cache-from type=gha \ --cache-to type=gha,mode=max \ -f /workspace/frontend/Dockerfile \ /workspace/frontend # Sign the frontend image with Cosign (optional) if [ -n "${{ secrets.COSIGN_PRIVATE_KEY }}" ]; then echo "Signing frontend image with Cosign..." echo "${{ secrets.COSIGN_PRIVATE_KEY }}" | docker exec -i ci-dind sh -c "cat > /tmp/cosign.key && chmod 600 /tmp/cosign.key" DIGEST=$(docker exec ci-dind docker image inspect "${IMAGE}:${TAG}" --format '{{index .RepoDigests 0}}' | cut -d'@' -f2) docker exec ci-dind sh -c "COSIGN_PASSWORD='${{ secrets.COSIGN_PASSWORD }}' cosign sign -y --key /tmp/cosign.key ${IMAGE}@${DIGEST}" docker exec ci-dind rm -f /tmp/cosign.key else echo "Skipping Cosign signing (no private key provided)" fi - 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_HOST=${{ secrets.REGISTRY_HOST }}" >> .env echo "OWNER_REPO=${{ gitea.repository }}" >> .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 Forgejo Container Registry access run: | # Configure Docker to access Forgejo Container Registry # Since we're using Forgejo's built-in registry, no certificate configuration is needed # Wait for Docker to be ready timeout 30 bash -c 'until docker info; do sleep 1; done' # Verify signed images before deployment (if Cosign is configured) if [ -n "${{ secrets.COSIGN_PRIVATE_KEY }}" ]; then echo "Verifying signed images..." cosign verify --key /etc/containers/keys/org-cosign.pub ${{ secrets.REGISTRY_HOST }}/${{ gitea.repository }}/backend:${{ gitea.sha }} cosign verify --key /etc/containers/keys/org-cosign.pub ${{ secrets.REGISTRY_HOST }}/${{ gitea.repository }}/frontend:${{ gitea.sha }} else echo "Skipping image verification (no Cosign key configured)" fi - 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: Deploy application using Kubernetes pod run: | # Set environment variables for the pod deployment export IMAGE_TAG="${{ gitea.sha }}" export REGISTRY_HOST="${{ secrets.REGISTRY_HOST }}" export OWNER_REPO="${{ gitea.repository }}" export POSTGRES_PASSWORD="${{ secrets.POSTGRES_PASSWORD || 'your_secure_password_here' }}" export POSTGRES_USER="${{ secrets.POSTGRES_USER || 'sharenet' }}" export POSTGRES_DB="${{ secrets.POSTGRES_DB || 'sharenet' }}" # Stop any existing production pod podman pod stop sharenet-production-pod || true podman pod rm sharenet-production-pod || true # Deploy the application pod with environment substitution echo "Deploying application pod..." envsubst < prod-pod.yaml | podman play kube - # Wait for pod to be ready echo "Waiting for pod to be ready..." timeout 120 bash -c 'until podman pod ps --filter name=sharenet-production-pod --format "{{.Status}}" | grep -q "Running"; do sleep 2; done' # Verify deployment echo "Verifying deployment..." podman pod ps podman pod logs sharenet-production-pod