Infrastructure (H3 CI gate, H5 KMS): - Gitea Actions enabled (GITEA__actions__ENABLED=true) - act_runner: Docker-outside-of-Docker for deploy job - Vault Transit RSA-2048 signing keys (casan-audit-key, casan-policy-key) Vault KMS scripts (H5 governance): - .specify/scripts/bash/vault-kms.sh — sign/verify/pubkey/ensure-key - .specify/scripts/bash/sign-audit-head.sh — sign audit chain via Vault - Updated sign-policy-bundle.sh — Vault path + local fallback - Updated security-gate.sh — KMS gate added (PASS=11 FAIL=0) OKR app deployment (port 80/3001): - Dockerfile.backend — node:22-slim (node:sqlite requires Node 22) - Dockerfile.frontend — node:20-alpine build + nginx:alpine runtime - nginx/nginx.conf — React SPA + /api/v1/* proxy to okr-backend:3001 - backend/entrypoint.sh — auto init DB on first run + seed - .dockerignore CI pipeline (.gitea/workflows/ci.yml): - Job 1: Vitest frontend tests (H3) - Job 2: CASAN security gate + Vault KMS signing (H4/H5) - Job 3: Deploy OKR → port 80 (runs on push to main after tests pass) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
209 lines
8.4 KiB
YAML
209 lines
8.4 KiB
YAML
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
|
|
|
|
jobs:
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# Job 1 — Frontend unit tests (fast gate, ~1 min)
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
frontend-tests:
|
|
name: "Frontend Tests (H3 gate)"
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v3
|
|
|
|
- name: Setup Node.js 20
|
|
uses: actions/setup-node@v3
|
|
with:
|
|
node-version: "20"
|
|
cache: "npm"
|
|
|
|
- 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)
|
|
#
|
|
# Steps:
|
|
# 1. Install tools
|
|
# 2. Run CASAN4 harness (35 tests) — rebuilds audit.jsonl from scratch
|
|
# 3. Run adversarial suite (44 tests)
|
|
# 4. Sign audit chain head via Vault KMS → anchor=signed (H5 improvement)
|
|
# 5. Sign policy bundle via Vault KMS → key_backend=vault-kms (H5)
|
|
# 6. security-gate.sh aggregate verdict
|
|
# PASS=7 SKIP=1(Ollama) FAIL=0
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
security-gate:
|
|
name: "CASAN Security Gate + Vault KMS (H4/H5)"
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
# Vault runs as a sibling service on the same Docker network.
|
|
# VAULT_TOKEN is stored as a Gitea Actions secret (Settings → Secrets).
|
|
# Without Vault, scripts fall back to local key file — anchor=unsigned.
|
|
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
|
|
# Non-blocking: if Vault is unreachable, scripts fall back gracefully.
|
|
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)
|
|
# This clears .specify/logs/ and rebuilds audit.jsonl from scratch.
|
|
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)
|
|
# After harness tests rebuild audit.jsonl, sign the head so that
|
|
# verify-audit-chain.sh reports "anchor=signed" (not "anchor=unsigned").
|
|
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=7 SKIP=1 FAIL=0)
|
|
# Ollama-dependent checks auto-SKIP (not FAIL) when Ollama is not reachable.
|
|
# security-gate.sh re-runs harness + adversarial + verify + scan + frontend.
|
|
# Exit 0 only when FAIL=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: |
|
|
docs/output/casan/evidence/harness-test-report.md
|
|
docs/output/casan/evidence/
|
|
retention-days: 14
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# Job 3 — Deploy OKR web app (main branch only)
|
|
#
|
|
# Architecture:
|
|
# okr-backend — NestJS + Prisma + SQLite, port 3001 (internal only)
|
|
# okr-frontend — nginx + React SPA, port 80 (public)
|
|
# nginx proxies /api/v1/* → okr-backend:3001
|
|
# Both on Docker network "okr-net" (separate from gitea/vault network)
|
|
#
|
|
# Secrets required in Gitea Settings → Secrets → Actions:
|
|
# JWT_SECRET — random string for NestJS JWT signing
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
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'
|
|
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
|