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 // /workflows//. 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 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 }