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,
|
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(
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
"domain": "abc123xy.threesix.ai",
|
||||||
|
"url": "https://abc123xy.threesix.ai",
|
||||||
"git": {
|
"git": {
|
||||||
"owner": "threesix",
|
"owner": "jordan",
|
||||||
"name": "landing",
|
"name": "my-landing",
|
||||||
"clone_ssh": "git@git.threesix.ai:threesix/landing.git",
|
"html_url": "https://git.threesix.ai/jordan/my-landing"
|
||||||
"clone_http": "https://git.threesix.ai/threesix/landing.git",
|
|
||||||
"html_url": "https://git.threesix.ai/threesix/landing"
|
|
||||||
},
|
},
|
||||||
"domain": "landing.threesix.ai",
|
"task_id": "task-uuid",
|
||||||
"url": "https://landing.threesix.ai",
|
"status": "pending",
|
||||||
"next_steps": []
|
"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
|
```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`)
|
||||||
```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."
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option C: Via work queue (build endpoint)**
|
When completed:
|
||||||
```bash
|
```json
|
||||||
# Enqueue a build task for a worker to execute
|
{
|
||||||
curl -X POST "https://rdev.masq-ops.orchard9.ai/projects/landing/builds" \
|
"task_id": "task-uuid",
|
||||||
-H "Authorization: Bearer $RDEV_KEY" \
|
"project_id": "my-landing",
|
||||||
-H "Content-Type: application/json" \
|
"status": "completed",
|
||||||
-d '{
|
"prompt": "Build a modern landing page...",
|
||||||
"prompt": "Customize the landing page with Coming Soon message and threesix.ai branding",
|
|
||||||
"auto_commit": true,
|
"auto_commit": true,
|
||||||
"auto_push": true
|
"auto_push": true,
|
||||||
}'
|
"started_at": "2025-01-29T10:00:00Z",
|
||||||
```
|
"completed_at": "2025-01-29T10:00:45Z",
|
||||||
|
"result": {
|
||||||
**Option D: Create + build in one call**
|
"success": true,
|
||||||
```bash
|
"commit_sha": "abc123",
|
||||||
curl -X POST "https://rdev.masq-ops.orchard9.ai/project/create-and-build" \
|
"files_changed": ["index.html", "styles.css", "Dockerfile", "nginx.conf"],
|
||||||
-H "Authorization: Bearer $RDEV_KEY" \
|
"duration_ms": 45000
|
||||||
-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
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
### Submit Build Task
|
||||||
|
|
||||||
The git push triggers Woodpecker CI automatically.
|
|
||||||
|
|
||||||
**Via rdev-api (recommended):**
|
|
||||||
```bash
|
```bash
|
||||||
# List recent pipelines
|
curl -X POST "$RDEV_API_URL/projects/my-landing/builds" \
|
||||||
curl -s "https://rdev.masq-ops.orchard9.ai/projects/landing/pipelines" \
|
-H "X-API-Key: $RDEV_API_KEY" \
|
||||||
-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" \
|
|
||||||
-H "Content-Type: application/json" \
|
-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" \
|
-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
|
Each build:
|
||||||
curl -s "https://rdev.masq-ops.orchard9.ai/projects/landing/domains" \
|
1. Claude clones the repo
|
||||||
-H "Authorization: Bearer $RDEV_KEY" | jq '.data.domains'
|
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
|
## 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
|
|
||||||
|
|||||||
@ -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,9 +25,11 @@ 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
|
||||||
|
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
|
SITE_TIMEOUT=60 # 1 minute max wait for site to be live
|
||||||
|
|
||||||
api_call() {
|
api_call() {
|
||||||
@ -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 [[ "$build_success" == "true" ]]; then
|
||||||
|
log_info "Step 3: Monitoring CI pipeline..."
|
||||||
if wait_for_pipeline "$project_name"; then
|
if wait_for_pipeline "$project_name"; then
|
||||||
pipeline_success=true
|
pipeline_success=true
|
||||||
else
|
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"
|
log_info "Check Woodpecker: https://ci.threesix.ai/threesix/$project_name"
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
log_info "Step 3: Skipping pipeline monitoring (build failed)"
|
||||||
|
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
|
||||||
|
|||||||
@ -21,6 +21,9 @@ import (
|
|||||||
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.
|
||||||
@ -40,6 +43,9 @@ func (h *ProjectsHandler) RunClaude(w http.ResponseWriter, r *http.Request) {
|
|||||||
ProjectID: domain.ProjectID(id),
|
ProjectID: domain.ProjectID(id),
|
||||||
Prompt: req.Prompt,
|
Prompt: req.Prompt,
|
||||||
StreamID: req.StreamID,
|
StreamID: req.StreamID,
|
||||||
|
SessionID: req.SessionID,
|
||||||
|
Model: req.Model,
|
||||||
|
AllowedTools: req.AllowedTools,
|
||||||
Audit: getAuditContext(r),
|
Audit: getAuditContext(r),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -18,6 +18,14 @@ 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.
|
||||||
|
|||||||
@ -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{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user