Documentation
¶
Overview ¶
Package actions provides the action handler system for mooncake.
Overview ¶
The actions package defines a standard interface (Handler) that all action implementations must follow, along with a registry for discovering handlers at runtime.
Architecture ¶
Actions are implemented as packages under internal/actions/. Each action package provides a Handler implementation that is registered globally on import via an init() function.
The executor looks up handlers from the registry based on the action type determined from the step configuration.
Backward Compatibility ¶
This new system is designed to work alongside the existing action implementations. The Step struct retains all existing action fields (Shell, File, Template, etc.), and actions are migrated incrementally to the new Handler interface.
Creating a New Action ¶
To create a new action handler:
1. Create a package under internal/actions/ (e.g., internal/actions/notify)
2. Implement the Handler interface:
type Handler struct{}
func (h *Handler) Metadata() actions.ActionMetadata {
return actions.ActionMetadata{
Name: "notify",
Description: "Send notifications",
Category: actions.CategorySystem,
SupportsDryRun: true,
}
}
func (h *Handler) Validate(step *config.Step) error {
// Validate step.Notify config
return nil
}
func (h *Handler) Execute(ctx *executor.ExecutionContext, step *config.Step) (*executor.Result, error) {
// Implement action logic
return &executor.Result{Changed: true}, nil
}
func (h *Handler) DryRun(ctx *executor.ExecutionContext, step *config.Step) error {
ctx.Logger.Infof(" [DRY-RUN] Would send notification")
return nil
}
3. Register the handler in init():
func init() {
actions.Register(&Handler{})
}
4. Import the package in the executor to ensure registration:
import _ "github.com/alehatsman/mooncake/internal/actions/notify"
Migration Strategy ¶
Existing actions are being migrated incrementally:
- Phase 1: Create Handler implementations for simple actions (print, vars)
- Phase 2: Migrate complex actions (shell, file, template)
- Phase 3: Migrate specialized actions (service, assert, preset)
- Phase 4: Remove legacy code paths
During migration, both old and new implementations coexist. The executor checks if a handler is registered and prefers it, falling back to legacy implementations for non-migrated actions.
Package actions provides the handler interface and registry for mooncake actions.
The actions package defines a standard interface that all action handlers must implement, along with a registry system for discovering and dispatching to handlers at runtime.
To create a new action handler:
- Create a new package under internal/actions (e.g., internal/actions/notify)
- Implement the Handler interface
- Register your handler in an init() function
- The handler will be automatically available for use
Example:
package notify
import "github.com/alehatsman/mooncake/internal/actions"
type Handler struct{}
func init() {
actions.Register(&Handler{})
}
func (h *Handler) Metadata() actions.ActionMetadata {
return actions.ActionMetadata{
Name: "notify",
Description: "Send notifications",
Category: actions.CategorySystem,
}
}
// ... implement other interface methods
Index ¶
- func Count() int
- func GetActionHint(actionName string, missingField string) string
- func GetFieldExample(actionName, fieldName string) string
- func Has(actionType string) bool
- func Register(handler Handler)
- type Action
- type ActionCategory
- type ActionDefinition
- type ActionMetadata
- type Context
- type Effect
- type Handler
- type HandlerFunc
- func (h *HandlerFunc) DryRun(ctx Context, step *config.Step) error
- func (h *HandlerFunc) Execute(ctx Context, step *config.Step) (Result, error)
- func (h *HandlerFunc) Metadata() ActionMetadata
- func (h *HandlerFunc) Run(ctx Context, step *config.Step) (Result, error)
- func (h *HandlerFunc) Validate(step *config.Step) error
- type Mode
- type Performer
- type PerformerOpts
- type PropertySchema
- type Registry
- type Result
- type Runner
- type Schema
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func GetActionHint ¶
GetActionHint generates a helpful hint for an action based on the schema. It includes: - Action description - Required parameters with descriptions - Optional parameters with descriptions - Examples where available
func GetFieldExample ¶
GetFieldExample returns an example value for a field based on schema
Types ¶
type Action ¶
type Action string
Action names a primitive an Effect represents. Used in plan output, logging, and event emission. See Performer.
type ActionCategory ¶
type ActionCategory string
ActionCategory groups related actions by their primary function.
const ( // CategoryCommand represents actions that execute commands (shell, command) CategoryCommand ActionCategory = "command" // CategoryFile represents actions that manipulate files (file, template, copy, download) CategoryFile ActionCategory = "file" // CategorySystem represents system-level actions (service, assert, preset) CategorySystem ActionCategory = "system" // CategoryData represents data manipulation actions (vars, include_vars) CategoryData ActionCategory = "data" // CategoryNetwork represents network-related actions (download, http requests) CategoryNetwork ActionCategory = "network" // CategoryOutput represents output/display actions (print) CategoryOutput ActionCategory = "output" )
type ActionDefinition ¶
type ActionDefinition struct {
Type string `json:"type"`
Description string `json:"description"`
Properties map[string]PropertySchema `json:"properties"`
Required []string `json:"required"`
}
ActionDefinition represents an action's schema definition
type ActionMetadata ¶
type ActionMetadata struct {
// Name is the action name as it appears in YAML (e.g., "shell", "file", "notify")
Name string
// Description is a human-readable description of what this action does
Description string
// Category groups related actions (command, file, system, etc.)
Category ActionCategory
// SupportsDryRun indicates whether this action can be executed in dry-run mode
SupportsDryRun bool
// SupportsBecome indicates whether this action supports privilege escalation (sudo)
SupportsBecome bool
// EmitsEvents lists the event types this action emits (e.g., "file.created", "notify.sent")
EmitsEvents []string
// Version is the action implementation version (semantic versioning)
Version string
// SupportedPlatforms lists the operating systems this action supports.
// Valid values: "linux", "darwin", "windows", "freebsd", "openbsd", "netbsd", "dragonfly", "solaris", "aix"
// Empty list means all platforms are supported
SupportedPlatforms []string
// RequiresSudo indicates whether this action typically requires elevated privileges.
// This is informational - actual privilege requirements may vary based on the operation.
RequiresSudo bool
// ImplementsCheck indicates whether this action implements idempotency checks.
// Actions with idempotency checks verify current state before making changes.
ImplementsCheck bool
}
ActionMetadata describes an action type and its capabilities.
type Context ¶
type Context interface {
// GetTemplate returns the template renderer for processing Jinja2-like templates.
//
// Use this to render:
// - Path strings with variables: "{{ home }}/{{ item }}"
// - Content with logic: "{% if os == 'linux' %}...{% endif %}"
// - Filters: "{{ path | expanduser }}"
//
// The renderer has access to all variables in scope (step vars, globals, facts).
GetTemplate() template.Renderer
// GetEvaluator returns the expression evaluator for conditions.
//
// Use this to evaluate:
// - when: "os == 'linux' && arch == 'amd64'"
// - changed_when: "result.rc == 0 and 'changed' in result.stdout"
// - failed_when: "result.rc != 0 and result.rc != 5"
//
// Returns interface{} which should be cast to bool for conditions.
GetEvaluator() expression.Evaluator
// GetLogger returns the logger for handler output.
//
// Use levels appropriately:
// - Infof: User-visible progress ("Installing package nginx")
// - Debugf: Detailed info ("Command: apt install nginx")
// - Warnf: Non-fatal issues ("File already exists, skipping")
// - Errorf: Failures ("Failed to create directory: permission denied")
//
// Output is formatted for TUI or text mode automatically.
GetLogger() logger.Logger
// GetVariables returns all variables in the current scope.
//
// Includes:
// - Step-level vars (defined in step.Vars)
// - Global vars (from vars actions)
// - System facts (os, arch, cpu_cores, memory_total_mb, etc.)
// - Registered results (from register: field on previous steps)
// - Loop context (item, item_index when in with_items/with_filetree)
//
// Keys are strings, values are interface{} (string, int, bool, []interface{}, map[string]interface{}).
GetVariables() map[string]interface{}
// GetEventPublisher returns the event publisher for observability.
//
// Emit events for:
// - State changes (EventFileCreated, EventServiceStarted)
// - Progress tracking (custom events for long operations)
// - Artifact generation (paths to created files)
//
// Events are consumed by:
// - Artifact collector (for rollback support)
// - External observers (CI/CD integrations)
// - Audit logs
GetEventPublisher() events.Publisher
// Mode reports the dispatch mode for this context. Handlers
// implementing Runner consult this to decide whether to perform side
// effects (ModeExecute) or only inspect state (ModePlan).
Mode() Mode
// Effects returns a Performer wired to this context. Handlers
// implementing Runner should call Performer methods instead of os.*
// directly so that ModePlan vs ModeExecute is decided in one place.
// The returned Performer is cheap to construct and may be called
// multiple times.
Effects() Performer
// GetCurrentStepID returns the unique ID of the currently executing step.
//
// Format: "step-{global_step_number}"
//
// Use this when:
// - Emitting events (so they're associated with the step)
// - Creating temporary files (include step ID to avoid conflicts)
// - Logging (though step ID is usually added automatically)
GetCurrentStepID() string
}
Context provides the execution environment for action handlers.
Context is the primary interface through which handlers interact with the mooncake runtime. It provides access to:
- Template rendering (Jinja2-like syntax with variables and filters)
- Expression evaluation (when/changed_when/failed_when conditions)
- Logging (structured output to TUI or text)
- Variables (step vars, global vars, facts, registered results)
- Event publishing (for observability and artifacts)
- Execution mode (dry-run vs actual execution)
This interface avoids circular imports between actions and executor packages.
Example usage in a handler:
func (h *Handler) Execute(ctx actions.Context, step *config.Step) (actions.Result, error) {
// Render template strings
path, err := ctx.GetTemplate().RenderString(step.File.Path, ctx.GetVariables())
// Log progress
ctx.GetLogger().Infof("Creating file at %s", path)
// Emit events for observability
ctx.GetEventPublisher().Publish(events.Event{
Type: events.EventFileCreated,
Data: events.FileOperationData{Path: path},
})
// Return result
result := executor.NewResult()
result.SetChanged(true)
return result, nil
}
type Effect ¶
type Effect struct {
Action Action
Path string
Reason string
Performed bool
WouldChange bool
AlreadyOk bool
Err error
Detail any
}
Effect is the result of a Performer call.
Field semantics by mode:
ModeExecute:
Performed true if a side effect actually happened
AlreadyOk true if the target was already in desired state (no-op)
WouldChange unused (false)
Err any error from the underlying syscall / command
ModePlan:
Performed false (no side effects in plan mode)
AlreadyOk true if the target is already in desired state
WouldChange true if applying ModeExecute would change state
Err any error encountered while *inspecting* state
Performed and WouldChange are mutually exclusive; AlreadyOk is set when the operation would be a no-op in either mode.
type Handler ¶
type Handler interface {
// Metadata returns metadata describing this action type.
Metadata() ActionMetadata
// Validate checks if the step configuration is valid for this action.
// Called before Run to fail fast on configuration errors.
Validate(step *config.Step) error
// Run executes the action when ctx.Mode() is ModeExecute, or
// inspects state and returns a prediction when ctx.Mode() is
// ModePlan. Implementations:
//
// - emit appropriate events via ctx.GetEventPublisher() (execute mode)
// - render templates via ctx.GetTemplate()
// - use ctx.GetLogger() for logging
// - return Result with Changed=true (execute) or
// WouldChange=true (plan) when state would change
// - route filesystem mutations through ctx.Effects() so that
// plan and execute modes share the same predicates
//
// Returns an error only on unrecoverable failure.
Run(ctx Context, step *config.Step) (Result, error)
}
Handler defines the interface that all action handlers must implement.
A handler is responsible for:
- Validating action configuration
- Executing the action
- Handling dry-run mode
- Emitting appropriate events
- Returning results
Handlers should be stateless - all execution state is passed via ExecutionContext.
Spec 16 collapsed the previous Execute / DryRun / Check trio into a single Run(ctx, step) method (the Runner interface). The legacy methods may still exist on concrete handler types — they are no longer part of the contract.
type HandlerFunc ¶
type HandlerFunc struct {
// contains filtered or unexported fields
}
HandlerFunc is a function type that implements Handler for simple actions. This allows creating handlers without defining a new type.
func (*HandlerFunc) Metadata ¶
func (h *HandlerFunc) Metadata() ActionMetadata
func (*HandlerFunc) Run ¶
Run satisfies the Spec 16 Handler contract. In ModePlan it reports "not checkable" by default; in ModeExecute it delegates to the underlying execute function. HandlerFunc users wanting accurate plan-mode behavior should construct a typed Handler with its own Run method instead.
type Mode ¶
type Mode int
Mode is the high-level dispatch mode for an action handler.
Spec 16 (docs-working/spec-16-unify-dryrun-execute.md) collapses the previous parallel non-mutating paths (Handler.DryRun and Handler.Check) into a single ModePlan. ModeExecute is the normal mutating path.
During the migration the legacy ExecutionContext.DryRun and CheckMode bools remain the source of truth; ExecutionContext.Mode() derives from them so new code can be written against Mode and migrated incrementally.
type Performer ¶
type Performer interface {
// Mode reports the mode the Performer will use for the next call.
Mode() Mode
// Mkdir ensures a directory exists at path with the given mode.
// Parents are created as needed.
Mkdir(path string, mode os.FileMode, opts PerformerOpts) Effect
// WriteFile writes content to path with the given mode, creating
// parent directories as needed. Idempotent: if existing content
// already matches, AlreadyOk is set.
WriteFile(path string, content []byte, mode os.FileMode, opts PerformerOpts) Effect
// Symlink creates a symbolic link at path pointing to target. If a
// link already exists with the correct target, AlreadyOk is set.
Symlink(target, path string, opts PerformerOpts) Effect
// Hardlink creates a hard link at path pointing to target.
Hardlink(target, path string, opts PerformerOpts) Effect
// Touch updates mtime, creating an empty file with the given mode
// if absent. Not idempotent: WouldChange is always true in ModePlan.
Touch(path string, mode os.FileMode, opts PerformerOpts) Effect
// Remove deletes path. If recursive is true, removes directories
// and their contents.
Remove(path string, recursive bool, opts PerformerOpts) Effect
// Chmod sets the permission bits on path.
Chmod(path string, mode os.FileMode, opts PerformerOpts) Effect
// Chown sets owner and group on path. Empty owner or group leaves
// that side unchanged. Owner/group may be names or numeric IDs.
Chown(path, owner, group string, opts PerformerOpts) Effect
}
Performer executes filesystem and command primitives in either ModeExecute (real side effects) or ModePlan (state inspection only, returning a prediction).
Spec 16 introduces Performer so that mutating primitives have exactly one site that decides what each operation means in each mode. Handlers should call Performer methods (via ctx-supplied accessor) instead of calling os.* directly.
type PerformerOpts ¶
PerformerOpts carries optional flags that apply to most Performer calls.
Today the only supported option is Become — when set, the underlying primitive runs via sudo (with the password supplied to the Performer implementation). Handlers read Step.Become and pass it through; they should not shell out to sudo themselves.
type PropertySchema ¶
type PropertySchema struct {
Type string `json:"type"`
Description string `json:"description"`
Pattern string `json:"pattern,omitempty"`
}
PropertySchema represents a property's schema
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry manages registered action handlers through a thread-safe map.
The registry pattern enables:
- Dynamic action discovery - handlers register themselves via init()
- Loose coupling - executor doesn't import all action packages
- Extensibility - new actions added without changing executor
- Thread safety - concurrent access from multiple goroutines
Registration flow:
- Action package imports actions: import "github.com/.../internal/actions"
- Action package defines handler: type Handler struct{}
- Action package registers in init(): func init() { actions.Register(&Handler{}) }
- Main imports register package: import _ "github.com/.../internal/register"
- Register package imports all actions: import _ ".../actions/shell"
- All handlers automatically registered before main() runs
Lookup flow:
- Executor determines action type from step: actionType := step.DetermineActionType()
- Executor queries registry: handler, ok := actions.Get(actionType)
- If found, executor calls: handler.Validate(step), handler.Execute(ctx, step)
- If not found, executor falls back to legacy implementation
This avoids circular imports because:
- actions package defines Handler interface
- action implementations (shell, file, etc.) import actions
- executor imports actions but NOT action implementations
- register package imports action implementations (triggers init())
- cmd imports register (triggers all registrations)
func (*Registry) Get ¶
Get retrieves a handler by action type name. Returns the handler and true if found, nil and false otherwise.
func (*Registry) List ¶
func (r *Registry) List() []ActionMetadata
List returns metadata for all registered handlers. Useful for introspection and documentation generation.
type Result ¶
type Result interface {
// SetChanged marks whether this action modified system state.
//
// Set to true if the action:
// - Created/modified/deleted files or directories
// - Started/stopped/restarted services
// - Installed/removed packages
// - Executed commands that changed state
//
// Set to false if the action:
// - Found state already as desired (idempotent)
// - Only read/queried information
// - Failed before making changes
//
// Changed count is reported in run summary and used for idempotency tracking.
SetChanged(changed bool)
// SetStdout captures standard output from the action.
//
// Used primarily by shell/command actions. Output is:
// - Available in registered results as {{ result.stdout }}
// - Shown in TUI output view
// - Logged to artifacts
// - Used in changed_when/failed_when expressions
SetStdout(stdout string)
// SetStderr captures standard error from the action.
//
// Used primarily by shell/command actions. Error output is:
// - Available in registered results as {{ result.stderr }}
// - Shown in TUI output view (usually in red)
// - Logged to artifacts
// - Used in changed_when/failed_when expressions
SetStderr(stderr string)
// SetFailed marks the result as failed.
//
// Usually you should return an error instead of calling this. Use this when:
// - The action completed but didn't achieve desired state
// - failed_when expression evaluated to true
// - Assertion failed (assert action)
//
// Failed steps:
// - Increment failure count in run summary
// - Stop execution (unless ignore_errors: true)
// - Are highlighted in TUI
SetFailed(failed bool)
// SetData attaches custom data to the result.
//
// Data becomes available when the result is registered via register: field.
//
// Example:
//
// result.SetData(map[string]interface{}{
// "checksum": "sha256:abc123",
// "size_bytes": 1024,
// "format": "json",
// })
//
// Then in subsequent steps:
// when: myfile.checksum == "sha256:abc123"
// shell: echo "File size: {{ myfile.size_bytes }}"
//
// Keys should be snake_case. Values should be JSON-serializable.
SetData(data map[string]interface{})
// RegisterTo registers this result to the variables map.
//
// Called automatically by the executor when a step has register: field.
// Creates a map in variables with:
// - changed: bool
// - failed: bool
// - stdout: string (if set)
// - stderr: string (if set)
// - rc: int (if applicable)
// - ...custom data from SetData()
//
// Handlers typically don't call this directly.
RegisterTo(variables map[string]interface{}, name string)
}
Result represents the outcome of an action execution.
Results track:
- Whether changes were made (for idempotency reporting)
- Output data (stdout/stderr from commands)
- Success/failure status
- Custom data (for result registration)
Results can be registered to variables for use in subsequent steps via the register: field.
Example:
result := executor.NewResult()
result.SetChanged(true) // File was created/modified
result.SetData(map[string]interface{}{
"path": "/etc/myapp/config.yml",
"size": 1024,
"checksum": "sha256:abc123...",
})
// If step has register: myfile, data is available as:
// {{ myfile.changed }} = true
// {{ myfile.path }} = "/etc/myapp/config.yml"
This interface avoids circular imports between actions and executor packages.
type Runner ¶
Runner is the unified handler entry point introduced by Spec 16. The Handler interface now embeds Run as a required method; Runner remains as a named type alias for clarity in call sites that want to express "the Run capability" specifically.
type Schema ¶
type Schema struct {
Definitions map[string]ActionDefinition `json:"definitions"`
}
Schema represents the JSON schema structure
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package artifact_capture implements the artifact.capture action handler.
|
Package artifact_capture implements the artifact.capture action handler. |
|
Package artifact_validate implements the artifact.validate action handler.
|
Package artifact_validate implements the artifact.validate action handler. |
|
Package assert implements the assert action handler.
|
Package assert implements the assert action handler. |
|
Package command implements the command action handler.
|
Package command implements the command action handler. |
|
Package copy implements the copy action handler.
|
Package copy implements the copy action handler. |
|
Package download implements the download action handler.
|
Package download implements the download action handler. |
|
Package file implements the file action handler.
|
Package file implements the file action handler. |
|
Package file_delete_range implements the file_delete_range action handler.
|
Package file_delete_range implements the file_delete_range action handler. |
|
Package file_insert implements the file_insert action handler.
|
Package file_insert implements the file_insert action handler. |
|
Package file_patch_apply implements the file_patch_apply action handler.
|
Package file_patch_apply implements the file_patch_apply action handler. |
|
Package file_replace implements the file_replace action handler.
|
Package file_replace implements the file_replace action handler. |
|
Package package_handler implements the package action handler.
|
Package package_handler implements the package action handler. |
|
Package preset implements the preset action handler.
|
Package preset implements the preset action handler. |
|
Package print implements the print action handler.
|
Package print implements the print action handler. |
|
Package repo_apply_patchset implements the repo_apply_patchset action handler.
|
Package repo_apply_patchset implements the repo_apply_patchset action handler. |
|
Package repo_search implements the repo_search action handler.
|
Package repo_search implements the repo_search action handler. |
|
Package repo_tree implements the repo_tree action handler.
|
Package repo_tree implements the repo_tree action handler. |
|
Package service implements the service action handler.
|
Package service implements the service action handler. |
|
Package shell implements the shell action handler.
|
Package shell implements the shell action handler. |
|
Package template implements the template action handler.
|
Package template implements the template action handler. |
|
Package testutil provides shared testing utilities for action handlers.
|
Package testutil provides shared testing utilities for action handlers. |
|
Package unarchive implements the unarchive action handler.
|
Package unarchive implements the unarchive action handler. |
|
Package vars implements the vars action handler.
|
Package vars implements the vars action handler. |
|
Package wait implements the wait action handler.
|
Package wait implements the wait action handler. |