sharenet/.forgejo/workflows/ci.yml
continuist 03dac72b90
Some checks are pending
CI/CD Pipeline (Fully Isolated DinD) / Run Tests (DinD) (push) Waiting to run
CI/CD Pipeline (Fully Isolated DinD) / Build and Push Docker Images (DinD) (push) Blocked by required conditions
CI/CD Pipeline (Fully Isolated DinD) / Deploy to Production (push) Blocked by required conditions
Switch to Caddy + Docker Registry
2025-07-13 10:26:07 -04:00

362 lines
No EOL
15 KiB
YAML

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