All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Add TimeoutAgentExecution (22m) to handlers for synchronous SDLC
execution, and TimeoutAgent{Default,Medium,Heavy} (12/22/47m) to
workers for tiered agent task execution. Aligns with SDLC action
complexity tiers and prevents inline duration literals.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
218 lines
6.6 KiB
Go
218 lines
6.6 KiB
Go
// Package domain contains pure domain models with no external dependencies.
|
|
package domain
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// BuildSpec defines what a build task should accomplish.
|
|
// It captures the user's intent and parameters for code generation.
|
|
type BuildSpec struct {
|
|
// Template is the project template to use (e.g., "nextjs-landing").
|
|
Template string `json:"template,omitempty"`
|
|
|
|
// Prompt is the user's instruction for the build.
|
|
Prompt string `json:"prompt"`
|
|
|
|
// Variables contains template-specific substitution values.
|
|
Variables map[string]string `json:"variables,omitempty"`
|
|
|
|
// AutoCommit controls whether changes are automatically committed.
|
|
AutoCommit bool `json:"auto_commit"`
|
|
|
|
// AutoPush controls whether commits are automatically pushed.
|
|
AutoPush bool `json:"auto_push"`
|
|
|
|
// CallbackURL is the webhook URL for completion notification.
|
|
CallbackURL string `json:"callback_url,omitempty"`
|
|
|
|
// GitCloneURL is the HTTPS URL for cloning the project repository.
|
|
// Required for builds that use AutoCommit/AutoPush on shared worker pods.
|
|
GitCloneURL string `json:"git_clone_url,omitempty"`
|
|
|
|
// TimeoutSeconds overrides the default agent execution timeout.
|
|
// 0 means use the default (10 minutes). Valid range: 60-5400 (1m to 90m).
|
|
TimeoutSeconds int `json:"timeout_seconds,omitempty"`
|
|
}
|
|
|
|
// Validate checks that the BuildSpec has required fields.
|
|
func (s *BuildSpec) Validate() error {
|
|
if s.Prompt == "" {
|
|
return ErrPromptRequired
|
|
}
|
|
if s.TimeoutSeconds != 0 && (s.TimeoutSeconds < 60 || s.TimeoutSeconds > 5400) {
|
|
return fmt.Errorf("timeout_seconds must be between 60 and 5400 (got %d)", s.TimeoutSeconds)
|
|
}
|
|
if s.CallbackURL != "" {
|
|
if err := ValidateCallbackURL(s.CallbackURL); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateCallbackURL checks that a callback URL is safe to use.
|
|
// It rejects non-HTTPS URLs and private/internal network addresses.
|
|
func ValidateCallbackURL(rawURL string) error {
|
|
u, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid callback URL: %w", err)
|
|
}
|
|
if u.Scheme != "https" {
|
|
return fmt.Errorf("callback URL must use HTTPS scheme, got %q", u.Scheme)
|
|
}
|
|
if u.Host == "" {
|
|
return fmt.Errorf("callback URL must have a host")
|
|
}
|
|
|
|
host := u.Hostname()
|
|
lower := strings.ToLower(host)
|
|
|
|
// Block localhost and loopback
|
|
if lower == "localhost" || lower == "127.0.0.1" || lower == "::1" || lower == "[::1]" {
|
|
return fmt.Errorf("callback URL must not point to localhost")
|
|
}
|
|
// Block common metadata endpoints
|
|
if lower == "metadata.google.internal" || lower == "169.254.169.254" {
|
|
return fmt.Errorf("callback URL must not point to cloud metadata service")
|
|
}
|
|
// Block private network ranges
|
|
if strings.HasPrefix(lower, "10.") || strings.HasPrefix(lower, "192.168.") {
|
|
return fmt.Errorf("callback URL must not point to private network addresses")
|
|
}
|
|
if strings.HasPrefix(lower, "172.") {
|
|
// 172.16.0.0 - 172.31.255.255
|
|
parts := strings.SplitN(lower, ".", 3)
|
|
if len(parts) >= 2 {
|
|
if octet, err := strconv.Atoi(parts[1]); err == nil && octet >= 16 && octet <= 31 {
|
|
return fmt.Errorf("callback URL must not point to private network addresses")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// BuildResult captures the outcome of a build execution.
|
|
type BuildResult struct {
|
|
// Success indicates whether the build completed successfully.
|
|
Success bool `json:"success"`
|
|
|
|
// Output is the agent's text output during the build.
|
|
Output string `json:"output,omitempty"`
|
|
|
|
// Error contains the error message if the build failed.
|
|
Error string `json:"error,omitempty"`
|
|
|
|
// ErrorCode categorizes the failure type for programmatic handling.
|
|
// Values: RATE_LIMITED, AUTH_FAILED, TIMEOUT, STALE_WORKER, AGENT_ERROR, INVALID_SPEC
|
|
ErrorCode WorkErrorCode `json:"error_code,omitempty"`
|
|
|
|
// CommitSHA is the git commit hash if auto-commit was enabled.
|
|
CommitSHA string `json:"commit_sha,omitempty"`
|
|
|
|
// FilesChanged lists files modified during the build.
|
|
FilesChanged []string `json:"files_changed,omitempty"`
|
|
|
|
// DurationMs is the total execution time in milliseconds.
|
|
DurationMs int64 `json:"duration_ms"`
|
|
|
|
// Artifacts contains named outputs from the build (e.g., deploy URLs).
|
|
Artifacts map[string]string `json:"artifacts,omitempty"`
|
|
}
|
|
|
|
// ToWorkResult converts a BuildResult to a WorkResult.
|
|
// Build-specific fields (commit_sha, files_changed, duration_ms) are
|
|
// promoted into the artifacts map, overwriting any existing keys with
|
|
// the same names.
|
|
func (r *BuildResult) ToWorkResult() *WorkResult {
|
|
if r == nil {
|
|
return &WorkResult{}
|
|
}
|
|
|
|
artifacts := make(map[string]string)
|
|
maps.Copy(artifacts, r.Artifacts)
|
|
|
|
// Promote build-specific fields into artifacts
|
|
if r.CommitSHA != "" {
|
|
artifacts["commit_sha"] = r.CommitSHA
|
|
}
|
|
if r.DurationMs > 0 {
|
|
artifacts["duration_ms"] = strconv.FormatInt(r.DurationMs, 10)
|
|
}
|
|
if len(r.FilesChanged) > 0 {
|
|
artifacts["files_changed_count"] = strconv.Itoa(len(r.FilesChanged))
|
|
}
|
|
|
|
output := r.Output
|
|
if !r.Success && r.Error != "" {
|
|
output = r.Error
|
|
}
|
|
|
|
return &WorkResult{
|
|
Output: output,
|
|
Artifacts: artifacts,
|
|
}
|
|
}
|
|
|
|
// BuildStatus represents the lifecycle state of a build.
|
|
type BuildStatus string
|
|
|
|
const (
|
|
BuildStatusPending BuildStatus = "pending"
|
|
BuildStatusRunning BuildStatus = "running"
|
|
BuildStatusCompleted BuildStatus = "completed"
|
|
BuildStatusFailed BuildStatus = "failed"
|
|
BuildStatusCancelled BuildStatus = "cancelled"
|
|
)
|
|
|
|
// IsValid returns true if the status is a known valid status.
|
|
func (s BuildStatus) IsValid() bool {
|
|
switch s {
|
|
case BuildStatusPending, BuildStatusRunning, BuildStatusCompleted,
|
|
BuildStatusFailed, BuildStatusCancelled:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsTerminal returns true if the build is in a final state.
|
|
func (s BuildStatus) IsTerminal() bool {
|
|
switch s {
|
|
case BuildStatusCompleted, BuildStatusFailed, BuildStatusCancelled:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// BuildAuditEntry represents a single build's audit record.
|
|
type BuildAuditEntry struct {
|
|
// TaskID is the work queue task ID.
|
|
TaskID string `json:"task_id"`
|
|
|
|
// ProjectID is the project this build belongs to.
|
|
ProjectID string `json:"project_id"`
|
|
|
|
// WorkerID is the worker that executed the build.
|
|
WorkerID string `json:"worker_id,omitempty"`
|
|
|
|
// Spec is the original build specification.
|
|
Spec BuildSpec `json:"spec"`
|
|
|
|
// Result is the build outcome (nil if not yet complete).
|
|
Result *BuildResult `json:"result,omitempty"`
|
|
|
|
// Status is the current build status.
|
|
Status BuildStatus `json:"status"`
|
|
|
|
// StartedAt is when the build was created/enqueued.
|
|
StartedAt time.Time `json:"started_at"`
|
|
|
|
// CompletedAt is when the build finished (nil if still running).
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
}
|