Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
## Template Version Alignment
- Go: 1.23 → 1.25 across all templates (go.work, go.mod, Dockerfiles, CI)
- Alpine: latest → 3.19 (explicit version pinning)
- Woodpecker: failure:retry → failure:ignore (invalid syntax fix)
## SDLC Tree Fixes (slackpath-5-full-lifecycle)
Fixed merge failures by correcting lifecycle flow:
1. **Branch Creation**: Added missing create-branch step (planned → ready)
- Bug: Merge command requires feature.Branch field to be set
- Fix: POST /projects/{id}/sdlc/features/{slug}/branch
2. **Artifact Status**: Changed approval to pass for execution artifacts
- Bug: Review/audit/QA need status="passed" not "approved"
- Fix: /artifacts/{type}/approve → /artifacts/{type}/pass
- Added: pass-qa step after wait-qa
3. **Phase Transition Order**: Reordered merge phase transition
- Bug: Merge command checks if phase == "merge" first
- Fix: transition-to-merge BEFORE merge-feature (not after)
## GCS Provisioner Fix
- Replaced deprecated option.WithCredentialsFile with env var approach
- Now uses GOOGLE_APPLICATION_CREDENTIALS for ADC (Application Default Credentials)
- Avoids security risk from deprecated credential options
- Fixed test: Added ComponentTypeGCS to ValidComponentTypes test
## Critical Rules Added
- Version alignment: All template versions must stay in sync
- When updating versions, grep entire templates/ tree
## Files Changed
- 27 template files: Go version + Woodpecker syntax
- 1 tree file: SDLC lifecycle flow corrections
- 1 CLAUDE.md: Version alignment rule
- 1 GCS provisioner: Deprecated API fix
- 1 test file: Added missing component type
Root cause: Skeleton templates lagged behind Go 1.25 release and had
invalid Woodpecker syntax. SDLC tree skipped required branch creation
and used wrong artifact approval endpoints.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1506 lines
46 KiB
Go
1506 lines
46 KiB
Go
package main
|
|
|
|
import "github.com/orchard9/rdev/pkg/api"
|
|
|
|
func registerAgentPaths(spec *api.OpenAPISpec) {
|
|
spec.AddPath("/agents", "get", withAuth(
|
|
"List code agents",
|
|
`Returns all registered code agent providers and their status.
|
|
|
|
Shows which agents are available, their supported models, and the current default.`,
|
|
"Code Agents",
|
|
"projects:read",
|
|
`{
|
|
"agents": [
|
|
{
|
|
"provider": "claudecode",
|
|
"name": "Claude Code",
|
|
"available": true,
|
|
"default": true,
|
|
"supported_models": ["claude-sonnet-4-20250514"],
|
|
"default_model": "claude-sonnet-4-20250514"
|
|
},
|
|
{
|
|
"provider": "opencode",
|
|
"name": "OpenCode",
|
|
"available": false,
|
|
"default": false,
|
|
"supported_models": ["gpt-4o", "claude-sonnet-4-20250514"],
|
|
"default_model": "claude-sonnet-4-20250514"
|
|
}
|
|
],
|
|
"default_agent": "claudecode",
|
|
"total_agents": 2,
|
|
"available_count": 1
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/agents/health", "get", withAuth(
|
|
"Get agent health status",
|
|
`Returns the health status of all registered code agents.
|
|
|
|
Checks connectivity to each agent backend and reports availability.`,
|
|
"Code Agents",
|
|
"projects:read",
|
|
`{
|
|
"agents": [
|
|
{
|
|
"provider": "claudecode",
|
|
"name": "Claude Code",
|
|
"healthy": true,
|
|
"message": "available",
|
|
"latency": "125ms",
|
|
"checked_at": "2026-01-27T12:00:00Z"
|
|
},
|
|
{
|
|
"provider": "opencode",
|
|
"name": "OpenCode",
|
|
"healthy": false,
|
|
"message": "unavailable",
|
|
"latency": "5.002s",
|
|
"checked_at": "2026-01-27T12:00:00Z"
|
|
}
|
|
],
|
|
"healthy_count": 1,
|
|
"total_count": 2,
|
|
"default_agent": "claudecode",
|
|
"default_healthy": true
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/agents/{provider}", "get", withAuthAndParams(
|
|
"Get agent capabilities",
|
|
`Returns detailed capabilities for a specific code agent provider.
|
|
|
|
Includes supported features, models, and configuration options.`,
|
|
"Code Agents",
|
|
"projects:read",
|
|
[]param{{Name: "provider", In: "path", Description: "Agent provider ID (e.g., 'claudecode', 'opencode')", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/agents/default", "post", withAuthAndBody(
|
|
"Set default agent",
|
|
`Changes the default code agent used for command execution.
|
|
|
|
The specified provider must be registered and ideally available.`,
|
|
"Code Agents",
|
|
"admin",
|
|
`{"provider": "opencode"}`,
|
|
`{
|
|
"default_agent": "opencode",
|
|
"message": "default agent updated"
|
|
}`,
|
|
))
|
|
}
|
|
|
|
// param represents an OpenAPI parameter.
|
|
type param struct {
|
|
Name string
|
|
In string
|
|
Description string
|
|
Required bool
|
|
}
|
|
|
|
// withAuth creates an operation that requires authentication.
|
|
func withAuth(summary, description, tag, scope, example string) map[string]any {
|
|
return map[string]any{
|
|
"summary": summary,
|
|
"description": description + "\n\n**Required scope**: `" + scope + "`",
|
|
"tags": []string{tag},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"responses": map[string]any{
|
|
"200": map[string]any{
|
|
"description": "Success",
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": example,
|
|
},
|
|
},
|
|
},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Insufficient permissions"},
|
|
},
|
|
}
|
|
}
|
|
|
|
// withAuthAndBody creates an operation with auth and request body.
|
|
func withAuthAndBody(summary, description, tag, scope, requestExample, responseExample string) map[string]any {
|
|
return map[string]any{
|
|
"summary": summary,
|
|
"description": description + "\n\n**Required scope**: `" + scope + "`",
|
|
"tags": []string{tag},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"requestBody": map[string]any{
|
|
"required": true,
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": requestExample,
|
|
},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"201": map[string]any{
|
|
"description": "Created",
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": responseExample,
|
|
},
|
|
},
|
|
},
|
|
"400": map[string]any{"description": "Bad Request - Invalid input"},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Insufficient permissions"},
|
|
},
|
|
}
|
|
}
|
|
|
|
// withAuthAndParams creates an operation with auth and path parameters.
|
|
func withAuthAndParams(summary, description, tag, scope string, params []param) map[string]any {
|
|
parameters := make([]map[string]any, len(params))
|
|
for i, p := range params {
|
|
parameters[i] = map[string]any{
|
|
"name": p.Name,
|
|
"in": p.In,
|
|
"description": p.Description,
|
|
"required": p.Required,
|
|
"schema": map[string]any{"type": "string"},
|
|
}
|
|
}
|
|
return map[string]any{
|
|
"summary": summary,
|
|
"description": description + "\n\n**Required scope**: `" + scope + "`",
|
|
"tags": []string{tag},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"parameters": parameters,
|
|
"responses": map[string]any{
|
|
"200": map[string]any{"description": "Success"},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Insufficient permissions"},
|
|
"404": map[string]any{"description": "Not Found"},
|
|
},
|
|
}
|
|
}
|
|
|
|
// withAuthBodyAndParams creates an operation with auth, body, and params.
|
|
func withAuthBodyAndParams(summary, description, tag, scope string, params []param, requestExample, responseExample string) map[string]any {
|
|
parameters := make([]map[string]any, len(params))
|
|
for i, p := range params {
|
|
parameters[i] = map[string]any{
|
|
"name": p.Name,
|
|
"in": p.In,
|
|
"description": p.Description,
|
|
"required": p.Required,
|
|
"schema": map[string]any{"type": "string"},
|
|
}
|
|
}
|
|
return map[string]any{
|
|
"summary": summary,
|
|
"description": description + "\n\n**Required scope**: `" + scope + "`",
|
|
"tags": []string{tag},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"parameters": parameters,
|
|
"requestBody": map[string]any{
|
|
"required": true,
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": requestExample,
|
|
},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"201": map[string]any{
|
|
"description": "Created",
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": responseExample,
|
|
},
|
|
},
|
|
},
|
|
"400": map[string]any{"description": "Bad Request - Invalid input"},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Insufficient permissions"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func registerWorkerPaths(spec *api.OpenAPISpec) {
|
|
spec.AddPath("/workers", "get", withAuth(
|
|
"List workers",
|
|
"Returns all registered workers in the pool with status summary.",
|
|
"Workers",
|
|
"admin",
|
|
`{
|
|
"workers": [
|
|
{
|
|
"id": "rdev-worker-0",
|
|
"hostname": "rdev-worker-0.rdev.svc",
|
|
"status": "idle",
|
|
"capabilities": ["build", "test", "deploy"],
|
|
"registered_at": "2026-01-27T12:00:00Z",
|
|
"last_heartbeat": "2026-01-27T12:05:00Z",
|
|
"version": "1.0.0"
|
|
}
|
|
],
|
|
"total": 1,
|
|
"summary": {"idle": 1, "busy": 0, "draining": 0, "offline": 0}
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/workers/{workerId}", "get", withAuthAndParams(
|
|
"Get worker",
|
|
"Returns details for a specific worker.",
|
|
"Workers",
|
|
"admin",
|
|
[]param{{Name: "workerId", In: "path", Description: "Worker ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/workers/{workerId}/drain", "post", withAuthAndParams(
|
|
"Drain worker",
|
|
"Sets a worker to draining status. It will finish its current task but stop accepting new work.",
|
|
"Workers",
|
|
"admin",
|
|
[]param{{Name: "workerId", In: "path", Description: "Worker ID", Required: true}},
|
|
))
|
|
}
|
|
|
|
func registerSDLCPaths(spec *api.OpenAPISpec) {
|
|
// State and classifier
|
|
spec.AddPath("/projects/{id}/sdlc/state", "get", withAuthAndParams(
|
|
"Get SDLC state",
|
|
"Returns the global SDLC state for a project.",
|
|
"SDLC",
|
|
"projects:read",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/next", "get", map[string]any{
|
|
"summary": "Get next action",
|
|
"description": "Returns the classifier's recommended next action.\n\n**Required scope**: `projects:read`",
|
|
"tags": []string{"SDLC"},
|
|
"security": []map[string]any{{"ApiKeyAuth": []string{}}},
|
|
"parameters": []map[string]any{
|
|
{"name": "id", "in": "path", "description": "Project ID", "required": true, "schema": map[string]any{"type": "string"}},
|
|
{"name": "feature", "in": "query", "description": "Feature slug (optional)", "schema": map[string]any{"type": "string"}},
|
|
},
|
|
"responses": map[string]any{
|
|
"200": map[string]any{"description": "Success"},
|
|
"401": map[string]any{"description": "Unauthorized"},
|
|
},
|
|
})
|
|
|
|
// Features
|
|
spec.AddPath("/projects/{id}/sdlc/features", "get", withAuthAndParams(
|
|
"List features",
|
|
"Returns all features in the project.",
|
|
"SDLC",
|
|
"projects:read",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/features", "post", withAuthBodyAndParams(
|
|
"Create feature",
|
|
"Creates a new feature in the draft phase.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
`{"slug": "auth-flow", "title": "User Authentication Flow"}`,
|
|
`{"slug": "auth-flow", "title": "User Authentication Flow", "phase": "draft"}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}", "get", withAuthAndParams(
|
|
"Get feature",
|
|
"Returns details for a specific feature.",
|
|
"SDLC",
|
|
"projects:read",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}", "delete", withAuthAndParams(
|
|
"Delete feature",
|
|
"Deletes a feature from the project.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/transition", "post", withAuthBodyAndParams(
|
|
"Transition feature",
|
|
"Moves a feature to a new phase.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
},
|
|
`{"phase": "specified"}`,
|
|
`{"status": "transitioned", "phase": "specified"}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/block", "post", withAuthBodyAndParams(
|
|
"Block feature",
|
|
"Adds a blocker reason to a feature.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
},
|
|
`{"reason": "Waiting for API design"}`,
|
|
`{"status": "blocked"}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/unblock", "post", withAuthAndParams(
|
|
"Unblock feature",
|
|
"Removes all blockers from a feature.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
},
|
|
))
|
|
|
|
// Artifacts
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/artifacts", "get", withAuthAndParams(
|
|
"Get artifact status",
|
|
"Returns artifact statuses for a feature.",
|
|
"SDLC",
|
|
"projects:read",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/artifacts/{type}/approve", "post", withAuthAndParams(
|
|
"Approve artifact",
|
|
"Approves a feature artifact.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
{Name: "type", In: "path", Description: "Artifact type (spec, design, tasks, qa_plan, review, audit, qa_results)", Required: true},
|
|
},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/artifacts/{type}/reject", "post", withAuthAndParams(
|
|
"Reject artifact",
|
|
"Rejects a feature artifact.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
{Name: "type", In: "path", Description: "Artifact type", Required: true},
|
|
},
|
|
))
|
|
|
|
// Tasks
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/tasks", "get", withAuthAndParams(
|
|
"List tasks",
|
|
"Returns all tasks for a feature.",
|
|
"SDLC",
|
|
"projects:read",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/tasks", "post", withAuthBodyAndParams(
|
|
"Add task",
|
|
"Adds a new task to a feature.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
},
|
|
`{"title": "Implement login handler"}`,
|
|
`{"id": "task-001", "title": "Implement login handler", "status": "pending"}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/tasks/{taskId}/start", "post", withAuthAndParams(
|
|
"Start task",
|
|
"Marks a task as in-progress.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
{Name: "taskId", In: "path", Description: "Task ID", Required: true},
|
|
},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/tasks/{taskId}/complete", "post", withAuthAndParams(
|
|
"Complete task",
|
|
"Marks a task as complete.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
{Name: "taskId", In: "path", Description: "Task ID", Required: true},
|
|
},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/tasks/{taskId}/block", "post", withAuthAndParams(
|
|
"Block task",
|
|
"Marks a task as blocked.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
{Name: "taskId", In: "path", Description: "Task ID", Required: true},
|
|
},
|
|
))
|
|
|
|
// Branch management
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/branch", "post", withAuthAndParams(
|
|
"Create branch",
|
|
"Creates a feature branch and its manifest.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/branch", "get", withAuthAndParams(
|
|
"Get branch status",
|
|
"Returns branch status and merge checklist.",
|
|
"SDLC",
|
|
"projects:read",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/branch/sync", "post", withAuthAndParams(
|
|
"Sync branch",
|
|
"Syncs feature branch with base branch.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
},
|
|
))
|
|
|
|
// Merge and archive
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/merge", "post", withAuthBodyAndParams(
|
|
"Merge feature",
|
|
"Merges a feature branch after all gates pass.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
},
|
|
`{"strategy": "squash"}`,
|
|
`{"status": "merged", "strategy": "squash"}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/features/{slug}/archive", "post", withAuthAndParams(
|
|
"Archive feature",
|
|
"Archives a released feature.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
|
|
},
|
|
))
|
|
|
|
// Query endpoints
|
|
spec.AddPath("/projects/{id}/sdlc/query/blocked", "get", withAuthAndParams(
|
|
"Query blocked features",
|
|
"Returns all blocked features.",
|
|
"SDLC",
|
|
"projects:read",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/query/ready", "get", withAuthAndParams(
|
|
"Query ready features",
|
|
"Returns features ready for work.",
|
|
"SDLC",
|
|
"projects:read",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/query/needs-approval", "get", withAuthAndParams(
|
|
"Query features needing approval",
|
|
"Returns features awaiting approval.",
|
|
"SDLC",
|
|
"projects:read",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
// Orchestrator endpoints
|
|
spec.AddPath("/projects/{id}/sdlc/execute", "post", withAuthBodyAndParams(
|
|
"Execute action",
|
|
"Executes the classifier's recommended next action.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
`{"feature": "auth-flow", "provider": "claude"}`,
|
|
`{"action": "CREATE_SPEC", "success": true, "output": "..."}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/resolve", "post", withAuthBodyAndParams(
|
|
"Resolve blocker",
|
|
"Resolves a feature blocker and re-classifies.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
`{"feature": "auth-flow"}`,
|
|
`{"action": "TRANSITION", "success": true, "output": "Feature unblocked"}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/sdlc/commit", "post", withAuthBodyAndParams(
|
|
"Commit changes",
|
|
"Commits and optionally pushes changes in the project.",
|
|
"SDLC",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
`{"feature": "auth-flow", "message": "feat: implement login", "push": true}`,
|
|
`{"commit_sha": "abc123", "files_changed": ["handler.go"], "pushed": true}`,
|
|
))
|
|
}
|
|
|
|
func registerBuildPaths(spec *api.OpenAPISpec) {
|
|
spec.AddPath("/projects/{id}/builds", "post", withAuthBodyAndParams(
|
|
"Start build",
|
|
"Enqueues a build task for a project. The build will be picked up by an available worker.",
|
|
"Builds",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
`{
|
|
"prompt": "Build a landing page with Next.js and Tailwind CSS",
|
|
"template": "nextjs-landing",
|
|
"auto_commit": true,
|
|
"auto_push": true,
|
|
"callback_url": "https://example.com/webhook"
|
|
}`,
|
|
`{
|
|
"task_id": "task-abc123",
|
|
"project_id": "my-project",
|
|
"status": "pending",
|
|
"status_url": "/builds/task-abc123"
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/builds", "get", withAuthAndParams(
|
|
"List builds",
|
|
"Returns build history for a project.",
|
|
"Builds",
|
|
"projects:read",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/builds/{taskId}", "get", withAuthAndParams(
|
|
"Get build status",
|
|
"Returns the status and result of a specific build.",
|
|
"Builds",
|
|
"projects:read",
|
|
[]param{{Name: "taskId", In: "path", Description: "Build task ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/project/create-and-build", "post", withAuthAndBody(
|
|
"Create project and build",
|
|
`Creates a new project and immediately enqueues a build task.
|
|
|
|
Combines project creation (git repo, DNS, CI activation) with build submission in a single call.`,
|
|
"Builds",
|
|
"admin",
|
|
`{
|
|
"name": "my-landing-page",
|
|
"description": "Landing page for product launch",
|
|
"template": "nextjs-landing",
|
|
"prompt": "Build a modern landing page with hero, features, and CTA sections",
|
|
"auto_commit": true,
|
|
"auto_push": true
|
|
}`,
|
|
`{
|
|
"project_id": "my-landing-page",
|
|
"name": "my-landing-page",
|
|
"domain": "my-landing-page.threesix.ai",
|
|
"url": "https://my-landing-page.threesix.ai",
|
|
"git": {
|
|
"owner": "jordan",
|
|
"name": "my-landing-page",
|
|
"clone_http": "https://git.threesix.ai/jordan/my-landing-page.git"
|
|
},
|
|
"task_id": "task-abc123",
|
|
"status": "pending",
|
|
"status_url": "/builds/task-abc123"
|
|
}`,
|
|
))
|
|
}
|
|
|
|
func registerComponentPaths(spec *api.OpenAPISpec) {
|
|
spec.AddPath("/projects/{id}/components", "get", withAuthAndParams(
|
|
"List components",
|
|
`Returns all components in a project's composable monorepo.
|
|
|
|
Each project can contain multiple components (services, workers, apps, CLIs) organized in a monorepo structure.`,
|
|
"Components",
|
|
"projects:read",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/components", "post", withAuthBodyAndParams(
|
|
"Add component",
|
|
`Adds a new component to a project's monorepo.
|
|
|
|
Generates component skeleton from template, updates configuration, and commits changes.`,
|
|
"Components",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
`{
|
|
"type": "service",
|
|
"name": "api-gateway",
|
|
"template": "go-rest-api",
|
|
"port": 8080
|
|
}`,
|
|
`{
|
|
"type": "service",
|
|
"name": "api-gateway",
|
|
"path": "services/api-gateway",
|
|
"port": 8080,
|
|
"template": "go-rest-api",
|
|
"dependencies": [],
|
|
"operation_id": "op-abc123"
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/components/batch", "post", withAuthBodyAndParams(
|
|
"Add multiple components",
|
|
`Adds multiple components to a project's monorepo atomically.
|
|
|
|
All components are created in a single transaction. If any component fails, the entire operation is rolled back.`,
|
|
"Components",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
`{
|
|
"components": [
|
|
{"type": "service", "name": "auth", "port": 8081},
|
|
{"type": "service", "name": "users", "port": 8082},
|
|
{"type": "worker", "name": "email-processor"}
|
|
]
|
|
}`,
|
|
`{
|
|
"components": [
|
|
{
|
|
"type": "service",
|
|
"name": "auth",
|
|
"path": "services/auth",
|
|
"port": 8081,
|
|
"template": "go-rest-api",
|
|
"dependencies": []
|
|
},
|
|
{
|
|
"type": "service",
|
|
"name": "users",
|
|
"path": "services/users",
|
|
"port": 8082,
|
|
"template": "go-rest-api",
|
|
"dependencies": []
|
|
},
|
|
{
|
|
"type": "worker",
|
|
"name": "email-processor",
|
|
"path": "workers/email-processor",
|
|
"port": 0,
|
|
"template": "go-worker",
|
|
"dependencies": []
|
|
}
|
|
],
|
|
"operation_id": "op-abc123"
|
|
}`,
|
|
))
|
|
|
|
// Use wildcard for dynamic component path
|
|
spec.AddPath("/projects/{id}/components/{path}", "delete", map[string]any{
|
|
"summary": "Remove component",
|
|
"description": "Removes a component from the project's monorepo.\n\n**Required scope**: `projects:execute`",
|
|
"tags": []string{"Components"},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"parameters": []map[string]any{
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"description": "Project ID",
|
|
"required": true,
|
|
"schema": map[string]any{"type": "string"},
|
|
},
|
|
{
|
|
"name": "path",
|
|
"in": "path",
|
|
"description": "Component path (e.g., services/api-gateway, workers/email-processor)",
|
|
"required": true,
|
|
"schema": map[string]any{"type": "string"},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"204": map[string]any{"description": "No Content - Component removed"},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Insufficient permissions"},
|
|
"404": map[string]any{"description": "Not Found - Component not found"},
|
|
},
|
|
})
|
|
}
|
|
|
|
func registerCredentialPaths(spec *api.OpenAPISpec) {
|
|
spec.AddPath("/credentials", "get", map[string]any{
|
|
"summary": "List credentials",
|
|
"description": `Returns all infrastructure credentials with values masked.
|
|
|
|
Optionally filter by category using ?category=<name> query parameter.
|
|
|
|
**Required scope**: ` + "`admin`",
|
|
"tags": []string{"Credentials"},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"parameters": []map[string]any{
|
|
{
|
|
"name": "category",
|
|
"in": "query",
|
|
"description": "Filter by category (e.g., git, ci, dns, registry)",
|
|
"required": false,
|
|
"schema": map[string]any{"type": "string"},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"200": map[string]any{
|
|
"description": "Success",
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": `[
|
|
{
|
|
"key": "GITEA_TOKEN",
|
|
"value": "***",
|
|
"description": "Gitea API token for repo operations",
|
|
"category": "git",
|
|
"created_at": "2026-01-01T00:00:00Z",
|
|
"updated_at": "2026-01-15T10:30:00Z",
|
|
"updated_by": "admin"
|
|
}
|
|
]`,
|
|
},
|
|
},
|
|
},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Admin scope required"},
|
|
},
|
|
})
|
|
|
|
spec.AddPath("/credentials/{key}", "get", withAuthAndParams(
|
|
"Get credential",
|
|
`Returns a single credential by key with full unmasked value.
|
|
|
|
**Security**: Only admin scope can retrieve unmasked credentials.`,
|
|
"Credentials",
|
|
"admin",
|
|
[]param{{Name: "key", In: "path", Description: "Credential key (e.g., GITEA_TOKEN, WOODPECKER_SECRET)", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/credentials", "post", withAuthAndBody(
|
|
"Set credential",
|
|
`Creates or updates a single credential.
|
|
|
|
Credentials are encrypted at rest in PostgreSQL.`,
|
|
"Credentials",
|
|
"admin",
|
|
`{
|
|
"key": "GITEA_TOKEN",
|
|
"value": "abc123xyz",
|
|
"description": "Gitea API token for repo operations",
|
|
"category": "git"
|
|
}`,
|
|
`{
|
|
"status": "stored",
|
|
"key": "GITEA_TOKEN"
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/credentials/batch", "post", withAuthAndBody(
|
|
"Set multiple credentials",
|
|
`Creates or updates multiple credentials atomically.
|
|
|
|
Useful for bulk credential loading from configuration files.`,
|
|
"Credentials",
|
|
"admin",
|
|
`{
|
|
"credentials": [
|
|
{"key": "GITEA_TOKEN", "value": "abc123", "category": "git"},
|
|
{"key": "WOODPECKER_SECRET", "value": "xyz789", "category": "ci"}
|
|
]
|
|
}`,
|
|
`{
|
|
"status": "stored",
|
|
"count": 2,
|
|
"keys": ["GITEA_TOKEN", "WOODPECKER_SECRET"]
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/credentials/{key}", "delete", map[string]any{
|
|
"summary": "Delete credential",
|
|
"description": "Removes a credential permanently.\n\n**Required scope**: `admin`",
|
|
"tags": []string{"Credentials"},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"parameters": []map[string]any{
|
|
{
|
|
"name": "key",
|
|
"in": "path",
|
|
"description": "Credential key to delete",
|
|
"required": true,
|
|
"schema": map[string]any{"type": "string"},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"200": map[string]any{
|
|
"description": "Success",
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": `{"status": "deleted", "key": "GITEA_TOKEN"}`,
|
|
},
|
|
},
|
|
},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Admin scope required"},
|
|
"404": map[string]any{"description": "Not Found - Credential not found"},
|
|
},
|
|
})
|
|
}
|
|
|
|
func registerDiagnosticsPaths(spec *api.OpenAPISpec) {
|
|
spec.AddPath("/projects/{projectId}/diagnostics", "get", withAuthAndParams(
|
|
"Get project diagnostics",
|
|
`Returns comprehensive health and diagnostic information for a project.
|
|
|
|
Includes pod status, resource usage, recent errors, external system connectivity, and deployment health.`,
|
|
"Diagnostics",
|
|
"projects:read",
|
|
[]param{{Name: "projectId", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
}
|
|
|
|
func registerVerifyPaths(spec *api.OpenAPISpec) {
|
|
spec.AddPath("/verify", "post", withAuthAndBody(
|
|
"Submit verification task",
|
|
`Submits a visual verification task using Playwright.
|
|
|
|
Captures screenshots at multiple viewports and optionally records video. Results include screenshot URLs and AI analysis of visual quality.`,
|
|
"Verify",
|
|
"verify:write",
|
|
`{
|
|
"project_id": "my-landing-page",
|
|
"url": "https://my-landing-page.threesix.ai",
|
|
"viewports": ["desktop", "tablet", "mobile"],
|
|
"wait_for": "networkidle",
|
|
"wait_timeout": 30000,
|
|
"full_page": true,
|
|
"video": true,
|
|
"callback_url": "https://myapp.com/webhooks/verify"
|
|
}`,
|
|
`{
|
|
"task_id": "verify-abc123",
|
|
"status_url": "/verify/verify-abc123",
|
|
"stream_url": "/verify/verify-abc123/stream"
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/verify/{taskId}", "get", withAuthAndParams(
|
|
"Get verification status",
|
|
`Returns the status and results of a verification task.
|
|
|
|
Includes screenshot URLs, video URL (if recorded), and duration.`,
|
|
"Verify",
|
|
"verify:read",
|
|
[]param{{Name: "taskId", In: "path", Description: "Verification task ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/verify/{taskId}/stream", "get", map[string]any{
|
|
"summary": "Stream verification events",
|
|
"description": `Streams real-time verification task events via Server-Sent Events (SSE).
|
|
|
|
Events include task started, screenshot captured, video recorded, task completed/failed.
|
|
|
|
**Required scope**: ` + "`verify:read`",
|
|
"tags": []string{"Verify"},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"parameters": []map[string]any{
|
|
{
|
|
"name": "taskId",
|
|
"in": "path",
|
|
"description": "Verification task ID",
|
|
"required": true,
|
|
"schema": map[string]any{"type": "string"},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"200": map[string]any{
|
|
"description": "Success - SSE stream",
|
|
"content": map[string]any{
|
|
"text/event-stream": map[string]any{
|
|
"example": `event: connected
|
|
data: {"task_id":"verify-abc123"}
|
|
|
|
event: started
|
|
data: {"viewport":"desktop","url":"https://my-landing-page.threesix.ai"}
|
|
|
|
event: screenshot_captured
|
|
data: {"viewport":"desktop","url":"https://storage/verify-abc123-desktop.png"}
|
|
|
|
event: completed
|
|
data: {"task_id":"verify-abc123","duration_ms":5432}`,
|
|
},
|
|
},
|
|
},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Insufficient permissions"},
|
|
"404": map[string]any{"description": "Not Found - Task not found"},
|
|
},
|
|
})
|
|
|
|
spec.AddPath("/verify/{taskId}", "delete", withAuthAndParams(
|
|
"Cancel verification task",
|
|
`Cancels a pending or running verification task.
|
|
|
|
Stops Playwright browser and marks task as cancelled.`,
|
|
"Verify",
|
|
"verify:write",
|
|
[]param{{Name: "taskId", In: "path", Description: "Verification task ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/verify", "get", map[string]any{
|
|
"summary": "List project verifications",
|
|
"description": `Returns verification tasks for a project with pagination.
|
|
|
|
**Required scope**: ` + "`verify:read`",
|
|
"tags": []string{"Verify"},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"parameters": []map[string]any{
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"description": "Project ID",
|
|
"required": true,
|
|
"schema": map[string]any{"type": "string"},
|
|
},
|
|
{
|
|
"name": "limit",
|
|
"in": "query",
|
|
"description": "Maximum number of results (default: 50)",
|
|
"required": false,
|
|
"schema": map[string]any{"type": "integer", "default": 50},
|
|
},
|
|
{
|
|
"name": "offset",
|
|
"in": "query",
|
|
"description": "Pagination offset (default: 0)",
|
|
"required": false,
|
|
"schema": map[string]any{"type": "integer", "default": 0},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"200": map[string]any{"description": "Success"},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Insufficient permissions"},
|
|
},
|
|
})
|
|
}
|
|
|
|
func registerInfrastructurePaths(spec *api.OpenAPISpec) {
|
|
// Git repository endpoints
|
|
spec.AddPath("/projects/{id}/repo", "post", withAuthBodyAndParams(
|
|
"Create repository",
|
|
`Creates a git repository for a project in Gitea.
|
|
|
|
Repository is created under the configured git owner (e.g., threesix organization).`,
|
|
"Infrastructure",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID (will be repository name)", Required: true}},
|
|
`{
|
|
"description": "Landing page for product launch",
|
|
"private": false
|
|
}`,
|
|
`{
|
|
"id": 123,
|
|
"owner": "threesix",
|
|
"name": "my-landing-page",
|
|
"full_name": "threesix/my-landing-page",
|
|
"description": "Landing page for product launch",
|
|
"private": false,
|
|
"clone_ssh": "git@git.threesix.ai:threesix/my-landing-page.git",
|
|
"clone_http": "https://git.threesix.ai/threesix/my-landing-page.git",
|
|
"html_url": "https://git.threesix.ai/threesix/my-landing-page"
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/repo", "get", withAuthAndParams(
|
|
"Get repository",
|
|
`Returns repository information for a project.
|
|
|
|
Includes clone URLs, visibility, and metadata.`,
|
|
"Infrastructure",
|
|
"projects:read",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/repo", "delete", withAuthAndParams(
|
|
"Delete repository",
|
|
`Deletes the git repository for a project.
|
|
|
|
**Warning**: This permanently deletes all git history and branches.`,
|
|
"Infrastructure",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
// Deployment endpoints
|
|
spec.AddPath("/projects/{id}/deploy", "post", withAuthAndParams(
|
|
"Deploy project",
|
|
`Deploys a project to Kubernetes.
|
|
|
|
Creates deployment, service, and ingress resources.`,
|
|
"Infrastructure",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/deploy/status", "get", withAuthAndParams(
|
|
"Get deployment status",
|
|
`Returns the current deployment status for a project.
|
|
|
|
Includes pod status, replica counts, and health information.`,
|
|
"Infrastructure",
|
|
"projects:read",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/deploy", "delete", withAuthAndParams(
|
|
"Undeploy project",
|
|
`Removes deployment, service, and ingress resources for a project.
|
|
|
|
Pods are terminated and all Kubernetes resources are deleted.`,
|
|
"Infrastructure",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/deploy/restart", "post", withAuthAndParams(
|
|
"Restart deployment",
|
|
`Restarts a project's deployment by recreating all pods.
|
|
|
|
Useful for applying configuration changes or recovering from errors.`,
|
|
"Infrastructure",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/deploy/scale", "post", withAuthBodyAndParams(
|
|
"Scale deployment",
|
|
`Scales a project's deployment to the specified replica count.`,
|
|
"Infrastructure",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
`{"replicas": 3}`,
|
|
`{"status": "scaled", "replicas": 3}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/deploy/logs", "get", map[string]any{
|
|
"summary": "Get deployment logs",
|
|
"description": `Returns recent logs from a project's deployment pods.
|
|
|
|
**Required scope**: ` + "`projects:read`",
|
|
"tags": []string{"Infrastructure"},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"parameters": []map[string]any{
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"description": "Project ID",
|
|
"required": true,
|
|
"schema": map[string]any{"type": "string"},
|
|
},
|
|
{
|
|
"name": "tail",
|
|
"in": "query",
|
|
"description": "Number of recent log lines to retrieve (default: 100)",
|
|
"required": false,
|
|
"schema": map[string]any{"type": "integer", "default": 100},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"200": map[string]any{"description": "Success"},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Insufficient permissions"},
|
|
"404": map[string]any{"description": "Not Found - Project not found"},
|
|
},
|
|
})
|
|
|
|
// Domain endpoints
|
|
spec.AddPath("/projects/{id}/domain", "post", withAuthBodyAndParams(
|
|
"Add domain",
|
|
`Adds a custom domain to a project.
|
|
|
|
Creates DNS A record if domain is a subdomain of the configured base domain (e.g., threesix.ai).`,
|
|
"Infrastructure",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
`{"domain": "custom.example.com"}`,
|
|
`{
|
|
"project": "my-landing-page",
|
|
"domain": "custom.example.com",
|
|
"status": "configured",
|
|
"note": "External domain configured. Point your DNS to 208.122.204.172"
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/domain", "delete", map[string]any{
|
|
"summary": "Remove domain",
|
|
"description": `Removes a custom domain from a project.
|
|
|
|
Deletes DNS A record if domain was a managed subdomain.
|
|
|
|
**Required scope**: ` + "`projects:execute`",
|
|
"tags": []string{"Infrastructure"},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"parameters": []map[string]any{
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"description": "Project ID",
|
|
"required": true,
|
|
"schema": map[string]any{"type": "string"},
|
|
},
|
|
{
|
|
"name": "domain",
|
|
"in": "query",
|
|
"description": "Domain to remove (e.g., custom.example.com)",
|
|
"required": true,
|
|
"schema": map[string]any{"type": "string"},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"200": map[string]any{"description": "Success"},
|
|
"400": map[string]any{"description": "Bad Request - Missing domain parameter"},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Insufficient permissions"},
|
|
},
|
|
})
|
|
|
|
spec.AddPath("/projects/{id}/domains", "get", withAuthAndParams(
|
|
"List domains",
|
|
`Returns all domains (primary and aliases) for a project.`,
|
|
"Infrastructure",
|
|
"projects:read",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/domains", "post", withAuthBodyAndParams(
|
|
"Add domain alias",
|
|
`Adds an additional domain alias to a project.
|
|
|
|
Projects can have multiple domains pointing to the same deployment.`,
|
|
"Infrastructure",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
`{"domain": "alias.example.com", "proxied": false}`,
|
|
`{"status": "added", "domain": "alias.example.com"}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/domains/{domain}", "delete", withAuthAndParams(
|
|
"Remove domain alias",
|
|
`Removes a domain alias from a project.
|
|
|
|
Primary domain cannot be removed via this endpoint.`,
|
|
"Infrastructure",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "domain", In: "path", Description: "Domain to remove", Required: true},
|
|
},
|
|
))
|
|
|
|
// CI pipeline endpoints
|
|
spec.AddPath("/projects/{id}/pipelines", "get", withAuthAndParams(
|
|
"List CI pipelines",
|
|
`Returns recent CI pipeline runs for a project.
|
|
|
|
Includes build status, branch, commit info, and duration.`,
|
|
"Infrastructure",
|
|
"projects:read",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/pipelines/{number}", "get", withAuthAndParams(
|
|
"Get pipeline details",
|
|
`Returns detailed information for a specific pipeline run.`,
|
|
"Infrastructure",
|
|
"projects:read",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "number", In: "path", Description: "Pipeline number", Required: true},
|
|
},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/pipelines/{number}/steps", "get", withAuthAndParams(
|
|
"Get pipeline steps",
|
|
`Returns the steps (stages) of a pipeline run with status and logs.`,
|
|
"Infrastructure",
|
|
"projects:read",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "number", In: "path", Description: "Pipeline number", Required: true},
|
|
},
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/pipelines/{number}/retry", "post", withAuthAndParams(
|
|
"Retry pipeline",
|
|
`Retries a failed pipeline from the failed step.
|
|
|
|
Part of the enterprise resilience architecture for handling transient failures.`,
|
|
"Infrastructure",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Project ID", Required: true},
|
|
{Name: "number", In: "path", Description: "Pipeline number to retry", Required: true},
|
|
},
|
|
))
|
|
}
|
|
|
|
func registerWebhookPaths(spec *api.OpenAPISpec) {
|
|
spec.AddPath("/webhooks/woodpecker", "post", map[string]any{
|
|
"summary": "Woodpecker CI webhook",
|
|
"description": `Receives build event webhooks from Woodpecker CI.
|
|
|
|
**Authentication**: Uses HMAC-SHA256 signature verification (X-Woodpecker-Signature header), not API key auth.
|
|
|
|
Processes successful builds on main/master branches to trigger deployments and DNS updates.`,
|
|
"tags": []string{"Webhooks"},
|
|
"parameters": []map[string]any{
|
|
{
|
|
"name": "X-Woodpecker-Signature",
|
|
"in": "header",
|
|
"description": "HMAC-SHA256 signature of request body",
|
|
"required": false,
|
|
"schema": map[string]any{"type": "string"},
|
|
},
|
|
},
|
|
"requestBody": map[string]any{
|
|
"required": true,
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": `{
|
|
"event": "push",
|
|
"repo": {
|
|
"owner": "threesix",
|
|
"name": "my-landing-page",
|
|
"full_name": "threesix/my-landing-page"
|
|
},
|
|
"build": {
|
|
"number": 42,
|
|
"status": "success",
|
|
"branch": "main",
|
|
"commit": "abc123def456",
|
|
"message": "Add feature X",
|
|
"author": "jordan"
|
|
}
|
|
}`,
|
|
},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"200": map[string]any{
|
|
"description": "Success - Event processed",
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": `{
|
|
"status": "success",
|
|
"project": "my-landing-page",
|
|
"image": "registry.threesix.ai/my-landing-page:abc123de",
|
|
"commit": "abc123def456",
|
|
"note": "component deployments managed by CI pipeline"
|
|
}`,
|
|
},
|
|
},
|
|
},
|
|
"400": map[string]any{"description": "Bad Request - Invalid payload"},
|
|
"401": map[string]any{"description": "Unauthorized - Invalid signature"},
|
|
},
|
|
})
|
|
}
|
|
|
|
func registerSagaPaths(spec *api.OpenAPISpec) {
|
|
spec.AddPath("/sagas", "post", withAuthAndBody(
|
|
"Create and start saga",
|
|
`Creates a new saga workflow and starts execution.
|
|
|
|
Sagas are distributed workflows with automatic compensation on failure. Useful for multi-step operations like project creation with rollback support.`,
|
|
"Sagas",
|
|
"projects:execute",
|
|
`{
|
|
"name": "create-full-stack-project",
|
|
"max_retries": 3,
|
|
"steps": [
|
|
{
|
|
"name": "create-repo",
|
|
"action": "api",
|
|
"compensate": "delete-repo",
|
|
"config": {"method": "POST", "endpoint": "/projects/{id}/repo"}
|
|
},
|
|
{
|
|
"name": "create-db",
|
|
"action": "api",
|
|
"depends_on": ["create-repo"],
|
|
"compensate": "delete-db",
|
|
"config": {"method": "POST", "endpoint": "/projects/{id}/database"}
|
|
}
|
|
]
|
|
}`,
|
|
`{
|
|
"id": "saga-abc123",
|
|
"name": "create-full-stack-project",
|
|
"status": "running",
|
|
"current_step": "create-repo",
|
|
"retry_count": 0,
|
|
"max_retries": 3,
|
|
"created_at": "2026-02-08T10:00:00Z",
|
|
"updated_at": "2026-02-08T10:00:00Z"
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/sagas", "get", map[string]any{
|
|
"summary": "List sagas",
|
|
"description": `Returns sagas with optional filtering.
|
|
|
|
**Query parameters**: ?name=<name>&status=<status>
|
|
|
|
**Required scope**: ` + "`projects:read`",
|
|
"tags": []string{"Sagas"},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"parameters": []map[string]any{
|
|
{
|
|
"name": "name",
|
|
"in": "query",
|
|
"description": "Filter by saga name",
|
|
"required": false,
|
|
"schema": map[string]any{"type": "string"},
|
|
},
|
|
{
|
|
"name": "status",
|
|
"in": "query",
|
|
"description": "Filter by status (pending, running, completed, failed, compensating, compensated)",
|
|
"required": false,
|
|
"schema": map[string]any{"type": "string"},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"200": map[string]any{"description": "Success"},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Insufficient permissions"},
|
|
},
|
|
})
|
|
|
|
spec.AddPath("/sagas/{id}", "get", withAuthAndParams(
|
|
"Get saga",
|
|
`Returns detailed saga execution state including all steps, outputs, and errors.`,
|
|
"Sagas",
|
|
"projects:read",
|
|
[]param{{Name: "id", In: "path", Description: "Saga ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/sagas/{id}", "delete", withAuthAndParams(
|
|
"Delete saga",
|
|
`Deletes a saga and all associated state.
|
|
|
|
Running sagas should be cancelled before deletion.`,
|
|
"Sagas",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Saga ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/sagas/{id}/retry", "post", withAuthAndParams(
|
|
"Retry failed saga",
|
|
`Resumes a failed saga from the last failed step.
|
|
|
|
Increments retry count and attempts to continue execution.`,
|
|
"Sagas",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Saga ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/sagas/{id}/rollback", "post", withAuthAndParams(
|
|
"Rollback saga",
|
|
`Triggers compensation for a failed saga.
|
|
|
|
Runs compensation actions in reverse order to undo completed steps.`,
|
|
"Sagas",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Saga ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/sagas/{id}/steps/{step}/retry", "post", withAuthAndParams(
|
|
"Retry specific step",
|
|
`Retries a specific failed step within a saga.
|
|
|
|
Does not increment saga-level retry count.`,
|
|
"Sagas",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Saga ID", Required: true},
|
|
{Name: "step", In: "path", Description: "Step name", Required: true},
|
|
},
|
|
))
|
|
|
|
spec.AddPath("/sagas/{id}/steps/{step}/skip", "post", withAuthAndParams(
|
|
"Skip step",
|
|
`Skips a failed step and continues saga execution.
|
|
|
|
Marks step as skipped and allows dependent steps to proceed.`,
|
|
"Sagas",
|
|
"projects:execute",
|
|
[]param{
|
|
{Name: "id", In: "path", Description: "Saga ID", Required: true},
|
|
{Name: "step", In: "path", Description: "Step name to skip", Required: true},
|
|
},
|
|
))
|
|
}
|