Go 91 lines
package workflow
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
)
// EntryName is the runnable entry a workflow directory must contain.
const EntryName = "run"
// Run executes a native builtin and normalises its exit code to the
// contract. A workflow that returns an out-of-contract code is treated
// as a failure (1) so a bug cannot report a false "clean".
func Run(w Workflow, env Env) (Result, error) {
if env.Config == nil {
return Result{}, errors.New("workflow.Run: nil config")
}
res, err := w.Run(env)
if err != nil {
return res, err
}
res.Code = normalizeCode(res.Code)
return res, nil
}
// ScriptRun executes a scaffolded workflow living at
// <workspace>/workflows/<name>/. The entry runs with the repository
// root as its working directory and the resolved config exported into
// the environment (the workflow contract). The entry's own exit code is
// returned verbatim after normalisation; it owns the contract.
//
// Blocked (2) is returned when the workflow directory or its runnable
// entry is missing, rather than failing as if it had run.
func ScriptRun(name string, env Env) (Result, error) {
cfg := env.Config
if cfg == nil {
return Result{}, errors.New("workflow.ScriptRun: nil config")
}
dir := filepath.Join(cfg.Workspace, "workflows", name)
entry := filepath.Join(dir, EntryName)
info, err := os.Stat(entry)
if err != nil || info.IsDir() {
return Result{
Code: CodeBlocked,
Summary: fmt.Sprintf("workflow %q has no runnable %s entry", name, EntryName),
}, nil
}
// A sentinel marker file flips the workflow off without removing it
// from disk (`eeco workflows <name> off`). Treated as blocked, not a
// finding: the workflow could not run, exactly like a missing tool.
if _, derr := os.Stat(filepath.Join(dir, DisabledMarker)); derr == nil {
return Result{
Code: CodeBlocked,
Summary: fmt.Sprintf("workflow %q is disabled (eeco workflows %s on)", name, name),
}, nil
}
cmd := exec.Command(entry)
cmd.Dir = cfg.RepoRoot
cmd.Env = append(os.Environ(),
"EECO_REPO_ROOT="+cfg.RepoRoot,
"EECO_WORKSPACE="+cfg.Workspace,
"EECO_PROFILE="+string(cfg.Profile),
"EECO_AI="+strconv.FormatBool(env.AI),
)
cmd.Stdout = env.Out
cmd.Stderr = env.Out
runErr := cmd.Run()
if runErr == nil {
return Result{Code: CodeClean, Summary: name + " passed"}, nil
}
var ee *exec.ExitError
if errors.As(runErr, &ee) {
code := normalizeCode(ee.ExitCode())
return Result{
Code: code,
Summary: fmt.Sprintf("%s exited %d", name, ee.ExitCode()),
}, nil
}
// Could not execute at all (not executable, bad interpreter): the
// required entry is effectively unusable -> blocked, not a finding.
return Result{
Code: CodeBlocked,
Summary: fmt.Sprintf("cannot execute %s: %v", entry, runErr),
}, nil
}