chore: upstream/downstream flow test (sync-check + lint fix) #36
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ============================================================================= | |
| # 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 |