name: CI/CD Pipeline on: push: branches: [main, develop] pull_request: branches: [main, develop] workflow_dispatch: env: NODE_VERSION: '20.x' REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: lint: name: Lint runs-on: ubuntu-latest steps: - 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 install - name: Run ESLint run: npm run lint:check - name: Type check run: npx tsc --noEmit test: name: Test runs-on: ubuntu-latest steps: - 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 install - name: Run unit tests run: npm test --if-present || echo "No test script found" - name: Upload coverage reports uses: codecov/codecov-action@v4 if: always() with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage/coverage-final.json flags: unittests fail_ci_if_error: false build: name: Build runs-on: ubuntu-latest needs: [lint, test] steps: - 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 install - name: Prepare build directories run: | mkdir -p /tmp/dist mkdir -p dist - name: Build application run: npm run build - name: Verify build output run: | if [ ! -f "dist/index.html" ]; then echo "Error: Build failed - index.html not found" exit 1 fi echo "Build successful - all artifacts generated" ls -lah dist/ - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: dist path: dist/ retention-days: 7 e2e-tests: name: E2E Tests runs-on: ubuntu-latest needs: build timeout-minutes: 30 steps: - 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 install - name: Install Playwright browsers run: npx playwright install --with-deps chromium - name: Download build artifacts uses: actions/download-artifact@v4 with: name: dist path: dist/ - name: Check if E2E tests exist id: check-tests run: | if [ -d "e2e" ] && [ "$(ls -A e2e/*.spec.ts 2>/dev/null)" ]; then echo "has_tests=true" >> $GITHUB_OUTPUT else echo "has_tests=false" >> $GITHUB_OUTPUT fi - name: Run E2E tests if: steps.check-tests.outputs.has_tests == 'true' run: npx playwright test timeout-minutes: 20 - name: Skip E2E tests if: steps.check-tests.outputs.has_tests == 'false' run: echo "No E2E tests configured - skipping" - name: Upload test results uses: actions/upload-artifact@v4 if: always() && steps.check-tests.outputs.has_tests == 'true' with: name: playwright-report path: playwright-report/ retention-days: 7 security-scan: name: Security Scan runs-on: ubuntu-latest permissions: contents: read security-events: write actions: read steps: - 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 install - name: Run npm audit run: npm audit --audit-level=moderate || true - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: scan-type: 'fs' scan-ref: '.' format: 'sarif' output: 'trivy-results.sarif' - name: Upload Trivy results to GitHub Security uses: github/codeql-action/upload-sarif@v4 if: always() with: sarif_file: 'trivy-results.sarif' docker-build: name: Build Docker Image runs-on: ubuntu-latest needs: [lint, test, build] if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: platforms: linux/amd64,linux/arm64 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: platforms: linux/amd64,linux/arm64 - 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={{branch}}- type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . push: true platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max deploy-staging: name: Deploy to Staging runs-on: ubuntu-latest needs: [docker-build] if: github.ref == 'refs/heads/develop' environment: name: staging url: https://staging.codeforge.example.com steps: - uses: actions/checkout@v4 - name: Deploy to staging run: | echo "Deploying to staging environment..." echo "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop-${{ github.sha }}" - name: Notify deployment uses: 8398a7/action-slack@v3 if: always() && env.SLACK_WEBHOOK_URL != '' with: status: ${{ job.status }} text: 'Staging deployment ${{ job.status }}' env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} deploy-production: name: Deploy to Production runs-on: ubuntu-latest needs: [docker-build, e2e-tests] if: github.ref == 'refs/heads/main' environment: name: production url: https://codeforge.example.com steps: - uses: actions/checkout@v4 - name: Deploy to production run: | echo "Deploying to production environment..." echo "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main-${{ github.sha }}" - name: Notify deployment uses: 8398a7/action-slack@v3 if: always() && env.SLACK_WEBHOOK_URL != '' with: status: ${{ job.status }} text: 'Production deployment ${{ job.status }}' env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}