name: CASAN CI Gate # Runs on every push/PR to catch regressions (H3) and validate security controls (H4/H5). on: push: branches: [main, develop, "feature/**"] pull_request: branches: [main] # Cancel in-flight runs of the same branch when a newer push arrives. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true # All run steps execute inside AINative_OKR_CASAN5/ (the app directory). # actions/* steps still reference $GITHUB_WORKSPACE root, so paths in `with:` blocks # must include AINative_OKR_CASAN5/ prefix. jobs: # ────────────────────────────────────────────────────────────────────────── # Job 1 — Frontend unit tests (fast gate, ~1 min) # ────────────────────────────────────────────────────────────────────────── frontend-tests: name: "Frontend Tests (H3 gate)" runs-on: ubuntu-latest defaults: run: working-directory: AINative_OKR_CASAN5 steps: - name: Checkout uses: actions/checkout@v3 - name: Setup Node.js 20 uses: actions/setup-node@v3 with: node-version: "20" cache: "npm" cache-dependency-path: AINative_OKR_CASAN5/package-lock.json - name: Install frontend dependencies run: npm ci -w frontend - name: Run Vitest (16 tests) run: npm test -w frontend # ────────────────────────────────────────────────────────────────────────── # Job 2 — CASAN Security Gate + Vault KMS signing (H4/H5/H2/H6/H7) # ────────────────────────────────────────────────────────────────────────── security-gate: name: "CASAN Security Gate + Vault KMS (H4/H5)" runs-on: ubuntu-latest defaults: run: working-directory: AINative_OKR_CASAN5 env: VAULT_ADDR: "http://vault:8200" VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }} steps: - name: Checkout uses: actions/checkout@v3 - name: Setup Node.js 20 uses: actions/setup-node@v3 with: node-version: "20" - name: Install test tools run: | apt-get update -qq 2>/dev/null && \ apt-get install -y -qq jq openssl python3 python-is-python3 uuid-runtime curl 2>/dev/null || true command -v python >/dev/null 2>&1 || \ ln -sf "$(command -v python3)" /usr/local/bin/python echo "python: $(python --version)" echo "jq: $(jq --version)" echo "openssl: $(openssl version)" - name: Install frontend dependencies run: npm ci -w frontend - name: Vault KMS — enable transit + pre-create keys run: | if curl -sf "$VAULT_ADDR/v1/sys/health" >/dev/null 2>&1; then bash .specify/scripts/bash/vault-kms.sh enable-transit bash .specify/scripts/bash/vault-kms.sh ensure-key casan-policy-key bash .specify/scripts/bash/vault-kms.sh ensure-key casan-audit-key echo "VAULT_KMS_READY" else echo "VAULT_KMS_SKIP (unreachable — will use local-file fallback)" fi - name: Run CASAN4 harness tests (35 tests) run: bash .specify/tests/run-casan4-harness-tests.sh - name: Run adversarial harness tests (44 tests) run: bash .specify/tests/adversarial-harness-tests.sh - name: Sign audit chain head via Vault KMS (H5) run: bash .specify/scripts/bash/sign-audit-head.sh - name: Sign policy bundle via Vault KMS (H5) run: bash .specify/scripts/bash/sign-policy-bundle.sh sign - name: Verify audit chain (anchor=signed expected in CI) run: bash .specify/scripts/bash/verify-audit-chain.sh - name: Security gate — aggregate verdict (PASS=11 FAIL=0 SKIP=0) run: bash .specify/scripts/bash/security-gate.sh - name: Upload test evidence if: always() uses: actions/upload-artifact@v3 with: name: casan-evidence-${{ github.run_number }} path: | AINative_OKR_CASAN5/docs/output/casan/evidence/harness-test-report.md AINative_OKR_CASAN5/docs/output/casan/evidence/ retention-days: 14 # ────────────────────────────────────────────────────────────────────────── # Job 3 — Deploy OKR web app (main branch only) # # okr-backend — NestJS + Prisma + SQLite, port 3001 (internal) # okr-frontend — nginx + React SPA, port 80 (public) # nginx proxies /api/v1/* → okr-backend:3001 # ────────────────────────────────────────────────────────────────────────── deploy-okr: name: "Deploy OKR → port 80 (H3 CI gate)" runs-on: ubuntu-latest needs: [frontend-tests, security-gate] if: github.ref == 'refs/heads/main' && github.event_name == 'push' defaults: run: working-directory: AINative_OKR_CASAN5 env: JWT_SECRET: ${{ secrets.JWT_SECRET }} FRONTEND_ORIGIN: "http://161.33.139.73" steps: - name: Checkout uses: actions/checkout@v3 - name: Build backend image (node:22-slim — required for node:sqlite) run: | docker build \ -t okr-backend:latest \ -f Dockerfile.backend \ . - name: Build frontend image (nginx + React SPA) run: | docker build \ --build-arg VITE_API_BASE_URL=/api/v1 \ -t okr-frontend:latest \ -f Dockerfile.frontend \ . - name: Create network + persistent volume run: | docker network create okr-net 2>/dev/null || true docker volume create okr-db 2>/dev/null || true - name: Deploy backend (port 3001, internal only) run: | docker rm -f okr-backend 2>/dev/null || true docker run -d \ --name okr-backend \ --network okr-net \ -e PORT=3001 \ -e DATABASE_URL=file:/data/okr.db \ -e JWT_SECRET="${JWT_SECRET}" \ -e FRONTEND_ORIGIN="${FRONTEND_ORIGIN}" \ -e NODE_ENV=production \ -v okr-db:/data \ --restart unless-stopped \ okr-backend:latest - name: Deploy frontend (port 80, public) run: | docker rm -f okr-frontend 2>/dev/null || true docker run -d \ --name okr-frontend \ --network okr-net \ -p 80:80 \ --restart unless-stopped \ okr-frontend:latest - name: Health check run: | echo "Waiting 20 s for containers to initialise..." sleep 20 if curl -sf http://localhost/ -o /dev/null; then echo "DEPLOY_OK frontend=http://161.33.139.73" else echo "DEPLOY_WARN frontend check failed — dumping logs" docker logs okr-frontend --tail 30 || true docker logs okr-backend --tail 30 || true fi