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:
parent
4a18b1cd07
commit
9c15976f86
@ -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(
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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**
|
||||
```bash
|
||||
curl -X POST "https://rdev.masq-ops.orchard9.ai/projects/pantheon/claude" \
|
||||
-H "Authorization: Bearer $RDEV_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."
|
||||
}'
|
||||
```
|
||||
**Status progression:** `pending` → `running` → `completed` (or `failed`)
|
||||
|
||||
**Option C: Via work queue (build endpoint)**
|
||||
```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" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "Customize the landing page with Coming Soon message and threesix.ai branding",
|
||||
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
|
||||
}'
|
||||
```
|
||||
|
||||
**Option D: Create + build in one call**
|
||||
```bash
|
||||
curl -X POST "https://rdev.masq-ops.orchard9.ai/project/create-and-build" \
|
||||
-H "Authorization: Bearer $RDEV_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",
|
||||
"auto_commit": true,
|
||||
"auto_push": 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 -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 '{
|
||||
"name": "my-landing",
|
||||
"description": "Company landing page"
|
||||
}'
|
||||
```
|
||||
|
||||
### Step 3: Monitor Build
|
||||
|
||||
The git push triggers Woodpecker CI automatically.
|
||||
|
||||
**Via rdev-api (recommended):**
|
||||
### Submit Build Task
|
||||
```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
|
||||
# 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" \
|
||||
curl -X POST "$RDEV_API_URL/projects/my-landing/builds" \
|
||||
-H "X-API-Key: $RDEV_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"domain": "www.threesix.ai", "type": "CNAME"}'
|
||||
-d '{
|
||||
"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
|
||||
}'
|
||||
```
|
||||
|
||||
# Add root A record
|
||||
curl -X POST "https://rdev.masq-ops.orchard9.ai/projects/landing/domains" \
|
||||
-H "Authorization: Bearer $RDEV_KEY" \
|
||||
---
|
||||
|
||||
## Iterating on the Site
|
||||
|
||||
Submit additional builds to modify the site:
|
||||
|
||||
```bash
|
||||
curl -X POST "$RDEV_API_URL/projects/my-landing/builds" \
|
||||
-H "X-API-Key: $RDEV_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"domain": "threesix.ai"}'
|
||||
-d '{
|
||||
"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
|
||||
}'
|
||||
```
|
||||
|
||||
# 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'
|
||||
Each build:
|
||||
1. Claude clones the repo
|
||||
2. Makes the requested changes
|
||||
3. Commits and pushes
|
||||
4. CI deploys automatically
|
||||
|
||||
# Remove an alias
|
||||
curl -X DELETE "https://rdev.masq-ops.orchard9.ai/projects/landing/domains/www.threesix.ai" \
|
||||
-H "Authorization: Bearer $RDEV_KEY"
|
||||
---
|
||||
|
||||
## Adding Custom Domains
|
||||
|
||||
```bash
|
||||
# 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.mycompany.com"}'
|
||||
|
||||
# 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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user