Repository navigation
Luxora CI #421
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: Luxora CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| schedule: | |
| - cron: '30 2 * * *' | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| guard: | |
| name: Protected Files Guard | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 3 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Run Protected Files Guard | |
| env: | |
| GITHUB_ACTOR: ${{ github.actor }} | |
| GITHUB_EVENT_PATH: ${{ github.event_path }} | |
| GITHUB_EVENT_NAME: ${{ github.event_name }} | |
| BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} | |
| HEAD_SHA: ${{ github.sha }} | |
| run: node scripts/ci/guard-protected-files.mjs | |
| classify: | |
| name: 01 - Plan and secret scan | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 3 | |
| outputs: | |
| quality: ${{ steps.plan.outputs.quality }} | |
| frontend: ${{ steps.plan.outputs.frontend }} | |
| backend: ${{ steps.plan.outputs.backend }} | |
| database: ${{ steps.plan.outputs.database }} | |
| docker: ${{ steps.plan.outputs.docker }} | |
| audit: ${{ steps.plan.outputs.audit }} | |
| knowledge_graph: ${{ steps.plan.outputs.knowledge_graph }} | |
| full: ${{ steps.plan.outputs.full }} | |
| docs_only: ${{ steps.plan.outputs.docs_only }} | |
| backend_suites: ${{ steps.plan.outputs.backend_suites }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Select required checks | |
| id: plan | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} | |
| HEAD_SHA: ${{ github.sha }} | |
| FORCE_FULL: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} | |
| run: node scripts/ci/plan-checks.mjs --base "$BASE_SHA" --head "$HEAD_SHA" | |
| - name: Gitleaks secret scan | |
| uses: gitleaks/gitleaks-action@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| quality: | |
| name: 02 - Code quality and frontend | |
| needs: [classify, guard] | |
| if: needs.classify.outputs.quality == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: | | |
| package-lock.json | |
| frontend/package-lock.json | |
| - name: Install root dependencies | |
| run: npm ci | |
| - name: Test CI classifier | |
| run: npm run test:ci-plan | |
| - name: Run linter | |
| run: npm run lint | |
| - name: Run frontend unit tests | |
| run: npm run test:frontend | |
| - name: Install frontend dependencies | |
| if: needs.classify.outputs.frontend == 'true' | |
| run: npm --prefix frontend ci | |
| - name: Build frontend SPA | |
| if: needs.classify.outputs.frontend == 'true' | |
| run: npm --prefix frontend run build | |
| knowledge_graph: | |
| name: 02b - Knowledge Graph verify and build | |
| needs: [classify, guard] | |
| if: needs.classify.outputs.knowledge_graph == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 4 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Generate Knowledge Graph | |
| run: npm run graph | |
| - name: Validate generated graph | |
| run: npm run graph:validate | |
| - name: Verify deterministic generation | |
| run: | | |
| cp Knowladge-Graph/knowledge-graph.json /tmp/knowledge-graph.json | |
| cp Knowladge-Graph/index.html /tmp/knowledge-graph.html | |
| npm run graph | |
| cmp --silent /tmp/knowledge-graph.json Knowladge-Graph/knowledge-graph.json | |
| cmp --silent /tmp/knowledge-graph.html Knowladge-Graph/index.html | |
| - name: Upload Knowledge Graph artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: knowledge-graph-preview | |
| path: Knowladge-Graph | |
| retention-days: 7 | |
| overwrite: true | |
| backend: | |
| name: 03 - Backend tests | |
| needs: [classify, guard] | |
| if: needs.classify.outputs.backend == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 7 | |
| services: | |
| redis: | |
| image: redis:7-alpine | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 5s | |
| --health-timeout 3s | |
| --health-retries 10 | |
| postgres: | |
| image: postgres:15-alpine | |
| env: | |
| POSTGRES_USER: luxora_test_user | |
| POSTGRES_PASSWORD: luxora_test_password | |
| POSTGRES_DB: luxora_test_db | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: backend/package-lock.json | |
| - name: Install backend dependencies | |
| run: npm --prefix backend ci | |
| - name: Generate Prisma Client | |
| run: npm --prefix backend run prisma:generate | |
| - name: Create ephemeral test secrets | |
| shell: bash | |
| run: | | |
| echo "JWT_SECRET=$(openssl rand -hex 32)" >> "$GITHUB_ENV" | |
| echo "BANK_ENCRYPTION_KEY=$(openssl rand -hex 32)" >> "$GITHUB_ENV" | |
| - name: Run isolated Redis rate-limit tests | |
| run: node --test --test-force-exit backend/tests/rate-limit.test.js | |
| env: | |
| TEST_REDIS_URL: redis://127.0.0.1:6379/15 | |
| REDIS_URL: '' | |
| NODE_ENV: test | |
| - name: Run selected backend suites | |
| run: npm --prefix backend run test:ci -- --suite=${{ needs.classify.outputs.backend_suites }} | |
| env: | |
| DATABASE_URL: postgresql://luxora_test_user:luxora_test_password@localhost:5432/luxora_test_db?schema=luxora_test | |
| PAYMENT_MODE: demo | |
| PORT: 5000 | |
| NODE_ENV: test | |
| audit: | |
| name: 04 - Dependency audit | |
| needs: [classify, guard] | |
| if: needs.classify.outputs.audit == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 4 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Audit lockfiles | |
| run: | | |
| npm audit --package-lock-only --omit=dev --audit-level=high | |
| npm --prefix backend audit --package-lock-only --omit=dev --audit-level=high | |
| npm --prefix frontend audit --package-lock-only --omit=dev --audit-level=high | |
| docker: | |
| name: 05 - Docker smoke | |
| needs: [classify, guard] | |
| if: needs.classify.outputs.docker == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 7 | |
| services: | |
| postgres: | |
| image: postgres:15-alpine | |
| env: | |
| POSTGRES_USER: luxora_test_user | |
| POSTGRES_PASSWORD: luxora_test_password | |
| POSTGRES_DB: luxora_test_db | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Build production image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| load: true | |
| tags: luxora:ci | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Create ephemeral container secrets | |
| shell: bash | |
| run: | | |
| echo "JWT_SECRET=$(openssl rand -hex 32)" >> "$GITHUB_ENV" | |
| echo "BANK_ENCRYPTION_KEY=$(openssl rand -hex 32)" >> "$GITHUB_ENV" | |
| echo "S3_SECRET_ACCESS_KEY=$(openssl rand -hex 32)" >> "$GITHUB_ENV" | |
| - name: Start production container | |
| run: >- | |
| docker run -d | |
| --name luxora-ci-container | |
| --network host | |
| -e DATABASE_URL="postgresql://luxora_test_user:luxora_test_password@127.0.0.1:5432/luxora_test_db?schema=public" | |
| -e JWT_SECRET="$JWT_SECRET" | |
| -e BANK_ENCRYPTION_KEY="$BANK_ENCRYPTION_KEY" | |
| -e PAYMENT_MODE="demo" | |
| -e PORT="5000" | |
| -e NODE_ENV="production" | |
| -e S3_BUCKET="luxora-ci-private" | |
| -e S3_REGION="auto" | |
| -e S3_ENDPOINT="http://127.0.0.1:9" | |
| -e S3_ACCESS_KEY_ID="ci_access_key" | |
| -e S3_SECRET_ACCESS_KEY="$S3_SECRET_ACCESS_KEY" | |
| luxora:ci | |
| - name: Verify production container health | |
| run: | | |
| for attempt in $(seq 1 30); do | |
| if [ "$(docker inspect -f '{{.State.Running}}' luxora-ci-container 2>/dev/null)" != "true" ]; then | |
| echo "Container stopped unexpectedly." | |
| docker logs luxora-ci-container | |
| exit 1 | |
| fi | |
| if curl -fsS http://127.0.0.1:5000/api/health >/dev/null 2>&1; then | |
| echo "Production container health check passed." | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| echo "Container health check timed out after 30 seconds." | |
| docker logs luxora-ci-container | |
| exit 1 | |
| - name: Cleanup production container | |
| if: always() | |
| run: | | |
| docker stop luxora-ci-container 2>/dev/null || true | |
| docker rm -f luxora-ci-container 2>/dev/null || true | |
| validate-and-test: | |
| name: 06 - Required gate | |
| if: always() | |
| needs: [guard, classify, quality, knowledge_graph, backend, audit, docker] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| steps: | |
| - name: Confirm selected checks passed | |
| env: | |
| GUARD_RESULT: ${{ needs.guard.result }} | |
| CLASSIFY_RESULT: ${{ needs.classify.result }} | |
| QUALITY_RESULT: ${{ needs.quality.result }} | |
| KG_RESULT: ${{ needs.knowledge_graph.result }} | |
| BACKEND_RESULT: ${{ needs.backend.result }} | |
| AUDIT_RESULT: ${{ needs.audit.result }} | |
| DOCKER_RESULT: ${{ needs.docker.result }} | |
| EXPECT_QUALITY: ${{ needs.classify.outputs.quality }} | |
| EXPECT_KG: ${{ needs.classify.outputs.knowledge_graph }} | |
| EXPECT_BACKEND: ${{ needs.classify.outputs.backend }} | |
| EXPECT_AUDIT: ${{ needs.classify.outputs.audit }} | |
| EXPECT_DOCKER: ${{ needs.classify.outputs.docker }} | |
| run: | | |
| if [ "$GUARD_RESULT" != "success" ]; then | |
| echo "Protected files guard check must pass. Current status: $GUARD_RESULT" | |
| exit 1 | |
| fi | |
| if [ "$CLASSIFY_RESULT" != "success" ]; then | |
| echo "Change classification and secret scanning must pass. Current status: $CLASSIFY_RESULT" | |
| exit 1 | |
| fi | |
| check_selective_job() { | |
| local name="$1" | |
| local result="$2" | |
| local expected="$3" | |
| echo "Validating $name: expected=$expected, result=$result" | |
| if [ "$expected" = "true" ]; then | |
| if [ "$result" != "success" ]; then | |
| echo "Error: Required CI job '$name' was planned to run but ended with status: $result (expected: success)." | |
| exit 1 | |
| fi | |
| else | |
| if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then | |
| echo "Error: Job '$name' failed or was cancelled with status: $result." | |
| exit 1 | |
| fi | |
| if [ "$result" != "skipped" ]; then | |
| echo "Notice: Job '$name' was not planned to run, but completed with status: $result." | |
| fi | |
| fi | |
| } | |
| check_selective_job "02 - Code quality and frontend" "$QUALITY_RESULT" "$EXPECT_QUALITY" | |
| check_selective_job "02b - Knowledge Graph verify and build" "$KG_RESULT" "$EXPECT_KG" | |
| check_selective_job "03 - Backend tests" "$BACKEND_RESULT" "$EXPECT_BACKEND" | |
| check_selective_job "04 - Dependency audit" "$AUDIT_RESULT" "$EXPECT_AUDIT" | |
| check_selective_job "05 - Docker smoke" "$DOCKER_RESULT" "$EXPECT_DOCKER" | |
| echo "All required and planned CI checks passed successfully." |