CI/CD Pipeline #475
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
| name: CI/CD Pipeline | |
| on: | |
| pull_request: | |
| branches: [ main, develop ] | |
| push: | |
| branches: [ main, develop ] | |
| workflow_dispatch: | |
| # Cancel previous runs for the same PR/branch | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| NODE_VERSION: '18' | |
| jobs: | |
| # Check if files have changed to skip unnecessary jobs | |
| changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| source: ${{ steps.filter.outputs.source }} | |
| config: ${{ steps.filter.outputs.config }} | |
| i18n: ${{ steps.filter.outputs.i18n }} | |
| styles: ${{ steps.filter.outputs.styles }} | |
| tests: ${{ steps.filter.outputs.tests }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for file changes | |
| uses: dorny/paths-filter@v3 | |
| id: filter | |
| with: | |
| filters: | | |
| source: | |
| - 'app/**/*.{ts,tsx,js,jsx}' | |
| - 'components/**/*.{ts,tsx,js,jsx}' | |
| - 'lib/**/*.{ts,tsx,js,jsx}' | |
| - 'middleware.ts' | |
| - '*.{ts,tsx,js,jsx}' | |
| config: | |
| - 'package.json' | |
| - 'pnpm-lock.yaml' | |
| - 'tsconfig.json' | |
| - 'next.config.ts' | |
| - 'tailwind.config.js' | |
| - 'eslint.config.mjs' | |
| - 'commitlint.config.mjs' | |
| - 'postcss.config.mjs' | |
| - '.prettierrc.json' | |
| - '.prettierignore' | |
| tests: | |
| - '__tests__/**/*.{ts,tsx,js,jsx}' | |
| - '**/*.test.{ts,tsx,js,jsx}' | |
| - '**/*.spec.{ts,tsx,js,jsx}' | |
| i18n: | |
| - 'messages/**/*.json' | |
| - 'i18n/**/*.{ts,js}' | |
| - 'scripts/**/*i18n*' | |
| styles: | |
| - 'styles/**/*.css' | |
| - 'app/globals.css' | |
| # Install dependencies and cache them | |
| install: | |
| name: Install Dependencies | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: needs.changes.outputs.source == 'true' || needs.changes.outputs.config == 'true' || needs.changes.outputs.tests == 'true' || needs.changes.outputs.styles == 'true' | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| - 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: Cache dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.pnpm-store | |
| node_modules | |
| key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pnpm- | |
| # Code formatting and linting checks | |
| format-and-lint: | |
| name: Format and Lint Check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| needs: [changes, install] | |
| if: needs.changes.outputs.source == 'true' || needs.changes.outputs.config == 'true' || needs.changes.outputs.styles == 'true' | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| - 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: Check code formatting | |
| run: pnpm run format:check | |
| - name: Run ESLint on changed files | |
| run: | | |
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| RANGE="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" | |
| elif [[ "${{ github.event.before }}" != "" && "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]]; then | |
| RANGE="${{ github.event.before }}..${{ github.sha }}" | |
| else | |
| RANGE="$(git hash-object -t tree /dev/null)..${{ github.sha }}" | |
| fi | |
| echo "Using diff range: $RANGE" | |
| CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT "$RANGE" | grep -E '\.(js|jsx|ts|tsx)$' | tr '\n' ' ' || true) | |
| if [ -n "$CHANGED_FILES" ]; then | |
| echo "Running ESLint on changed files: $CHANGED_FILES" | |
| pnpm exec eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache $CHANGED_FILES | |
| else | |
| echo "No JS/TS files changed in this event" | |
| fi | |
| # TypeScript type checking | |
| type-check: | |
| name: TypeScript Type Check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| needs: [changes, install] | |
| if: needs.changes.outputs.source == 'true' || needs.changes.outputs.config == 'true' | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| - 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 TypeScript type check | |
| run: pnpm run type-check | |
| # Build the application | |
| build: | |
| name: Build Application | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: [changes, install] | |
| if: needs.changes.outputs.source == 'true' || needs.changes.outputs.config == 'true' | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| - 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 run build | |
| env: | |
| # Provide dummy environment variables for build | |
| BETTER_AUTH_SECRET: ci-build-only-secret-do-not-use-in-prod | |
| DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/agentifui | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build-artifacts | |
| path: .next/ | |
| retention-days: 7 | |
| # Run tests | |
| test: | |
| name: Run Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: [changes, install] | |
| if: needs.changes.outputs.tests == 'true' || needs.changes.outputs.source == 'true' | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| - 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 tests | |
| run: pnpm test | |
| # Security checks | |
| security-check: | |
| name: Security Check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| needs: [changes, install] | |
| if: needs.changes.outputs.source == 'true' || needs.changes.outputs.config == 'true' | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| - 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 dependency audit | |
| run: pnpm audit --audit-level=moderate | |
| - name: Scan for secrets | |
| run: | | |
| echo "Scanning for hardcoded secrets..." | |
| # Check for potential secrets in source code | |
| if grep -r -i -E "(api_key|apikey|secret|token|password)\s*[:=]\s*['\"][a-zA-Z0-9+/]{20,}['\"]" \ | |
| --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \ | |
| app components lib | grep -v -E "(placeholder|example|test|demo|sample|mock|dummy)"; then | |
| echo "⚠️ Potential hardcoded secret detected" | |
| exit 1 | |
| fi | |
| echo "✅ No hardcoded secrets found" | |
| # Final status check - all jobs must pass | |
| ci-status: | |
| name: CI Status Check | |
| runs-on: ubuntu-latest | |
| needs: | |
| - changes | |
| - format-and-lint | |
| - type-check | |
| - build | |
| - test | |
| - security-check | |
| if: always() | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check all jobs status | |
| run: | | |
| echo "Checking CI pipeline status..." | |
| # Check if any job failed | |
| if [[ "${{ needs.format-and-lint.result }}" == "failure" ]] || \ | |
| [[ "${{ needs.type-check.result }}" == "failure" ]] || \ | |
| [[ "${{ needs.build.result }}" == "failure" ]] || \ | |
| [[ "${{ needs.test.result }}" == "failure" ]] || \ | |
| [[ "${{ needs.security-check.result }}" == "failure" ]]; then | |
| echo "❌ CI pipeline failed" | |
| exit 1 | |
| fi | |
| # Check if any required job was skipped when it shouldn't be | |
| if [[ "${{ needs.changes.outputs.source }}" == "true" ]] || \ | |
| [[ "${{ needs.changes.outputs.config }}" == "true" ]] || \ | |
| [[ "${{ needs.changes.outputs.styles }}" == "true" ]]; then | |
| if [[ "${{ needs.format-and-lint.result }}" == "skipped" ]]; then | |
| echo "❌ Format and lint job was skipped" | |
| exit 1 | |
| fi | |
| fi | |
| if [[ "${{ needs.changes.outputs.source }}" == "true" ]] || [[ "${{ needs.changes.outputs.config }}" == "true" ]]; then | |
| if [[ "${{ needs.type-check.result }}" == "skipped" ]] || \ | |
| [[ "${{ needs.build.result }}" == "skipped" ]] || \ | |
| [[ "${{ needs.security-check.result }}" == "skipped" ]]; then | |
| echo "❌ Required job was skipped" | |
| exit 1 | |
| fi | |
| fi | |
| # Check test job when tests or source files changed | |
| if [[ "${{ needs.changes.outputs.tests }}" == "true" ]] || [[ "${{ needs.changes.outputs.source }}" == "true" ]]; then | |
| if [[ "${{ needs.test.result }}" == "skipped" ]]; then | |
| echo "❌ Test job was skipped when it should run" | |
| exit 1 | |
| fi | |
| fi | |
| echo "✅ All CI checks passed successfully" |