Compare commits
1 Commits
feat/ms22-
...
de37e7be90
| Author | SHA1 | Date | |
|---|---|---|---|
| de37e7be90 |
232
.woodpecker/api.yml
Normal file
232
.woodpecker/api.yml
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
# API Pipeline - Mosaic Stack
|
||||||
|
# Quality gates, build, and Docker publish for @mosaic/api
|
||||||
|
#
|
||||||
|
# Triggers on: apps/api/**, packages/**, root configs
|
||||||
|
# Security chain: source audit + Trivy container scan
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: [push, pull_request, manual]
|
||||||
|
path:
|
||||||
|
include:
|
||||||
|
- "apps/api/**"
|
||||||
|
- "packages/**"
|
||||||
|
- "pnpm-lock.yaml"
|
||||||
|
- "pnpm-workspace.yaml"
|
||||||
|
- "turbo.json"
|
||||||
|
- "package.json"
|
||||||
|
- ".woodpecker/api.yml"
|
||||||
|
- ".trivyignore"
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- &node_image "node:24-alpine"
|
||||||
|
- &install_deps |
|
||||||
|
corepack enable
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
- &use_deps |
|
||||||
|
corepack enable
|
||||||
|
- &turbo_env
|
||||||
|
TURBO_API:
|
||||||
|
from_secret: turbo_api
|
||||||
|
TURBO_TOKEN:
|
||||||
|
from_secret: turbo_token
|
||||||
|
TURBO_TEAM:
|
||||||
|
from_secret: turbo_team
|
||||||
|
- &kaniko_setup |
|
||||||
|
mkdir -p /kaniko/.docker
|
||||||
|
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$GITEA_USER\",\"password\":\"$GITEA_TOKEN\"}}}" > /kaniko/.docker/config.json
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17.7-alpine3.22
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: test_db
|
||||||
|
POSTGRES_USER: test_user
|
||||||
|
POSTGRES_PASSWORD: test_password
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# === Quality Gates ===
|
||||||
|
|
||||||
|
install:
|
||||||
|
image: *node_image
|
||||||
|
commands:
|
||||||
|
- *install_deps
|
||||||
|
|
||||||
|
security-audit:
|
||||||
|
image: *node_image
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm audit --audit-level=high
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
prisma-generate:
|
||||||
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
SKIP_ENV_VALIDATION: "true"
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm --filter "@mosaic/api" prisma:generate
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
lint:
|
||||||
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
SKIP_ENV_VALIDATION: "true"
|
||||||
|
<<: *turbo_env
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm turbo lint --filter=@mosaic/api
|
||||||
|
depends_on:
|
||||||
|
- prisma-generate
|
||||||
|
|
||||||
|
typecheck:
|
||||||
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
SKIP_ENV_VALIDATION: "true"
|
||||||
|
<<: *turbo_env
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm turbo typecheck --filter=@mosaic/api
|
||||||
|
depends_on:
|
||||||
|
- prisma-generate
|
||||||
|
|
||||||
|
prisma-migrate:
|
||||||
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
SKIP_ENV_VALIDATION: "true"
|
||||||
|
DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/test_db?schema=public"
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm --filter "@mosaic/api" prisma migrate deploy
|
||||||
|
depends_on:
|
||||||
|
- prisma-generate
|
||||||
|
|
||||||
|
test:
|
||||||
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
SKIP_ENV_VALIDATION: "true"
|
||||||
|
DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/test_db?schema=public"
|
||||||
|
ENCRYPTION_KEY: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm --filter "@mosaic/api" exec vitest run --exclude 'src/auth/auth-rls.integration.spec.ts' --exclude 'src/credentials/user-credential.model.spec.ts' --exclude 'src/job-events/job-events.performance.spec.ts' --exclude 'src/knowledge/services/fulltext-search.spec.ts' --exclude 'src/mosaic-telemetry/mosaic-telemetry.module.spec.ts'
|
||||||
|
depends_on:
|
||||||
|
- prisma-migrate
|
||||||
|
|
||||||
|
# === Build ===
|
||||||
|
|
||||||
|
build:
|
||||||
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
SKIP_ENV_VALIDATION: "true"
|
||||||
|
NODE_ENV: "production"
|
||||||
|
<<: *turbo_env
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm turbo build --filter=@mosaic/api
|
||||||
|
depends_on:
|
||||||
|
- lint
|
||||||
|
- typecheck
|
||||||
|
- test
|
||||||
|
- security-audit
|
||||||
|
|
||||||
|
# === Docker Build & Push ===
|
||||||
|
|
||||||
|
docker-build-api:
|
||||||
|
image: gcr.io/kaniko-project/executor:debug
|
||||||
|
environment:
|
||||||
|
GITEA_USER:
|
||||||
|
from_secret: gitea_username
|
||||||
|
GITEA_TOKEN:
|
||||||
|
from_secret: gitea_token
|
||||||
|
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||||
|
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||||
|
commands:
|
||||||
|
- *kaniko_setup
|
||||||
|
- |
|
||||||
|
DESTINATIONS=""
|
||||||
|
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||||
|
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-api:$CI_COMMIT_TAG"
|
||||||
|
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||||
|
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-api:latest"
|
||||||
|
fi
|
||||||
|
/kaniko/executor --context . --dockerfile apps/api/Dockerfile --snapshot-mode=redo $DESTINATIONS
|
||||||
|
when:
|
||||||
|
- branch: [main]
|
||||||
|
event: [push, manual, tag]
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
|
|
||||||
|
# === Container Security Scan ===
|
||||||
|
|
||||||
|
security-trivy-api:
|
||||||
|
image: aquasec/trivy:latest
|
||||||
|
environment:
|
||||||
|
GITEA_USER:
|
||||||
|
from_secret: gitea_username
|
||||||
|
GITEA_TOKEN:
|
||||||
|
from_secret: gitea_token
|
||||||
|
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||||
|
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
if [ -n "$$CI_COMMIT_TAG" ]; then
|
||||||
|
SCAN_TAG="$$CI_COMMIT_TAG"
|
||||||
|
elif [ "$$CI_COMMIT_BRANCH" = "main" ]; then
|
||||||
|
SCAN_TAG="latest"
|
||||||
|
else
|
||||||
|
SCAN_TAG="latest"
|
||||||
|
fi
|
||||||
|
mkdir -p ~/.docker
|
||||||
|
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||||
|
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
|
||||||
|
--ignorefile .trivyignore \
|
||||||
|
git.mosaicstack.dev/mosaic/stack-api:$$SCAN_TAG
|
||||||
|
when:
|
||||||
|
- branch: [main]
|
||||||
|
event: [push, manual, tag]
|
||||||
|
depends_on:
|
||||||
|
- docker-build-api
|
||||||
|
|
||||||
|
# === Package Linking ===
|
||||||
|
|
||||||
|
link-packages:
|
||||||
|
image: alpine:3
|
||||||
|
environment:
|
||||||
|
GITEA_TOKEN:
|
||||||
|
from_secret: gitea_token
|
||||||
|
commands:
|
||||||
|
- apk add --no-cache curl
|
||||||
|
- sleep 10
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
link_package() {
|
||||||
|
PKG="$$1"
|
||||||
|
echo "Linking $$PKG..."
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
STATUS=$$(curl -s -o /tmp/link-response.txt -w "%{http_code}" -X POST \
|
||||||
|
-H "Authorization: token $$GITEA_TOKEN" \
|
||||||
|
"https://git.mosaicstack.dev/api/v1/packages/mosaic/container/$$PKG/-/link/stack")
|
||||||
|
if [ "$$STATUS" = "201" ] || [ "$$STATUS" = "204" ]; then
|
||||||
|
echo " Linked $$PKG"
|
||||||
|
return 0
|
||||||
|
elif [ "$$STATUS" = "400" ]; then
|
||||||
|
echo " $$PKG already linked"
|
||||||
|
return 0
|
||||||
|
elif [ "$$STATUS" = "404" ] && [ $$attempt -lt 3 ]; then
|
||||||
|
echo " $$PKG not found yet, retrying in 5s (attempt $$attempt/3)..."
|
||||||
|
sleep 5
|
||||||
|
else
|
||||||
|
echo " FAILED: $$PKG status $$STATUS"
|
||||||
|
cat /tmp/link-response.txt
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
link_package "stack-api"
|
||||||
|
when:
|
||||||
|
- branch: [main]
|
||||||
|
event: [push, manual, tag]
|
||||||
|
depends_on:
|
||||||
|
- security-trivy-api
|
||||||
@@ -1,337 +0,0 @@
|
|||||||
# Unified CI Pipeline - Mosaic Stack
|
|
||||||
# Single install, parallel quality gates, sequential deploy
|
|
||||||
#
|
|
||||||
# Replaces: api.yml, orchestrator.yml, web.yml
|
|
||||||
# Keeps: coordinator.yml (Python), infra.yml (separate concerns)
|
|
||||||
#
|
|
||||||
# Flow:
|
|
||||||
# install → security-audit
|
|
||||||
# → prisma-generate → lint + typecheck (parallel)
|
|
||||||
# → prisma-migrate → test
|
|
||||||
# → build (after all gates pass)
|
|
||||||
# → docker builds (main only, parallel)
|
|
||||||
# → trivy scans (main only, parallel)
|
|
||||||
# → package linking (main only)
|
|
||||||
|
|
||||||
when:
|
|
||||||
- event: [push, pull_request, manual]
|
|
||||||
path:
|
|
||||||
include:
|
|
||||||
- "apps/api/**"
|
|
||||||
- "apps/orchestrator/**"
|
|
||||||
- "apps/web/**"
|
|
||||||
- "packages/**"
|
|
||||||
- "pnpm-lock.yaml"
|
|
||||||
- "pnpm-workspace.yaml"
|
|
||||||
- "turbo.json"
|
|
||||||
- "package.json"
|
|
||||||
- ".woodpecker/ci.yml"
|
|
||||||
- ".trivyignore"
|
|
||||||
|
|
||||||
variables:
|
|
||||||
- &node_image "node:24-alpine"
|
|
||||||
- &install_deps |
|
|
||||||
corepack enable
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
- &use_deps |
|
|
||||||
corepack enable
|
|
||||||
- &turbo_env
|
|
||||||
TURBO_API:
|
|
||||||
from_secret: turbo_api
|
|
||||||
TURBO_TOKEN:
|
|
||||||
from_secret: turbo_token
|
|
||||||
TURBO_TEAM:
|
|
||||||
from_secret: turbo_team
|
|
||||||
- &kaniko_setup |
|
|
||||||
mkdir -p /kaniko/.docker
|
|
||||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$GITEA_USER\",\"password\":\"$GITEA_TOKEN\"}}}" > /kaniko/.docker/config.json
|
|
||||||
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:17.7-alpine3.22
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: test_db
|
|
||||||
POSTGRES_USER: test_user
|
|
||||||
POSTGRES_PASSWORD: test_password
|
|
||||||
|
|
||||||
steps:
|
|
||||||
# ─── Install (once) ─────────────────────────────────────────
|
|
||||||
install:
|
|
||||||
image: *node_image
|
|
||||||
commands:
|
|
||||||
- *install_deps
|
|
||||||
|
|
||||||
# ─── Security Audit (once) ──────────────────────────────────
|
|
||||||
security-audit:
|
|
||||||
image: *node_image
|
|
||||||
commands:
|
|
||||||
- *use_deps
|
|
||||||
- pnpm audit --audit-level=high
|
|
||||||
depends_on:
|
|
||||||
- install
|
|
||||||
|
|
||||||
# ─── Prisma Generate ────────────────────────────────────────
|
|
||||||
prisma-generate:
|
|
||||||
image: *node_image
|
|
||||||
environment:
|
|
||||||
SKIP_ENV_VALIDATION: "true"
|
|
||||||
commands:
|
|
||||||
- *use_deps
|
|
||||||
- pnpm --filter "@mosaic/api" prisma:generate
|
|
||||||
depends_on:
|
|
||||||
- install
|
|
||||||
|
|
||||||
# ─── Lint (all packages) ────────────────────────────────────
|
|
||||||
lint:
|
|
||||||
image: *node_image
|
|
||||||
environment:
|
|
||||||
SKIP_ENV_VALIDATION: "true"
|
|
||||||
<<: *turbo_env
|
|
||||||
commands:
|
|
||||||
- *use_deps
|
|
||||||
- pnpm turbo lint
|
|
||||||
depends_on:
|
|
||||||
- prisma-generate
|
|
||||||
|
|
||||||
# ─── Typecheck (all packages, parallel with lint) ───────────
|
|
||||||
typecheck:
|
|
||||||
image: *node_image
|
|
||||||
environment:
|
|
||||||
SKIP_ENV_VALIDATION: "true"
|
|
||||||
<<: *turbo_env
|
|
||||||
commands:
|
|
||||||
- *use_deps
|
|
||||||
- pnpm turbo typecheck
|
|
||||||
depends_on:
|
|
||||||
- prisma-generate
|
|
||||||
|
|
||||||
# ─── Prisma Migrate (test DB) ──────────────────────────────
|
|
||||||
prisma-migrate:
|
|
||||||
image: *node_image
|
|
||||||
environment:
|
|
||||||
SKIP_ENV_VALIDATION: "true"
|
|
||||||
DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/test_db?schema=public"
|
|
||||||
commands:
|
|
||||||
- *use_deps
|
|
||||||
- pnpm --filter "@mosaic/api" prisma migrate deploy
|
|
||||||
depends_on:
|
|
||||||
- prisma-generate
|
|
||||||
|
|
||||||
# ─── Test (all packages) ───────────────────────────────────
|
|
||||||
test:
|
|
||||||
image: *node_image
|
|
||||||
environment:
|
|
||||||
SKIP_ENV_VALIDATION: "true"
|
|
||||||
DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/test_db?schema=public"
|
|
||||||
ENCRYPTION_KEY: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
|
||||||
<<: *turbo_env
|
|
||||||
commands:
|
|
||||||
- *use_deps
|
|
||||||
- pnpm --filter "@mosaic/api" exec vitest run --exclude 'src/auth/auth-rls.integration.spec.ts' --exclude 'src/credentials/user-credential.model.spec.ts' --exclude 'src/job-events/job-events.performance.spec.ts' --exclude 'src/knowledge/services/fulltext-search.spec.ts' --exclude 'src/mosaic-telemetry/mosaic-telemetry.module.spec.ts'
|
|
||||||
- pnpm turbo test --filter=@mosaic/orchestrator --filter=@mosaic/web
|
|
||||||
depends_on:
|
|
||||||
- prisma-migrate
|
|
||||||
|
|
||||||
# ─── Build (all packages) ──────────────────────────────────
|
|
||||||
build:
|
|
||||||
image: *node_image
|
|
||||||
environment:
|
|
||||||
SKIP_ENV_VALIDATION: "true"
|
|
||||||
NODE_ENV: "production"
|
|
||||||
<<: *turbo_env
|
|
||||||
commands:
|
|
||||||
- *use_deps
|
|
||||||
- pnpm turbo build
|
|
||||||
depends_on:
|
|
||||||
- lint
|
|
||||||
- typecheck
|
|
||||||
- test
|
|
||||||
- security-audit
|
|
||||||
|
|
||||||
# ─── Docker Builds (main only, parallel) ───────────────────
|
|
||||||
|
|
||||||
docker-build-api:
|
|
||||||
image: gcr.io/kaniko-project/executor:debug
|
|
||||||
environment:
|
|
||||||
GITEA_USER:
|
|
||||||
from_secret: gitea_username
|
|
||||||
GITEA_TOKEN:
|
|
||||||
from_secret: gitea_token
|
|
||||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
|
||||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
|
||||||
commands:
|
|
||||||
- *kaniko_setup
|
|
||||||
- |
|
|
||||||
DESTINATIONS=""
|
|
||||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
|
||||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-api:$CI_COMMIT_TAG"
|
|
||||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
|
||||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-api:latest"
|
|
||||||
fi
|
|
||||||
/kaniko/executor --context . --dockerfile apps/api/Dockerfile --snapshot-mode=redo $DESTINATIONS
|
|
||||||
when:
|
|
||||||
- branch: [main]
|
|
||||||
event: [push, manual, tag]
|
|
||||||
depends_on:
|
|
||||||
- build
|
|
||||||
|
|
||||||
docker-build-orchestrator:
|
|
||||||
image: gcr.io/kaniko-project/executor:debug
|
|
||||||
environment:
|
|
||||||
GITEA_USER:
|
|
||||||
from_secret: gitea_username
|
|
||||||
GITEA_TOKEN:
|
|
||||||
from_secret: gitea_token
|
|
||||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
|
||||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
|
||||||
commands:
|
|
||||||
- *kaniko_setup
|
|
||||||
- |
|
|
||||||
DESTINATIONS=""
|
|
||||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
|
||||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-orchestrator:$CI_COMMIT_TAG"
|
|
||||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
|
||||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-orchestrator:latest"
|
|
||||||
fi
|
|
||||||
/kaniko/executor --context . --dockerfile apps/orchestrator/Dockerfile --snapshot-mode=redo $DESTINATIONS
|
|
||||||
when:
|
|
||||||
- branch: [main]
|
|
||||||
event: [push, manual, tag]
|
|
||||||
depends_on:
|
|
||||||
- build
|
|
||||||
|
|
||||||
docker-build-web:
|
|
||||||
image: gcr.io/kaniko-project/executor:debug
|
|
||||||
environment:
|
|
||||||
GITEA_USER:
|
|
||||||
from_secret: gitea_username
|
|
||||||
GITEA_TOKEN:
|
|
||||||
from_secret: gitea_token
|
|
||||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
|
||||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
|
||||||
commands:
|
|
||||||
- *kaniko_setup
|
|
||||||
- |
|
|
||||||
DESTINATIONS=""
|
|
||||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
|
||||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-web:$CI_COMMIT_TAG"
|
|
||||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
|
||||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-web:latest"
|
|
||||||
fi
|
|
||||||
/kaniko/executor --context . --dockerfile apps/web/Dockerfile --snapshot-mode=redo --build-arg NEXT_PUBLIC_API_URL=https://api.mosaicstack.dev $DESTINATIONS
|
|
||||||
when:
|
|
||||||
- branch: [main]
|
|
||||||
event: [push, manual, tag]
|
|
||||||
depends_on:
|
|
||||||
- build
|
|
||||||
|
|
||||||
# ─── Container Security Scans (main only) ──────────────────
|
|
||||||
|
|
||||||
security-trivy-api:
|
|
||||||
image: aquasec/trivy:latest
|
|
||||||
environment:
|
|
||||||
GITEA_USER:
|
|
||||||
from_secret: gitea_username
|
|
||||||
GITEA_TOKEN:
|
|
||||||
from_secret: gitea_token
|
|
||||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
|
||||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
|
||||||
commands:
|
|
||||||
- |
|
|
||||||
if [ -n "$$CI_COMMIT_TAG" ]; then SCAN_TAG="$$CI_COMMIT_TAG"; else SCAN_TAG="latest"; fi
|
|
||||||
mkdir -p ~/.docker
|
|
||||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
|
||||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --ignorefile .trivyignore git.mosaicstack.dev/mosaic/stack-api:$$SCAN_TAG
|
|
||||||
when:
|
|
||||||
- branch: [main]
|
|
||||||
event: [push, manual, tag]
|
|
||||||
depends_on:
|
|
||||||
- docker-build-api
|
|
||||||
|
|
||||||
security-trivy-orchestrator:
|
|
||||||
image: aquasec/trivy:latest
|
|
||||||
environment:
|
|
||||||
GITEA_USER:
|
|
||||||
from_secret: gitea_username
|
|
||||||
GITEA_TOKEN:
|
|
||||||
from_secret: gitea_token
|
|
||||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
|
||||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
|
||||||
commands:
|
|
||||||
- |
|
|
||||||
if [ -n "$$CI_COMMIT_TAG" ]; then SCAN_TAG="$$CI_COMMIT_TAG"; else SCAN_TAG="latest"; fi
|
|
||||||
mkdir -p ~/.docker
|
|
||||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
|
||||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --ignorefile .trivyignore git.mosaicstack.dev/mosaic/stack-orchestrator:$$SCAN_TAG
|
|
||||||
when:
|
|
||||||
- branch: [main]
|
|
||||||
event: [push, manual, tag]
|
|
||||||
depends_on:
|
|
||||||
- docker-build-orchestrator
|
|
||||||
|
|
||||||
security-trivy-web:
|
|
||||||
image: aquasec/trivy:latest
|
|
||||||
environment:
|
|
||||||
GITEA_USER:
|
|
||||||
from_secret: gitea_username
|
|
||||||
GITEA_TOKEN:
|
|
||||||
from_secret: gitea_token
|
|
||||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
|
||||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
|
||||||
commands:
|
|
||||||
- |
|
|
||||||
if [ -n "$$CI_COMMIT_TAG" ]; then SCAN_TAG="$$CI_COMMIT_TAG"; else SCAN_TAG="latest"; fi
|
|
||||||
mkdir -p ~/.docker
|
|
||||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
|
||||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --ignorefile .trivyignore git.mosaicstack.dev/mosaic/stack-web:$$SCAN_TAG
|
|
||||||
when:
|
|
||||||
- branch: [main]
|
|
||||||
event: [push, manual, tag]
|
|
||||||
depends_on:
|
|
||||||
- docker-build-web
|
|
||||||
|
|
||||||
# ─── Package Linking (main only, once) ─────────────────────
|
|
||||||
|
|
||||||
link-packages:
|
|
||||||
image: alpine:3
|
|
||||||
environment:
|
|
||||||
GITEA_TOKEN:
|
|
||||||
from_secret: gitea_token
|
|
||||||
commands:
|
|
||||||
- apk add --no-cache curl
|
|
||||||
- sleep 10
|
|
||||||
- |
|
|
||||||
set -e
|
|
||||||
link_package() {
|
|
||||||
PKG="$$1"
|
|
||||||
echo "Linking $$PKG..."
|
|
||||||
for attempt in 1 2 3; do
|
|
||||||
STATUS=$$(curl -s -o /tmp/link-response.txt -w "%{http_code}" -X POST \
|
|
||||||
-H "Authorization: token $$GITEA_TOKEN" \
|
|
||||||
"https://git.mosaicstack.dev/api/v1/packages/mosaic/container/$$PKG/-/link/stack")
|
|
||||||
if [ "$$STATUS" = "201" ] || [ "$$STATUS" = "204" ]; then
|
|
||||||
echo " Linked $$PKG"
|
|
||||||
return 0
|
|
||||||
elif [ "$$STATUS" = "400" ]; then
|
|
||||||
echo " $$PKG already linked"
|
|
||||||
return 0
|
|
||||||
elif [ "$$STATUS" = "404" ] && [ $$attempt -lt 3 ]; then
|
|
||||||
echo " $$PKG not found yet, retrying in 5s (attempt $$attempt/3)..."
|
|
||||||
sleep 5
|
|
||||||
else
|
|
||||||
echo " FAILED: $$PKG status $$STATUS"
|
|
||||||
cat /tmp/link-response.txt
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
link_package "stack-api"
|
|
||||||
link_package "stack-orchestrator"
|
|
||||||
link_package "stack-web"
|
|
||||||
when:
|
|
||||||
- branch: [main]
|
|
||||||
event: [push, manual, tag]
|
|
||||||
depends_on:
|
|
||||||
- security-trivy-api
|
|
||||||
- security-trivy-orchestrator
|
|
||||||
- security-trivy-web
|
|
||||||
202
.woodpecker/orchestrator.yml
Normal file
202
.woodpecker/orchestrator.yml
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# Orchestrator Pipeline - Mosaic Stack
|
||||||
|
# Quality gates, build, and Docker publish for @mosaic/orchestrator
|
||||||
|
#
|
||||||
|
# Triggers on: apps/orchestrator/**, packages/**, root configs
|
||||||
|
# Security chain: source audit + Trivy container scan
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: [push, pull_request, manual]
|
||||||
|
path:
|
||||||
|
include:
|
||||||
|
- "apps/orchestrator/**"
|
||||||
|
- "packages/**"
|
||||||
|
- "pnpm-lock.yaml"
|
||||||
|
- "pnpm-workspace.yaml"
|
||||||
|
- "turbo.json"
|
||||||
|
- "package.json"
|
||||||
|
- ".woodpecker/orchestrator.yml"
|
||||||
|
- ".trivyignore"
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- &node_image "node:24-alpine"
|
||||||
|
- &install_deps |
|
||||||
|
corepack enable
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
- &use_deps |
|
||||||
|
corepack enable
|
||||||
|
- &turbo_env
|
||||||
|
TURBO_API:
|
||||||
|
from_secret: turbo_api
|
||||||
|
TURBO_TOKEN:
|
||||||
|
from_secret: turbo_token
|
||||||
|
TURBO_TEAM:
|
||||||
|
from_secret: turbo_team
|
||||||
|
- &kaniko_setup |
|
||||||
|
mkdir -p /kaniko/.docker
|
||||||
|
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$GITEA_USER\",\"password\":\"$GITEA_TOKEN\"}}}" > /kaniko/.docker/config.json
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# === Quality Gates ===
|
||||||
|
|
||||||
|
install:
|
||||||
|
image: *node_image
|
||||||
|
commands:
|
||||||
|
- *install_deps
|
||||||
|
|
||||||
|
security-audit:
|
||||||
|
image: *node_image
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm audit --audit-level=high
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
lint:
|
||||||
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
SKIP_ENV_VALIDATION: "true"
|
||||||
|
<<: *turbo_env
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm turbo lint --filter=@mosaic/orchestrator
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
typecheck:
|
||||||
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
SKIP_ENV_VALIDATION: "true"
|
||||||
|
<<: *turbo_env
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm turbo typecheck --filter=@mosaic/orchestrator
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
test:
|
||||||
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
SKIP_ENV_VALIDATION: "true"
|
||||||
|
<<: *turbo_env
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm turbo test --filter=@mosaic/orchestrator
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
# === Build ===
|
||||||
|
|
||||||
|
build:
|
||||||
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
SKIP_ENV_VALIDATION: "true"
|
||||||
|
NODE_ENV: "production"
|
||||||
|
<<: *turbo_env
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm turbo build --filter=@mosaic/orchestrator
|
||||||
|
depends_on:
|
||||||
|
- lint
|
||||||
|
- typecheck
|
||||||
|
- test
|
||||||
|
- security-audit
|
||||||
|
|
||||||
|
# === Docker Build & Push ===
|
||||||
|
|
||||||
|
docker-build-orchestrator:
|
||||||
|
image: gcr.io/kaniko-project/executor:debug
|
||||||
|
environment:
|
||||||
|
GITEA_USER:
|
||||||
|
from_secret: gitea_username
|
||||||
|
GITEA_TOKEN:
|
||||||
|
from_secret: gitea_token
|
||||||
|
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||||
|
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||||
|
commands:
|
||||||
|
- *kaniko_setup
|
||||||
|
- |
|
||||||
|
DESTINATIONS=""
|
||||||
|
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||||
|
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-orchestrator:$CI_COMMIT_TAG"
|
||||||
|
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||||
|
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-orchestrator:latest"
|
||||||
|
fi
|
||||||
|
/kaniko/executor --context . --dockerfile apps/orchestrator/Dockerfile --snapshot-mode=redo $DESTINATIONS
|
||||||
|
when:
|
||||||
|
- branch: [main]
|
||||||
|
event: [push, manual, tag]
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
|
|
||||||
|
# === Container Security Scan ===
|
||||||
|
|
||||||
|
security-trivy-orchestrator:
|
||||||
|
image: aquasec/trivy:latest
|
||||||
|
environment:
|
||||||
|
GITEA_USER:
|
||||||
|
from_secret: gitea_username
|
||||||
|
GITEA_TOKEN:
|
||||||
|
from_secret: gitea_token
|
||||||
|
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||||
|
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
if [ -n "$$CI_COMMIT_TAG" ]; then
|
||||||
|
SCAN_TAG="$$CI_COMMIT_TAG"
|
||||||
|
elif [ "$$CI_COMMIT_BRANCH" = "main" ]; then
|
||||||
|
SCAN_TAG="latest"
|
||||||
|
else
|
||||||
|
SCAN_TAG="latest"
|
||||||
|
fi
|
||||||
|
mkdir -p ~/.docker
|
||||||
|
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||||
|
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
|
||||||
|
--ignorefile .trivyignore \
|
||||||
|
git.mosaicstack.dev/mosaic/stack-orchestrator:$$SCAN_TAG
|
||||||
|
when:
|
||||||
|
- branch: [main]
|
||||||
|
event: [push, manual, tag]
|
||||||
|
depends_on:
|
||||||
|
- docker-build-orchestrator
|
||||||
|
|
||||||
|
# === Package Linking ===
|
||||||
|
|
||||||
|
link-packages:
|
||||||
|
image: alpine:3
|
||||||
|
environment:
|
||||||
|
GITEA_TOKEN:
|
||||||
|
from_secret: gitea_token
|
||||||
|
commands:
|
||||||
|
- apk add --no-cache curl
|
||||||
|
- sleep 10
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
link_package() {
|
||||||
|
PKG="$$1"
|
||||||
|
echo "Linking $$PKG..."
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
STATUS=$$(curl -s -o /tmp/link-response.txt -w "%{http_code}" -X POST \
|
||||||
|
-H "Authorization: token $$GITEA_TOKEN" \
|
||||||
|
"https://git.mosaicstack.dev/api/v1/packages/mosaic/container/$$PKG/-/link/stack")
|
||||||
|
if [ "$$STATUS" = "201" ] || [ "$$STATUS" = "204" ]; then
|
||||||
|
echo " Linked $$PKG"
|
||||||
|
return 0
|
||||||
|
elif [ "$$STATUS" = "400" ]; then
|
||||||
|
echo " $$PKG already linked"
|
||||||
|
return 0
|
||||||
|
elif [ "$$STATUS" = "404" ] && [ $$attempt -lt 3 ]; then
|
||||||
|
echo " $$PKG not found yet, retrying in 5s (attempt $$attempt/3)..."
|
||||||
|
sleep 5
|
||||||
|
else
|
||||||
|
echo " FAILED: $$PKG status $$STATUS"
|
||||||
|
cat /tmp/link-response.txt
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
link_package "stack-orchestrator"
|
||||||
|
when:
|
||||||
|
- branch: [main]
|
||||||
|
event: [push, manual, tag]
|
||||||
|
depends_on:
|
||||||
|
- security-trivy-orchestrator
|
||||||
202
.woodpecker/web.yml
Normal file
202
.woodpecker/web.yml
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# Web Pipeline - Mosaic Stack
|
||||||
|
# Quality gates, build, and Docker publish for @mosaic/web
|
||||||
|
#
|
||||||
|
# Triggers on: apps/web/**, packages/**, root configs
|
||||||
|
# Security chain: source audit + Trivy container scan
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: [push, pull_request, manual]
|
||||||
|
path:
|
||||||
|
include:
|
||||||
|
- "apps/web/**"
|
||||||
|
- "packages/**"
|
||||||
|
- "pnpm-lock.yaml"
|
||||||
|
- "pnpm-workspace.yaml"
|
||||||
|
- "turbo.json"
|
||||||
|
- "package.json"
|
||||||
|
- ".woodpecker/web.yml"
|
||||||
|
- ".trivyignore"
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- &node_image "node:24-alpine"
|
||||||
|
- &install_deps |
|
||||||
|
corepack enable
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
- &use_deps |
|
||||||
|
corepack enable
|
||||||
|
- &turbo_env
|
||||||
|
TURBO_API:
|
||||||
|
from_secret: turbo_api
|
||||||
|
TURBO_TOKEN:
|
||||||
|
from_secret: turbo_token
|
||||||
|
TURBO_TEAM:
|
||||||
|
from_secret: turbo_team
|
||||||
|
- &kaniko_setup |
|
||||||
|
mkdir -p /kaniko/.docker
|
||||||
|
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$GITEA_USER\",\"password\":\"$GITEA_TOKEN\"}}}" > /kaniko/.docker/config.json
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# === Quality Gates ===
|
||||||
|
|
||||||
|
install:
|
||||||
|
image: *node_image
|
||||||
|
commands:
|
||||||
|
- *install_deps
|
||||||
|
|
||||||
|
security-audit:
|
||||||
|
image: *node_image
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm audit --audit-level=high
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
lint:
|
||||||
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
SKIP_ENV_VALIDATION: "true"
|
||||||
|
<<: *turbo_env
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm turbo lint --filter=@mosaic/web
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
typecheck:
|
||||||
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
SKIP_ENV_VALIDATION: "true"
|
||||||
|
<<: *turbo_env
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm turbo typecheck --filter=@mosaic/web
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
test:
|
||||||
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
SKIP_ENV_VALIDATION: "true"
|
||||||
|
<<: *turbo_env
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm turbo test --filter=@mosaic/web
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
# === Build ===
|
||||||
|
|
||||||
|
build:
|
||||||
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
SKIP_ENV_VALIDATION: "true"
|
||||||
|
NODE_ENV: "production"
|
||||||
|
<<: *turbo_env
|
||||||
|
commands:
|
||||||
|
- *use_deps
|
||||||
|
- pnpm turbo build --filter=@mosaic/web
|
||||||
|
depends_on:
|
||||||
|
- lint
|
||||||
|
- typecheck
|
||||||
|
- test
|
||||||
|
- security-audit
|
||||||
|
|
||||||
|
# === Docker Build & Push ===
|
||||||
|
|
||||||
|
docker-build-web:
|
||||||
|
image: gcr.io/kaniko-project/executor:debug
|
||||||
|
environment:
|
||||||
|
GITEA_USER:
|
||||||
|
from_secret: gitea_username
|
||||||
|
GITEA_TOKEN:
|
||||||
|
from_secret: gitea_token
|
||||||
|
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||||
|
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||||
|
commands:
|
||||||
|
- *kaniko_setup
|
||||||
|
- |
|
||||||
|
DESTINATIONS=""
|
||||||
|
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||||
|
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-web:$CI_COMMIT_TAG"
|
||||||
|
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||||
|
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-web:latest"
|
||||||
|
fi
|
||||||
|
/kaniko/executor --context . --dockerfile apps/web/Dockerfile --snapshot-mode=redo --build-arg NEXT_PUBLIC_API_URL=https://api.mosaicstack.dev $DESTINATIONS
|
||||||
|
when:
|
||||||
|
- branch: [main]
|
||||||
|
event: [push, manual, tag]
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
|
|
||||||
|
# === Container Security Scan ===
|
||||||
|
|
||||||
|
security-trivy-web:
|
||||||
|
image: aquasec/trivy:latest
|
||||||
|
environment:
|
||||||
|
GITEA_USER:
|
||||||
|
from_secret: gitea_username
|
||||||
|
GITEA_TOKEN:
|
||||||
|
from_secret: gitea_token
|
||||||
|
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||||
|
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
if [ -n "$$CI_COMMIT_TAG" ]; then
|
||||||
|
SCAN_TAG="$$CI_COMMIT_TAG"
|
||||||
|
elif [ "$$CI_COMMIT_BRANCH" = "main" ]; then
|
||||||
|
SCAN_TAG="latest"
|
||||||
|
else
|
||||||
|
SCAN_TAG="latest"
|
||||||
|
fi
|
||||||
|
mkdir -p ~/.docker
|
||||||
|
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||||
|
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
|
||||||
|
--ignorefile .trivyignore \
|
||||||
|
git.mosaicstack.dev/mosaic/stack-web:$$SCAN_TAG
|
||||||
|
when:
|
||||||
|
- branch: [main]
|
||||||
|
event: [push, manual, tag]
|
||||||
|
depends_on:
|
||||||
|
- docker-build-web
|
||||||
|
|
||||||
|
# === Package Linking ===
|
||||||
|
|
||||||
|
link-packages:
|
||||||
|
image: alpine:3
|
||||||
|
environment:
|
||||||
|
GITEA_TOKEN:
|
||||||
|
from_secret: gitea_token
|
||||||
|
commands:
|
||||||
|
- apk add --no-cache curl
|
||||||
|
- sleep 10
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
link_package() {
|
||||||
|
PKG="$$1"
|
||||||
|
echo "Linking $$PKG..."
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
STATUS=$$(curl -s -o /tmp/link-response.txt -w "%{http_code}" -X POST \
|
||||||
|
-H "Authorization: token $$GITEA_TOKEN" \
|
||||||
|
"https://git.mosaicstack.dev/api/v1/packages/mosaic/container/$$PKG/-/link/stack")
|
||||||
|
if [ "$$STATUS" = "201" ] || [ "$$STATUS" = "204" ]; then
|
||||||
|
echo " Linked $$PKG"
|
||||||
|
return 0
|
||||||
|
elif [ "$$STATUS" = "400" ]; then
|
||||||
|
echo " $$PKG already linked"
|
||||||
|
return 0
|
||||||
|
elif [ "$$STATUS" = "404" ] && [ $$attempt -lt 3 ]; then
|
||||||
|
echo " $$PKG not found yet, retrying in 5s (attempt $$attempt/3)..."
|
||||||
|
sleep 5
|
||||||
|
else
|
||||||
|
echo " FAILED: $$PKG status $$STATUS"
|
||||||
|
cat /tmp/link-response.txt
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
link_package "stack-web"
|
||||||
|
when:
|
||||||
|
- branch: [main]
|
||||||
|
event: [push, manual, tag]
|
||||||
|
depends_on:
|
||||||
|
- security-trivy-web
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "agent_memories" (
|
|
||||||
"id" UUID NOT NULL,
|
|
||||||
"workspace_id" UUID NOT NULL,
|
|
||||||
"agent_id" TEXT NOT NULL,
|
|
||||||
"key" TEXT NOT NULL,
|
|
||||||
"value" JSONB NOT NULL,
|
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "agent_memories_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "agent_memories_workspace_id_agent_id_key_key" ON "agent_memories"("workspace_id", "agent_id", "key");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "agent_memories_workspace_id_idx" ON "agent_memories"("workspace_id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "agent_memories_agent_id_idx" ON "agent_memories"("agent_id");
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "agent_memories" ADD CONSTRAINT "agent_memories_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "tasks" ADD COLUMN "assigned_agent" TEXT;
|
|
||||||
@@ -299,7 +299,6 @@ model Workspace {
|
|||||||
agentSessions AgentSession[]
|
agentSessions AgentSession[]
|
||||||
agentTasks AgentTask[]
|
agentTasks AgentTask[]
|
||||||
findings Finding[]
|
findings Finding[]
|
||||||
agentMemories AgentMemory[]
|
|
||||||
userLayouts UserLayout[]
|
userLayouts UserLayout[]
|
||||||
knowledgeEntries KnowledgeEntry[]
|
knowledgeEntries KnowledgeEntry[]
|
||||||
knowledgeTags KnowledgeTag[]
|
knowledgeTags KnowledgeTag[]
|
||||||
@@ -379,7 +378,6 @@ model Task {
|
|||||||
creatorId String @map("creator_id") @db.Uuid
|
creatorId String @map("creator_id") @db.Uuid
|
||||||
projectId String? @map("project_id") @db.Uuid
|
projectId String? @map("project_id") @db.Uuid
|
||||||
parentId String? @map("parent_id") @db.Uuid
|
parentId String? @map("parent_id") @db.Uuid
|
||||||
assignedAgent String? @map("assigned_agent")
|
|
||||||
domainId String? @map("domain_id") @db.Uuid
|
domainId String? @map("domain_id") @db.Uuid
|
||||||
sortOrder Int @default(0) @map("sort_order")
|
sortOrder Int @default(0) @map("sort_order")
|
||||||
metadata Json @default("{}")
|
metadata Json @default("{}")
|
||||||
@@ -767,23 +765,6 @@ model AgentSession {
|
|||||||
@@map("agent_sessions")
|
@@map("agent_sessions")
|
||||||
}
|
}
|
||||||
|
|
||||||
model AgentMemory {
|
|
||||||
id String @id @default(uuid()) @db.Uuid
|
|
||||||
workspaceId String @map("workspace_id") @db.Uuid
|
|
||||||
agentId String @map("agent_id")
|
|
||||||
key String
|
|
||||||
value Json
|
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
||||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
|
||||||
|
|
||||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@unique([workspaceId, agentId, key])
|
|
||||||
@@index([workspaceId])
|
|
||||||
@@index([agentId])
|
|
||||||
@@map("agent_memories")
|
|
||||||
}
|
|
||||||
|
|
||||||
model WidgetDefinition {
|
model WidgetDefinition {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
import { Test, TestingModule } from "@nestjs/testing";
|
|
||||||
import { AgentMemoryController } from "./agent-memory.controller";
|
|
||||||
import { AgentMemoryService } from "./agent-memory.service";
|
|
||||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
|
||||||
import { WorkspaceGuard, PermissionGuard } from "../common/guards";
|
|
||||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
||||||
|
|
||||||
describe("AgentMemoryController", () => {
|
|
||||||
let controller: AgentMemoryController;
|
|
||||||
|
|
||||||
const mockAgentMemoryService = {
|
|
||||||
upsert: vi.fn(),
|
|
||||||
findAll: vi.fn(),
|
|
||||||
findOne: vi.fn(),
|
|
||||||
remove: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockGuard = { canActivate: vi.fn(() => true) };
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
controllers: [AgentMemoryController],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: AgentMemoryService,
|
|
||||||
useValue: mockAgentMemoryService,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
.overrideGuard(AuthGuard)
|
|
||||||
.useValue(mockGuard)
|
|
||||||
.overrideGuard(WorkspaceGuard)
|
|
||||||
.useValue(mockGuard)
|
|
||||||
.overrideGuard(PermissionGuard)
|
|
||||||
.useValue(mockGuard)
|
|
||||||
.compile();
|
|
||||||
|
|
||||||
controller = module.get<AgentMemoryController>(AgentMemoryController);
|
|
||||||
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
const workspaceId = "workspace-1";
|
|
||||||
const agentId = "agent-1";
|
|
||||||
const key = "context";
|
|
||||||
|
|
||||||
describe("upsert", () => {
|
|
||||||
it("should upsert a memory entry", async () => {
|
|
||||||
const dto = { value: { foo: "bar" } };
|
|
||||||
const mockEntry = { id: "mem-1", workspaceId, agentId, key, value: dto.value };
|
|
||||||
|
|
||||||
mockAgentMemoryService.upsert.mockResolvedValue(mockEntry);
|
|
||||||
|
|
||||||
const result = await controller.upsert(agentId, key, dto, workspaceId);
|
|
||||||
|
|
||||||
expect(mockAgentMemoryService.upsert).toHaveBeenCalledWith(workspaceId, agentId, key, dto);
|
|
||||||
expect(result).toEqual(mockEntry);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("findAll", () => {
|
|
||||||
it("should list all memory entries for an agent", async () => {
|
|
||||||
const mockEntries = [
|
|
||||||
{ id: "mem-1", key: "a", value: 1 },
|
|
||||||
{ id: "mem-2", key: "b", value: 2 },
|
|
||||||
];
|
|
||||||
|
|
||||||
mockAgentMemoryService.findAll.mockResolvedValue(mockEntries);
|
|
||||||
|
|
||||||
const result = await controller.findAll(agentId, workspaceId);
|
|
||||||
|
|
||||||
expect(mockAgentMemoryService.findAll).toHaveBeenCalledWith(workspaceId, agentId);
|
|
||||||
expect(result).toEqual(mockEntries);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("findOne", () => {
|
|
||||||
it("should get a single memory entry", async () => {
|
|
||||||
const mockEntry = { id: "mem-1", key, value: "v" };
|
|
||||||
|
|
||||||
mockAgentMemoryService.findOne.mockResolvedValue(mockEntry);
|
|
||||||
|
|
||||||
const result = await controller.findOne(agentId, key, workspaceId);
|
|
||||||
|
|
||||||
expect(mockAgentMemoryService.findOne).toHaveBeenCalledWith(workspaceId, agentId, key);
|
|
||||||
expect(result).toEqual(mockEntry);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("remove", () => {
|
|
||||||
it("should delete a memory entry", async () => {
|
|
||||||
const mockResponse = { message: "Memory entry deleted successfully" };
|
|
||||||
|
|
||||||
mockAgentMemoryService.remove.mockResolvedValue(mockResponse);
|
|
||||||
|
|
||||||
const result = await controller.remove(agentId, key, workspaceId);
|
|
||||||
|
|
||||||
expect(mockAgentMemoryService.remove).toHaveBeenCalledWith(workspaceId, agentId, key);
|
|
||||||
expect(result).toEqual(mockResponse);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import {
|
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
Put,
|
|
||||||
Delete,
|
|
||||||
Body,
|
|
||||||
Param,
|
|
||||||
UseGuards,
|
|
||||||
HttpCode,
|
|
||||||
HttpStatus,
|
|
||||||
} from "@nestjs/common";
|
|
||||||
import { AgentMemoryService } from "./agent-memory.service";
|
|
||||||
import { UpsertAgentMemoryDto } from "./dto";
|
|
||||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
|
||||||
import { WorkspaceGuard, PermissionGuard } from "../common/guards";
|
|
||||||
import { Workspace, Permission, RequirePermission } from "../common/decorators";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controller for per-agent key/value memory endpoints.
|
|
||||||
* All endpoints require authentication and workspace context.
|
|
||||||
*
|
|
||||||
* Guards are applied in order:
|
|
||||||
* 1. AuthGuard - Verifies user authentication
|
|
||||||
* 2. WorkspaceGuard - Validates workspace access
|
|
||||||
* 3. PermissionGuard - Checks role-based permissions
|
|
||||||
*/
|
|
||||||
@Controller("agents/:agentId/memory")
|
|
||||||
@UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard)
|
|
||||||
export class AgentMemoryController {
|
|
||||||
constructor(private readonly agentMemoryService: AgentMemoryService) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PUT /api/agents/:agentId/memory/:key
|
|
||||||
* Upsert a memory entry for an agent
|
|
||||||
* Requires: MEMBER role or higher
|
|
||||||
*/
|
|
||||||
@Put(":key")
|
|
||||||
@RequirePermission(Permission.WORKSPACE_MEMBER)
|
|
||||||
async upsert(
|
|
||||||
@Param("agentId") agentId: string,
|
|
||||||
@Param("key") key: string,
|
|
||||||
@Body() dto: UpsertAgentMemoryDto,
|
|
||||||
@Workspace() workspaceId: string
|
|
||||||
) {
|
|
||||||
return this.agentMemoryService.upsert(workspaceId, agentId, key, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/agents/:agentId/memory
|
|
||||||
* List all memory entries for an agent
|
|
||||||
* Requires: Any workspace member (including GUEST)
|
|
||||||
*/
|
|
||||||
@Get()
|
|
||||||
@RequirePermission(Permission.WORKSPACE_ANY)
|
|
||||||
async findAll(@Param("agentId") agentId: string, @Workspace() workspaceId: string) {
|
|
||||||
return this.agentMemoryService.findAll(workspaceId, agentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/agents/:agentId/memory/:key
|
|
||||||
* Get a single memory entry by key
|
|
||||||
* Requires: Any workspace member (including GUEST)
|
|
||||||
*/
|
|
||||||
@Get(":key")
|
|
||||||
@RequirePermission(Permission.WORKSPACE_ANY)
|
|
||||||
async findOne(
|
|
||||||
@Param("agentId") agentId: string,
|
|
||||||
@Param("key") key: string,
|
|
||||||
@Workspace() workspaceId: string
|
|
||||||
) {
|
|
||||||
return this.agentMemoryService.findOne(workspaceId, agentId, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE /api/agents/:agentId/memory/:key
|
|
||||||
* Remove a memory entry
|
|
||||||
* Requires: MEMBER role or higher
|
|
||||||
*/
|
|
||||||
@Delete(":key")
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@RequirePermission(Permission.WORKSPACE_MEMBER)
|
|
||||||
async remove(
|
|
||||||
@Param("agentId") agentId: string,
|
|
||||||
@Param("key") key: string,
|
|
||||||
@Workspace() workspaceId: string
|
|
||||||
) {
|
|
||||||
return this.agentMemoryService.remove(workspaceId, agentId, key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Module } from "@nestjs/common";
|
|
||||||
import { AgentMemoryController } from "./agent-memory.controller";
|
|
||||||
import { AgentMemoryService } from "./agent-memory.service";
|
|
||||||
import { PrismaModule } from "../prisma/prisma.module";
|
|
||||||
import { AuthModule } from "../auth/auth.module";
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [PrismaModule, AuthModule],
|
|
||||||
controllers: [AgentMemoryController],
|
|
||||||
providers: [AgentMemoryService],
|
|
||||||
exports: [AgentMemoryService],
|
|
||||||
})
|
|
||||||
export class AgentMemoryModule {}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import { Test, TestingModule } from "@nestjs/testing";
|
|
||||||
import { AgentMemoryService } from "./agent-memory.service";
|
|
||||||
import { PrismaService } from "../prisma/prisma.service";
|
|
||||||
import { NotFoundException } from "@nestjs/common";
|
|
||||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
||||||
|
|
||||||
describe("AgentMemoryService", () => {
|
|
||||||
let service: AgentMemoryService;
|
|
||||||
|
|
||||||
const mockPrismaService = {
|
|
||||||
agentMemory: {
|
|
||||||
upsert: vi.fn(),
|
|
||||||
findMany: vi.fn(),
|
|
||||||
findUnique: vi.fn(),
|
|
||||||
delete: vi.fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
AgentMemoryService,
|
|
||||||
{
|
|
||||||
provide: PrismaService,
|
|
||||||
useValue: mockPrismaService,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
service = module.get<AgentMemoryService>(AgentMemoryService);
|
|
||||||
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
const workspaceId = "workspace-1";
|
|
||||||
const agentId = "agent-1";
|
|
||||||
const key = "session-context";
|
|
||||||
|
|
||||||
describe("upsert", () => {
|
|
||||||
it("should upsert a memory entry", async () => {
|
|
||||||
const dto = { value: { data: "some context" } };
|
|
||||||
const mockEntry = {
|
|
||||||
id: "mem-1",
|
|
||||||
workspaceId,
|
|
||||||
agentId,
|
|
||||||
key,
|
|
||||||
value: dto.value,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockPrismaService.agentMemory.upsert.mockResolvedValue(mockEntry);
|
|
||||||
|
|
||||||
const result = await service.upsert(workspaceId, agentId, key, dto);
|
|
||||||
|
|
||||||
expect(mockPrismaService.agentMemory.upsert).toHaveBeenCalledWith({
|
|
||||||
where: { workspaceId_agentId_key: { workspaceId, agentId, key } },
|
|
||||||
create: { workspaceId, agentId, key, value: dto.value },
|
|
||||||
update: { value: dto.value },
|
|
||||||
});
|
|
||||||
expect(result).toEqual(mockEntry);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("findAll", () => {
|
|
||||||
it("should return all memory entries for an agent", async () => {
|
|
||||||
const mockEntries = [
|
|
||||||
{ id: "mem-1", key: "a", value: 1 },
|
|
||||||
{ id: "mem-2", key: "b", value: 2 },
|
|
||||||
];
|
|
||||||
|
|
||||||
mockPrismaService.agentMemory.findMany.mockResolvedValue(mockEntries);
|
|
||||||
|
|
||||||
const result = await service.findAll(workspaceId, agentId);
|
|
||||||
|
|
||||||
expect(mockPrismaService.agentMemory.findMany).toHaveBeenCalledWith({
|
|
||||||
where: { workspaceId, agentId },
|
|
||||||
orderBy: { key: "asc" },
|
|
||||||
});
|
|
||||||
expect(result).toEqual(mockEntries);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("findOne", () => {
|
|
||||||
it("should return a memory entry by key", async () => {
|
|
||||||
const mockEntry = { id: "mem-1", workspaceId, agentId, key, value: "ctx" };
|
|
||||||
|
|
||||||
mockPrismaService.agentMemory.findUnique.mockResolvedValue(mockEntry);
|
|
||||||
|
|
||||||
const result = await service.findOne(workspaceId, agentId, key);
|
|
||||||
|
|
||||||
expect(mockPrismaService.agentMemory.findUnique).toHaveBeenCalledWith({
|
|
||||||
where: { workspaceId_agentId_key: { workspaceId, agentId, key } },
|
|
||||||
});
|
|
||||||
expect(result).toEqual(mockEntry);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw NotFoundException when key not found", async () => {
|
|
||||||
mockPrismaService.agentMemory.findUnique.mockResolvedValue(null);
|
|
||||||
|
|
||||||
await expect(service.findOne(workspaceId, agentId, key)).rejects.toThrow(NotFoundException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("remove", () => {
|
|
||||||
it("should delete a memory entry", async () => {
|
|
||||||
const mockEntry = { id: "mem-1", workspaceId, agentId, key, value: "x" };
|
|
||||||
|
|
||||||
mockPrismaService.agentMemory.findUnique.mockResolvedValue(mockEntry);
|
|
||||||
mockPrismaService.agentMemory.delete.mockResolvedValue(mockEntry);
|
|
||||||
|
|
||||||
const result = await service.remove(workspaceId, agentId, key);
|
|
||||||
|
|
||||||
expect(mockPrismaService.agentMemory.delete).toHaveBeenCalledWith({
|
|
||||||
where: { workspaceId_agentId_key: { workspaceId, agentId, key } },
|
|
||||||
});
|
|
||||||
expect(result).toEqual({ message: "Memory entry deleted successfully" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw NotFoundException when key not found", async () => {
|
|
||||||
mockPrismaService.agentMemory.findUnique.mockResolvedValue(null);
|
|
||||||
|
|
||||||
await expect(service.remove(workspaceId, agentId, key)).rejects.toThrow(NotFoundException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
|
||||||
import { PrismaService } from "../prisma/prisma.service";
|
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
import type { UpsertAgentMemoryDto } from "./dto";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AgentMemoryService {
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upsert a memory entry for an agent.
|
|
||||||
*/
|
|
||||||
async upsert(workspaceId: string, agentId: string, key: string, dto: UpsertAgentMemoryDto) {
|
|
||||||
return this.prisma.agentMemory.upsert({
|
|
||||||
where: {
|
|
||||||
workspaceId_agentId_key: { workspaceId, agentId, key },
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
workspaceId,
|
|
||||||
agentId,
|
|
||||||
key,
|
|
||||||
value: dto.value as Prisma.InputJsonValue,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
value: dto.value as Prisma.InputJsonValue,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List all memory entries for an agent in a workspace.
|
|
||||||
*/
|
|
||||||
async findAll(workspaceId: string, agentId: string) {
|
|
||||||
return this.prisma.agentMemory.findMany({
|
|
||||||
where: { workspaceId, agentId },
|
|
||||||
orderBy: { key: "asc" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a single memory entry by key.
|
|
||||||
*/
|
|
||||||
async findOne(workspaceId: string, agentId: string, key: string) {
|
|
||||||
const entry = await this.prisma.agentMemory.findUnique({
|
|
||||||
where: {
|
|
||||||
workspaceId_agentId_key: { workspaceId, agentId, key },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!entry) {
|
|
||||||
throw new NotFoundException(`Memory key "${key}" not found for agent "${agentId}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a memory entry by key.
|
|
||||||
*/
|
|
||||||
async remove(workspaceId: string, agentId: string, key: string) {
|
|
||||||
const entry = await this.prisma.agentMemory.findUnique({
|
|
||||||
where: {
|
|
||||||
workspaceId_agentId_key: { workspaceId, agentId, key },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!entry) {
|
|
||||||
throw new NotFoundException(`Memory key "${key}" not found for agent "${agentId}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.prisma.agentMemory.delete({
|
|
||||||
where: {
|
|
||||||
workspaceId_agentId_key: { workspaceId, agentId, key },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return { message: "Memory entry deleted successfully" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./upsert-agent-memory.dto";
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { IsNotEmpty } from "class-validator";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO for upserting an agent memory entry.
|
|
||||||
* The value accepts any JSON-serializable data.
|
|
||||||
*/
|
|
||||||
export class UpsertAgentMemoryDto {
|
|
||||||
@IsNotEmpty({ message: "value must not be empty" })
|
|
||||||
value!: unknown;
|
|
||||||
}
|
|
||||||
@@ -28,7 +28,6 @@ import { BrainModule } from "./brain/brain.module";
|
|||||||
import { CronModule } from "./cron/cron.module";
|
import { CronModule } from "./cron/cron.module";
|
||||||
import { AgentTasksModule } from "./agent-tasks/agent-tasks.module";
|
import { AgentTasksModule } from "./agent-tasks/agent-tasks.module";
|
||||||
import { FindingsModule } from "./findings/findings.module";
|
import { FindingsModule } from "./findings/findings.module";
|
||||||
import { AgentMemoryModule } from "./agent-memory/agent-memory.module";
|
|
||||||
import { ValkeyModule } from "./valkey/valkey.module";
|
import { ValkeyModule } from "./valkey/valkey.module";
|
||||||
import { BullMqModule } from "./bullmq/bullmq.module";
|
import { BullMqModule } from "./bullmq/bullmq.module";
|
||||||
import { StitcherModule } from "./stitcher/stitcher.module";
|
import { StitcherModule } from "./stitcher/stitcher.module";
|
||||||
@@ -104,7 +103,6 @@ import { RlsContextInterceptor } from "./common/interceptors/rls-context.interce
|
|||||||
CronModule,
|
CronModule,
|
||||||
AgentTasksModule,
|
AgentTasksModule,
|
||||||
FindingsModule,
|
FindingsModule,
|
||||||
AgentMemoryModule,
|
|
||||||
RunnerJobsModule,
|
RunnerJobsModule,
|
||||||
JobEventsModule,
|
JobEventsModule,
|
||||||
JobStepsModule,
|
JobStepsModule,
|
||||||
|
|||||||
@@ -50,12 +50,6 @@ export class CreateTaskDto {
|
|||||||
@IsUUID("4", { message: "parentId must be a valid UUID" })
|
@IsUUID("4", { message: "parentId must be a valid UUID" })
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString({ message: "assignedAgent must be a string" })
|
|
||||||
@MinLength(1, { message: "assignedAgent must not be empty" })
|
|
||||||
@MaxLength(255, { message: "assignedAgent must not exceed 255 characters" })
|
|
||||||
assignedAgent?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsInt({ message: "sortOrder must be an integer" })
|
@IsInt({ message: "sortOrder must be an integer" })
|
||||||
@Min(0, { message: "sortOrder must be at least 0" })
|
@Min(0, { message: "sortOrder must be at least 0" })
|
||||||
|
|||||||
@@ -52,12 +52,6 @@ export class UpdateTaskDto {
|
|||||||
@IsUUID("4", { message: "parentId must be a valid UUID" })
|
@IsUUID("4", { message: "parentId must be a valid UUID" })
|
||||||
parentId?: string | null;
|
parentId?: string | null;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString({ message: "assignedAgent must be a string" })
|
|
||||||
@MinLength(1, { message: "assignedAgent must not be empty" })
|
|
||||||
@MaxLength(255, { message: "assignedAgent must not exceed 255 characters" })
|
|
||||||
assignedAgent?: string | null;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsInt({ message: "sortOrder must be an integer" })
|
@IsInt({ message: "sortOrder must be an integer" })
|
||||||
@Min(0, { message: "sortOrder must be at least 0" })
|
@Min(0, { message: "sortOrder must be at least 0" })
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ describe("TasksService", () => {
|
|||||||
creatorId: mockUserId,
|
creatorId: mockUserId,
|
||||||
projectId: null,
|
projectId: null,
|
||||||
parentId: null,
|
parentId: null,
|
||||||
assignedAgent: null,
|
|
||||||
sortOrder: 0,
|
sortOrder: 0,
|
||||||
metadata: {},
|
metadata: {},
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -159,28 +158,6 @@ describe("TasksService", () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should include assignedAgent when provided", async () => {
|
|
||||||
const createDto = {
|
|
||||||
title: "Agent-owned Task",
|
|
||||||
assignedAgent: "fleet-worker-1",
|
|
||||||
};
|
|
||||||
|
|
||||||
mockPrismaService.task.create.mockResolvedValue({
|
|
||||||
...mockTask,
|
|
||||||
assignedAgent: createDto.assignedAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
await service.create(mockWorkspaceId, mockUserId, createDto);
|
|
||||||
|
|
||||||
expect(prisma.task.create).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
data: expect.objectContaining({
|
|
||||||
assignedAgent: createDto.assignedAgent,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("findAll", () => {
|
describe("findAll", () => {
|
||||||
@@ -492,26 +469,6 @@ describe("TasksService", () => {
|
|||||||
service.update(mockTaskId, mockWorkspaceId, mockUserId, { title: "Test" })
|
service.update(mockTaskId, mockWorkspaceId, mockUserId, { title: "Test" })
|
||||||
).rejects.toThrow(NotFoundException);
|
).rejects.toThrow(NotFoundException);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update assignedAgent when provided", async () => {
|
|
||||||
const updateDto = { assignedAgent: "fleet-worker-2" };
|
|
||||||
|
|
||||||
mockPrismaService.task.findUnique.mockResolvedValue(mockTask);
|
|
||||||
mockPrismaService.task.update.mockResolvedValue({
|
|
||||||
...mockTask,
|
|
||||||
assignedAgent: updateDto.assignedAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
await service.update(mockTaskId, mockWorkspaceId, mockUserId, updateDto);
|
|
||||||
|
|
||||||
expect(prisma.task.update).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
data: expect.objectContaining({
|
|
||||||
assignedAgent: updateDto.assignedAgent,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("remove", () => {
|
describe("remove", () => {
|
||||||
|
|||||||
@@ -67,9 +67,6 @@ export class TasksService {
|
|||||||
metadata: createTaskDto.metadata
|
metadata: createTaskDto.metadata
|
||||||
? (createTaskDto.metadata as unknown as Prisma.InputJsonValue)
|
? (createTaskDto.metadata as unknown as Prisma.InputJsonValue)
|
||||||
: {},
|
: {},
|
||||||
...(createTaskDto.assignedAgent !== undefined && {
|
|
||||||
assignedAgent: createTaskDto.assignedAgent,
|
|
||||||
}),
|
|
||||||
...(assigneeConnection && { assignee: assigneeConnection }),
|
...(assigneeConnection && { assignee: assigneeConnection }),
|
||||||
...(projectConnection && { project: projectConnection }),
|
...(projectConnection && { project: projectConnection }),
|
||||||
...(parentConnection && { parent: parentConnection }),
|
...(parentConnection && { parent: parentConnection }),
|
||||||
@@ -294,9 +291,6 @@ export class TasksService {
|
|||||||
if (updateTaskDto.parentId !== undefined && updateTaskDto.parentId !== null) {
|
if (updateTaskDto.parentId !== undefined && updateTaskDto.parentId !== null) {
|
||||||
data.parent = { connect: { id: updateTaskDto.parentId } };
|
data.parent = { connect: { id: updateTaskDto.parentId } };
|
||||||
}
|
}
|
||||||
if (updateTaskDto.assignedAgent !== undefined) {
|
|
||||||
data.assignedAgent = updateTaskDto.assignedAgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle completedAt based on status changes
|
// Handle completedAt based on status changes
|
||||||
if (updateTaskDto.status) {
|
if (updateTaskDto.status) {
|
||||||
|
|||||||
@@ -52,22 +52,3 @@
|
|||||||
| **Total** | **31** | **15** | **~371K** | **~175K** |
|
| **Total** | **31** | **15** | **~371K** | **~175K** |
|
||||||
|
|
||||||
Remaining estimate: ~143K tokens (Codex budget).
|
Remaining estimate: ~143K tokens (Codex budget).
|
||||||
|
|
||||||
## MS22 — Fleet Evolution (Phase 0: Knowledge Layer)
|
|
||||||
|
|
||||||
| id | status | milestone | description | issue | repo | branch | depends_on | blocks | agent | started_at | completed_at | estimate | used | notes |
|
|
||||||
| --------------- | ----------- | ------------ | ------------------------------------------------------------ | -------- | ----- | ------------------------------ | --------------------------------------------------------- | ------------- | ------------ | ---------- | ------------ | -------- | ---- | --------------------------------------------- |
|
|
||||||
| MS22-PLAN-001 | done | p0-knowledge | PRD + mission bootstrap + TASKS.md | TASKS:P0 | stack | feat/ms22-knowledge-schema | — | MS22-DB-001 | orchestrator | 2026-02-28 | 2026-02-28 | 10K | 8K | PRD-MS22.md, mission fleet-evolution-20260228 |
|
|
||||||
| MS22-DB-001 | done | p0-knowledge | Findings module (pgvector, CRUD, similarity search) | TASKS:P0 | api | feat/ms22-findings | MS22-PLAN-001 | — | codex | 2026-02-28 | 2026-02-28 | 20K | ~22K | PR #585 merged, CI green |
|
|
||||||
| MS22-API-001 | done | p0-knowledge | Findings API endpoints | TASKS:P0 | api | feat/ms22-findings | MS22-DB-001 | — | codex | 2026-02-28 | 2026-02-28 | — | — | Combined with DB-001 |
|
|
||||||
| MS22-DB-002 | done | p0-knowledge | AgentMemory module (key/value store, upsert) | TASKS:P0 | api | feat/ms22-agent-memory | MS22-DB-001 | — | codex | 2026-02-28 | 2026-02-28 | 15K | ~16K | PR #586 merged, CI green |
|
|
||||||
| MS22-API-002 | done | p0-knowledge | AgentMemory API endpoints | TASKS:P0 | api | feat/ms22-agent-memory | MS22-DB-002 | — | codex | 2026-02-28 | 2026-02-28 | — | — | Combined with DB-002 |
|
|
||||||
| MS22-DB-004 | done | p0-knowledge | ConversationArchive module (pgvector, ingest, search) | TASKS:P0 | api | feat/ms22-conversation-archive | MS22-DB-001 | — | codex | 2026-02-28 | 2026-02-28 | 20K | ~18K | PR #587 merged, CI green |
|
|
||||||
| MS22-API-004 | done | p0-knowledge | ConversationArchive API endpoints | TASKS:P0 | api | feat/ms22-conversation-archive | MS22-DB-004 | — | codex | 2026-02-28 | 2026-02-28 | — | — | Combined with DB-004 |
|
|
||||||
| MS22-API-005 | done | p0-knowledge | EmbeddingService (reuse existing KnowledgeModule) | TASKS:P0 | api | — | — | — | orchestrator | 2026-02-28 | 2026-02-28 | 0 | 0 | Already existed; no work needed |
|
|
||||||
| MS22-DB-003 | not-started | p0-knowledge | Task model: add assigned_agent field + migration | TASKS:P0 | api | feat/ms22-task-agent | MS22-DB-001 | MS22-API-003 | — | — | — | 8K | — | Small schema + migration only |
|
|
||||||
| MS22-API-003 | not-started | p0-knowledge | Task API: expose assigned_agent in CRUD | TASKS:P0 | api | feat/ms22-task-agent | MS22-DB-003 | MS22-TEST-001 | — | — | — | 8K | — | Extend existing TaskModule |
|
|
||||||
| MS22-TEST-001 | not-started | p0-knowledge | Integration tests: Findings + AgentMemory + ConvArchive | TASKS:P0 | api | test/ms22-integration | MS22-API-001,MS22-API-002,MS22-API-004 | MS22-VER-P0 | — | — | — | 20K | — | E2E with live postgres |
|
|
||||||
| MS22-SKILL-001 | not-started | p0-knowledge | OpenClaw mosaic skill (agents read/write findings/memory) | TASKS:P0 | stack | feat/ms22-openclaw-skill | MS22-API-001,MS22-API-002 | MS22-VER-P0 | — | — | — | 15K | — | Skill in ~/.agents/skills/mosaic/ |
|
|
||||||
| MS22-INGEST-001 | not-started | p0-knowledge | Session log ingestion pipeline (OpenClaw logs → ConvArchive) | TASKS:P0 | stack | feat/ms22-ingest | MS22-API-004 | MS22-VER-P0 | — | — | — | 20K | — | Script to batch-ingest existing logs |
|
|
||||||
| MS22-VER-P0 | not-started | p0-knowledge | Phase 0 verification: all modules deployed + smoke tested | TASKS:P0 | stack | — | MS22-TEST-001,MS22-SKILL-001,MS22-INGEST-001,MS22-API-003 | — | — | — | — | 5K | — | |
|
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
# MS22 Agent Memory Module
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
|
|
||||||
Add per-agent key/value store: AgentMemory model + NestJS module with CRUD endpoints.
|
|
||||||
|
|
||||||
## Issues
|
|
||||||
|
|
||||||
- MS22-DB-002: Add AgentMemory schema model
|
|
||||||
- MS22-API-002: Add agent-memory NestJS module
|
|
||||||
|
|
||||||
## Plan
|
|
||||||
|
|
||||||
1. AgentMemory model → schema.prisma (after AgentSession, line 736)
|
|
||||||
2. Add `agentMemories AgentMemory[]` relation to Workspace model
|
|
||||||
3. Create apps/api/src/agent-memory/ with service, controller, DTOs, specs
|
|
||||||
4. Register in app.module.ts
|
|
||||||
5. Migrate: `prisma migrate dev --name ms22_agent_memory`
|
|
||||||
6. lint + build
|
|
||||||
7. Commit
|
|
||||||
|
|
||||||
## Endpoints
|
|
||||||
|
|
||||||
- PUT /api/agents/:agentId/memory/:key (upsert)
|
|
||||||
- GET /api/agents/:agentId/memory (list all)
|
|
||||||
- GET /api/agents/:agentId/memory/:key (get one)
|
|
||||||
- DELETE /api/agents/:agentId/memory/:key (remove)
|
|
||||||
|
|
||||||
## Auth
|
|
||||||
|
|
||||||
- @UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard)
|
|
||||||
- @Workspace() decorator for workspaceId
|
|
||||||
- Permission.WORKSPACE_MEMBER for write ops
|
|
||||||
- Permission.WORKSPACE_ANY for read ops
|
|
||||||
|
|
||||||
## Schema
|
|
||||||
|
|
||||||
```prisma
|
|
||||||
model AgentMemory {
|
|
||||||
id String @id @default(uuid()) @db.Uuid
|
|
||||||
workspaceId String @map("workspace_id") @db.Uuid
|
|
||||||
agentId String @map("agent_id")
|
|
||||||
key String
|
|
||||||
value Json
|
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
||||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
|
||||||
|
|
||||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@unique([workspaceId, agentId, key])
|
|
||||||
@@index([workspaceId])
|
|
||||||
@@index([agentId])
|
|
||||||
@@map("agent_memories")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Progress
|
|
||||||
|
|
||||||
- [ ] Schema
|
|
||||||
- [ ] Module files
|
|
||||||
- [ ] app.module.ts
|
|
||||||
- [ ] Migration
|
|
||||||
- [ ] lint/build
|
|
||||||
- [ ] Commit
|
|
||||||
Reference in New Issue
Block a user