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,
})
}
buildExecutor := worker.NewBuildExecutor(agentRegistry, gitOps, logger)
buildExecutor := worker.NewBuildExecutor(agentRegistry, gitOps, logger, nil)
workerCfg := worker.DefaultWorkExecutorConfig()
workerCfg.Logger = logger
workExecutor := worker.NewWorkExecutor(

View File

@ -180,13 +180,16 @@ func registerProjectPaths(spec *api.OpenAPISpec) {
func registerCommandPaths(spec *api.OpenAPISpec) {
spec.AddPath("/projects/{id}/claude", "post", withAuthBodyAndParams(
"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",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
`{
"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",

View File

@ -1,337 +1,312 @@
# 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
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)
**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 |
**No templates. Claude builds it from scratch based on your prompt.**
---
## Prerequisites
### Credentials Required
| Secret | Location | Purpose |
|--------|----------|---------|
| 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 |
### API Access
```bash
export RDEV_API_URL="https://rdev.masq-ops.orchard9.ai"
export RDEV_API_KEY="<your-api-key>"
```
### Infrastructure Required
- [x] rdev-api running with infrastructure handlers
- [x] Gitea at https://git.threesix.ai
- [x] Woodpecker CI at https://ci.threesix.ai
- [x] Zot registry at registry.threesix.ai
- [x] `projects` namespace in K8s with RBAC
- [x] Wildcard TLS cert for *.threesix.ai
- rdev-api running with embedded worker
- Gitea at https://git.threesix.ai
- Woodpecker CI at https://ci.threesix.ai
- claudebox-0 pod running in rdev namespace
---
## Architecture
## Step 1: Create Project and Build in One Call
```
┌──────────────────────────────────────────────────────────────────────┐
│ 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.
Single API call that creates infrastructure AND enqueues agent work:
```bash
curl -X POST https://rdev.masq-ops.orchard9.ai/project \
-H "Authorization: Bearer $RDEV_KEY" \
curl -X POST "$RDEV_API_URL/project/create-and-build" \
-H "X-API-Key: $RDEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "landing",
"description": "threesix.ai landing page",
"template": "astro-landing"
"name": "my-landing",
"description": "Company landing page",
"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:**
```json
{
"project_id": "landing",
"name": "landing",
"description": "threesix.ai landing page",
"data": {
"project_id": "my-landing",
"name": "my-landing",
"domain": "abc123xy.threesix.ai",
"url": "https://abc123xy.threesix.ai",
"git": {
"owner": "threesix",
"name": "landing",
"clone_ssh": "git@git.threesix.ai:threesix/landing.git",
"clone_http": "https://git.threesix.ai/threesix/landing.git",
"html_url": "https://git.threesix.ai/threesix/landing"
"owner": "jordan",
"name": "my-landing",
"html_url": "https://git.threesix.ai/jordan/my-landing"
},
"domain": "landing.threesix.ai",
"url": "https://landing.threesix.ai",
"next_steps": []
"task_id": "task-uuid",
"status": "pending",
"status_url": "/builds/task-uuid"
},
"meta": { "timestamp": "..." }
}
```
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
git clone https://git.threesix.ai/threesix/landing.git
cd landing
# Use Claude Code to customize the landing page
# Then commit and push
curl -s "$RDEV_API_URL/builds/{task_id}" \
-H "X-API-Key: $RDEV_API_KEY" | jq .data
```
**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
curl -X POST "https://rdev.masq-ops.orchard9.ai/projects/pantheon/claude" \
-H "Authorization: Bearer $RDEV_KEY" \
curl -s "$RDEV_API_URL/projects/my-landing/pipelines" \
-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" \
-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
# Enqueue a build task for a worker to execute
curl -X POST "https://rdev.masq-ops.orchard9.ai/projects/landing/builds" \
-H "Authorization: Bearer $RDEV_KEY" \
curl -X POST "$RDEV_API_URL/projects/my-landing/builds" \
-H "X-API-Key: $RDEV_API_KEY" \
-H "Content-Type: application/json" \
-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_push": true
}'
```
**Option D: Create + build in one call**
---
## Iterating on the Site
Submit additional builds to modify the site:
```bash
curl -X POST "https://rdev.masq-ops.orchard9.ai/project/create-and-build" \
-H "Authorization: Bearer $RDEV_KEY" \
curl -X POST "$RDEV_API_URL/projects/my-landing/builds" \
-H "X-API-Key: $RDEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "landing",
"description": "threesix.ai landing page",
"template": "astro-landing",
"build": {
"prompt": "Customize with Coming Soon message and threesix.ai branding",
"prompt": "Add a pricing section with three tiers: Free, Pro ($29/mo), Enterprise (contact us). Match the existing dark theme.",
"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):**
```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
## Adding Custom Domains
```bash
# Check project status via rdev-api
curl -s "https://rdev.masq-ops.orchard9.ai/project/landing" \
-H "Authorization: Bearer $RDEV_KEY" | jq '.data.deployment'
# 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" \
# Add www subdomain
curl -X POST "$RDEV_API_URL/projects/my-landing/domains" \
-H "X-API-Key: $RDEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "www.threesix.ai", "type": "CNAME"}'
-d '{"domain": "www.mycompany.com"}'
# Add root A record
curl -X POST "https://rdev.masq-ops.orchard9.ai/projects/landing/domains" \
-H "Authorization: Bearer $RDEV_KEY" \
-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"
# List all domains
curl -s "$RDEV_API_URL/projects/my-landing/domains" \
-H "X-API-Key: $RDEV_API_KEY" | jq '.data.domains'
```
---
## Template: astro-landing
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:
## Teardown
```bash
# Check DNS
dig landing.threesix.ai
curl -X DELETE "$RDEV_API_URL/project/my-landing" \
-H "X-API-Key: $RDEV_API_KEY"
```
# Check site
curl -I https://landing.threesix.ai
Removes: DNS records, K8s deployment, project metadata. Gitea repo preserved for safety.
# 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
# Delete via rdev-api (removes DNS, K8s deployment; Gitea repo preserved for safety)
curl -X DELETE https://rdev.masq-ops.orchard9.ai/project/landing \
-H "Authorization: Bearer $RDEV_KEY"
# Check worker status
curl -s "$RDEV_API_URL/workers" -H "X-API-Key: $RDEV_API_KEY" | jq '.data.summary'
# 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
- [Build Orchestration](../ai-lookup/features/build-orchestration.md) - Build system documentation
- [Worker Pool](../ai-lookup/services/worker-pool.md) - Worker pool management
- [Work Queue](../.claude/guides/services/work-queue.md) - Work queue guide
- [Templates](../.claude/guides/services/templates.md) - Project template guide
- [Full-Stack App Cookbook](./fullstack-app.md) - Next.js + Go backend
- [Worker Pool Guide](../.claude/guides/services/worker-pool.md)
- [Build Orchestration](../.claude/guides/services/build-orchestration.md)

View File

@ -1,19 +1,17 @@
#!/bin/bash
# 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:
# ./cookbooks/scripts/landing-test.sh run # Run the full flow
# ./cookbooks/scripts/landing-test.sh teardown # Clean up test resources
# ./cookbooks/scripts/landing-test.sh status # Check current status
# ./cookbooks/scripts/landing-test.sh run [name] # Run the full flow
# ./cookbooks/scripts/landing-test.sh teardown [name] # Clean up test resources
# ./cookbooks/scripts/landing-test.sh status [name] # Check current status
set -euo pipefail
# Configuration
API_URL="${RDEV_API_URL:-https://rdev.masq-ops.orchard9.ai}"
API_KEY="${RDEV_API_KEY:?RDEV_API_KEY environment variable required}"
PROJECT_NAME="${1:-landing-test}"
TEMPLATE="astro-landing"
# Colors
RED='\033[0;31m'
@ -27,9 +25,11 @@ log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Configuration for polling
PIPELINE_TIMEOUT=300 # 5 minutes max wait for pipeline
PIPELINE_POLL_INTERVAL=10 # Check every 10 seconds
# Timeouts
BUILD_TIMEOUT=180 # 3 minutes for Claude to build the site
BUILD_POLL_INTERVAL=5 # Check every 5 seconds
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() {
@ -62,6 +62,61 @@ check_health() {
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
# Returns: 0 on success, 1 on failure/timeout
wait_for_pipeline() {
@ -83,7 +138,7 @@ wait_for_pipeline() {
local response
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
pipeline_count=$(echo "$response" | jq -r '.data | length' 2>/dev/null || echo "0")
@ -177,7 +232,7 @@ test_dns_alias() {
fi
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
}
@ -208,15 +263,15 @@ remove_dns_alias() {
run_flow() {
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 " Landing Page Cookbook Test"
echo " Project: $project_name"
if [[ -n "$custom_subdomain" ]]; then
echo " Custom subdomain: $custom_subdomain"
fi
echo " Flow: Agent-driven (create-and-build)"
echo "=========================================="
echo ""
@ -224,21 +279,25 @@ run_flow() {
check_health || exit 1
echo ""
# Step 1: Create project
log_info "Step 1: Creating project with $TEMPLATE template..."
local create_payload="{
\"name\": \"$project_name\",
\"description\": \"Cookbook test: landing page flow\",
\"template\": \"$TEMPLATE\""
if [[ -n "$custom_subdomain" ]]; then
create_payload="$create_payload,
\"custom_subdomain\": \"$custom_subdomain\""
fi
create_payload="$create_payload
}"
# Step 1: Create project AND enqueue build in one call
log_info "Step 1: Creating project and enqueuing build task..."
log_info "Prompt: ${build_prompt:0:80}..."
local create_payload
create_payload=$(jq -n \
--arg name "$project_name" \
--arg desc "Cookbook test: agent-driven landing page" \
--arg prompt "$build_prompt" \
'{
name: $name,
description: $desc,
prompt: $prompt,
auto_commit: true,
auto_push: true
}')
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
log_error "Failed to create project"
@ -246,104 +305,62 @@ run_flow() {
exit 1
fi
log_success "Project created"
echo "$create_response" | jq '{
project_id: .data.project_id,
slug: .data.slug,
git_url: .data.git.html_url,
domain: .data.domain,
url: .data.url,
domains: .data.domains,
next_steps: .data.next_steps
log_success "Project created and build enqueued"
echo "$create_response" | jq '.data | {
project_id,
domain,
url,
git: .git.html_url,
task_id,
status,
status_url
}'
# Extract domain info
# Extract key info
local primary_domain
local slug
local task_id
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
log_success "Auto-generated slug: $slug"
if [[ -z "$task_id" || "$task_id" == "null" ]]; then
log_error "No task_id returned - build was not enqueued"
exit 1
fi
# Check for manual steps
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
log_success "Build task ID: $task_id"
echo ""
# Step 2: List all domains
log_info "Step 2: Listing all project domains..."
local domains_response
domains_response=$(api_call GET "/projects/$project_name/domains")
if echo "$domains_response" | jq -e '.error' > /dev/null 2>&1; then
log_warn "Could not list domains"
echo "$domains_response" | jq .
# Step 2: Monitor build progress (Claude building the site)
log_info "Step 2: Monitoring build progress..."
echo ""
local build_success=false
if wait_for_build "$task_id"; then
build_success=true
else
local domain_count
domain_count=$(echo "$domains_response" | jq -r '.data.total')
log_success "Found $domain_count domain(s)"
echo "$domains_response" | jq '.data.domains[] | {domain, type, verified}'
log_error "Build did not complete successfully"
log_info "Check build details: curl -s \"\$RDEV_API_URL/builds/$task_id\" -H \"X-API-Key: \$RDEV_API_KEY\" | jq ."
fi
echo ""
# Step 3: Verify project status
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..."
# Step 3: Monitor CI pipeline (only if build succeeded)
local pipeline_success=false
if [[ "$build_success" == "true" ]]; then
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 - site may not deploy"
log_warn "Pipeline did not complete successfully"
log_info "Check Woodpecker: https://ci.threesix.ai/threesix/$project_name"
fi
else
log_info "Step 3: Skipping pipeline monitoring (build failed)"
fi
echo ""
# Step 6: Verify site is live (only if pipeline succeeded)
# Step 4: Verify site is live
local site_live=false
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
site_live=true
# Show a snippet of the response
@ -351,28 +368,29 @@ run_flow() {
curl -s "https://$primary_domain" | head -20 | grep -E '<title>|<h1' || true
fi
else
log_info "Step 6: Skipping site verification (pipeline not successful)"
log_info "Step 4: Skipping site verification (pipeline not successful)"
fi
echo ""
# Step 7: Test DNS alias functionality
# Step 5: Test adding custom domains
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
log_success "DNS alias test passed"
# Clean up test alias immediately
log_success "Domain alias test passed"
# Clean up test alias
sleep 2
remove_dns_alias "$project_name" "$test_alias"
else
log_warn "DNS alias test failed - check Cloudflare permissions"
log_warn "Domain alias test failed - check Cloudflare permissions"
fi
echo ""
# Step 8: Final domain listing
log_info "Step 8: Final domain listing..."
# List all domains
log_info "Listing all project domains..."
local domains_response
domains_response=$(api_call GET "/projects/$project_name/domains")
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
echo ""
@ -382,30 +400,35 @@ run_flow() {
echo "=========================================="
echo ""
echo " Project: $project_name"
echo " Git repo: $(echo "$create_response" | jq -r '.data.git.html_url')"
if [[ -n "$slug" ]]; then
echo " Slug: $slug"
fi
echo " Task ID: $task_id"
echo " Git repo: $(echo "$create_response" | jq -r '.data.git.html_url // "N/A"')"
echo " Primary: https://$primary_domain"
echo ""
echo " Test Results:"
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
echo -e " CI Pipeline: ${GREEN}PASS${NC}"
else
elif [[ "$build_success" == "true" ]]; then
echo -e " CI Pipeline: ${RED}FAIL${NC}"
else
echo -e " CI Pipeline: ${YELLOW}SKIPPED${NC}"
fi
if [[ "$site_live" == "true" ]]; then
echo -e " Site accessible: ${GREEN}PASS${NC}"
else
elif [[ "$pipeline_success" == "true" ]]; then
echo -e " Site accessible: ${YELLOW}PENDING${NC}"
else
echo -e " Site accessible: ${YELLOW}SKIPPED${NC}"
fi
echo -e " DNS alias: ${GREEN}TESTED${NC}"
echo ""
echo " All domains:"
echo "$domains_response" | jq -r '.data.domains[]? | " - \(.domain) (\(.type))"' 2>/dev/null || echo " (none listed)"
echo -e " Custom domains: ${GREEN}TESTED${NC}"
echo ""
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 " View logs: ./scripts/logs.sh -e"
echo " Woodpecker: https://ci.threesix.ai/threesix/$project_name"
@ -413,14 +436,17 @@ run_flow() {
echo ""
# 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"
return 0
elif [[ "$pipeline_success" == "true" ]]; then
log_warn "Partial success - pipeline passed but site not yet live"
elif [[ "$build_success" == "true" && "$pipeline_success" == "true" ]]; then
log_warn "Partial success - build and pipeline passed but site not yet live"
return 0
elif [[ "$build_success" == "true" ]]; then
log_warn "Partial success - build passed but pipeline failed"
return 1
else
log_error "E2E test FAILED - pipeline did not complete"
log_error "E2E test FAILED - build did not complete"
return 1
fi
}
@ -453,7 +479,7 @@ teardown() {
if echo "$response" | jq -e '.data.status == "deleted"' > /dev/null 2>&1; then
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
log_warn "Project not found (already deleted?)"
else
@ -464,7 +490,7 @@ teardown() {
echo ""
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 ""
}
@ -475,6 +501,7 @@ status() {
log_info "Fetching status for: $project_name"
echo ""
# Get project info
local response
response=$(api_call GET "/project/$project_name")
@ -484,15 +511,13 @@ status() {
exit 1
fi
echo "$response" | jq '{
name: .data.name,
description: .data.description,
slug: .data.slug,
domain: .data.domain,
url: .data.url,
domains: .data.domains,
git: .data.git,
deployment: .data.deployment
echo "$response" | jq '.data | {
name,
description,
domain,
url,
git: .git.html_url,
deployment
}'
echo ""
@ -503,13 +528,35 @@ status() {
if echo "$domains_response" | jq -e '.data.domains' > /dev/null 2>&1; then
echo "$domains_response" | jq '.data.domains'
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
case "${1:-}" in
run)
shift
run_flow "${1:-landing-test}" "${2:-}"
run_flow "${1:-landing-test}"
;;
teardown)
shift
@ -520,35 +567,33 @@ case "${1:-}" in
status "${1:-landing-test}"
;;
*)
echo "Usage: $0 {run|teardown|status} [project-name] [custom-subdomain]"
echo "Usage: $0 {run|teardown|status} [project-name]"
echo ""
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 " status [name] Check current project status"
echo " status [name] Check current project status, builds, and pipelines"
echo ""
echo "Full E2E flow tested:"
echo " 1. Project creation with template (POST /project)"
echo " 2. Gitea repo + DNS + Woodpecker CI activation"
echo " 3. Template seeding (astro-landing)"
echo " 4. CI pipeline monitoring (GET /projects/{id}/pipelines)"
echo " 5. Site deployment verification (HTTP 200 check)"
echo " 6. DNS alias add/remove (POST/DELETE /projects/{id}/domains)"
echo " 7. Multi-domain listing"
echo "E2E Flow (matches cookbooks/landing-page.md):"
echo " 1. POST /project/create-and-build - Create project + enqueue agent build"
echo " 2. GET /builds/{task_id} - Monitor Claude building the site"
echo " 3. GET /projects/{id}/pipelines - Monitor CI pipeline"
echo " 4. Verify site is live (HTTP 200)"
echo " 5. Test custom domains (POST/DELETE /projects/{id}/domains)"
echo " 6. DELETE /project/{name} - Teardown"
echo ""
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 "Environment:"
echo " RDEV_API_URL API endpoint (default: https://rdev.masq-ops.orchard9.ai)"
echo " RDEV_API_KEY API key (required)"
echo ""
echo "Examples:"
echo " $0 run # Test with auto-slug only"
echo " $0 run my-landing # Test with custom project name"
echo " $0 run my-landing my-site # Also create my-site.threesix.ai"
echo " $0 status my-landing # Check status and domains"
echo " $0 teardown my-landing # Clean up (deletes all domains)"
echo " $0 run # Run with default project name 'landing-test'"
echo " $0 run my-landing # Run with custom project name"
echo " $0 status my-landing # Check status, builds, and pipelines"
echo " $0 teardown my-landing # Clean up project"
exit 1
;;
esac

View File

@ -21,6 +21,9 @@ import (
type ClaudeRequest struct {
Prompt string `json:"prompt"`
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.
@ -40,6 +43,9 @@ func (h *ProjectsHandler) RunClaude(w http.ResponseWriter, r *http.Request) {
ProjectID: domain.ProjectID(id),
Prompt: req.Prompt,
StreamID: req.StreamID,
SessionID: req.SessionID,
Model: req.Model,
AllowedTools: req.AllowedTools,
Audit: getAuditContext(r),
})
if err != nil {

View File

@ -18,6 +18,14 @@ type BuildExecutor struct {
agentRegistry port.CodeAgentRegistry
gitOps *GitOperations
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.
@ -25,14 +33,23 @@ func NewBuildExecutor(
agentRegistry port.CodeAgentRegistry,
gitOps *GitOperations,
logger *slog.Logger,
cfg *BuildExecutorConfig,
) *BuildExecutor {
if logger == nil {
logger = slog.Default()
}
if cfg == nil {
cfg = &BuildExecutorConfig{
DefaultPodName: "claudebox-0",
Namespace: "rdev",
}
}
return &BuildExecutor{
agentRegistry: agentRegistry,
gitOps: gitOps,
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{
Prompt: spec.Prompt,
ProjectID: domain.ProjectID(task.ProjectID),
WorkingDir: workDir,
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.

View File

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