package workflow import ( "errors" "fmt" "os/exec" "strings" ) // buildGate runs the project's declared parse/build gate — the ordered // command chain in cfg.Gate — step by step, with the repository root as // the working directory, stopping at the first failure. The chain is the // profile default (a single step) or the operator's repeatable `gate` // key in config.local; a project with no gate (the generic profile, or // a lone `gate=`) is a clean no-op, so the gate is opt-in per project. // // Exit-code contract: // - every step exits 0 -> CodeClean // - a step exits non-zero -> CodeFinding (chain stops there) // - a step's command is not on PATH, or -> CodeBlocked // a step that pre-flighted cannot run // - no gate declared -> CodeClean // // Every step's command is checked on PATH before the first step runs, so // a chain that cannot complete is reported blocked rather than running // partway and reporting a finding (a missing tool outranks a finding). type buildGate struct{} func (buildGate) Name() string { return "gate" } func (buildGate) Summary() string { return "run the project's declared parse/build gate chain" } func (buildGate) Run(env Env) (Result, error) { steps := env.Config.Gate if len(steps) == 0 { return Result{Code: CodeClean, Summary: "no gate declared"}, nil } // Pre-flight: a chain is runnable only if every step's command is on // PATH. Report the first missing tool as blocked before running any // step, so a partly-run chain never masquerades as a finding. for _, step := range steps { if len(step) == 0 { continue } if _, err := exec.LookPath(step[0]); err != nil { return Result{ Code: CodeBlocked, Summary: fmt.Sprintf("gate step %q is not on PATH", step[0]), }, nil } } for i, step := range steps { if len(step) == 0 { continue } label := strings.Join(step, " ") if env.Out != nil { fmt.Fprintf(env.Out, "gate step %d/%d: %s\n", i+1, len(steps), label) } cmd := exec.Command(step[0], step[1:]...) cmd.Dir = env.Config.RepoRoot cmd.Stdout = env.Out cmd.Stderr = env.Out runErr := cmd.Run() if runErr == nil { continue } var ee *exec.ExitError if errors.As(runErr, &ee) { return Result{ Code: CodeFinding, Summary: fmt.Sprintf("gate step %d/%d failed: %s", i+1, len(steps), label), Findings: []Finding{{ Path: label, Line: 0, Msg: fmt.Sprintf("exited %d", ee.ExitCode()), }}, }, nil } // The tool was on PATH at pre-flight but still could not run // (removed mid-run, lost the executable bit): the chain cannot // complete, so this is blocked rather than a finding. return Result{ Code: CodeBlocked, Summary: fmt.Sprintf("gate step %d/%d could not run: %s", i+1, len(steps), label), }, nil } return Result{ Code: CodeClean, Summary: fmt.Sprintf("%d gate step(s) passed", len(steps)), }, nil }