ajhahn.de
← eeco
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
}