// quarry's own CI worker (RFC 0009, dogfooded): each task checks out
// the check-in under test in a fresh clone and runs one Go toolchain
// step, streaming its output to the engine's log lane. Deliberately
// boring — three task types, one helper, no state. Anything worth
// knowing is either in the run's logs (what the tools said) or on this
// process's stdout (what the worker did, one line per task).
//
// Its own module: the log-lane SDK (dagnats#624) is newer than the
// dagnats release quarry's main module pins, and a worker is a
// deployment artifact, not part of the forge.
package main
import (
"bytes"
"flag"
"fmt"
"io"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"time"
"github.com/danmestas/dagnats/worker"
"github.com/nats-io/nats.go"
)
var (
natsURL = flag.String("nats", nats.DefaultURL, "engine NATS address")
workdir = flag.String("work", filepath.Join(os.TempDir(), "quarry-ci-worker"), "scratch directory for clones")
keep = flag.Bool("keep", false, "keep a failed step's tree for debugging")
)
func main() {
flag.Parse()
if err := os.MkdirAll(*workdir, 0o755); err != nil {
log.Fatal(err)
}
nc, err := nats.Connect(*natsURL)
if err != nil {
log.Fatalf("connect %s: %v", *natsURL, err)
}
defer nc.Close()
w := worker.NewWorker(nc)
step(w, "quarry-test", []string{"go", "test", "./..."}, false)
step(w, "quarry-build", []string{"go", "build", "./..."}, false)
// gofmt -l exits 0 even when files need formatting; its OUTPUT is
// the verdict, so this step fails on any.
step(w, "quarry-fmt", []string{"gofmt", "-l", "cmd", "internal"}, true)
log.Printf("quarry-ci-worker ready · nats %s · work %s", *natsURL, *workdir)
w.Start()
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
<-sig
w.Stop()
}
// step registers one task type: clone, open the check-in, run argv.
func step(w *worker.Worker, task string, argv []string, failOnOutput bool) {
worker.HandleTyped(w, task, func(ctx worker.TaskContext, in map[string]any) (string, error) {
repo, _ := in["repo"].(string)
checkin, _ := in["checkin"].(string)
clone, _ := in["cloneUrl"].(string)
if clone == "" || checkin == "" {
return "", fmt.Errorf("input missing cloneUrl or checkin")
}
start := time.Now()
out, errw := ctx.LogOut(), ctx.LogErr()
dir, err := os.MkdirTemp(*workdir, task+"-")
if err != nil {
return "", err
}
failed := false
defer func() {
if failed && *keep {
log.Printf("%s: kept %s", task, dir)
return
}
os.RemoveAll(dir)
}()
fail := func(err error) (string, error) {
failed = true
log.Printf("%s %s @ %.12s: FAIL in %s: %v", task, repo, checkin, time.Since(start).Round(time.Second), err)
return "", err
}
run := func(extra io.Writer, name string, args ...string) error {
fmt.Fprintf(out, "$ %s %s\n", name, strings.Join(args, " "))
cmd := exec.Command(name, args...)
cmd.Dir = dir
cmd.Stdout, cmd.Stderr = out, errw
if extra != nil {
cmd.Stdout = io.MultiWriter(out, extra)
}
return cmd.Run()
}
if err := run(nil, "zit", "clone", clone, "."); err != nil {
return fail(fmt.Errorf("clone: %w", err))
}
// zit 0.2.0 opens the tree during clone; moving to the check-in
// under test is then an update. Older zits still want the open.
if err := run(nil, "zit", "update", checkin); err != nil {
if oerr := run(nil, "zit", "open", checkin); oerr != nil {
return fail(fmt.Errorf("open %s: %w", checkin, err))
}
}
var caught bytes.Buffer
var sink io.Writer
if failOnOutput {
sink = &caught
}
if err := run(sink, argv[0], argv[1:]...); err != nil {
return fail(err)
}
if failOnOutput && strings.TrimSpace(caught.String()) != "" {
n := len(strings.Fields(caught.String()))
fmt.Fprintf(errw, "%d file(s) need gofmt\n", n)
return fail(fmt.Errorf("%d file(s) need gofmt", n))
}
log.Printf("%s %s @ %.12s: ok in %s", task, repo, checkin, time.Since(start).Round(time.Second))
return "ok", nil
})
}