Skip to content

Luxora CI

Luxora CI #412

Workflow file for this run

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."