feat(wave5): CI/CD pipeline + Vault KMS + OKR deploy to port 80/3001
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>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
6e95e929f0
commit
9892e82221
@@ -0,0 +1,36 @@
|
||||
# act_runner configuration for CASAN CI on Oracle Cloud VPS (1 GB RAM)
|
||||
# Deploy to: /opt/gitea/act-runner-config.yaml on the VPS.
|
||||
|
||||
log:
|
||||
level: info
|
||||
|
||||
runner:
|
||||
# Unique name shown in Gitea → Site Administration → Runners
|
||||
name: "casan-runner-oracle"
|
||||
|
||||
# Only 1 concurrent job — VPS has 1 GB RAM (Gitea ≈ 200 MB + job container ≈ 300 MB)
|
||||
capacity: 1
|
||||
|
||||
# Map "ubuntu-latest" jobs to the catthehacker image that has git, curl, jq,
|
||||
# openssl, Node.js 18, and Python3 pre-installed. The workflow steps upgrade
|
||||
# Node.js to 20 via actions/setup-node and add python-is-python3.
|
||||
labels:
|
||||
- "ubuntu-latest:docker://catthehacker/ubuntu:act-22.04"
|
||||
|
||||
# How often the runner polls Gitea for new jobs
|
||||
fetch_interval: 5s
|
||||
fetch_timeout: 30s
|
||||
|
||||
# Where to store the runner's working directory (inside the container)
|
||||
# Default is /workspace — works fine.
|
||||
|
||||
container:
|
||||
network: bridge
|
||||
|
||||
# --memory 512m: prevent OOM on 1 GB VPS (build runs in HOST daemon, not job container)
|
||||
# -v socket: Docker-outside-of-Docker for the deploy job; lets docker CLI in the job
|
||||
# container talk to the HOST daemon to build + run OKR images.
|
||||
options: "--memory 512m --cpus 1.5 -v /var/run/docker.sock:/var/run/docker.sock"
|
||||
|
||||
valid_volumes:
|
||||
- "**"
|
||||
@@ -0,0 +1,158 @@
|
||||
# OKR Web App — Deploy Runbook
|
||||
# Oracle Cloud VPS: 161.33.139.73
|
||||
|
||||
## What this deploys
|
||||
|
||||
| Container | Image | Port | Notes |
|
||||
|---------------|--------------------|--------------|--------------------------------|
|
||||
| okr-frontend | nginx:alpine | **80** (public) | React SPA, proxies /api/v1/* → backend |
|
||||
| okr-backend | node:22-slim | 3001 (internal) | NestJS + Prisma + SQLite |
|
||||
|
||||
Network: `okr-net` (bridge, separate from Gitea/Vault)
|
||||
Volume: `okr-db` → mounted at `/data/okr.db` inside backend
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Add JWT_SECRET to Gitea Secrets
|
||||
|
||||
1. Open http://161.33.139.73:3000 → your repo → **Settings → Secrets → Actions**
|
||||
2. Add:
|
||||
- Name: `JWT_SECRET`
|
||||
- Value: `$(openssl rand -hex 32)` ← run this locally to generate
|
||||
3. Save.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Update act_runner config (Docker socket passthrough)
|
||||
|
||||
The deploy job needs Docker CLI inside the job container to build and run images.
|
||||
The updated `act-runner-config.yaml` already includes:
|
||||
|
||||
```yaml
|
||||
container:
|
||||
options: "--memory 512m --cpus 1.5 -v /var/run/docker.sock:/var/run/docker.sock"
|
||||
```
|
||||
|
||||
Apply the new config on the VPS:
|
||||
|
||||
```bash
|
||||
# Copy updated config
|
||||
scp .gitea/act-runner-config.yaml ubuntu@161.33.139.73:/opt/gitea/act-runner-config.yaml
|
||||
|
||||
# Restart act_runner to pick up the new config
|
||||
ssh ubuntu@161.33.139.73 "cd /opt/gitea && docker compose restart act-runner"
|
||||
```
|
||||
|
||||
> The `-v /var/run/docker.sock:...` flag passes the HOST Docker socket into each
|
||||
> job container, enabling Docker-outside-of-Docker (DooD). The socket is already
|
||||
> in `valid_volumes: ["**"]` so no extra ACL change is needed.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Push to main to trigger CI + deploy
|
||||
|
||||
```bash
|
||||
git add Dockerfile.backend Dockerfile.frontend nginx/ backend/entrypoint.sh \
|
||||
.dockerignore .gitea/
|
||||
git commit -m "feat: deploy OKR app to port 80/3001 via CI"
|
||||
git push gitea main
|
||||
```
|
||||
|
||||
Watch the pipeline: http://161.33.139.73:3000/<USER>/casan5/actions
|
||||
|
||||
Expected jobs:
|
||||
1. **Frontend Tests** — 16 Vitest tests (~1 min)
|
||||
2. **CASAN Security Gate** — harness + Vault signing (~3 min)
|
||||
3. **Deploy OKR** — builds images + deploys (~5 min first time, ~2 min cached)
|
||||
|
||||
After deploy:
|
||||
- **App**: http://161.33.139.73 (login: `admin@okr.local` / `Password@123`)
|
||||
- **Gitea**: http://161.33.139.73:3000 (unchanged)
|
||||
|
||||
---
|
||||
|
||||
## Manual deploy (without CI)
|
||||
|
||||
If you need to deploy manually from the VPS:
|
||||
|
||||
```bash
|
||||
ssh ubuntu@161.33.139.73
|
||||
|
||||
# Clone or pull repo
|
||||
cd /opt/okr-app # or wherever you checked out the repo
|
||||
|
||||
# Build images
|
||||
docker build -t okr-backend:latest -f Dockerfile.backend .
|
||||
docker build \
|
||||
--build-arg VITE_API_BASE_URL=/api/v1 \
|
||||
-t okr-frontend:latest \
|
||||
-f Dockerfile.frontend .
|
||||
|
||||
# Network + volume
|
||||
docker network create okr-net 2>/dev/null || true
|
||||
docker volume create okr-db 2>/dev/null || true
|
||||
|
||||
# Backend
|
||||
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="$(openssl rand -hex 32)" \
|
||||
-e FRONTEND_ORIGIN="http://161.33.139.73" \
|
||||
-e NODE_ENV=production \
|
||||
-v okr-db:/data \
|
||||
--restart unless-stopped \
|
||||
okr-backend:latest
|
||||
|
||||
# Frontend (nginx)
|
||||
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
|
||||
|
||||
# Check
|
||||
sleep 15
|
||||
curl -sf http://localhost/ | grep -c "html" && echo "FRONTEND_UP"
|
||||
docker logs okr-backend --tail 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RAM budget (1 GB VPS)
|
||||
|
||||
| Service | Idle RAM |
|
||||
|----------------|-----------|
|
||||
| Gitea | ~180 MB |
|
||||
| act_runner | ~50 MB |
|
||||
| Vault | ~60 MB |
|
||||
| okr-backend | ~120 MB |
|
||||
| okr-frontend | ~25 MB |
|
||||
| **Total idle** | **~435 MB** ✅ |
|
||||
| During CI build (Docker build on HOST) | +400 MB peak → ~835 MB ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
```bash
|
||||
# Container status
|
||||
docker ps -a | grep okr
|
||||
|
||||
# Backend logs (shows DB init + startup)
|
||||
docker logs okr-backend --tail 50
|
||||
|
||||
# Frontend logs
|
||||
docker logs okr-frontend --tail 20
|
||||
|
||||
# Re-seed database (drops + recreates)
|
||||
docker exec okr-backend sh -c "rm /data/okr.db && kill 1"
|
||||
# container restarts → auto re-initialises
|
||||
|
||||
# Check nginx proxy config
|
||||
docker exec okr-frontend nginx -t
|
||||
```
|
||||
@@ -0,0 +1,225 @@
|
||||
# HashiCorp Vault Setup — CASAN H5 KMS
|
||||
# Oracle Cloud VPS: 161.33.139.73
|
||||
|
||||
## What this achieves
|
||||
|
||||
Before: private key = file on disk, `verify-audit-chain` → `anchor=unsigned` in CI
|
||||
After: private key = Vault Transit (never leaves Vault), CI → `anchor=signed`
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Add Vault to /opt/gitea/docker-compose.yml
|
||||
|
||||
Add the `vault` service and a named volume. The key sections to add:
|
||||
|
||||
```yaml
|
||||
# ── Add to the top-level volumes section ─────────────────────────
|
||||
volumes:
|
||||
gitea-data:
|
||||
act-runner-data:
|
||||
vault-data: # ← ADD THIS
|
||||
|
||||
# ── Add the vault service ─────────────────────────────────────────
|
||||
vault:
|
||||
image: hashicorp/vault:latest
|
||||
container_name: vault
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- gitea
|
||||
ports:
|
||||
- "8200:8200" # expose so you can init from outside
|
||||
environment:
|
||||
# Dev mode: auto-init, auto-unseal, in-memory storage.
|
||||
# Root token is fixed — store it as a Gitea secret after setup.
|
||||
VAULT_DEV_ROOT_TOKEN_ID: "${VAULT_ROOT_TOKEN}"
|
||||
VAULT_DEV_LISTEN_ADDRESS: "0.0.0.0:8200"
|
||||
VAULT_LOG_LEVEL: "warn"
|
||||
cap_add:
|
||||
- IPC_LOCK # required by Vault
|
||||
command: server -dev
|
||||
```
|
||||
|
||||
> Dev mode stores keys in RAM — keys survive container restarts because the token
|
||||
> and key name are fixed, and Vault re-creates them on each start.
|
||||
> For production: switch to `server` mode with `file` storage backend.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Add VAULT_ROOT_TOKEN to /opt/gitea/.env
|
||||
|
||||
```bash
|
||||
# Pick a strong token (or generate one)
|
||||
echo "VAULT_ROOT_TOKEN=casan-vault-$(openssl rand -hex 16)" >> /opt/gitea/.env
|
||||
|
||||
# Verify
|
||||
grep VAULT_ROOT_TOKEN /opt/gitea/.env
|
||||
```
|
||||
|
||||
Save this token — you'll need it for the Gitea secret in Step 5.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Start Vault
|
||||
|
||||
```bash
|
||||
cd /opt/gitea
|
||||
docker compose pull vault
|
||||
docker compose up -d vault
|
||||
|
||||
# Wait ~5 seconds for Vault to boot, then check:
|
||||
docker compose logs vault | tail -20
|
||||
# Look for: "Development mode should NOT be used in production installations!"
|
||||
# and: "Root Token: <your token>"
|
||||
```
|
||||
|
||||
Verify from outside:
|
||||
```bash
|
||||
curl http://161.33.139.73:8200/v1/sys/health
|
||||
# Expected: {"initialized":true,"sealed":false,...}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Enable Transit engine + create signing keys
|
||||
|
||||
Run these from anywhere that can reach port 8200.
|
||||
Replace `<TOKEN>` with your `VAULT_ROOT_TOKEN` value.
|
||||
|
||||
```bash
|
||||
export VAULT_ADDR=http://161.33.139.73:8200
|
||||
export VAULT_TOKEN=<TOKEN>
|
||||
|
||||
# Enable Transit secrets engine
|
||||
curl -sf -X POST \
|
||||
-H "X-Vault-Token: $VAULT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type":"transit"}' \
|
||||
"$VAULT_ADDR/v1/sys/mounts/transit"
|
||||
|
||||
# Create RSA-2048 key for policy bundle signing
|
||||
curl -sf -X POST \
|
||||
-H "X-Vault-Token: $VAULT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type":"rsa-2048","exportable":false}' \
|
||||
"$VAULT_ADDR/v1/transit/keys/casan-policy-key"
|
||||
|
||||
# Create RSA-2048 key for audit chain signing
|
||||
curl -sf -X POST \
|
||||
-H "X-Vault-Token: $VAULT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type":"rsa-2048","exportable":false}' \
|
||||
"$VAULT_ADDR/v1/transit/keys/casan-audit-key"
|
||||
|
||||
echo "Vault Transit keys created."
|
||||
```
|
||||
|
||||
Verify keys exist:
|
||||
```bash
|
||||
curl -sf -H "X-Vault-Token: $VAULT_TOKEN" \
|
||||
"$VAULT_ADDR/v1/transit/keys/casan-policy-key" | python3 -c "
|
||||
import sys,json; d=json.load(sys.stdin)
|
||||
print('key type:', d['data']['type'])
|
||||
print('exportable:', d['data']['exportable'])
|
||||
"
|
||||
# Expected: key type: rsa-2048 / exportable: False
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Add VAULT_TOKEN as Gitea Actions Secret
|
||||
|
||||
1. Open Gitea: http://161.33.139.73:3000
|
||||
2. Go to your repo → Settings → Secrets → Actions
|
||||
3. Add new secret:
|
||||
- Name: `VAULT_TOKEN`
|
||||
- Value: your `VAULT_ROOT_TOKEN` value
|
||||
4. Save.
|
||||
|
||||
The CI workflow references `${{ secrets.VAULT_TOKEN }}` — this injects the token
|
||||
into the `security-gate` job without exposing it in logs.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Test Vault signing locally (optional)
|
||||
|
||||
From macOS with the project checked out:
|
||||
|
||||
```bash
|
||||
export VAULT_ADDR=http://161.33.139.73:8200
|
||||
export VAULT_TOKEN=<TOKEN>
|
||||
|
||||
cd Output_CASAN5_REFINED/AINative_OKR_CASAN5
|
||||
|
||||
# Test sign-policy-bundle with Vault
|
||||
bash .specify/scripts/bash/sign-policy-bundle.sh sign
|
||||
# Expected: POLICY_BUNDLE_SIGNED ... key_backend=vault-kms
|
||||
|
||||
# Test sign-audit-head
|
||||
bash .specify/scripts/bash/sign-audit-head.sh
|
||||
# Expected: SIGN_AUDIT_HEAD_OK ... anchor=vault-kms
|
||||
|
||||
# Verify audit chain
|
||||
bash .specify/scripts/bash/verify-audit-chain.sh
|
||||
# Expected: AUDIT_CHAIN_VALID anchor=signed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 7 — Push code to trigger CI with Vault
|
||||
|
||||
```bash
|
||||
git add .gitea/ .specify/scripts/bash/vault-kms.sh \
|
||||
.specify/scripts/bash/sign-audit-head.sh \
|
||||
.specify/scripts/bash/sign-policy-bundle.sh
|
||||
git commit -m "feat(H5): Vault KMS for policy + audit chain signing"
|
||||
git push gitea main
|
||||
```
|
||||
|
||||
Watch the CI run: http://161.33.139.73:3000/<USER>/casan5/actions
|
||||
|
||||
Expected security-gate job output:
|
||||
```
|
||||
VAULT_KMS_READY
|
||||
...
|
||||
SIGN_AUDIT_HEAD_OK head=<hash> anchor=vault-kms
|
||||
POLICY_BUNDLE_SIGNED ... key_backend=vault-kms
|
||||
AUDIT_CHAIN_VALID anchor=signed ← changed from "unsigned"
|
||||
...
|
||||
== verdict: PASS=7 FAIL=0 SKIP=1 ==
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture summary
|
||||
|
||||
```
|
||||
CI Job (act_runner container)
|
||||
│
|
||||
├── vault-kms.sh sign ──── POST /v1/transit/sign/casan-audit-key ──► Vault container
|
||||
│ (key never leaves)
|
||||
├── vault-kms.sh pubkey ──── GET /v1/transit/keys/casan-audit-key ──► export public key
|
||||
│ as PEM for openssl verify
|
||||
└── verify-audit-chain.sh ── openssl dgst -verify audit-public.pem
|
||||
→ AUDIT_CHAIN_VALID anchor=signed ✅
|
||||
```
|
||||
|
||||
Private key:
|
||||
- Stored only inside Vault's in-memory transit engine
|
||||
- Never written to disk as `.pem`
|
||||
- Never visible in CI logs
|
||||
- Access gated by `VAULT_TOKEN` Gitea secret
|
||||
|
||||
---
|
||||
|
||||
## RAM footprint on VPS (1 GB)
|
||||
|
||||
| Service | Idle RAM |
|
||||
|---|---|
|
||||
| Gitea | ~180 MB |
|
||||
| act_runner | ~50 MB |
|
||||
| **Vault** | **~60 MB** |
|
||||
| Total idle | ~290 MB |
|
||||
| During CI | +300 MB (job container) |
|
||||
| **Peak** | **~590 MB** ✅ |
|
||||
|
||||
Vault in dev mode uses ~60 MB — well within the 1 GB budget.
|
||||
@@ -0,0 +1,222 @@
|
||||
# VPS CI/CD Setup Runbook
|
||||
# Oracle Cloud Tokyo — 161.33.139.73
|
||||
|
||||
## Architecture
|
||||
```
|
||||
VPS (Ubuntu 24.04, 1 GB RAM)
|
||||
├── /opt/gitea/
|
||||
│ ├── docker-compose.yml ← add act-runner service here
|
||||
│ ├── .env ← add RUNNER_REGISTRATION_TOKEN here
|
||||
│ ├── act-runner-config.yaml ← copy from .gitea/act-runner-config.yaml
|
||||
│ └── gitea-data/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Enable Gitea Actions
|
||||
|
||||
SSH into the VPS:
|
||||
```bash
|
||||
ssh ubuntu@161.33.139.73
|
||||
```
|
||||
|
||||
Add the Actions env variable to the Gitea service in docker-compose.yml:
|
||||
```yaml
|
||||
# In the gitea service environment section, add:
|
||||
- GITEA__actions__ENABLED=true
|
||||
```
|
||||
|
||||
Restart Gitea:
|
||||
```bash
|
||||
cd /opt/gitea
|
||||
docker compose restart gitea
|
||||
```
|
||||
|
||||
Verify: Open http://161.33.139.73:3000 → Site Administration → Runners
|
||||
You should see "Runners" menu item (confirming Actions is enabled).
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Get the Runner Registration Token
|
||||
|
||||
1. Log into Gitea as admin: http://161.33.139.73:3000
|
||||
2. Go to: Site Administration (⚙) → Runners → "Create new runner"
|
||||
3. Copy the **Registration Token** shown (looks like: `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`)
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Copy act_runner config to VPS
|
||||
|
||||
From your local machine:
|
||||
```bash
|
||||
scp AINative_OKR_CASAN5/.gitea/act-runner-config.yaml \
|
||||
ubuntu@161.33.139.73:/opt/gitea/act-runner-config.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Add RUNNER_REGISTRATION_TOKEN to /opt/gitea/.env
|
||||
|
||||
On the VPS:
|
||||
```bash
|
||||
# Replace <TOKEN> with the token copied in Step 2
|
||||
echo "RUNNER_REGISTRATION_TOKEN=<TOKEN>" >> /opt/gitea/.env
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Add act_runner service to docker-compose.yml
|
||||
|
||||
Edit `/opt/gitea/docker-compose.yml` and add the `act-runner` service.
|
||||
|
||||
**Typical Gitea docker-compose.yml after changes:**
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
|
||||
networks:
|
||||
gitea:
|
||||
external: false
|
||||
|
||||
volumes:
|
||||
gitea-data:
|
||||
act-runner-data:
|
||||
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea:latest
|
||||
container_name: gitea
|
||||
restart: always
|
||||
networks:
|
||||
- gitea
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
- GITEA__actions__ENABLED=true # ← ADD THIS LINE
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "2222:22"
|
||||
volumes:
|
||||
- ./gitea-data:/data
|
||||
|
||||
act-runner:
|
||||
image: gitea/act_runner:latest
|
||||
container_name: act-runner
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- gitea # same network as gitea
|
||||
depends_on:
|
||||
- gitea
|
||||
environment:
|
||||
- GITEA_INSTANCE_URL=http://gitea:3000 # internal Docker hostname
|
||||
- GITEA_RUNNER_REGISTRATION_TOKEN=${RUNNER_REGISTRATION_TOKEN}
|
||||
- GITEA_RUNNER_NAME=casan-runner-oracle
|
||||
- CONFIG_FILE=/config/act-runner-config.yaml
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock # DinD for job containers
|
||||
- ./act-runner-config.yaml:/config/act-runner-config.yaml:ro
|
||||
- act-runner-data:/data
|
||||
```
|
||||
|
||||
> If your current docker-compose.yml already has a `networks:` or `volumes:` section,
|
||||
> merge them — don't add duplicate top-level keys.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Start act_runner
|
||||
|
||||
```bash
|
||||
cd /opt/gitea
|
||||
docker compose pull act-runner
|
||||
docker compose up -d act-runner
|
||||
```
|
||||
|
||||
Check registration:
|
||||
```bash
|
||||
docker compose logs -f act-runner
|
||||
# Look for: "runner registered" or "connected to Gitea"
|
||||
```
|
||||
|
||||
In Gitea Web UI: Site Administration → Runners → you should see "casan-runner-oracle" with status **Online**.
|
||||
|
||||
---
|
||||
|
||||
## Step 7 — Pull the catthehacker runner image (one-time)
|
||||
|
||||
The first CI run will pull `catthehacker/ubuntu:act-22.04` (~2 GB). Pre-pull to avoid timeout:
|
||||
|
||||
```bash
|
||||
docker pull catthehacker/ubuntu:act-22.04
|
||||
```
|
||||
|
||||
This takes ~2-5 minutes depending on internet speed.
|
||||
|
||||
---
|
||||
|
||||
## Step 8 — Create repo in Gitea and push the project
|
||||
|
||||
On the VPS (or via Gitea web UI), create a new repository:
|
||||
- URL: http://161.33.139.73:3000
|
||||
- Name: `casan5` (or any name)
|
||||
- Make it public or private (your choice)
|
||||
|
||||
On your local machine:
|
||||
```bash
|
||||
cd Output_CASAN5_REFINED/AINative_OKR_CASAN5
|
||||
|
||||
# Add Gitea as remote
|
||||
git remote add gitea ssh://git@161.33.139.73:2222/<YOUR_USER>/casan5.git
|
||||
|
||||
# Push
|
||||
git push gitea main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 9 — Verify CI triggered
|
||||
|
||||
After push, go to:
|
||||
http://161.33.139.73:3000/<YOUR_USER>/casan5/actions
|
||||
|
||||
You should see a workflow run in progress. Click it to see live logs.
|
||||
|
||||
Expected final result:
|
||||
```
|
||||
✅ Frontend Tests (H3 gate) — 16/16 PASS
|
||||
✅ CASAN Security Gate (H4/H5/H2) — PASS=7 SKIP=1 FAIL=0
|
||||
```
|
||||
|
||||
SKIP=1 is expected (Ollama is not on the CI server — this is the model-router/red-team group).
|
||||
The gate exits 0 because FAIL=0.
|
||||
|
||||
---
|
||||
|
||||
## Memory Monitoring
|
||||
|
||||
```bash
|
||||
# Watch RAM usage while CI runs
|
||||
watch -n 2 'free -h && docker stats --no-stream'
|
||||
```
|
||||
|
||||
If OOM occurs, reduce the job container memory limit in act-runner-config.yaml
|
||||
or add swap:
|
||||
```bash
|
||||
sudo fallocate -l 1G /swapfile
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile
|
||||
sudo swapon /swapfile
|
||||
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Fix |
|
||||
|---|---|
|
||||
| Runner shows "Offline" | Check `docker compose logs act-runner`; verify token is correct |
|
||||
| `GITEA_INSTANCE_URL` unreachable | Ensure gitea and act-runner are on the same Docker network |
|
||||
| Job stuck "Waiting for runner" | Runner is busy (capacity=1); wait or increase capacity |
|
||||
| `python: command not found` | The workflow's "Install test tools" step installs `python-is-python3` |
|
||||
| OOM during npm ci | Switch to `npm ci -w frontend` (already done in workflow) or add swap |
|
||||
| `catthehacker/ubuntu:act-22.04` pull fails | Run `docker pull` manually on VPS first |
|
||||
@@ -0,0 +1,208 @@
|
||||
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
|
||||
Reference in New Issue
Block a user