Clarify run-if skip reasons with the resolved variable value.

Failed conditions now explain skips in plain language (e.g. UpdateCount was 0) and truncate long captures so misconfigured apt listings cannot flood the UI.
This commit is contained in:
2026-07-19 21:59:35 +02:00
parent eade2ef0c7
commit 0e06063c1d
8 changed files with 865 additions and 46 deletions
+122 -28
View File
@@ -108,12 +108,13 @@ func (executor *Executor) tickSchedules() {
// ResolvedAction is the effective action definition after library resolution.
type ResolvedAction struct {
Name string
Description string
Kind settings.ActionKind
Body string
Env []settings.ActionEnvVar
Source settings.ActionItemSource
Name string
Description string
Kind settings.ActionKind
Body string
Env []settings.ActionEnvVar
RequiresSudo bool
Source settings.ActionItemSource
}
// ResolveItem resolves a library or local item to an executable action.
@@ -130,12 +131,13 @@ func ResolveItem(item settings.NodeActionItem, library []settings.Action) (Resol
env = []settings.ActionEnvVar{}
}
return ResolvedAction{
Name: action.Name,
Description: action.Description,
Kind: action.Kind,
Body: action.Body,
Env: env,
Source: settings.ActionItemSourceLibrary,
Name: action.Name,
Description: action.Description,
Kind: action.Kind,
Body: action.Body,
Env: env,
RequiresSudo: action.RequiresSudo,
Source: settings.ActionItemSourceLibrary,
}, nil
}
}
@@ -155,18 +157,54 @@ func ResolveItem(item settings.NodeActionItem, library []settings.Action) (Resol
env = []settings.ActionEnvVar{}
}
return ResolvedAction{
Name: item.Name,
Description: item.Description,
Kind: item.Kind,
Body: item.Body,
Env: env,
Source: settings.ActionItemSourceLocal,
Name: item.Name,
Description: item.Description,
Kind: item.Kind,
Body: item.Body,
Env: env,
RequiresSudo: item.RequiresSudo,
Source: settings.ActionItemSourceLocal,
}, nil
default:
return ResolvedAction{}, fmt.Errorf("unknown action item source %q", item.Source)
}
}
// WrapShellWithSudo prepends "sudo -n " when requiresSudo is true.
func WrapShellWithSudo(command string, requiresSudo bool) string {
if !requiresSudo {
return command
}
return "sudo -n " + command
}
// ScriptRemoteCommand is the remote argv used for script actions (body on stdin).
func ScriptRemoteCommand(requiresSudo bool) string {
if requiresSudo {
return "sudo -n bash -s"
}
return "bash -s"
}
// BuildRemoteCommand returns the final SSH exec string for an action (including env exports).
func BuildRemoteCommand(
kind settings.ActionKind,
body string,
requiresSudo bool,
env map[string]string,
) string {
var command string
switch kind {
case settings.ActionKindShell:
command = WrapShellWithSudo(body, requiresSudo)
case settings.ActionKindScript:
command = ScriptRemoteCommand(requiresSudo)
default:
command = body
}
return auth.PrependEnvExports(env, command)
}
// RunGroup executes all items in order, appends a log file entry, and updates LastRunAt.
func (executor *Executor) RunGroup(
group settings.NodeActionGroup,
@@ -220,6 +258,7 @@ func (executor *Executor) runItems(
startedAt := time.Now().UTC()
results := make([]settings.ActionItemRunResult, 0, len(items))
runVars := map[string]string{}
for _, item := range items {
itemStarted := time.Now().UTC()
@@ -239,9 +278,47 @@ func (executor *Executor) runItems(
continue
}
runIf := strings.TrimSpace(item.RunIf)
if runIf != "" {
shouldRun, detail, condErr := EvaluateCondition(runIf, runVars)
if condErr != nil || !shouldRun {
skipReason := fmt.Sprintf("%s skipped: run condition was not met", resolved.Name)
if detail != "" {
skipReason = fmt.Sprintf("%s skipped: %s", resolved.Name, detail)
}
if condErr != nil {
skipReason = fmt.Sprintf("%s skipped because condition error: %s", resolved.Name, condErr.Error())
}
results = append(results, settings.ActionItemRunResult{
ItemID: item.ID,
ActionName: resolved.Name,
Source: string(resolved.Source),
ExitCode: 0,
Stdout: "",
Stderr: "",
Skipped: true,
SkipReason: skipReason,
StartedAt: itemStarted,
FinishedAt: time.Now().UTC(),
})
continue
}
}
rawEnv := make(map[string]string, len(resolved.Env))
for _, envVar := range resolved.Env {
rawEnv[envVar.Name] = envVar.Value
}
envMap := make(map[string]string, len(resolved.Env))
for _, envVar := range resolved.Env {
envMap[envVar.Name] = envVar.Value
expandedValue := auth.ExpandPlaceholders(envVar.Value, auth.PlaceholderContext{
Host: node.Name,
IP: node.HostIP,
Username: node.Username,
Env: rawEnv,
Secrets: map[string]string{},
})
envMap[envVar.Name] = ExpandDollarVars(expandedValue, runVars)
}
expandedBody := auth.ExpandPlaceholders(resolved.Body, auth.PlaceholderContext{
@@ -251,6 +328,14 @@ func (executor *Executor) runItems(
Env: envMap,
Secrets: map[string]string{},
})
expandedBody = ExpandDollarVars(expandedBody, runVars)
remoteCommand := BuildRemoteCommand(
resolved.Kind,
expandedBody,
resolved.RequiresSudo,
envMap,
)
var sshResult auth.SSHRunResult
var runErr error
@@ -261,7 +346,7 @@ func (executor *Executor) runItems(
node.Username,
privateKey,
passphrase,
expandedBody,
WrapShellWithSudo(expandedBody, resolved.RequiresSudo),
envMap,
defaultRunTimeout,
)
@@ -274,26 +359,35 @@ func (executor *Executor) runItems(
expandedBody,
envMap,
defaultRunTimeout,
resolved.RequiresSudo,
)
default:
runErr = fmt.Errorf("unsupported action kind %q", resolved.Kind)
}
itemResult := settings.ActionItemRunResult{
ItemID: item.ID,
ActionName: resolved.Name,
Source: string(resolved.Source),
ExitCode: sshResult.ExitCode,
Stdout: sshResult.Stdout,
Stderr: sshResult.Stderr,
StartedAt: itemStarted,
FinishedAt: time.Now().UTC(),
ItemID: item.ID,
ActionName: resolved.Name,
Source: string(resolved.Source),
Kind: resolved.Kind,
RemoteCommand: remoteCommand,
SSHUsername: node.Username,
ExitCode: sshResult.ExitCode,
Stdout: sshResult.Stdout,
Stderr: sshResult.Stderr,
StartedAt: itemStarted,
FinishedAt: time.Now().UTC(),
}
if runErr != nil {
itemResult.Error = runErr.Error()
if itemResult.ExitCode == 0 {
itemResult.ExitCode = -1
}
} else if setName := strings.TrimSpace(item.SetVariable); setName != "" {
captured := strings.TrimSpace(sshResult.Stdout)
runVars[setName] = captured
itemResult.SetVariable = setName
itemResult.VariableValue = captured
}
results = append(results, itemResult)
}