sharenet/.forgejo/workflows/ci.yml
continuist eb6e373981
Some checks are pending
CI/CD Pipeline (Forgejo Container Registry) / Run Tests (DinD) (push) Waiting to run
CI/CD Pipeline (Forgejo Container Registry) / Build and Push Docker Images (DinD) (push) Blocked by required conditions
CI/CD Pipeline (Forgejo Container Registry) / Deploy to Production (push) Blocked by required conditions
Change to using Forgejo Container Registry
2025-08-30 19:38:54 -04:00

431 lines
No EOL
19 KiB
YAML

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