Documentation
¶
Index ¶
- func BuildComposeEnv(projectName, envName string, ports PortMap) []string
- func ComposeProjectName(projectName, envName string) string
- func ExecuteCoreHook(ctx context.Context, hookScript string, declaredOutputs []string, env []string, ...) (map[string]string, error)
- func GenerateComposeFile(cfg *ProjectConfig, manifest *Manifest) ([]byte, error)
- func GenerateErrorPages(envName string) map[string][]byte
- func GenerateNginxConfig(cfg *ProjectConfig, manifest *Manifest, domain string) ([]byte, error)
- func Hostname() string
- func ParseComposeFile(path string) (map[string]InfraService, error)
- func RenderEnvFileContent(env map[string]string) []byte
- func RenderEnvMap(envMap map[string]string, ctx *TemplateContext) (map[string]string, error)
- func RenderTemplate(tmpl string, ctx *TemplateContext) (string, error)
- func ResolveEnvironmentFromCwd(cwd string, environments map[string]*EnvironmentEntry) (string, error)
- func SanitizeName(name string) string
- func TopologicalSort(services map[string]ServiceConfig) ([]string, error)
- func ValidateConfig(cfg *ProjectConfig) error
- func ValidateConfigWithFS(cfg *ProjectConfig, projectRoot string, fileExists func(string) bool) error
- func WriteManifest(m *Manifest, w io.Writer) error
- type AuditEntry
- type CommandHook
- type ComposeConfig
- type ComputeAccess
- type ComputeAccessInfo
- type ComputeHooks
- type ComputePort
- type ComputeResources
- type CoreResetOpts
- type DestroyOptions
- type DomainLocalComputeAccess
- func (l *DomainLocalComputeAccess) Exec(ctx context.Context, command string, env []string) (string, error)
- func (l *DomainLocalComputeAccess) ReadFile(_ context.Context, relPath string) ([]byte, error)
- func (l *DomainLocalComputeAccess) Root() string
- func (l *DomainLocalComputeAccess) SetStderr(w io.Writer)
- func (l *DomainLocalComputeAccess) VerboseExec(ctx context.Context, command string, env []string) (string, error)
- func (l *DomainLocalComputeAccess) WriteFile(_ context.Context, relPath string, data []byte, mode os.FileMode) error
- type DomainSSHComputeAccess
- func (s *DomainSSHComputeAccess) EnsureRootWritable(ctx context.Context) error
- func (s *DomainSSHComputeAccess) Exec(ctx context.Context, command string, env []string) (string, error)
- func (s *DomainSSHComputeAccess) Host() string
- func (s *DomainSSHComputeAccess) ProxyCommand() string
- func (s *DomainSSHComputeAccess) ReadFile(ctx context.Context, relPath string) ([]byte, error)
- func (s *DomainSSHComputeAccess) Root() string
- func (s *DomainSSHComputeAccess) SSHArgs() []string
- func (s *DomainSSHComputeAccess) SetStderr(w io.Writer)
- func (s *DomainSSHComputeAccess) User() string
- func (s *DomainSSHComputeAccess) VerboseExec(ctx context.Context, command string, env []string) (string, error)
- func (s *DomainSSHComputeAccess) WriteFile(ctx context.Context, relPath string, data []byte, _ os.FileMode) error
- type EnvironmentDetail
- type EnvironmentEntry
- func (e *EnvironmentEntry) AppendAudit(entry AuditEntry)
- func (e *EnvironmentEntry) DisableService(name string)
- func (e *EnvironmentEntry) EnableService(name string)
- func (e *EnvironmentEntry) GetEnv(key string) (string, bool)
- func (e *EnvironmentEntry) InvalidateStep(name, reason string) bool
- func (e *EnvironmentEntry) InvalidateStepsFrom(stepName string, orderedSteps []string)
- func (e *EnvironmentEntry) IsManagedWorktree() bool
- func (e *EnvironmentEntry) IsServiceEnabled(name string) bool
- func (e *EnvironmentEntry) SetEnv(key, value string)
- func (e *EnvironmentEntry) SetStepRecord(rec *StepRecord)
- func (e *EnvironmentEntry) StepCompleted(name string) bool
- func (e *EnvironmentEntry) StepOutputs(name string) map[string]any
- func (e *EnvironmentEntry) WorktreePath() string
- type EnvironmentMode
- type EnvironmentStatus
- type InfraService
- type InfrastructureConfig
- type Manager
- func (m *Manager) Attach(ctx context.Context, envName string, worktreePath string) (*EnvironmentEntry, error)
- func (m *Manager) BuildProvisionerStepOrder() []string
- func (m *Manager) BuildRunnerStepOrder() []string
- func (m *Manager) BuildSSHComputeAccess(entry *EnvironmentEntry) ComputeAccess
- func (m *Manager) CoreInit(ctx context.Context, svcName string) error
- func (m *Manager) CoreReset(ctx context.Context, svcName, envName string, opts CoreResetOpts) error
- func (m *Manager) Destroy(ctx context.Context, envName string, opts DestroyOptions) error
- func (m *Manager) DryRunStep(ctx context.Context, envName, stepName string) (string, error)
- func (m *Manager) GetEnvironment(ctx context.Context, envName string) (*EnvironmentEntry, error)
- func (m *Manager) Init(ctx context.Context, envName, branch, baseBranch string) (*EnvironmentEntry, error)
- func (m *Manager) List(ctx context.Context) ([]*EnvironmentEntry, error)
- func (m *Manager) PrintStep(ctx context.Context, envName, stepName string) (string, error)
- func (m *Manager) Provision(ctx context.Context, envName, branch, baseBranch, fromStep string) (*EnvironmentEntry, error)
- func (m *Manager) ProvisionAttach(ctx context.Context, envName, worktreePath, fromStep string) (*EnvironmentEntry, error)
- func (m *Manager) Reconcile(ctx context.Context, envName string, dryRun ...bool) (*ReconcileReport, error)
- func (m *Manager) Run(ctx context.Context, manifestPath, fromStep string) error
- func (m *Manager) RunCoreHook(ctx context.Context, svcName, action, envName string) (map[string]string, error)
- func (m *Manager) RunStep(ctx context.Context, envName, stepName string) error
- func (m *Manager) RunSteps(ctx context.Context, envName string, stepNames []string) error
- func (m *Manager) SaveEnvironment(ctx context.Context, envName string, entry *EnvironmentEntry) error
- func (m *Manager) SetNoCache(v bool)
- func (m *Manager) SetStatus(ctx context.Context, envName string, status EnvironmentStatus) error
- func (m *Manager) Status(ctx context.Context, envName string) (*EnvironmentDetail, error)
- func (m *Manager) SyncRemote(ctx context.Context, envName string) error
- type ManagerDeps
- type Manifest
- type ManifestInfrastructure
- type ManifestService
- type NetworkingPort
- type NoopReporter
- type PortMap
- type ProgressReporter
- type ProjectConfig
- type ProvisionerConfig
- type ProvisionerServiceConfig
- type ProxyConfig
- type ReconcileReport
- type ReconcileResult
- type RunnerConfig
- type SSHComputeAccessOpts
- type SSHConfig
- type ServiceConfig
- type ServiceProxy
- type ServiceProxyTarget
- type State
- type StatePort
- type StepEvent
- type StepOpts
- type StepRecord
- type StepRecordStatus
- type StepStatus
- type TemplateContext
- type VMInfo
- type VMSpec
- type ValidationError
- type VerifyFunc
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BuildComposeEnv ¶
BuildComposeEnv builds the environment variables for running docker compose commands. Includes COMPOSE_PROJECT_NAME and port mappings (e.g., REDIS_PORT=6421). The returned slice is suitable for passing to ComputeAccess.Exec.
func ComposeProjectName ¶
ComposeProjectName returns the compose project name for this environment. Sanitizes to match Docker Compose requirements: lowercase alphanumeric, hyphens, underscores.
func ExecuteCoreHook ¶
func ExecuteCoreHook(ctx context.Context, hookScript string, declaredOutputs []string, env []string, workdir string, stderr io.Writer) (map[string]string, error)
ExecuteCoreHook runs a core service hook script and captures KEY=VALUE outputs. Both stdout and stderr stream to the terminal in real-time for visibility. KEY=VALUE lines on stdout are redacted (value replaced with ********) before display. stdout is also captured and parsed for KEY=VALUE pairs. Returns the captured outputs, validated against declaredOutputs.
func GenerateComposeFile ¶
func GenerateComposeFile(cfg *ProjectConfig, manifest *Manifest) ([]byte, error)
GenerateComposeFile generates a Docker Compose YAML file from service definitions. Only services with a `start` command are included. nginx is always added implicitly. The compose file contains ALL startable services — autostart controls which are started on create, not which are generated.
func GenerateErrorPages ¶
GenerateErrorPages returns a map of filename → HTML content for nginx error pages.
func GenerateNginxConfig ¶
func GenerateNginxConfig(cfg *ProjectConfig, manifest *Manifest, domain string) ([]byte, error)
GenerateNginxConfig generates an nginx configuration file from the manifest and service definitions. Each service gets its own server block with explicit hostname matching.
Enabled services get full proxy blocks with a custom 502 error page. Disabled services (known but not started) get a friendly status page telling the user how to start the service. Unknown subdomains hit the default server with a 404 page.
func ParseComposeFile ¶
func ParseComposeFile(path string) (map[string]InfraService, error)
ParseComposeFile reads a docker compose YAML file and extracts infrastructure service definitions including service names, images, and port mappings.
func RenderEnvFileContent ¶
RenderEnvFileContent renders a map of env vars to KEY=VALUE format content. Keys are sorted for deterministic output.
func RenderEnvMap ¶
RenderEnvMap renders all template variables in a map of env vars.
func RenderTemplate ¶
func RenderTemplate(tmpl string, ctx *TemplateContext) (string, error)
RenderTemplate replaces {{var}} placeholders in a string with values from the context. Supported variable patterns:
- {{services.<name>.port}} — allocated port for an application service
- {{infrastructure.<name>.port}} — allocated port for an infrastructure service
- {{provisioner.<service>.<OUTPUT>}} — output value from a provisioner service
- {{env.name}} — the current environment name
- {{store.<key>}} — value from the persistent key-value store
- {{proxy.url.<service>}} — full URL for a service (https://{env}--{service}.{domain})
- {{proxy.domain}} — the proxy domain (e.g., "preview.airgoods.com")
func ResolveEnvironmentFromCwd ¶
func ResolveEnvironmentFromCwd(cwd string, environments map[string]*EnvironmentEntry) (string, error)
ResolveEnvironmentFromCwd determines the environment name by checking if the current working directory is inside any known worktree path from state.
func SanitizeName ¶
SanitizeName replaces characters not safe for use in database names, file paths, and Docker Compose project names. Lowercase alphanumeric, hyphens, and underscores only.
func TopologicalSort ¶
func TopologicalSort(services map[string]ServiceConfig) ([]string, error)
TopologicalSort performs Kahn's algorithm to produce a topological ordering of services based on their dependsOn relationships. Returns an error if a cycle is detected or a dependency references an unknown service.
func ValidateConfig ¶
func ValidateConfig(cfg *ProjectConfig) error
ValidateConfig performs deep validation of a ProjectConfig. It checks structural correctness, port collisions, dependency references, and template variable references. It does NOT check file system paths — use ValidateConfigWithFS for that.
func ValidateConfigWithFS ¶
func ValidateConfigWithFS(cfg *ProjectConfig, projectRoot string, fileExists func(string) bool) error
ValidateConfigWithFS performs all config validation plus file system checks. projectRoot is the directory containing previewctl.yaml. fileExists is a function that checks if a path exists (allows testing).
Types ¶
type AuditEntry ¶
type AuditEntry struct {
Timestamp time.Time `json:"timestamp"`
Step string `json:"step"`
Action string `json:"action"` // "executed", "skipped", "verified", "verify_failed", "invalidated", "failed"
Machine string `json:"machine"`
DurationMs int64 `json:"durationMs,omitempty"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
}
AuditEntry is an append-only log entry recording what happened during lifecycle operations.
type CommandHook ¶
type CommandHook struct {
Command string `yaml:"command,omitempty"`
AllowCache *bool `yaml:"allow_cache,omitempty"`
}
CommandHook is a command-valued hook that can be configured either as a legacy scalar string or as an object with hook metadata.
func (CommandHook) CacheAllowed ¶
func (h CommandHook) CacheAllowed() bool
CacheAllowed reports whether completed checkpoints may skip this hook. Hooks are cacheable by default for backward compatibility.
func (CommandHook) IsConfigured ¶
func (h CommandHook) IsConfigured() bool
IsConfigured reports whether this hook has a command to run.
func (*CommandHook) UnmarshalYAML ¶
func (h *CommandHook) UnmarshalYAML(value *yaml.Node) error
UnmarshalYAML accepts both:
runner: after: ./script.sh
and:
runner:
after:
command: ./script.sh
allow_cache: false
type ComposeConfig ¶
type ComposeConfig struct {
Autostart []string `yaml:"autostart"` // services started on create (proxy is always implicit if enabled)
Image string `yaml:"image"` // base Docker image for app containers (e.g., "node:20")
Proxy *ProxyConfig `yaml:"proxy,omitempty"` // reverse proxy configuration
}
ComposeConfig defines how previewctl generates and manages Docker Compose for application services in remote mode.
type ComputeAccess ¶
type ComputeAccess interface {
// WriteFile writes content to a path relative to the compute root.
WriteFile(ctx context.Context, relPath string, data []byte, mode os.FileMode) error
// ReadFile reads content from a path relative to the compute root.
ReadFile(ctx context.Context, relPath string) ([]byte, error)
// Exec runs a command in the compute root directory.
// Stderr streams to the configured writer. Stdout is captured and returned silently.
Exec(ctx context.Context, command string, env []string) (stdout string, err error)
// VerboseExec runs a command and tees stdout to stderr for real-time visibility.
// Use for user-defined hooks and build commands where output should always be visible.
VerboseExec(ctx context.Context, command string, env []string) (stdout string, err error)
// Root returns the compute root path (local path or remote working dir).
Root() string
}
ComputeAccess provides uniform access to a compute location. Local mode wraps filesystem operations; remote mode wraps SSH/SCP.
func NewDomainLocalComputeAccess ¶
func NewDomainLocalComputeAccess(root string) ComputeAccess
func NewDomainSSHComputeAccess ¶
func NewDomainSSHComputeAccess(host, user, root string) ComputeAccess
NewDomainSSHComputeAccess creates a ComputeAccess backed by SSH to a remote host. This is the direct-mode constructor (backward compatible).
func NewDomainSSHComputeAccessWithOpts ¶
func NewDomainSSHComputeAccessWithOpts(opts SSHComputeAccessOpts) ComputeAccess
NewDomainSSHComputeAccessWithOpts creates a ComputeAccess with full SSH options.
type ComputeAccessInfo ¶
type ComputeAccessInfo struct {
Type string `json:"type"` // "local" or "ssh"
Path string `json:"path,omitempty"` // local worktree path or remote root
Host string `json:"host,omitempty"` // VM hostname (ssh)
User string `json:"user,omitempty"` // SSH user
ManagedWorktree bool `json:"managedWorktree,omitempty"` // true = created by previewctl
Metadata map[string]string `json:"metadata,omitempty"` // hook-provided metadata (e.g., "vm_zone", "gcp_project")
}
ComputeAccessInfo stores how to reach the compute location so environments can be reconnected across CLI invocations.
type ComputeHooks ¶
type ComputeHooks struct {
Create string `yaml:"create"`
Destroy string `yaml:"destroy"`
Outputs []string `yaml:"outputs,omitempty"`
SSH *SSHConfig `yaml:"ssh,omitempty"`
}
ComputeHooks defines lifecycle hooks for compute resources.
type ComputePort ¶
type ComputePort interface {
// Create sets up compute resources for an environment.
// branch is the target branch to use/create.
// baseBranch is the branch to create from (empty = use branch as-is).
Create(ctx context.Context, envName string, branch string, baseBranch string) (*ComputeResources, error)
// Start starts per-environment services (infra containers, etc).
Start(ctx context.Context, envName string, ports PortMap) error
// Stop stops services without destroying data or resources.
Stop(ctx context.Context, envName string) error
// Destroy tears down all compute resources.
Destroy(ctx context.Context, envName string) error
// IsRunning checks if environment compute resources are active.
IsRunning(ctx context.Context, envName string) (bool, error)
// DetectBranch returns the current git branch for a worktree path.
DetectBranch(ctx context.Context, worktreePath string) (string, error)
}
ComputePort manages the compute substrate for an environment. Local: git worktree + docker compose for per-env infrastructure. Preview: VM provisioning + full compose stack. Sandbox: isolated VM with network policies.
type ComputeResources ¶
type ComputeResources struct {
WorktreePath string `json:"worktreePath,omitempty"` // local mode
VMId string `json:"vmId,omitempty"` // preview/sandbox mode
ExternalIP string `json:"externalIp,omitempty"` // preview/sandbox mode
}
ComputeResources holds the result of creating compute resources for an environment.
type CoreResetOpts ¶
type CoreResetOpts struct {
// NoPropagate skips regenerating remote manifest/env/compose files and
// restarting dependent services after a successful reset. State is still
// updated with fresh provisioner outputs. Use this when you want to batch
// several resets and run a single SyncRemote at the end.
NoPropagate bool
}
CoreResetOpts configures the behavior of CoreReset.
type DestroyOptions ¶ added in v0.0.5
type DestroyOptions struct {
// Force continues past destroy-hook failures and always removes the
// environment from the state store. Useful when the create lifecycle
// failed and left state in an inconsistent shape that hooks can't handle.
Force bool
}
DestroyOptions configures Destroy behavior.
type DomainLocalComputeAccess ¶
type DomainLocalComputeAccess struct {
// contains filtered or unexported fields
}
func (*DomainLocalComputeAccess) Root ¶
func (l *DomainLocalComputeAccess) Root() string
func (*DomainLocalComputeAccess) SetStderr ¶
func (l *DomainLocalComputeAccess) SetStderr(w io.Writer)
func (*DomainLocalComputeAccess) VerboseExec ¶
type DomainSSHComputeAccess ¶
type DomainSSHComputeAccess struct {
// contains filtered or unexported fields
}
DomainSSHComputeAccess implements ComputeAccess over SSH. It uses the system ssh binary. Lives in domain to avoid circular imports.
Two connection modes:
- ProxyCommand mode: uses -o ProxyCommand=... (cloud-agnostic, no local SSH config needed)
- Direct mode: uses user@host (requires SSH config or direct access)
func (*DomainSSHComputeAccess) EnsureRootWritable ¶
func (s *DomainSSHComputeAccess) EnsureRootWritable(ctx context.Context) error
EnsureRootWritable reassigns ownership of the remote root tree to the current SSH user and relaxes its mode so subsequent host-side tools (pnpm, turbo, etc.) can create and chmod files regardless of who touched the tree last.
This exists because OS Login hands each actor a distinct UID; files created by one actor (CI service account, another human) are owned by that UID, and chmod(2) on those inodes fails for anyone but the owner even when the mode is world-writable. Call this at the top of any operation that writes into the remote root from a potentially different actor than the last writer.
Idempotent and best-effort: failures are swallowed (`|| true`) because the common case is already-correctly-owned, and we don't want to block the happy path on an unrelated permission blip.
func (*DomainSSHComputeAccess) Host ¶
func (s *DomainSSHComputeAccess) Host() string
Host returns the SSH host.
func (*DomainSSHComputeAccess) ProxyCommand ¶
func (s *DomainSSHComputeAccess) ProxyCommand() string
ProxyCommand returns the configured proxy command, if any.
func (*DomainSSHComputeAccess) Root ¶
func (s *DomainSSHComputeAccess) Root() string
func (*DomainSSHComputeAccess) SSHArgs ¶
func (s *DomainSSHComputeAccess) SSHArgs() []string
sshArgs returns the SSH arguments for building external SSH commands (e.g., for syscall.Exec).
func (*DomainSSHComputeAccess) SetStderr ¶
func (s *DomainSSHComputeAccess) SetStderr(w io.Writer)
func (*DomainSSHComputeAccess) User ¶
func (s *DomainSSHComputeAccess) User() string
User returns the SSH user.
func (*DomainSSHComputeAccess) VerboseExec ¶
type EnvironmentDetail ¶
type EnvironmentDetail struct {
Entry *EnvironmentEntry `json:"entry"`
InfraRunning bool `json:"infraRunning"`
}
EnvironmentDetail is an enriched view with live infrastructure checks.
type EnvironmentEntry ¶
type EnvironmentEntry struct {
Name string `json:"name"`
Mode EnvironmentMode `json:"mode"`
Branch string `json:"branch"`
Status EnvironmentStatus `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Ports PortMap `json:"ports"`
ProvisionerOutputs map[string]map[string]string `json:"provisionerOutputs"`
Compute *ComputeAccessInfo `json:"compute,omitempty"`
Env map[string]string `json:"env,omitempty"` // persistent key-value store for hooks
EnabledServices []string `json:"enabledServices,omitempty"` // services currently enabled (seeded from autostart, updated by service start/stop)
Steps map[string]*StepRecord `json:"steps,omitempty"`
AuditLog []AuditEntry `json:"auditLog,omitempty"`
}
EnvironmentEntry is a tracked environment persisted in state.
func (*EnvironmentEntry) AppendAudit ¶
func (e *EnvironmentEntry) AppendAudit(entry AuditEntry)
AppendAudit adds an audit log entry.
func (*EnvironmentEntry) DisableService ¶
func (e *EnvironmentEntry) DisableService(name string)
DisableService removes a service from the enabled set.
func (*EnvironmentEntry) EnableService ¶
func (e *EnvironmentEntry) EnableService(name string)
EnableService adds a service to the enabled set (idempotent).
func (*EnvironmentEntry) GetEnv ¶
func (e *EnvironmentEntry) GetEnv(key string) (string, bool)
GetEnv reads a value from the environment's persistent store.
func (*EnvironmentEntry) InvalidateStep ¶
func (e *EnvironmentEntry) InvalidateStep(name, reason string) bool
InvalidateStep marks a completed step as invalidated so it will re-run. Returns true if the step was actually invalidated (was previously completed).
func (*EnvironmentEntry) InvalidateStepsFrom ¶
func (e *EnvironmentEntry) InvalidateStepsFrom(stepName string, orderedSteps []string)
InvalidateStepsFrom removes checkpoint records for the named step and all steps that come after it in the given ordered step list.
func (*EnvironmentEntry) IsManagedWorktree ¶
func (e *EnvironmentEntry) IsManagedWorktree() bool
IsManagedWorktree returns whether the worktree was created by previewctl.
func (*EnvironmentEntry) IsServiceEnabled ¶
func (e *EnvironmentEntry) IsServiceEnabled(name string) bool
IsServiceEnabled returns true if the service is in the enabled set.
func (*EnvironmentEntry) SetEnv ¶
func (e *EnvironmentEntry) SetEnv(key, value string)
SetEnv sets a key-value pair in the environment's persistent store.
func (*EnvironmentEntry) SetStepRecord ¶
func (e *EnvironmentEntry) SetStepRecord(rec *StepRecord)
SetStepRecord records a step completion/failure.
func (*EnvironmentEntry) StepCompleted ¶
func (e *EnvironmentEntry) StepCompleted(name string) bool
StepCompleted returns true if the named step has a "completed" record.
func (*EnvironmentEntry) StepOutputs ¶
func (e *EnvironmentEntry) StepOutputs(name string) map[string]any
StepOutputs returns the outputs map for a completed step, or nil.
func (*EnvironmentEntry) WorktreePath ¶
func (e *EnvironmentEntry) WorktreePath() string
WorktreePath returns the worktree path from ComputeAccessInfo, or empty string.
type EnvironmentMode ¶
type EnvironmentMode string
EnvironmentMode represents the deployment mode of an environment.
const ( ModeLocal EnvironmentMode = "local" ModePreview EnvironmentMode = "preview" ModeSandbox EnvironmentMode = "sandbox" )
type EnvironmentStatus ¶
type EnvironmentStatus string
EnvironmentStatus represents the lifecycle status of an environment.
const ( StatusCreating EnvironmentStatus = "creating" StatusProvisioned EnvironmentStatus = "provisioned" StatusRunning EnvironmentStatus = "running" StatusStopped EnvironmentStatus = "stopped" StatusError EnvironmentStatus = "error" )
type InfraService ¶
type InfraService struct {
Name string
Image string
Port int
EnvVar string // e.g., "REDIS_PORT" (extracted from ${REDIS_PORT:-6379} patterns)
}
InfraService holds parsed infrastructure service information from a compose file.
type InfrastructureConfig ¶
type InfrastructureConfig struct {
ComposeFile string `yaml:"compose_file"`
}
InfrastructureConfig holds infrastructure configuration.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager orchestrates environment lifecycle by coordinating all ports.
func NewManager ¶
func NewManager(deps ManagerDeps) *Manager
NewManager creates a new Manager with the given dependencies.
func (*Manager) Attach ¶
func (m *Manager) Attach(ctx context.Context, envName string, worktreePath string) (*EnvironmentEntry, error)
Attach creates a preview environment using an existing worktree.
func (*Manager) BuildProvisionerStepOrder ¶
BuildProvisionerStepOrder returns the canonical step order for the provisioner phase.
func (*Manager) BuildRunnerStepOrder ¶
BuildRunnerStepOrder returns the canonical step order for the runner phase.
func (*Manager) BuildSSHComputeAccess ¶
func (m *Manager) BuildSSHComputeAccess(entry *EnvironmentEntry) ComputeAccess
buildSSHComputeAccess constructs SSH compute access from an environment entry. It resolves the SSH config template from the project config using store values, or falls back to stored proxy_command in metadata, or direct host connection.
func (*Manager) DryRunStep ¶
DryRunStep shows a diff of what a step would change. For generation steps, compares the current file on the VM with what would be generated. For other steps, describes what would happen.
func (*Manager) GetEnvironment ¶
GetEnvironment returns a single environment entry from state, or nil if not found.
func (*Manager) Init ¶
func (m *Manager) Init(ctx context.Context, envName, branch, baseBranch string) (*EnvironmentEntry, error)
Init creates a new environment end-to-end: provisions then runs. branch is the target branch. baseBranch is the branch to create from (empty = use branch as-is).
func (*Manager) PrintStep ¶
PrintStep generates and returns the full content for a step without executing it.
func (*Manager) Provision ¶
func (m *Manager) Provision(ctx context.Context, envName, branch, baseBranch, fromStep string) (*EnvironmentEntry, error)
Provision runs the provisioner phase only. Does NOT run the runner. fromStep invalidates that step and all subsequent steps, forcing re-execution.
func (*Manager) ProvisionAttach ¶
func (m *Manager) ProvisionAttach(ctx context.Context, envName, worktreePath, fromStep string) (*EnvironmentEntry, error)
ProvisionAttach runs the provisioner phase on an existing worktree.
func (*Manager) Reconcile ¶
func (m *Manager) Reconcile(ctx context.Context, envName string, dryRun ...bool) (*ReconcileReport, error)
Reconcile verifies all runner steps and re-executes any whose side effects are missing. Hook-owned steps are skipped since previewctl can't verify user-defined hooks.
Progress is reported via the ProgressReporter for each step:
- StepStarted: "Verifying <step>..."
- StepCompleted: verification passed or heal succeeded
- StepFailed: heal failed
- StepSkipped: hook-owned or never completed
func (*Manager) Run ¶
Run reads a manifest and executes the runner phase only. Stateless — does not persist state.
func (*Manager) RunCoreHook ¶
func (*Manager) RunStep ¶
RunStep executes a single runner-phase step in isolation. Loads the environment from state, reconstructs compute access, reads the manifest, and runs only the specified step (always executes, ignoring cache).
func (*Manager) RunSteps ¶
RunSteps executes a sequence of runner steps in order, reusing a single SSH connection and step registry. All steps are forced (cache bypassed).
func (*Manager) SaveEnvironment ¶
func (m *Manager) SaveEnvironment(ctx context.Context, envName string, entry *EnvironmentEntry) error
SaveEnvironment persists an environment entry to state.
func (*Manager) SetNoCache ¶
SetNoCache sets whether checkpoint caching is disabled.
func (*Manager) SyncRemote ¶
SyncRemote regenerates every VM-side artifact we know how to regenerate (manifest, env files, compose, nginx) from current state and restarts every currently-enabled service. Useful as a recovery tool when the VM has drifted from state (e.g., a propagation failed mid-flight after a reset).
No-op for local environments. Unlike `refresh`, SyncRemote does not rerun runner_before / build_services / runner_deploy — it only reconciles what could have changed as a result of state mutations (creds, store values, port allocations, enabled-service set).
type ManagerDeps ¶
type ManagerDeps struct {
Compute ComputePort
Networking NetworkingPort
State StatePort
Progress ProgressReporter
Config *ProjectConfig
ProjectRoot string
NoCache bool
}
ManagerDeps holds the dependencies for creating a Manager.
type Manifest ¶
type Manifest struct {
Version int `json:"version"`
EnvName string `json:"env_name"`
ProjectName string `json:"project_name"`
Branch string `json:"branch"`
Mode string `json:"mode"`
Ports PortMap `json:"ports"`
ProvisionerOutputs map[string]map[string]string `json:"provisioner_outputs,omitempty"`
Services map[string]ManifestService `json:"services,omitempty"`
Infrastructure *ManifestInfrastructure `json:"infrastructure,omitempty"`
EnabledServices []string `json:"enabled_services,omitempty"`
}
Manifest is the single source of truth written to the compute location. It contains fully resolved values — no template variables.
func BuildManifest ¶
func BuildManifest( cfg *ProjectConfig, envName, branch, mode string, ports PortMap, provisionerOutputs map[string]map[string]string, store map[string]string, ) (*Manifest, error)
BuildManifest resolves all template variables and builds a complete manifest.
func ReadManifest ¶
ReadManifest decodes a manifest from the given reader.
type ManifestInfrastructure ¶
type ManifestInfrastructure struct {
ComposeFile string `json:"compose_file"`
}
ManifestInfrastructure describes infrastructure configuration.
type ManifestService ¶
type ManifestService struct {
Path string `json:"path"`
EnvFile string `json:"env_file"`
Env map[string]string `json:"env,omitempty"`
}
ManifestService describes a service with fully resolved env vars.
type NetworkingPort ¶
type NetworkingPort interface {
// AllocatePorts returns deterministic port assignments for all services and infrastructure.
AllocatePorts(envName string) (PortMap, error)
// GetServiceURL returns the URL to reach a named service in the environment.
GetServiceURL(envName string, service string) (string, error)
}
NetworkingPort handles port allocation and service URL resolution. Local: deterministic offset from base ports. Preview: reverse proxy with subdomain routing (future).
type NoopReporter ¶
type NoopReporter struct{}
NoopReporter is a ProgressReporter that discards all events.
func (NoopReporter) OnStep ¶
func (NoopReporter) OnStep(StepEvent)
func (NoopReporter) StderrWriter ¶
func (NoopReporter) StderrWriter() io.Writer
type PortMap ¶
PortMap maps service names to allocated ports.
func AllocatePortBlock ¶
AllocatePortBlock selects a block of ports for an environment and assigns one port per service name. Ports are checked for availability. Returns a PortMap with service names mapped to allocated ports.
type ProgressReporter ¶
type ProgressReporter interface {
OnStep(event StepEvent)
// StderrWriter returns a writer for hook stderr output.
// The reporter may indent or buffer this output for display.
StderrWriter() io.Writer
}
ProgressReporter receives lifecycle step events from the manager. Inbound adapters implement this to render progress (CLI spinners, SSE, etc).
type ProjectConfig ¶
type ProjectConfig struct {
Version int `yaml:"version"`
Name string `yaml:"name"`
Provisioner ProvisionerConfig `yaml:"provisioner"`
Infrastructure *InfrastructureConfig `yaml:"infrastructure,omitempty"`
Services map[string]ServiceConfig `yaml:"services"`
Runner *RunnerConfig `yaml:"runner,omitempty"`
// Mode is the deployment mode (e.g., "local"). Set at load time, not from YAML.
Mode string `yaml:"-"`
// InfraServices is populated by parsing the compose file referenced in
// Infrastructure.ComposeFile. It is not read from YAML directly.
InfraServices map[string]InfraService `yaml:"-"`
}
ProjectConfig is the top-level previewctl.yaml configuration.
func LoadConfig ¶
func LoadConfig(path string) (*ProjectConfig, error)
LoadConfig reads and parses a previewctl.yaml file.
func LoadConfigWithOverlay ¶
func LoadConfigWithOverlay(basePath, mode string) (*ProjectConfig, error)
LoadConfigWithOverlay loads a base config and merges a mode-specific overlay if present.
func ParseConfig ¶
func ParseConfig(data []byte) (*ProjectConfig, error)
ParseConfig parses YAML bytes into a ProjectConfig.
func (*ProjectConfig) ServiceNames ¶
func (c *ProjectConfig) ServiceNames() []string
ServiceNames returns a sorted list of all service and infrastructure names that need port assignments.
type ProvisionerConfig ¶
type ProvisionerConfig struct {
Before string `yaml:"before,omitempty"`
After string `yaml:"after,omitempty"`
Compute *ComputeHooks `yaml:"compute,omitempty"`
Services map[string]ProvisionerServiceConfig `yaml:"services,omitempty"`
}
ProvisionerConfig holds managed provisioner services with hook-driven lifecycle.
type ProvisionerServiceConfig ¶
type ProvisionerServiceConfig struct {
Outputs []string `yaml:"outputs,omitempty"`
Init string `yaml:"init,omitempty"`
Seed string `yaml:"seed,omitempty"`
Reset string `yaml:"reset,omitempty"`
Destroy string `yaml:"destroy,omitempty"`
}
ProvisionerServiceConfig defines a provisioner service managed by hooks.
type ProxyConfig ¶
type ProxyConfig struct {
Enabled *bool `yaml:"enabled,omitempty"` // defaults to true if omitted
Domain string `yaml:"domain"` // e.g., "preview.airgoods.com"
Type string `yaml:"type,omitempty"` // "nginx" (default). Future: "traefik", "caddy"
}
ProxyConfig defines the reverse proxy that sits in front of preview services.
func (*ProxyConfig) IsEnabled ¶
func (p *ProxyConfig) IsEnabled() bool
IsEnabled returns whether the proxy is enabled (defaults to true).
func (*ProxyConfig) ResolvedType ¶
func (p *ProxyConfig) ResolvedType() string
ResolvedType returns the proxy type, defaulting to "nginx".
type ReconcileReport ¶
type ReconcileReport struct {
Results []ReconcileResult
Healed int
Failed int
OK int
Skipped int
NotRun int
}
ReconcileReport is the full outcome of a reconcile run.
type ReconcileResult ¶
type ReconcileResult struct {
Step string
Action string // "ok", "healed", "failed", "skipped", "not_run"
Message string
DurationMs int64
}
ReconcileResult describes the outcome of reconciling a single step.
type RunnerConfig ¶
type RunnerConfig struct {
Before CommandHook `yaml:"before,omitempty"`
// Build, when set, runs once during build_services in place of the
// per-service Build loop. Intended for monorepo tools (turborepo, nx,
// lerna) where one bulk command builds everything more efficiently
// than N filtered invocations.
Build CommandHook `yaml:"build,omitempty"`
Deploy CommandHook `yaml:"deploy,omitempty"`
Destroy CommandHook `yaml:"destroy,omitempty"`
After CommandHook `yaml:"after,omitempty"`
Compose *ComposeConfig `yaml:"compose,omitempty"`
}
RunnerConfig holds runner lifecycle hooks.
type SSHComputeAccessOpts ¶
type SSHComputeAccessOpts struct {
Host string // SSH host (direct mode) or logical hostname (proxy mode)
User string
Root string
ProxyCommand string // when set, uses -o ProxyCommand=... instead of relying on SSH config
IdentityFile string // path to SSH private key (optional)
}
SSHComputeAccessOpts configures SSH compute access creation.
type SSHConfig ¶
type SSHConfig struct {
// ProxyCommand is the SSH ProxyCommand used to tunnel into the VM.
// Example: "gcloud compute start-iap-tunnel {{store.VM_NAME}} %p --listen-on-stdin --zone={{store.GCP_ZONE}} --project={{store.GCP_PROJECT}}"
ProxyCommand string `yaml:"proxy_command"`
// User is the SSH username. Example: "{{store.SSH_USER}}"
User string `yaml:"user"`
// UserCommand is a shell command that resolves the SSH username dynamically.
// Useful when different users (CI vs human) SSH into the same VM.
// Example: "gcloud compute os-login describe-profile --format='value(posixAccounts[0].username)'"
// Takes precedence over User when set.
UserCommand string `yaml:"user_command"`
// IdentityFile is the path to the SSH private key. Supports ~ expansion.
// Example: "~/.ssh/google_compute_engine"
IdentityFile string `yaml:"identity_file"`
// Root is the remote working directory. Defaults to "/app".
Root string `yaml:"root,omitempty"`
}
SSHConfig defines how previewctl connects to remote compute via SSH. All fields support {{store.KEY}} template resolution.
type ServiceConfig ¶
type ServiceConfig struct {
Path string `yaml:"path"`
Port int `yaml:"port,omitempty"` // fixed port — skips the port allocator when set
Command string `yaml:"command,omitempty"`
DependsOn []string `yaml:"depends_on,omitempty"`
Env map[string]string `yaml:"env,omitempty"`
EnvFile string `yaml:"env_file,omitempty"` // relative to path, defaults to ".env.local"
Build string `yaml:"build,omitempty"` // build command (run on host before container starts)
Start string `yaml:"start,omitempty"` // start command (run inside container). Required for compose generation.
Proxy []ServiceProxy `yaml:"proxy,omitempty"` // optional reverse proxy rules for nginx
}
ServiceConfig defines an application service.
func (ServiceConfig) ResolvedEnvFile ¶
func (s ServiceConfig) ResolvedEnvFile() string
ResolvedEnvFile returns the env file path relative to the service path. Defaults to ".env.local" if not configured.
type ServiceProxy ¶
type ServiceProxy struct {
Path string `yaml:"path"` // source path the browser sends, e.g., "/api" or "/iapi"
TargetPath string `yaml:"target_path,omitempty"` // path rewritten to on the target service. Defaults to Path if omitted.
To ServiceProxyTarget `yaml:"to"`
}
ServiceProxy defines a reverse proxy rule on a service's subdomain. When configured, nginx generates a location block that proxies the given path to the target service's port (same-origin for IAP cookie compatibility).
func (*ServiceProxy) ResolvedTargetPath ¶
func (p *ServiceProxy) ResolvedTargetPath() string
ResolvedTargetPath returns the target path, defaulting to Path if not set.
type ServiceProxyTarget ¶
type ServiceProxyTarget struct {
Service string `yaml:"service"` // target service name, resolved to its port at generation time
}
ServiceProxyTarget identifies the target service for a proxy rule.
type State ¶
type State struct {
Version int `json:"version"`
Environments map[string]*EnvironmentEntry `json:"environments"`
}
State is the top-level persisted state.
type StatePort ¶
type StatePort interface {
// Load returns the full state.
Load(ctx context.Context) (*State, error)
// Save persists the full state.
Save(ctx context.Context, state *State) error
// GetEnvironment returns a single environment entry, or nil if not found.
GetEnvironment(ctx context.Context, name string) (*EnvironmentEntry, error)
// SetEnvironment creates or updates an environment entry.
SetEnvironment(ctx context.Context, name string, entry *EnvironmentEntry) error
// RemoveEnvironment deletes an environment entry.
RemoveEnvironment(ctx context.Context, name string) error
}
StatePort persists previewctl state. File-based for POC; interface accommodates Postgres/etcd later.
type StepEvent ¶
type StepEvent struct {
Step string // e.g. "allocate_ports", "create_worktree", "create_database"
Status StepStatus
Message string // human-readable detail
Error error // non-nil when Status == StepFailed
}
StepEvent is emitted by the manager at each lifecycle transition.
type StepOpts ¶
type StepOpts struct {
Name string
StartMsg string
CompleteMsg *string
Fn func() error
Verify VerifyFunc // nil = pure, skip on checkpoint alone
Outputs func() map[string]any // capture outputs after success
AllowCache *bool // nil = true; false = always execute
}
StepOpts configures step execution behavior.
type StepRecord ¶
type StepRecord struct {
Name string `json:"name"`
Status StepRecordStatus `json:"status"`
StartedAt time.Time `json:"startedAt"`
FinishedAt time.Time `json:"finishedAt"`
DurationMs int64 `json:"durationMs"`
Machine string `json:"machine"`
Error string `json:"error,omitempty"`
Outputs map[string]any `json:"outputs,omitempty"`
}
StepRecord is the checkpoint persisted for a single step execution.
type StepRecordStatus ¶
type StepRecordStatus string
StepRecordStatus represents the persisted outcome of a step.
const ( StepRecordCompleted StepRecordStatus = "completed" StepRecordFailed StepRecordStatus = "failed" StepRecordInvalidated StepRecordStatus = "invalidated" )
type StepStatus ¶
type StepStatus string
StepStatus represents the status of a lifecycle step.
const ( StepStarted StepStatus = "started" StepCompleted StepStatus = "completed" StepFailed StepStatus = "failed" StepSkipped StepStatus = "skipped" StepStreaming StepStatus = "streaming" // stop spinner, hook will stream its own output )
type TemplateContext ¶
type TemplateContext struct {
ServicePorts PortMap
InfraPorts PortMap
ProvisionerOutputs map[string]map[string]string
CurrentService string // set per-service during rendering, enables {{self.port}}
EnvName string // environment name, enables {{env.name}}
Store map[string]string // persistent key-value store, enables {{store.KEY}}
ProxyDomain string // proxy domain (e.g., "preview.airgoods.com"), enables {{proxy.*}}
}
TemplateContext holds the values available for template substitution.
type ValidationError ¶
type ValidationError struct {
Errors []string
}
ValidationError collects multiple validation issues.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
type VerifyFunc ¶
VerifyFunc checks that a previously-completed step's side effects still hold.