Files
portfolio/.gitea/workflows/ci.yml
denshooter fa48610e3e
Some checks failed
CI / CD / test-build (push) Failing after 5m31s
CI / CD / deploy-dev (push) Has been skipped
CI / CD / deploy-production (push) Has been skipped
perf: cut CI time, fix CSS chain, fix iPad hero layout, fix contrast
CI:
- Replace `npm run build` with `tsc --noEmit` in test-build job
  → eliminates duplicate Next.js build (~5 min saved per push)
  → Docker deploy job already does the full build

PageSpeed:
- Add `cssChunking: 'loose'` to merge CSS into one chunk and break
  the 84dc7384→3aefc04b render-blocking CSS waterfall chain (450ms mobile)
- Remove @shadergradient/react, @react-three/fiber, three from
  package.json — packages were already unused in code, removes any
  residual bundling risk for chunk 7001

Hero:
- Change lg:flex-row → xl:flex-row so iPad (1024px) stays in column
  layout; the 9.5rem heading overflowed into the image at lg causing
  the photo to be clipped by overflow-hidden on the section
- Update image sizes attribute to match new xl breakpoint
- Fix contrast: "GET IN TOUCH" link text-stone-500 → text-stone-700
  (contrast 3.7:1 → 7:1, now WCAG AA compliant)
- Change text-center/justify-center to xl: variants to match layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 07:18:54 +01:00

280 lines
11 KiB
YAML

name: CI / CD
on:
push:
branches: [main, dev, production]
pull_request:
branches: [main, dev, production]
env:
NODE_VERSION: '25'
DOCKER_IMAGE: portfolio-app
jobs:
# ── Job 1: Lint, Test, Build (runs on every push/PR) ──
test-build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm run test
- name: Type check
run: npx tsc --noEmit
# ── Job 2: Deploy to dev (only on dev branch, after tests pass) ──
deploy-dev:
needs: test-build
if: github.ref == 'refs/heads/dev' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build Docker image
run: |
echo "🏗️ Building dev Docker image..."
DOCKER_BUILDKIT=1 docker build \
--cache-from ${{ env.DOCKER_IMAGE }}:dev \
--cache-from ${{ env.DOCKER_IMAGE }}:latest \
-t ${{ env.DOCKER_IMAGE }}:dev \
.
echo "✅ Docker image built successfully"
- name: Deploy dev container
run: |
echo "🚀 Starting dev deployment..."
CONTAINER_NAME="portfolio-app-dev"
HEALTH_PORT="3001"
IMAGE_NAME="${{ env.DOCKER_IMAGE }}:dev"
# Check for existing container
EXISTING_CONTAINER=$(docker ps -aq -f name=$CONTAINER_NAME || echo "")
# Ensure networks exist
echo "🌐 Ensuring networks exist..."
docker network create portfolio_net 2>/dev/null || true
docker network create proxy 2>/dev/null || true
# Verify production DB is reachable
if docker exec portfolio-postgres pg_isready -U portfolio_user -d portfolio_db >/dev/null 2>&1; then
echo "✅ Production database is ready!"
else
echo "⚠️ Production database not reachable, app will use fallbacks"
fi
# Stop and remove existing container
if [ ! -z "$EXISTING_CONTAINER" ]; then
echo "🛑 Stopping existing container..."
docker stop $EXISTING_CONTAINER 2>/dev/null || true
docker rm $EXISTING_CONTAINER 2>/dev/null || true
sleep 3
fi
# Ensure port is free
PORT_CONTAINER=$(docker ps -a --format "{{.ID}}\t{{.Ports}}" | grep -E "(:${HEALTH_PORT}->)" | awk '{print $1}' | head -1 || echo "")
if [ ! -z "$PORT_CONTAINER" ]; then
echo "⚠️ Port ${HEALTH_PORT} still in use, freeing..."
docker stop $PORT_CONTAINER 2>/dev/null || true
docker rm $PORT_CONTAINER 2>/dev/null || true
sleep 3
fi
# Start new container
echo "🆕 Starting new dev container..."
docker run -d \
--name $CONTAINER_NAME \
--restart unless-stopped \
--network portfolio_net \
-p ${HEALTH_PORT}:3000 \
-e NODE_ENV=production \
-e LOG_LEVEL=${LOG_LEVEL:-debug} \
-e NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL_DEV:-https://dev.dk0.dev} \
-e DATABASE_URL="${DATABASE_URL}" \
-e REDIS_URL="${REDIS_URL}" \
-e MY_EMAIL="${MY_EMAIL}" \
-e MY_INFO_EMAIL="${MY_INFO_EMAIL}" \
-e MY_PASSWORD="${MY_PASSWORD}" \
-e MY_INFO_PASSWORD="${MY_INFO_PASSWORD}" \
-e ADMIN_BASIC_AUTH="${ADMIN_BASIC_AUTH}" \
-e ADMIN_SESSION_SECRET="${ADMIN_SESSION_SECRET}" \
-e N8N_WEBHOOK_URL="${N8N_WEBHOOK_URL}" \
-e N8N_SECRET_TOKEN="${N8N_SECRET_TOKEN}" \
-e N8N_API_KEY="${N8N_API_KEY}" \
-e DIRECTUS_URL="${DIRECTUS_URL}" \
-e DIRECTUS_STATIC_TOKEN="${DIRECTUS_STATIC_TOKEN}" \
$IMAGE_NAME
# Connect to proxy network
docker network connect proxy $CONTAINER_NAME 2>/dev/null || true
# Wait for health
echo "⏳ Waiting for container to be healthy..."
for i in {1..60}; do
if curl -f -s http://localhost:$HEALTH_PORT/api/health > /dev/null 2>&1; then
echo "✅ Dev container is healthy!"
break
fi
HEALTH=$(docker inspect $CONTAINER_NAME --format='{{.State.Health.Status}}' 2>/dev/null || echo "starting")
if [ "$HEALTH" == "healthy" ]; then
echo "✅ Docker health check passed!"
break
fi
if [ $i -eq 60 ]; then
echo "⚠️ Health check timed out, showing logs:"
docker logs $CONTAINER_NAME --tail=30
fi
sleep 2
done
echo "✅ Dev deployment completed!"
env:
LOG_LEVEL: ${{ vars.LOG_LEVEL || 'debug' }}
NEXT_PUBLIC_BASE_URL_DEV: ${{ vars.NEXT_PUBLIC_BASE_URL_DEV || 'https://dev.dk0.dev' }}
DATABASE_URL: postgresql://portfolio_user:portfolio_pass@portfolio-postgres:5432/portfolio_db?schema=public
REDIS_URL: redis://portfolio-redis:6379
MY_EMAIL: ${{ vars.MY_EMAIL }}
MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }}
MY_PASSWORD: ${{ secrets.MY_PASSWORD }}
MY_INFO_PASSWORD: ${{ secrets.MY_INFO_PASSWORD }}
ADMIN_BASIC_AUTH: ${{ secrets.ADMIN_BASIC_AUTH }}
ADMIN_SESSION_SECRET: ${{ secrets.ADMIN_SESSION_SECRET }}
N8N_WEBHOOK_URL: ${{ vars.N8N_WEBHOOK_URL || '' }}
N8N_SECRET_TOKEN: ${{ secrets.N8N_SECRET_TOKEN || '' }}
N8N_API_KEY: ${{ vars.N8N_API_KEY || '' }}
DIRECTUS_URL: ${{ vars.DIRECTUS_URL || 'https://cms.dk0.dev' }}
DIRECTUS_STATIC_TOKEN: ${{ secrets.DIRECTUS_STATIC_TOKEN || '' }}
- name: Cleanup
run: docker image prune -f
# ── Job 3: Deploy to production (only on production branch, after tests pass) ──
deploy-production:
needs: test-build
if: github.ref == 'refs/heads/production' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build Docker image
run: |
echo "🏗️ Building production Docker image..."
DOCKER_BUILDKIT=1 docker build \
--cache-from ${{ env.DOCKER_IMAGE }}:production \
--cache-from ${{ env.DOCKER_IMAGE }}:latest \
-t ${{ env.DOCKER_IMAGE }}:production \
-t ${{ env.DOCKER_IMAGE }}:latest \
.
echo "✅ Docker image built successfully"
- name: Deploy production container
run: |
echo "🚀 Starting production deployment..."
COMPOSE_FILE="docker-compose.production.yml"
CONTAINER_NAME="portfolio-app"
HEALTH_PORT="3000"
# Backup current container ID
OLD_CONTAINER=$(docker ps -q -f "name=^/${CONTAINER_NAME}$" || echo "")
# Ensure network exists
docker network create portfolio_net 2>/dev/null || true
# Export variables for docker-compose
export N8N_WEBHOOK_URL="${N8N_WEBHOOK_URL}"
export N8N_SECRET_TOKEN="${N8N_SECRET_TOKEN}"
export N8N_API_KEY="${N8N_API_KEY}"
export MY_EMAIL="${MY_EMAIL}"
export MY_INFO_EMAIL="${MY_INFO_EMAIL}"
export MY_PASSWORD="${MY_PASSWORD}"
export MY_INFO_PASSWORD="${MY_INFO_PASSWORD}"
export ADMIN_BASIC_AUTH="${ADMIN_BASIC_AUTH}"
export ADMIN_SESSION_SECRET="${ADMIN_SESSION_SECRET}"
export DIRECTUS_URL="${DIRECTUS_URL}"
export DIRECTUS_STATIC_TOKEN="${DIRECTUS_STATIC_TOKEN}"
# Start new container via compose
echo "🆕 Starting new production container..."
docker compose -f $COMPOSE_FILE up -d portfolio
# Wait for health
echo "⏳ Waiting for container to be healthy..."
HEALTH_CHECK_PASSED=false
for i in {1..90}; do
NEW_CONTAINER=$(docker compose -f $COMPOSE_FILE ps -q portfolio 2>/dev/null | head -1)
if [ -z "$NEW_CONTAINER" ]; then
NEW_CONTAINER=$(docker ps -q -f "name=^/${CONTAINER_NAME}$")
fi
if [ ! -z "$NEW_CONTAINER" ]; then
HEALTH=$(docker inspect $NEW_CONTAINER --format='{{.State.Health.Status}}' 2>/dev/null || echo "starting")
if [ "$HEALTH" == "healthy" ]; then
echo "✅ Production container is healthy!"
HEALTH_CHECK_PASSED=true
break
fi
if curl -f -s --max-time 2 http://localhost:$HEALTH_PORT/api/health > /dev/null 2>&1; then
echo "✅ Production HTTP health check passed!"
HEALTH_CHECK_PASSED=true
break
fi
fi
if [ $((i % 15)) -eq 0 ]; then
echo "📊 Health: ${HEALTH:-unknown} (attempt $i/90)"
docker compose -f $COMPOSE_FILE logs --tail=5 portfolio 2>/dev/null || true
fi
sleep 2
done
if [ "$HEALTH_CHECK_PASSED" != "true" ]; then
echo "❌ Production health check failed!"
docker compose -f $COMPOSE_FILE logs --tail=50 portfolio 2>/dev/null || true
exit 1
fi
# Remove old container if different
if [ ! -z "$OLD_CONTAINER" ]; then
NEW_CONTAINER=$(docker ps -q -f "name=^/${CONTAINER_NAME}$")
if [ ! -z "$NEW_CONTAINER" ] && [ "$OLD_CONTAINER" != "$NEW_CONTAINER" ]; then
echo "🧹 Removing old container..."
docker stop $OLD_CONTAINER 2>/dev/null || true
docker rm $OLD_CONTAINER 2>/dev/null || true
fi
fi
echo "✅ Production deployment completed!"
env:
NODE_ENV: production
LOG_LEVEL: ${{ vars.LOG_LEVEL || 'info' }}
NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL_PRODUCTION || 'https://dk0.dev' }}
MY_EMAIL: ${{ vars.MY_EMAIL }}
MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }}
MY_PASSWORD: ${{ secrets.MY_PASSWORD }}
MY_INFO_PASSWORD: ${{ secrets.MY_INFO_PASSWORD }}
ADMIN_BASIC_AUTH: ${{ secrets.ADMIN_BASIC_AUTH }}
ADMIN_SESSION_SECRET: ${{ secrets.ADMIN_SESSION_SECRET }}
N8N_WEBHOOK_URL: ${{ vars.N8N_WEBHOOK_URL || '' }}
N8N_SECRET_TOKEN: ${{ secrets.N8N_SECRET_TOKEN || '' }}
N8N_API_KEY: ${{ vars.N8N_API_KEY || '' }}
DIRECTUS_URL: ${{ vars.DIRECTUS_URL || 'https://cms.dk0.dev' }}
DIRECTUS_STATIC_TOKEN: ${{ secrets.DIRECTUS_STATIC_TOKEN || '' }}
- name: Cleanup
run: docker image prune -f