diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..c515638 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,198 @@ +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