feat: Complete Claude endpoint and update cookbook

- Add session_id, model, allowed_tools to Claude request handler
- Update OpenAPI spec for Claude endpoint
- Fix BuildExecutor constructor call sites
- Rewrite landing-test.sh for agent-driven flow
- Fix cookbook documentation for correct API format

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jordan 2026-01-29 21:25:29 -07:00
parent 4a18b1cd07
commit 9c15976f86
7 changed files with 488 additions and 432 deletions

View File

@ -413,7 +413,7 @@ func main() {
Logger: logger, Logger: logger,
}) })
} }
buildExecutor := worker.NewBuildExecutor(agentRegistry, gitOps, logger) buildExecutor := worker.NewBuildExecutor(agentRegistry, gitOps, logger, nil)
workerCfg := worker.DefaultWorkExecutorConfig() workerCfg := worker.DefaultWorkExecutorConfig()
workerCfg.Logger = logger workerCfg.Logger = logger
workExecutor := worker.NewWorkExecutor( workExecutor := worker.NewWorkExecutor(

View File

@ -180,13 +180,16 @@ func registerProjectPaths(spec *api.OpenAPISpec) {
func registerCommandPaths(spec *api.OpenAPISpec) { func registerCommandPaths(spec *api.OpenAPISpec) {
spec.AddPath("/projects/{id}/claude", "post", withAuthBodyAndParams( spec.AddPath("/projects/{id}/claude", "post", withAuthBodyAndParams(
"Run Claude command", "Run Claude command",
"Executes a Claude Code prompt in the project's claudebox pod. Requires projects:execute scope.", "Executes a Claude Code prompt in the project's claudebox pod. Supports session continuation, model selection (OpenCode), and tool restrictions. Requires projects:execute scope.",
"Commands", "Commands",
"projects:execute", "projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}}, []param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
`{ `{
"prompt": "fix the bug in auth handler", "prompt": "fix the bug in auth handler",
"stream_id": "optional-correlation-id" "stream_id": "optional-correlation-id",
"session_id": "prev-session-123",
"model": "claude-sonnet-4-20250514",
"allowed_tools": ["Read", "Write", "Bash(git:*)"]
}`, }`,
`{ `{
"id": "cmd-pantheon-001", "id": "cmd-pantheon-001",

View File

@ -1,337 +1,312 @@
# Landing Page Cookbook # Landing Page Cookbook
> Deploy a static landing page through the threesix.ai infrastructure with agent-driven development. > Deploy a landing page built by a Claude agent through the threesix.ai infrastructure.
## Overview ## Overview
This cookbook creates and deploys a simple landing page using the full threesix.ai autonomous infrastructure: This cookbook creates and deploys a landing page using **agent-driven development**:
``` ```
POST /project → Gitea repo + DNS + Woodpecker CI + template seed → Claude agent → git push → CI build → K8s deployment POST /project/create-and-build
Creates: Gitea repo + DNS + Woodpecker CI + K8s deployment
Enqueues build task with prompt
Worker picks up task → Claude builds the site
Agent commits + pushes
CI builds and deploys
Live site
``` ```
**Target:** `landing.threesix.ai` (with future DNS aliases for www/root) **No templates. Claude builds it from scratch based on your prompt.**
**Stack:** Astro (static site generator) via `astro-landing` template
**Status:** Coming Soon page
---
## What's Automated Today
`POST /project` orchestrates the full infrastructure setup in a single call:
| Step | Status | How |
|------|--------|-----|
| Gitea repo creation | Automated | `port.GitRepository` adapter |
| DNS A record | Automated | `port.DNSProvider` (Cloudflare) adapter |
| Woodpecker CI activation | Automated | `port.CIProvider` adapter, called during project creation |
| Template seeding | Automated | `port.TemplateProvider` with `astro-landing` template |
| K8s deployment | Automated | `port.Deployer` adapter (triggered by CI webhook) |
## Full Pipeline Status
All infrastructure gaps have been closed. The full pipeline from project creation through code generation, CI monitoring, and multi-domain DNS is operational:
| Capability | Endpoint | Status |
|------------|----------|--------|
| Project creation | `POST /project` | Operational |
| Code generation (worker) | `POST /projects/{id}/builds` | Operational |
| Create + build combo | `POST /project/create-and-build` | Operational |
| CI pipeline monitoring | `GET /projects/{id}/pipelines` | Operational |
| DNS alias management | `POST /projects/{id}/domains` | Operational |
--- ---
## Prerequisites ## Prerequisites
### Credentials Required ### API Access
```bash
| Secret | Location | Purpose | export RDEV_API_URL="https://rdev.masq-ops.orchard9.ai"
|--------|----------|---------| export RDEV_API_KEY="<your-api-key>"
| RDEV_ADMIN_KEY | `rdev-credentials` secret | rdev-api authentication | ```
| GITEA_TOKEN | `rdev-credentials` secret | Gitea API access |
| WOODPECKER_API_TOKEN | `rdev-credentials` secret | Woodpecker repo activation |
| CLOUDFLARE_API_TOKEN | `rdev-credentials` secret | DNS management |
### Infrastructure Required ### Infrastructure Required
- rdev-api running with embedded worker
- [x] rdev-api running with infrastructure handlers - Gitea at https://git.threesix.ai
- [x] Gitea at https://git.threesix.ai - Woodpecker CI at https://ci.threesix.ai
- [x] Woodpecker CI at https://ci.threesix.ai - claudebox-0 pod running in rdev namespace
- [x] Zot registry at registry.threesix.ai
- [x] `projects` namespace in K8s with RBAC
- [x] Wildcard TLS cert for *.threesix.ai
--- ---
## Architecture ## Step 1: Create Project and Build in One Call
``` Single API call that creates infrastructure AND enqueues agent work:
┌──────────────────────────────────────────────────────────────────────┐
│ Landing Page Flow │
│ │
│ 1. Create Project (single API call) │
│ POST /project {"name": "landing", "template": "astro-landing"} │
│ │ │
│ ├──▶ Creates Gitea repo: threesix/landing │
│ ├──▶ Creates DNS: landing.threesix.ai → cluster IP │
│ ├──▶ Activates Woodpecker CI (auto) │
│ └──▶ Seeds repo with astro-landing template │
│ │
│ 2. Generate Code (3 options) │
│ Via worker pool, claudebox, or local Claude Code │
│ │ │
│ ├──▶ Customizes Astro landing page │
│ ├──▶ Commits and pushes to Gitea │
│ └──▶ Worker executor polls queue, dispatches to agent │
│ │
│ 3. CI/CD Pipeline (automatic on push) │
│ Woodpecker triggered by git push │
│ │ │
│ ├──▶ npm install + npm build │
│ ├──▶ Docker build (nginx) │
│ ├──▶ Push to Zot registry │
│ └──▶ kubectl set image (deploy) │
│ │
│ 4. Live at https://landing.threesix.ai │
│ │
└──────────────────────────────────────────────────────────────────────┘
```
---
## Step-by-Step Implementation
### Step 1: Create Project via rdev-api
This single call creates the Gitea repo, DNS record, activates Woodpecker CI, and seeds the repo with the `astro-landing` template.
```bash ```bash
curl -X POST https://rdev.masq-ops.orchard9.ai/project \ curl -X POST "$RDEV_API_URL/project/create-and-build" \
-H "Authorization: Bearer $RDEV_KEY" \ -H "X-API-Key: $RDEV_API_KEY" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"name": "landing", "name": "my-landing",
"description": "threesix.ai landing page", "description": "Company landing page",
"template": "astro-landing" "prompt": "Build a modern landing page with: dark gradient background, centered hero section with company name and tagline, email signup form, responsive design. Use vanilla HTML/CSS/JS. Create index.html, styles.css, and a simple Dockerfile that serves with nginx.",
"auto_commit": true,
"auto_push": true
}' }'
``` ```
**Response:** **Response:**
```json ```json
{ {
"project_id": "landing", "data": {
"name": "landing", "project_id": "my-landing",
"description": "threesix.ai landing page", "name": "my-landing",
"git": { "domain": "abc123xy.threesix.ai",
"owner": "threesix", "url": "https://abc123xy.threesix.ai",
"name": "landing", "git": {
"clone_ssh": "git@git.threesix.ai:threesix/landing.git", "owner": "jordan",
"clone_http": "https://git.threesix.ai/threesix/landing.git", "name": "my-landing",
"html_url": "https://git.threesix.ai/threesix/landing" "html_url": "https://git.threesix.ai/jordan/my-landing"
},
"task_id": "task-uuid",
"status": "pending",
"status_url": "/builds/task-uuid"
}, },
"domain": "landing.threesix.ai", "meta": { "timestamp": "..." }
"url": "https://landing.threesix.ai",
"next_steps": []
} }
``` ```
If any infrastructure step fails, `next_steps` will contain manual instructions for that step. The remaining steps still execute. ---
### Step 2: Generate Code ## Step 2: Monitor Build Progress
The `astro-landing` template seeds the repo with a working Astro project, Dockerfile, nginx.conf, and `.woodpecker.yml`. You can customize it via Claude. Poll the build status:
**Option A: Local Claude Code (recommended for now)**
```bash ```bash
git clone https://git.threesix.ai/threesix/landing.git curl -s "$RDEV_API_URL/builds/{task_id}" \
cd landing -H "X-API-Key: $RDEV_API_KEY" | jq .data
# Use Claude Code to customize the landing page
# Then commit and push
``` ```
**Option B: Via existing claudebox** **Status progression:** `pending``running``completed` (or `failed`)
When completed:
```json
{
"task_id": "task-uuid",
"project_id": "my-landing",
"status": "completed",
"prompt": "Build a modern landing page...",
"auto_commit": true,
"auto_push": true,
"started_at": "2025-01-29T10:00:00Z",
"completed_at": "2025-01-29T10:00:45Z",
"result": {
"success": true,
"commit_sha": "abc123",
"files_changed": ["index.html", "styles.css", "Dockerfile", "nginx.conf"],
"duration_ms": 45000
}
}
```
---
## Step 3: Monitor CI Pipeline
The agent's push triggers Woodpecker CI:
```bash ```bash
curl -X POST "https://rdev.masq-ops.orchard9.ai/projects/pantheon/claude" \ curl -s "$RDEV_API_URL/projects/my-landing/pipelines" \
-H "Authorization: Bearer $RDEV_KEY" \ -H "X-API-Key: $RDEV_API_KEY" | jq '.data[0]'
```
Wait for `status: "success"`.
---
## Step 4: Verify Deployment
```bash
# Check site is live
curl -I https://abc123xy.threesix.ai
# View the site
open https://abc123xy.threesix.ai
```
---
## Alternative: Two-Step Flow
If you prefer to create the project first, then submit builds separately:
### Create Project (empty repo)
```bash
curl -X POST "$RDEV_API_URL/project" \
-H "X-API-Key: $RDEV_API_KEY" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"prompt": "Clone https://git.threesix.ai/threesix/landing.git to /tmp/landing, then customize the Astro landing page with: Coming Soon message, threesix.ai branding (dark theme, gradient background), responsive layout. Commit and push when done." "name": "my-landing",
"description": "Company landing page"
}' }'
``` ```
**Option C: Via work queue (build endpoint)** ### Submit Build Task
```bash ```bash
# Enqueue a build task for a worker to execute curl -X POST "$RDEV_API_URL/projects/my-landing/builds" \
curl -X POST "https://rdev.masq-ops.orchard9.ai/projects/landing/builds" \ -H "X-API-Key: $RDEV_API_KEY" \
-H "Authorization: Bearer $RDEV_KEY" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"prompt": "Customize the landing page with Coming Soon message and threesix.ai branding", "prompt": "Build a modern landing page with dark theme, hero section, and email signup form. Use HTML/CSS/JS with nginx Dockerfile.",
"auto_commit": true, "auto_commit": true,
"auto_push": true "auto_push": true
}' }'
``` ```
**Option D: Create + build in one call** ---
## Iterating on the Site
Submit additional builds to modify the site:
```bash ```bash
curl -X POST "https://rdev.masq-ops.orchard9.ai/project/create-and-build" \ curl -X POST "$RDEV_API_URL/projects/my-landing/builds" \
-H "Authorization: Bearer $RDEV_KEY" \ -H "X-API-Key: $RDEV_API_KEY" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"name": "landing", "prompt": "Add a pricing section with three tiers: Free, Pro ($29/mo), Enterprise (contact us). Match the existing dark theme.",
"description": "threesix.ai landing page", "auto_commit": true,
"template": "astro-landing", "auto_push": true
"build": {
"prompt": "Customize with Coming Soon message and threesix.ai branding",
"auto_commit": true,
"auto_push": true
}
}' }'
``` ```
### Step 3: Monitor Build Each build:
1. Claude clones the repo
2. Makes the requested changes
3. Commits and pushes
4. CI deploys automatically
The git push triggers Woodpecker CI automatically. ---
**Via rdev-api (recommended):** ## Adding Custom Domains
```bash
# List recent pipelines
curl -s "https://rdev.masq-ops.orchard9.ai/projects/landing/pipelines" \
-H "Authorization: Bearer $RDEV_KEY" | jq '.data.pipelines'
# Get specific pipeline
curl -s "https://rdev.masq-ops.orchard9.ai/projects/landing/pipelines/1" \
-H "Authorization: Bearer $RDEV_KEY" | jq '.data'
```
**Via Woodpecker UI:**
- https://ci.threesix.ai/threesix/landing
### Step 4: Verify Deployment
```bash ```bash
# Check project status via rdev-api # Add www subdomain
curl -s "https://rdev.masq-ops.orchard9.ai/project/landing" \ curl -X POST "$RDEV_API_URL/projects/my-landing/domains" \
-H "Authorization: Bearer $RDEV_KEY" | jq '.data.deployment' -H "X-API-Key: $RDEV_API_KEY" \
# Check deployment via K8s
export KUBECONFIG=~/.kube/orchard9-k3sf.yaml
kubectl get deploy -n projects landing
# Check the site
curl -I https://landing.threesix.ai
```
### Step 5: Configure DNS Aliases (Optional)
Point `www.threesix.ai` and `threesix.ai` to the landing page via the rdev-api domain alias endpoints.
```bash
# Add www.threesix.ai as a CNAME alias
curl -X POST "https://rdev.masq-ops.orchard9.ai/projects/landing/domains" \
-H "Authorization: Bearer $RDEV_KEY" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"domain": "www.threesix.ai", "type": "CNAME"}' -d '{"domain": "www.mycompany.com"}'
# Add root A record # List all domains
curl -X POST "https://rdev.masq-ops.orchard9.ai/projects/landing/domains" \ curl -s "$RDEV_API_URL/projects/my-landing/domains" \
-H "Authorization: Bearer $RDEV_KEY" \ -H "X-API-Key: $RDEV_API_KEY" | jq '.data.domains'
-H "Content-Type: application/json" \
-d '{"domain": "threesix.ai"}'
# List all domains for the project
curl -s "https://rdev.masq-ops.orchard9.ai/projects/landing/domains" \
-H "Authorization: Bearer $RDEV_KEY" | jq '.data.domains'
# Remove an alias
curl -X DELETE "https://rdev.masq-ops.orchard9.ai/projects/landing/domains/www.threesix.ai" \
-H "Authorization: Bearer $RDEV_KEY"
``` ```
--- ---
## Template: astro-landing ## Teardown
The `astro-landing` template (`deployments/k8s/base/templates/astro-landing/`) seeds the repo with:
| File | Purpose |
|------|---------|
| `.woodpecker.yml` | CI pipeline: npm build, Docker build, push, deploy |
| `.claude/CLAUDE.md` | Project instructions for Claude Code |
| `Dockerfile` | Multi-stage build (Node 20 build, nginx serve) |
| `nginx.conf` | Production config with gzip, caching, SPA fallback |
| `package.json` | Astro 4.0+ with Tailwind CSS |
| `astro.config.mjs` | Astro configuration |
| `tailwind.config.mjs` | Tailwind configuration |
| `src/pages/index.astro` | Landing page (dark theme with gradient) |
| `src/layouts/Layout.astro` | Base HTML layout |
| `README.md` | Development and deployment docs |
Variables substituted during seeding: `{{PROJECT_NAME}}`, `{{DOMAIN}}`, `{{GIT_URL}}`
---
## Implementation Status
All components for the full landing page pipeline are implemented:
| Component | Location | Status |
|-----------|----------|--------|
| Work queue (enqueue/dequeue) | `internal/adapter/postgres/work_queue.go` | Implemented |
| Worker registry | `internal/adapter/postgres/worker_registry.go` | Implemented |
| Build audit tracking | `internal/adapter/postgres/build_audit.go` | Implemented |
| Build service | `internal/service/build_service.go` | Implemented |
| Worker service | `internal/service/worker_service.go` | Implemented |
| Work handlers (REST) | `internal/handlers/work.go` | Implemented |
| Code agent interface | `internal/port/code_agent.go` | Implemented |
| Worker executor daemon | `internal/worker/work_executor.go` | Implemented |
| BuildSpec-to-agent bridge | `internal/worker/build_executor.go` | Implemented |
| Git credential resolution | `internal/service/credential_service.go` | Implemented |
| DNS alias endpoints | `internal/handlers/infrastructure_domains.go` | Implemented |
| CI pipeline proxy | `internal/handlers/infrastructure_pipelines.go` | Implemented |
| Create-and-build endpoint | `internal/handlers/create_and_build.go` | Implemented |
| Multi-domain support | `internal/adapter/postgres/project_domain.go` | Implemented |
| Auto-generated slugs | `internal/domain/project_domain.go` | Implemented |
| Site health check | `internal/service/project_infra.go` | Implemented |
**Ready for E2E testing:** Run `./cookbooks/scripts/landing-test.sh run` to test the full flow.
---
## Verification
After deployment, verify:
```bash ```bash
# Check DNS curl -X DELETE "$RDEV_API_URL/project/my-landing" \
dig landing.threesix.ai -H "X-API-Key: $RDEV_API_KEY"
```
# Check site Removes: DNS records, K8s deployment, project metadata. Gitea repo preserved for safety.
curl -I https://landing.threesix.ai
# Check deployment status ---
curl https://rdev.masq-ops.orchard9.ai/project/landing \
-H "Authorization: Bearer $RDEV_KEY" ## E2E Test Script
Run the full flow:
```bash
./cookbooks/scripts/landing-test.sh run my-test-landing
```
Check status:
```bash
./cookbooks/scripts/landing-test.sh status my-test-landing
```
Cleanup:
```bash
./cookbooks/scripts/landing-test.sh teardown my-test-landing
``` ```
--- ---
## Rollback ## Architecture
To remove the landing page: ```
┌─────────────────────────────────────────────────────────────────────┐
│ Agent-Driven Landing Page │
│ │
│ POST /project/create-and-build │
│ │ │
│ ├──► Gitea: creates repo │
│ ├──► Cloudflare: creates DNS │
│ ├──► Woodpecker: activates CI │
│ ├──► K8s: creates Deployment/Service/Ingress │
│ └──► Work Queue: enqueues build task │
│ │ │
│ ▼ │
│ Worker polls queue, claims task │
│ │ │
│ ▼ │
│ Claude Code executes in claudebox-0: │
│ - Clones repo │
│ - Builds site from prompt │
│ - Commits and pushes │
│ │ │
│ ▼ │
│ Woodpecker CI triggered by push: │
│ - Builds Docker image │
│ - Pushes to registry │
│ - Updates K8s deployment │
│ │ │
│ ▼ │
│ Site live at https://{slug}.threesix.ai │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Troubleshooting
### Build stuck in pending
```bash ```bash
# Delete via rdev-api (removes DNS, K8s deployment; Gitea repo preserved for safety) # Check worker status
curl -X DELETE https://rdev.masq-ops.orchard9.ai/project/landing \ curl -s "$RDEV_API_URL/workers" -H "X-API-Key: $RDEV_API_KEY" | jq '.data.summary'
-H "Authorization: Bearer $RDEV_KEY"
# Should show at least 1 idle worker
```
### Build failed
```bash
# Get build details
curl -s "$RDEV_API_URL/builds/{task_id}" -H "X-API-Key: $RDEV_API_KEY" | jq '.result'
# Check rdev-api logs
./scripts/logs.sh -e
```
### Pipeline not triggering
```bash
# Check if commit was pushed
curl -s "https://git.threesix.ai/api/v1/repos/jordan/my-landing/commits" | jq '.[0]'
# Check Woodpecker
open https://ci.threesix.ai/jordan/my-landing
``` ```
--- ---
## Related ## Related
- [Build Orchestration](../ai-lookup/features/build-orchestration.md) - Build system documentation - [Full-Stack App Cookbook](./fullstack-app.md) - Next.js + Go backend
- [Worker Pool](../ai-lookup/services/worker-pool.md) - Worker pool management - [Worker Pool Guide](../.claude/guides/services/worker-pool.md)
- [Work Queue](../.claude/guides/services/work-queue.md) - Work queue guide - [Build Orchestration](../.claude/guides/services/build-orchestration.md)
- [Templates](../.claude/guides/services/templates.md) - Project template guide

View File

@ -1,19 +1,17 @@
#!/bin/bash #!/bin/bash
# Landing Page Cookbook Test Script # Landing Page Cookbook Test Script
# Tests the full landing page flow from cookbooks/landing-page.md # Tests the full agent-driven landing page flow from cookbooks/landing-page.md
# #
# Usage: # Usage:
# ./cookbooks/scripts/landing-test.sh run # Run the full flow # ./cookbooks/scripts/landing-test.sh run [name] # Run the full flow
# ./cookbooks/scripts/landing-test.sh teardown # Clean up test resources # ./cookbooks/scripts/landing-test.sh teardown [name] # Clean up test resources
# ./cookbooks/scripts/landing-test.sh status # Check current status # ./cookbooks/scripts/landing-test.sh status [name] # Check current status
set -euo pipefail set -euo pipefail
# Configuration # Configuration
API_URL="${RDEV_API_URL:-https://rdev.masq-ops.orchard9.ai}" API_URL="${RDEV_API_URL:-https://rdev.masq-ops.orchard9.ai}"
API_KEY="${RDEV_API_KEY:?RDEV_API_KEY environment variable required}" API_KEY="${RDEV_API_KEY:?RDEV_API_KEY environment variable required}"
PROJECT_NAME="${1:-landing-test}"
TEMPLATE="astro-landing"
# Colors # Colors
RED='\033[0;31m' RED='\033[0;31m'
@ -27,10 +25,12 @@ log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Configuration for polling # Timeouts
PIPELINE_TIMEOUT=300 # 5 minutes max wait for pipeline BUILD_TIMEOUT=180 # 3 minutes for Claude to build the site
PIPELINE_POLL_INTERVAL=10 # Check every 10 seconds BUILD_POLL_INTERVAL=5 # Check every 5 seconds
SITE_TIMEOUT=60 # 1 minute max wait for site to be live PIPELINE_TIMEOUT=300 # 5 minutes max wait for CI pipeline
PIPELINE_POLL_INTERVAL=10
SITE_TIMEOUT=60 # 1 minute max wait for site to be live
api_call() { api_call() {
local method="$1" local method="$1"
@ -62,6 +62,61 @@ check_health() {
fi fi
} }
# Wait for build to complete (Claude building the site)
# Returns: 0 on success, 1 on failure/timeout
wait_for_build() {
local task_id="$1"
local start_time=$(date +%s)
log_info "Waiting for Claude to build the site (timeout: ${BUILD_TIMEOUT}s)..."
while true; do
local elapsed=$(($(date +%s) - start_time))
if [[ $elapsed -ge $BUILD_TIMEOUT ]]; then
log_error "Build timeout after ${BUILD_TIMEOUT}s"
return 1
fi
local response
response=$(api_call GET "/builds/$task_id" 2>/dev/null || echo "{}")
local status
status=$(echo "$response" | jq -r '.data.status // "unknown"' 2>/dev/null)
case "$status" in
completed)
local success
success=$(echo "$response" | jq -r '.data.result.success // false')
if [[ "$success" == "true" ]]; then
log_success "Build completed successfully (${elapsed}s)"
echo "$response" | jq '.data.result | {success, commit_sha, files_changed, duration_ms}'
return 0
else
log_error "Build completed but failed"
echo "$response" | jq '.data.result'
return 1
fi
;;
failed)
log_error "Build failed"
echo "$response" | jq '.data.result // .data'
return 1
;;
running)
echo -ne "\r${BLUE}[INFO]${NC} Build status: running (${elapsed}s)... "
;;
pending)
echo -ne "\r${BLUE}[INFO]${NC} Build status: pending (${elapsed}s)... "
;;
*)
echo -ne "\r${BLUE}[INFO]${NC} Build status: $status (${elapsed}s)... "
;;
esac
sleep $BUILD_POLL_INTERVAL
done
}
# Wait for pipeline to appear and complete # Wait for pipeline to appear and complete
# Returns: 0 on success, 1 on failure/timeout # Returns: 0 on success, 1 on failure/timeout
wait_for_pipeline() { wait_for_pipeline() {
@ -83,7 +138,7 @@ wait_for_pipeline() {
local response local response
response=$(api_call GET "/projects/$project_name/pipelines" 2>/dev/null || echo "{}") response=$(api_call GET "/projects/$project_name/pipelines" 2>/dev/null || echo "{}")
# Check if we have pipelines (API returns array at .data, not .data.pipelines) # Check if we have pipelines (API returns array at .data)
local pipeline_count local pipeline_count
pipeline_count=$(echo "$response" | jq -r '.data | length' 2>/dev/null || echo "0") pipeline_count=$(echo "$response" | jq -r '.data | length' 2>/dev/null || echo "0")
@ -177,7 +232,7 @@ test_dns_alias() {
fi fi
log_success "DNS alias added: $alias_domain" log_success "DNS alias added: $alias_domain"
echo "$response" | jq '.data | {domain, type, dns_record_id}' echo "$response" | jq '.data | {domain, type, record_type}'
return 0 return 0
} }
@ -208,15 +263,15 @@ remove_dns_alias() {
run_flow() { run_flow() {
local project_name="${1:-landing-test}" local project_name="${1:-landing-test}"
local custom_subdomain="${2:-}"
# Default prompt for building a landing page
local build_prompt="Build a modern landing page with: dark gradient background (#1a1a2e to #16213e), centered hero section with company name 'Acme Corp' and tagline 'Building the future', email signup form with a submit button, responsive design for mobile. Use vanilla HTML/CSS/JS. Create index.html, styles.css, and a Dockerfile that serves with nginx on port 80."
echo "" echo ""
echo "==========================================" echo "=========================================="
echo " Landing Page Cookbook Test" echo " Landing Page Cookbook Test"
echo " Project: $project_name" echo " Project: $project_name"
if [[ -n "$custom_subdomain" ]]; then echo " Flow: Agent-driven (create-and-build)"
echo " Custom subdomain: $custom_subdomain"
fi
echo "==========================================" echo "=========================================="
echo "" echo ""
@ -224,21 +279,25 @@ run_flow() {
check_health || exit 1 check_health || exit 1
echo "" echo ""
# Step 1: Create project # Step 1: Create project AND enqueue build in one call
log_info "Step 1: Creating project with $TEMPLATE template..." log_info "Step 1: Creating project and enqueuing build task..."
local create_payload="{ log_info "Prompt: ${build_prompt:0:80}..."
\"name\": \"$project_name\",
\"description\": \"Cookbook test: landing page flow\", local create_payload
\"template\": \"$TEMPLATE\"" create_payload=$(jq -n \
if [[ -n "$custom_subdomain" ]]; then --arg name "$project_name" \
create_payload="$create_payload, --arg desc "Cookbook test: agent-driven landing page" \
\"custom_subdomain\": \"$custom_subdomain\"" --arg prompt "$build_prompt" \
fi '{
create_payload="$create_payload name: $name,
}" description: $desc,
prompt: $prompt,
auto_commit: true,
auto_push: true
}')
local create_response local create_response
create_response=$(api_call POST "/project" "$create_payload") create_response=$(api_call POST "/project/create-and-build" "$create_payload")
if echo "$create_response" | jq -e '.error' > /dev/null 2>&1; then if echo "$create_response" | jq -e '.error' > /dev/null 2>&1; then
log_error "Failed to create project" log_error "Failed to create project"
@ -246,104 +305,62 @@ run_flow() {
exit 1 exit 1
fi fi
log_success "Project created" log_success "Project created and build enqueued"
echo "$create_response" | jq '{ echo "$create_response" | jq '.data | {
project_id: .data.project_id, project_id,
slug: .data.slug, domain,
git_url: .data.git.html_url, url,
domain: .data.domain, git: .git.html_url,
url: .data.url, task_id,
domains: .data.domains, status,
next_steps: .data.next_steps status_url
}' }'
# Extract domain info # Extract key info
local primary_domain local primary_domain
local slug local task_id
primary_domain=$(echo "$create_response" | jq -r '.data.domain') primary_domain=$(echo "$create_response" | jq -r '.data.domain')
slug=$(echo "$create_response" | jq -r '.data.slug // empty') task_id=$(echo "$create_response" | jq -r '.data.task_id')
if [[ -n "$slug" ]]; then if [[ -z "$task_id" || "$task_id" == "null" ]]; then
log_success "Auto-generated slug: $slug" log_error "No task_id returned - build was not enqueued"
exit 1
fi fi
# Check for manual steps log_success "Build task ID: $task_id"
local next_steps
next_steps=$(echo "$create_response" | jq -r '.data.next_steps[]?' 2>/dev/null || echo "")
if [[ -n "$next_steps" ]]; then
echo ""
log_warn "Some steps require manual intervention:"
echo "$create_response" | jq -r '.data.next_steps[]'
echo ""
log_info "Check API logs for details: ./scripts/logs.sh -e"
fi
echo "" echo ""
# Step 2: List all domains # Step 2: Monitor build progress (Claude building the site)
log_info "Step 2: Listing all project domains..." log_info "Step 2: Monitoring build progress..."
local domains_response echo ""
domains_response=$(api_call GET "/projects/$project_name/domains") local build_success=false
if wait_for_build "$task_id"; then
if echo "$domains_response" | jq -e '.error' > /dev/null 2>&1; then build_success=true
log_warn "Could not list domains"
echo "$domains_response" | jq .
else else
local domain_count log_error "Build did not complete successfully"
domain_count=$(echo "$domains_response" | jq -r '.data.total') log_info "Check build details: curl -s \"\$RDEV_API_URL/builds/$task_id\" -H \"X-API-Key: \$RDEV_API_KEY\" | jq ."
log_success "Found $domain_count domain(s)"
echo "$domains_response" | jq '.data.domains[] | {domain, type, verified}'
fi fi
echo "" echo ""
# Step 3: Verify project status # Step 3: Monitor CI pipeline (only if build succeeded)
log_info "Step 3: Verifying project status..."
sleep 2 # Give DNS/Gitea a moment
local status_response
status_response=$(api_call GET "/project/$project_name")
if echo "$status_response" | jq -e '.error' > /dev/null 2>&1; then
log_warn "Could not fetch project status"
echo "$status_response" | jq .
else
log_success "Project status retrieved"
echo "$status_response" | jq '{
name: .data.name,
slug: .data.slug,
domain: .data.domain,
url: .data.url,
domains: .data.domains,
git: .data.git.html_url,
deployment: .data.deployment
}'
fi
echo ""
# Step 4: Check DNS
log_info "Step 4: Checking DNS resolution..."
if host "$primary_domain" > /dev/null 2>&1; then
log_success "DNS resolves: $primary_domain"
host "$primary_domain" | head -1
else
log_warn "DNS not yet resolving: $primary_domain (may take a few minutes)"
fi
echo ""
# Step 5: Wait for CI pipeline
log_info "Step 5: Monitoring CI pipeline..."
local pipeline_success=false local pipeline_success=false
if wait_for_pipeline "$project_name"; then if [[ "$build_success" == "true" ]]; then
pipeline_success=true log_info "Step 3: Monitoring CI pipeline..."
if wait_for_pipeline "$project_name"; then
pipeline_success=true
else
log_warn "Pipeline did not complete successfully"
log_info "Check Woodpecker: https://ci.threesix.ai/threesix/$project_name"
fi
else else
log_warn "Pipeline did not complete successfully - site may not deploy" log_info "Step 3: Skipping pipeline monitoring (build failed)"
log_info "Check Woodpecker: https://ci.threesix.ai/threesix/$project_name"
fi fi
echo "" echo ""
# Step 6: Verify site is live (only if pipeline succeeded) # Step 4: Verify site is live
local site_live=false local site_live=false
if [[ "$pipeline_success" == "true" ]]; then if [[ "$pipeline_success" == "true" ]]; then
log_info "Step 6: Verifying site is accessible..." log_info "Step 4: Verifying site is accessible..."
if wait_for_site "$primary_domain"; then if wait_for_site "$primary_domain"; then
site_live=true site_live=true
# Show a snippet of the response # Show a snippet of the response
@ -351,28 +368,29 @@ run_flow() {
curl -s "https://$primary_domain" | head -20 | grep -E '<title>|<h1' || true curl -s "https://$primary_domain" | head -20 | grep -E '<title>|<h1' || true
fi fi
else else
log_info "Step 6: Skipping site verification (pipeline not successful)" log_info "Step 4: Skipping site verification (pipeline not successful)"
fi fi
echo "" echo ""
# Step 7: Test DNS alias functionality # Step 5: Test adding custom domains
local test_alias="${project_name}-alias.threesix.ai" local test_alias="${project_name}-alias.threesix.ai"
log_info "Step 7: Testing DNS alias functionality..." log_info "Step 5: Testing custom domain functionality..."
if test_dns_alias "$project_name" "$test_alias"; then if test_dns_alias "$project_name" "$test_alias"; then
log_success "DNS alias test passed" log_success "Domain alias test passed"
# Clean up test alias immediately # Clean up test alias
sleep 2 sleep 2
remove_dns_alias "$project_name" "$test_alias" remove_dns_alias "$project_name" "$test_alias"
else else
log_warn "DNS alias test failed - check Cloudflare permissions" log_warn "Domain alias test failed - check Cloudflare permissions"
fi fi
echo "" echo ""
# Step 8: Final domain listing # List all domains
log_info "Step 8: Final domain listing..." log_info "Listing all project domains..."
local domains_response
domains_response=$(api_call GET "/projects/$project_name/domains") domains_response=$(api_call GET "/projects/$project_name/domains")
if echo "$domains_response" | jq -e '.data.domains' > /dev/null 2>&1; then if echo "$domains_response" | jq -e '.data.domains' > /dev/null 2>&1; then
echo "$domains_response" | jq '.data.domains[] | {domain, type, is_primary}' echo "$domains_response" | jq '.data.domains[] | {domain, type, verified}'
fi fi
echo "" echo ""
@ -382,30 +400,35 @@ run_flow() {
echo "==========================================" echo "=========================================="
echo "" echo ""
echo " Project: $project_name" echo " Project: $project_name"
echo " Git repo: $(echo "$create_response" | jq -r '.data.git.html_url')" echo " Task ID: $task_id"
if [[ -n "$slug" ]]; then echo " Git repo: $(echo "$create_response" | jq -r '.data.git.html_url // "N/A"')"
echo " Slug: $slug"
fi
echo " Primary: https://$primary_domain" echo " Primary: https://$primary_domain"
echo "" echo ""
echo " Test Results:" echo " Test Results:"
echo -e " Project created: ${GREEN}PASS${NC}" echo -e " Project created: ${GREEN}PASS${NC}"
if [[ "$build_success" == "true" ]]; then
echo -e " Agent build: ${GREEN}PASS${NC}"
else
echo -e " Agent build: ${RED}FAIL${NC}"
fi
if [[ "$pipeline_success" == "true" ]]; then if [[ "$pipeline_success" == "true" ]]; then
echo -e " CI Pipeline: ${GREEN}PASS${NC}" echo -e " CI Pipeline: ${GREEN}PASS${NC}"
else elif [[ "$build_success" == "true" ]]; then
echo -e " CI Pipeline: ${RED}FAIL${NC}" echo -e " CI Pipeline: ${RED}FAIL${NC}"
else
echo -e " CI Pipeline: ${YELLOW}SKIPPED${NC}"
fi fi
if [[ "$site_live" == "true" ]]; then if [[ "$site_live" == "true" ]]; then
echo -e " Site accessible: ${GREEN}PASS${NC}" echo -e " Site accessible: ${GREEN}PASS${NC}"
else elif [[ "$pipeline_success" == "true" ]]; then
echo -e " Site accessible: ${YELLOW}PENDING${NC}" echo -e " Site accessible: ${YELLOW}PENDING${NC}"
else
echo -e " Site accessible: ${YELLOW}SKIPPED${NC}"
fi fi
echo -e " DNS alias: ${GREEN}TESTED${NC}" echo -e " Custom domains: ${GREEN}TESTED${NC}"
echo ""
echo " All domains:"
echo "$domains_response" | jq -r '.data.domains[]? | " - \(.domain) (\(.type))"' 2>/dev/null || echo " (none listed)"
echo "" echo ""
echo " Useful commands:" echo " Useful commands:"
echo " Build status: curl -s \"\$RDEV_API_URL/builds/$task_id\" -H \"X-API-Key: \$RDEV_API_KEY\" | jq .data"
echo " Check status: ./cookbooks/scripts/landing-test.sh status $project_name" echo " Check status: ./cookbooks/scripts/landing-test.sh status $project_name"
echo " View logs: ./scripts/logs.sh -e" echo " View logs: ./scripts/logs.sh -e"
echo " Woodpecker: https://ci.threesix.ai/threesix/$project_name" echo " Woodpecker: https://ci.threesix.ai/threesix/$project_name"
@ -413,14 +436,17 @@ run_flow() {
echo "" echo ""
# Return appropriate exit code # Return appropriate exit code
if [[ "$pipeline_success" == "true" && "$site_live" == "true" ]]; then if [[ "$build_success" == "true" && "$pipeline_success" == "true" && "$site_live" == "true" ]]; then
log_success "Full E2E test PASSED" log_success "Full E2E test PASSED"
return 0 return 0
elif [[ "$pipeline_success" == "true" ]]; then elif [[ "$build_success" == "true" && "$pipeline_success" == "true" ]]; then
log_warn "Partial success - pipeline passed but site not yet live" log_warn "Partial success - build and pipeline passed but site not yet live"
return 0 return 0
elif [[ "$build_success" == "true" ]]; then
log_warn "Partial success - build passed but pipeline failed"
return 1
else else
log_error "E2E test FAILED - pipeline did not complete" log_error "E2E test FAILED - build did not complete"
return 1 return 1
fi fi
} }
@ -453,7 +479,7 @@ teardown() {
if echo "$response" | jq -e '.data.status == "deleted"' > /dev/null 2>&1; then if echo "$response" | jq -e '.data.status == "deleted"' > /dev/null 2>&1; then
log_success "Project deleted" log_success "Project deleted"
echo "$response" | jq . echo "$response" | jq .data
elif echo "$response" | jq -e '.error.code == "NOT_FOUND"' > /dev/null 2>&1; then elif echo "$response" | jq -e '.error.code == "NOT_FOUND"' > /dev/null 2>&1; then
log_warn "Project not found (already deleted?)" log_warn "Project not found (already deleted?)"
else else
@ -464,7 +490,7 @@ teardown() {
echo "" echo ""
log_info "Note: Gitea repo is preserved for safety. Delete manually if needed:" log_info "Note: Gitea repo is preserved for safety. Delete manually if needed:"
echo " https://git.threesix.ai/jordan/$project_name/settings" echo " https://git.threesix.ai/threesix/$project_name/settings"
echo "" echo ""
} }
@ -475,6 +501,7 @@ status() {
log_info "Fetching status for: $project_name" log_info "Fetching status for: $project_name"
echo "" echo ""
# Get project info
local response local response
response=$(api_call GET "/project/$project_name") response=$(api_call GET "/project/$project_name")
@ -484,15 +511,13 @@ status() {
exit 1 exit 1
fi fi
echo "$response" | jq '{ echo "$response" | jq '.data | {
name: .data.name, name,
description: .data.description, description,
slug: .data.slug, domain,
domain: .data.domain, url,
url: .data.url, git: .git.html_url,
domains: .data.domains, deployment
git: .data.git,
deployment: .data.deployment
}' }'
echo "" echo ""
@ -503,13 +528,35 @@ status() {
if echo "$domains_response" | jq -e '.data.domains' > /dev/null 2>&1; then if echo "$domains_response" | jq -e '.data.domains' > /dev/null 2>&1; then
echo "$domains_response" | jq '.data.domains' echo "$domains_response" | jq '.data.domains'
fi fi
echo ""
log_info "Checking recent builds..."
local builds_response
builds_response=$(api_call GET "/projects/$project_name/builds?limit=3")
if echo "$builds_response" | jq -e '.data.builds' > /dev/null 2>&1; then
echo "$builds_response" | jq '.data.builds[] | {task_id, status, started_at, result: .result.success}'
else
log_info "No builds found"
fi
echo ""
log_info "Checking recent pipelines..."
local pipelines_response
pipelines_response=$(api_call GET "/projects/$project_name/pipelines")
if echo "$pipelines_response" | jq -e '.data | length > 0' > /dev/null 2>&1; then
echo "$pipelines_response" | jq '.data[0:3][] | {number, status, branch, commit: .commit[0:8]}'
else
log_info "No pipelines found"
fi
} }
# Main # Main
case "${1:-}" in case "${1:-}" in
run) run)
shift shift
run_flow "${1:-landing-test}" "${2:-}" run_flow "${1:-landing-test}"
;; ;;
teardown) teardown)
shift shift
@ -520,35 +567,33 @@ case "${1:-}" in
status "${1:-landing-test}" status "${1:-landing-test}"
;; ;;
*) *)
echo "Usage: $0 {run|teardown|status} [project-name] [custom-subdomain]" echo "Usage: $0 {run|teardown|status} [project-name]"
echo "" echo ""
echo "Commands:" echo "Commands:"
echo " run [name] [subdomain] Create project and run full flow" echo " run [name] Create project with agent-driven build and run full E2E flow"
echo " teardown [name] Delete project and clean up" echo " teardown [name] Delete project and clean up"
echo " status [name] Check current project status" echo " status [name] Check current project status, builds, and pipelines"
echo "" echo ""
echo "Full E2E flow tested:" echo "E2E Flow (matches cookbooks/landing-page.md):"
echo " 1. Project creation with template (POST /project)" echo " 1. POST /project/create-and-build - Create project + enqueue agent build"
echo " 2. Gitea repo + DNS + Woodpecker CI activation" echo " 2. GET /builds/{task_id} - Monitor Claude building the site"
echo " 3. Template seeding (astro-landing)" echo " 3. GET /projects/{id}/pipelines - Monitor CI pipeline"
echo " 4. CI pipeline monitoring (GET /projects/{id}/pipelines)" echo " 4. Verify site is live (HTTP 200)"
echo " 5. Site deployment verification (HTTP 200 check)" echo " 5. Test custom domains (POST/DELETE /projects/{id}/domains)"
echo " 6. DNS alias add/remove (POST/DELETE /projects/{id}/domains)" echo " 6. DELETE /project/{name} - Teardown"
echo " 7. Multi-domain listing"
echo "" echo ""
echo "Timeouts:" echo "Timeouts:"
echo " Pipeline: ${PIPELINE_TIMEOUT:-300}s, Site: ${SITE_TIMEOUT:-60}s" echo " Build: ${BUILD_TIMEOUT}s, Pipeline: ${PIPELINE_TIMEOUT}s, Site: ${SITE_TIMEOUT}s"
echo "" echo ""
echo "Environment:" echo "Environment:"
echo " RDEV_API_URL API endpoint (default: https://rdev.masq-ops.orchard9.ai)" echo " RDEV_API_URL API endpoint (default: https://rdev.masq-ops.orchard9.ai)"
echo " RDEV_API_KEY API key (required)" echo " RDEV_API_KEY API key (required)"
echo "" echo ""
echo "Examples:" echo "Examples:"
echo " $0 run # Test with auto-slug only" echo " $0 run # Run with default project name 'landing-test'"
echo " $0 run my-landing # Test with custom project name" echo " $0 run my-landing # Run with custom project name"
echo " $0 run my-landing my-site # Also create my-site.threesix.ai" echo " $0 status my-landing # Check status, builds, and pipelines"
echo " $0 status my-landing # Check status and domains" echo " $0 teardown my-landing # Clean up project"
echo " $0 teardown my-landing # Clean up (deletes all domains)"
exit 1 exit 1
;; ;;
esac esac

View File

@ -19,8 +19,11 @@ import (
// ClaudeRequest is the request body for POST /projects/{id}/claude. // ClaudeRequest is the request body for POST /projects/{id}/claude.
type ClaudeRequest struct { type ClaudeRequest struct {
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
StreamID string `json:"stream_id,omitempty"` StreamID string `json:"stream_id,omitempty"`
SessionID string `json:"session_id,omitempty"` // Resume a previous session
Model string `json:"model,omitempty"` // Model override (OpenCode only)
AllowedTools []string `json:"allowed_tools,omitempty"` // Restrict tool access
} }
// RunClaude executes a Claude command in the project's claudebox. // RunClaude executes a Claude command in the project's claudebox.
@ -37,10 +40,13 @@ func (h *ProjectsHandler) RunClaude(w http.ResponseWriter, r *http.Request) {
// Use new service if available // Use new service if available
if h.projectService != nil { if h.projectService != nil {
result, err := h.projectService.ExecuteClaude(r.Context(), service.ExecuteClaudeRequest{ result, err := h.projectService.ExecuteClaude(r.Context(), service.ExecuteClaudeRequest{
ProjectID: domain.ProjectID(id), ProjectID: domain.ProjectID(id),
Prompt: req.Prompt, Prompt: req.Prompt,
StreamID: req.StreamID, StreamID: req.StreamID,
Audit: getAuditContext(r), SessionID: req.SessionID,
Model: req.Model,
AllowedTools: req.AllowedTools,
Audit: getAuditContext(r),
}) })
if err != nil { if err != nil {
if errors.Is(err, domain.ErrProjectNotFound) { if errors.Is(err, domain.ErrProjectNotFound) {

View File

@ -15,9 +15,17 @@ import (
// It translates BuildSpec fields from the work task's Spec map into an // It translates BuildSpec fields from the work task's Spec map into an
// AgentRequest, executes via a CodeAgent, and returns a BuildResult. // AgentRequest, executes via a CodeAgent, and returns a BuildResult.
type BuildExecutor struct { type BuildExecutor struct {
agentRegistry port.CodeAgentRegistry agentRegistry port.CodeAgentRegistry
gitOps *GitOperations gitOps *GitOperations
logger *slog.Logger logger *slog.Logger
defaultPodName string // Default claudebox pod for agent execution
namespace string // Kubernetes namespace for the pod
}
// BuildExecutorConfig holds configuration for the build executor.
type BuildExecutorConfig struct {
DefaultPodName string // Default pod to execute Claude Code in (e.g., "claudebox-0")
Namespace string // Kubernetes namespace (e.g., "rdev")
} }
// NewBuildExecutor creates a new build executor. // NewBuildExecutor creates a new build executor.
@ -25,14 +33,23 @@ func NewBuildExecutor(
agentRegistry port.CodeAgentRegistry, agentRegistry port.CodeAgentRegistry,
gitOps *GitOperations, gitOps *GitOperations,
logger *slog.Logger, logger *slog.Logger,
cfg *BuildExecutorConfig,
) *BuildExecutor { ) *BuildExecutor {
if logger == nil { if logger == nil {
logger = slog.Default() logger = slog.Default()
} }
if cfg == nil {
cfg = &BuildExecutorConfig{
DefaultPodName: "claudebox-0",
Namespace: "rdev",
}
}
return &BuildExecutor{ return &BuildExecutor{
agentRegistry: agentRegistry, agentRegistry: agentRegistry,
gitOps: gitOps, gitOps: gitOps,
logger: logger.With("component", "build-executor"), logger: logger.With("component", "build-executor"),
defaultPodName: cfg.DefaultPodName,
namespace: cfg.Namespace,
} }
} }
@ -77,12 +94,22 @@ func (b *BuildExecutor) Execute(ctx context.Context, task *domain.WorkTask) *dom
} }
} }
// Build the agent request // Determine which pod to execute in (from task spec or default)
podName, _ := task.Spec["pod_name"].(string)
if podName == "" {
podName = b.defaultPodName
}
// Build the agent request with pod metadata for Claude Code adapter
agentReq := &domain.AgentRequest{ agentReq := &domain.AgentRequest{
Prompt: spec.Prompt, Prompt: spec.Prompt,
ProjectID: domain.ProjectID(task.ProjectID), ProjectID: domain.ProjectID(task.ProjectID),
WorkingDir: workDir, WorkingDir: workDir,
Timeout: 10 * time.Minute, Timeout: 10 * time.Minute,
Metadata: map[string]string{
"pod_name": podName,
"namespace": b.namespace,
},
} }
// Collect output with a size cap to prevent OOM on verbose builds. // Collect output with a size cap to prevent OOM on verbose builds.

View File

@ -200,7 +200,7 @@ func TestBuildExecutor_Execute(t *testing.T) {
result: &domain.AgentResult{ExitCode: 0, DurationMs: 500}, result: &domain.AgentResult{ExitCode: 0, DurationMs: 500},
} }
registry := &mockCodeAgentRegistry{agent: agent} registry := &mockCodeAgentRegistry{agent: agent}
exec := NewBuildExecutor(registry, nil, nil) exec := NewBuildExecutor(registry, nil, nil, nil)
task := &domain.WorkTask{ task := &domain.WorkTask{
ID: "task-1", ID: "task-1",
@ -220,7 +220,7 @@ func TestBuildExecutor_Execute(t *testing.T) {
t.Run("missing prompt", func(t *testing.T) { t.Run("missing prompt", func(t *testing.T) {
registry := &mockCodeAgentRegistry{agent: &mockCodeAgent{}} registry := &mockCodeAgentRegistry{agent: &mockCodeAgent{}}
exec := NewBuildExecutor(registry, nil, nil) exec := NewBuildExecutor(registry, nil, nil, nil)
task := &domain.WorkTask{ task := &domain.WorkTask{
ID: "task-1", ID: "task-1",
@ -236,7 +236,7 @@ func TestBuildExecutor_Execute(t *testing.T) {
t.Run("no agent available", func(t *testing.T) { t.Run("no agent available", func(t *testing.T) {
registry := &mockCodeAgentRegistry{agent: nil} registry := &mockCodeAgentRegistry{agent: nil}
exec := NewBuildExecutor(registry, nil, nil) exec := NewBuildExecutor(registry, nil, nil, nil)
task := &domain.WorkTask{ task := &domain.WorkTask{
ID: "task-1", ID: "task-1",
@ -253,7 +253,7 @@ func TestBuildExecutor_Execute(t *testing.T) {
t.Run("agent execution error", func(t *testing.T) { t.Run("agent execution error", func(t *testing.T) {
agent := &mockCodeAgent{err: fmt.Errorf("connection refused")} agent := &mockCodeAgent{err: fmt.Errorf("connection refused")}
registry := &mockCodeAgentRegistry{agent: agent} registry := &mockCodeAgentRegistry{agent: agent}
exec := NewBuildExecutor(registry, nil, nil) exec := NewBuildExecutor(registry, nil, nil, nil)
task := &domain.WorkTask{ task := &domain.WorkTask{
ID: "task-1", ID: "task-1",
@ -275,7 +275,7 @@ func TestBuildExecutor_Execute(t *testing.T) {
result: &domain.AgentResult{ExitCode: 1, DurationMs: 500}, result: &domain.AgentResult{ExitCode: 1, DurationMs: 500},
} }
registry := &mockCodeAgentRegistry{agent: agent} registry := &mockCodeAgentRegistry{agent: agent}
exec := NewBuildExecutor(registry, nil, nil) exec := NewBuildExecutor(registry, nil, nil, nil)
task := &domain.WorkTask{ task := &domain.WorkTask{
ID: "task-1", ID: "task-1",
@ -291,7 +291,7 @@ func TestBuildExecutor_Execute(t *testing.T) {
} }
func TestBuildExecutor_ParseSpec(t *testing.T) { func TestBuildExecutor_ParseSpec(t *testing.T) {
exec := NewBuildExecutor(nil, nil, nil) exec := NewBuildExecutor(nil, nil, nil, nil)
t.Run("valid spec", func(t *testing.T) { t.Run("valid spec", func(t *testing.T) {
spec, err := exec.parseSpec(map[string]any{ spec, err := exec.parseSpec(map[string]any{