# Node.js CI/CD Pipeline # Optimized workflow with caching, matrix testing, and deployment name: Node.js CI on: push: branches: [main, develop] paths-ignore: - '**.md' - 'docs/**' pull_request: branches: [main] # Cancel in-progress runs for same workflow concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: # Security: Secret Scanning secret-scan: name: Secret Scanning runs-on: ubuntu-latest timeout-minutes: 5 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: TruffleHog Secret Scan uses: trufflesecurity/trufflehog@main with: path: ./ base: ${{ github.event.repository.default_branch }} head: HEAD - name: Gitleaks uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Security: SAST sast: name: Static Analysis runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: read security-events: write steps: - uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: javascript queries: security-and-quality - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 - name: Run Semgrep uses: returntocorp/semgrep-action@v1 with: config: >- p/security-audit p/owasp-top-ten # Security: Dependency Scanning dependency-scan: name: Dependency Security runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - name: Install dependencies run: npm ci - name: npm audit run: | npm audit --audit-level=moderate --json > npm-audit.json || true npm audit --audit-level=high continue-on-error: false - name: Upload audit results if: always() uses: actions/upload-artifact@v4 with: name: npm-audit-report path: npm-audit.json lint: name: Lint runs-on: ubuntu-latest needs: [secret-scan] timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - name: Install dependencies run: npm ci - name: Run linter run: npm run lint - name: Check formatting run: npm run format:check test: name: Test (Node ${{ matrix.node }}) runs-on: ubuntu-latest timeout-minutes: 20 strategy: matrix: node: [18, 20, 22] fail-fast: false steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} cache: 'npm' - name: Install dependencies run: npm ci - name: Run unit tests run: npm run test:unit - name: Run integration tests run: npm run test:integration if: matrix.node == 20 # Only run on one version - name: Upload coverage uses: codecov/codecov-action@v4 if: matrix.node == 20 with: files: ./coverage/lcov.info fail_ci_if_error: false build: name: Build runs-on: ubuntu-latest needs: [lint, test, sast, dependency-scan] timeout-minutes: 15 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - name: Install dependencies run: npm ci - name: Build application run: npm run build - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: dist-${{ github.sha }} path: dist/ retention-days: 7 e2e: name: E2E Tests runs-on: ubuntu-latest needs: build if: github.ref == 'refs/heads/main' timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - name: Install dependencies run: npm ci - name: Download build artifacts uses: actions/download-artifact@v4 with: name: dist-${{ github.sha }} path: dist/ - name: Run E2E tests run: npm run test:e2e - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: name: e2e-results path: test-results/ deploy-staging: name: Deploy to Staging runs-on: ubuntu-latest needs: [build, test] if: github.ref == 'refs/heads/develop' environment: name: staging url: https://staging.example.com permissions: contents: read id-token: write # For OIDC steps: - uses: actions/checkout@v4 - name: Download build artifacts uses: actions/download-artifact@v4 with: name: dist-${{ github.sha }} path: dist/ - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} aws-region: us-east-1 - name: Deploy to S3 run: | aws s3 sync dist/ s3://${{ secrets.STAGING_BUCKET }} aws cloudfront create-invalidation --distribution-id ${{ secrets.STAGING_CF_DIST }} --paths "/*" - name: Smoke tests run: | sleep 10 curl -f https://staging.example.com/health || exit 1 deploy-production: name: Deploy to Production runs-on: ubuntu-latest needs: [e2e] if: github.ref == 'refs/heads/main' environment: name: production url: https://example.com permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 - name: Download build artifacts uses: actions/download-artifact@v4 with: name: dist-${{ github.sha }} path: dist/ - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} aws-region: us-east-1 - name: Deploy to S3 run: | aws s3 sync dist/ s3://${{ secrets.PRODUCTION_BUCKET }} aws cloudfront create-invalidation --distribution-id ${{ secrets.PRODUCTION_CF_DIST }} --paths "/*" - name: Health check run: | for i in {1..10}; do if curl -f https://example.com/health; then echo "Health check passed" exit 0 fi echo "Attempt $i failed, retrying..." sleep 10 done echo "Health check failed" exit 1 - name: Create deployment record run: | echo "Deployed version: ${{ github.sha }}" echo "Deployment time: $(date -u +%Y-%m-%dT%H:%M:%SZ)" # Optionally create release with gh CLI: # gh release create v${{ github.run_number }} \ # --title "Release v${{ github.run_number }}" \ # --notes "Deployed commit ${{ github.sha }}"