Skip to content

chore: upstream/downstream flow test (sync-check + lint fix) #36

chore: upstream/downstream flow test (sync-check + lint fix)

chore: upstream/downstream flow test (sync-check + lint fix) #36

Workflow file for this run

# =============================================================================
# GitHub Actions CI/CD Pipeline
# =============================================================================
#
# Required GitHub Secrets:
# -------------------------
# PROJECT_NAME - Unique project identifier (e.g., mysite)
# Used for Docker container/volume names to avoid collisions
# SITE_URL - Production URL (e.g., https://yoursite.com)
# SITE_NAME - Display name for the site (e.g., My Site)
#
# DEPLOY_HOST - Server hostname or IP (e.g., yoursite.com)
# DEPLOY_USER - SSH username for deployment
# DEPLOY_KEY - Private SSH key for authentication
# DEPLOY_PATH - Path on server (e.g., /var/www/yoursite.com)
#
# POSTGRES_USER - PostgreSQL username
# POSTGRES_PASSWORD - PostgreSQL password
# POSTGRES_DB - PostgreSQL database name
#
# NEXTAUTH_SECRET - Secret for NextAuth.js session encryption
# ADMIN_EMAIL - Initial admin email address
# ADMIN_PASSWORD - Initial admin password (will be hashed)
#
# Optional Secrets:
# -----------------
# APP_PORT - Host port (default: 3000, use different for multiple sites)
# CSRF_SECRET - CSRF protection secret
# DOCKERHUB_USERNAME - For pushing to Docker Hub instead of GHCR
# DOCKERHUB_TOKEN - Docker Hub access token
#
# =============================================================================
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
paths-ignore:
- '**.md'
- 'docs/**'
- '.gitignore'
- 'LICENSE'
- '.vscode/**'
- '.idea/**'
- '*.txt'
pull_request:
branches: [main, develop]
paths-ignore:
- '**.md'
- 'docs/**'
- '.gitignore'
- 'LICENSE'
- '.vscode/**'
- '.idea/**'
- '*.txt'
workflow_dispatch: # Allow manual trigger
env:
NODE_VERSION: '22'
PNPM_VERSION: '10'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# ===========================================================================
# Lint & Type Check
# ===========================================================================
lint:
name: Lint & Type Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run ESLint
run: pnpm lint
- name: Run TypeScript type check
run: pnpm type-check
# ===========================================================================
# Unit Tests
# ===========================================================================
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
needs: lint
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run unit tests
run: pnpm test -- --run --coverage
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 7
# ===========================================================================
# Build
# ===========================================================================
build:
name: Build Application
runs-on: ubuntu-latest
needs: [lint, unit-tests]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build application
run: pnpm build
env:
# Dummy env vars for build (not used at runtime)
DATABASE_URL: 'postgresql://user:pass@localhost:5432/db'
NEXTAUTH_URL: 'http://localhost:3000'
NEXTAUTH_SECRET: 'build-secret-not-for-production'
- name: Remove build cache before upload
run: rm -rf .next/cache
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: .next/
retention-days: 1
if-no-files-found: error
include-hidden-files: true
# ===========================================================================
# E2E Tests (Optional - requires test database)
# ===========================================================================
e2e-tests:
name: E2E Tests
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps chromium
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .next/
- name: Run E2E tests
run: pnpm test:e2e --project=chromium
env:
DATABASE_URL: 'postgresql://test:test@localhost:5432/test'
NEXTAUTH_URL: 'http://localhost:3000'
NEXTAUTH_SECRET: 'test-secret'
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
# ===========================================================================
# Docker Build & Push (main branch only)
# ===========================================================================
docker:
name: Build & Push Docker Image
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix=
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ===========================================================================
# Deploy (main branch only, manual trigger available)
# ===========================================================================
deploy:
name: Deploy to Production
runs-on: ubuntu-latest
needs: docker
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
environment: production
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.DEPLOY_KEY }}
- name: Add host to known_hosts
run: |
mkdir -p ~/.ssh
ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
- name: Generate docker-compose.override.yml
run: |
# Generate override file with environment variables baked in
# This file persists on the server and survives restarts
# Uses native PostgreSQL on host instead of Docker PostgreSQL
cat > docker-compose.override.yml << EOF
# =============================================================================
# Production Environment Override
# Auto-generated by GitHub Actions - DO NOT EDIT MANUALLY
# Uses native PostgreSQL on host machine
# =============================================================================
services:
app:
container_name: ${{ secrets.PROJECT_NAME }}-app
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
restart: unless-stopped
ports:
- "${{ secrets.APP_PORT || '3000' }}:3000"
environment:
- NODE_ENV=production
- NEXT_PUBLIC_APP_URL=${{ secrets.SITE_URL }}
- NEXT_PUBLIC_SITE_NAME=${{ secrets.SITE_NAME }}
- DATABASE_URL=postgresql://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@host.docker.internal:5432/${{ secrets.POSTGRES_DB }}
- DATABASE_HOST=host.docker.internal
- DATABASE_PORT=5432
- DATABASE_NAME=${{ secrets.POSTGRES_DB }}
- DATABASE_USER=${{ secrets.POSTGRES_USER }}
- DATABASE_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}
- NEXTAUTH_URL=${{ secrets.SITE_URL }}
- NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}
- AUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}
- ADMIN_EMAIL=${{ secrets.ADMIN_EMAIL }}
- ADMIN_PASSWORD=${{ secrets.ADMIN_PASSWORD }}
volumes:
- ${{ secrets.PROJECT_NAME }}-uploads:/app/public/uploads
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on: []
# Disable Docker PostgreSQL (using native PostgreSQL on host)
postgres:
profiles:
- disabled
# Disable built-in nginx (using external nginx on server)
nginx:
profiles:
- disabled
volumes:
${{ secrets.PROJECT_NAME }}-uploads:
name: ${{ secrets.PROJECT_NAME }}-uploads
EOF
- name: Deploy to server
run: |
echo "🚀 Starting deployment to ${{ secrets.DEPLOY_HOST }}"
# Copy docker-compose files to server
scp -o StrictHostKeyChecking=no docker-compose.yml ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/
scp -o StrictHostKeyChecking=no docker-compose.override.yml ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/
# Deploy on server
ssh -o StrictHostKeyChecking=no ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'ENDSSH'
set -e # Exit on any error
cd ${{ secrets.DEPLOY_PATH }}
# Secure the override file (contains secrets)
chmod 600 docker-compose.override.yml
echo "📥 Pulling latest Docker image..."
docker compose pull app
echo "🔄 Starting services..."
# Docker Compose automatically reads both docker-compose.yml AND docker-compose.override.yml
docker compose up -d --remove-orphans
echo "⏳ Waiting for health check (45s)..."
sleep 45
echo "🔍 Checking service status..."
docker compose ps
# Verify container is actually running
CONTAINER_STATUS=$(docker compose ps --format json | grep -o '"State":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ "$CONTAINER_STATUS" != "running" ]; then
echo "❌ Container failed to start! Status: $CONTAINER_STATUS"
docker compose logs --tail 50
exit 1
fi
echo "✅ Container is running"
echo "🧹 Cleaning up old images..."
docker image prune -f
echo "✅ Deployment complete!"
ENDSSH
- name: Verify deployment
run: |
echo "🔍 Verifying deployment..."
MAX_ATTEMPTS=10
ATTEMPT=0
until curl -sf --max-time 10 ${{ secrets.SITE_URL }}/api/health; do
ATTEMPT=$((ATTEMPT + 1))
if [ $ATTEMPT -ge $MAX_ATTEMPTS ]; then
echo "❌ Health check failed after $MAX_ATTEMPTS attempts!"
exit 1
fi
echo "⏳ Attempt $ATTEMPT/$MAX_ATTEMPTS failed, retrying in 10s..."
sleep 10
done
echo "✅ Deployment verification complete"
# ===========================================================================
# Security Scanning
# ===========================================================================
security:
name: Security Scan
runs-on: ubuntu-latest
needs: lint
permissions:
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run npm audit
run: pnpm audit --audit-level=high || true
continue-on-error: true
- name: Run Dependency Review
uses: actions/dependency-review-action@v4
if: github.event_name == 'pull_request'
continue-on-error: true