gitmote / internal / ci / local.go · history · branches
2420 bytes · raw
package ci
import (
"context"
"fmt"
"log/slog"
"os"
"os/exec"
)
// NoopTrigger records a run but executes nothing. It is the fallback when no
// runner substrate is configured (neither local nor Scaleway) — dispatch stays
// fire-and-forget and the run simply never leaves the queued state.
type NoopTrigger struct{}
// Trigger implements Trigger.
func (NoopTrigger) Trigger(context.Context, map[string]string) error { return nil }
// LocalTrigger runs a CI job by spawning the gitmote-runner binary as a local
// process, mirroring how the Scaleway client starts a container: same runner
// code, same env contract, a local substrate instead of a cloud job. It is
// fire-and-forget — Start (not Wait) returns immediately so dispatch never
// blocks the push; a background goroutine reaps the process and logs its exit.
type LocalTrigger struct {
binary string
logger *slog.Logger
}
// NewLocalTrigger returns a LocalTrigger that spawns the runner at binaryPath.
func NewLocalTrigger(binaryPath string, logger *slog.Logger) *LocalTrigger {
if logger == nil {
logger = slog.New(slog.DiscardHandler)
}
return &LocalTrigger{binary: binaryPath, logger: logger}
}
// Trigger starts the runner with env layered over the parent environment (so the
// child inherits PATH, DOCKER_HOST, etc. that act needs). It returns the Start
// error only; a non-zero runner exit is reported to the report API by the runner
// itself, and reaped here for logging.
func (t *LocalTrigger) Trigger(_ context.Context, env map[string]string) error {
// Deliberately not tied to the dispatch ctx: the runner outlives the push
// request that triggered it. It reports its own outcome and bounds its own
// work.
cmd := exec.Command(t.binary)
cmd.Env = append(os.Environ(), flattenEnv(env)...)
if err := cmd.Start(); err != nil {
return fmt.Errorf("start local runner %s: %w", t.binary, err)
}
jobID := env["GITMOTE_CI_JOB_ID"]
go func() {
if err := cmd.Wait(); err != nil {
t.logger.Warn("local runner exited nonzero", "job", jobID, "pid", cmd.Process.Pid, "error", err)
} else {
t.logger.Debug("local runner finished", "job", jobID, "pid", cmd.Process.Pid)
}
}()
return nil
}
// flattenEnv turns a map into the KEY=VALUE slice exec expects.
func flattenEnv(env map[string]string) []string {
out := make([]string, 0, len(env))
for k, v := range env {
out = append(out, k+"="+v)
}
return out
}