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
368 lines
No EOL
16 KiB
YAML
368 lines
No EOL
16 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"
|
|
|
|
# Copy Harbor certificate to DinD container
|
|
docker cp /etc/ssl/registry/registry.crt ci-dind:/usr/local/share/ca-certificates/
|
|
docker exec ci-dind chown root:root /usr/local/share/ca-certificates/registry.crt
|
|
docker exec ci-dind update-ca-certificates
|
|
|
|
# Login to Harbor registry (using HTTPS port 443)
|
|
echo "${{ secrets.HARBOR_CI_PASSWORD }}" | docker exec -i ci-dind docker login ${{ secrets.CI_HOST }}:443 -u ${{ secrets.HARBOR_CI_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 Harbor base images
|
|
run: |
|
|
# Set environment variables
|
|
export CI_HOST="${{ secrets.CI_HOST }}"
|
|
export APP_NAME="${{ secrets.APP_NAME || 'sharenet' }}"
|
|
export HARBOR_CI_USER="${{ secrets.HARBOR_CI_USER }}"
|
|
export HARBOR_CI_PASSWORD="${{ secrets.HARBOR_CI_PASSWORD }}"
|
|
|
|
# Login to Harbor
|
|
echo "Logging into Harbor registry..."
|
|
echo "$HARBOR_CI_PASSWORD" | docker exec -i ci-dind docker login "$CI_HOST:443" -u "$HARBOR_CI_USER" --password-stdin
|
|
|
|
# Check if base images exist in Harbor, 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)
|
|
harbor_image="$CI_HOST:443/$APP_NAME/$image_name:$image_tag"
|
|
|
|
echo "Checking if $harbor_image exists in Harbor..."
|
|
|
|
# Try to pull from Harbor first
|
|
if docker exec ci-dind docker pull "$harbor_image" 2>/dev/null; then
|
|
echo "✓ Found $harbor_image in Harbor"
|
|
else
|
|
echo "✗ $harbor_image not found in Harbor, 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 Harbor
|
|
docker exec ci-dind docker tag "$image" "$harbor_image"
|
|
|
|
# Push to Harbor
|
|
if docker exec ci-dind docker push "$harbor_image"; then
|
|
echo "✓ Successfully pushed $harbor_image to Harbor"
|
|
else
|
|
echo "✗ Failed to push $harbor_image to Harbor"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "✗ Failed to pull $image from Docker Hub"
|
|
exit 1
|
|
fi
|
|
fi
|
|
done
|
|
|
|
echo "All base images are ready in Harbor!"
|
|
|
|
- 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 Harbor access
|
|
run: |
|
|
# Configure Docker to access Harbor registry on CI Linode (using HTTPS)
|
|
# The Harbor certificate should already be installed on the production server
|
|
# as described in the CI guide Step 13
|
|
|
|
# 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 Harbor registry
|
|
echo "Pulling latest images from Harbor 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 |