package runner
import (
"encoding/json"
"fmt"
"sort"
)
// ManifestPath is where a repository says what its runner tasks run.
// It is read AT the check-in under test, like a pipeline, so the
// command can never drift from the code it tests.
const ManifestPath = "ci/runner.json"
// Task is one entry in the manifest: the argv to exec in the check-in's
// tree, and whether any output at all means failure — which is how a
// formatter that lists the files it would change reports a failure
// while exiting zero.
type Task struct {
Argv []string `json:"argv"`
FailOnOutput bool `json:"fail_on_output,omitempty"`
}
// Manifest maps task types to what runs for them.
//
// {"tasks": {
// "quarry-test": ["go", "test", "./..."],
// "quarry-fmt": {"argv": ["gofmt", "-l", "cmd", "internal"], "fail_on_output": true}
// }}
//
// An entry is either an argv array or a Task object; the array form is
// the common case and reads as the command it is.
type Manifest struct {
Tasks map[string]Task
}
// ParseManifest reads a manifest, refusing an empty one and any entry
// with no command, so a typo fails loudly rather than as a task that
// never matches.
func ParseManifest(b []byte) (Manifest, error) {
var raw struct {
Tasks map[string]json.RawMessage `json:"tasks"`
}
if err := json.Unmarshal(b, &raw); err != nil {
return Manifest{}, fmt.Errorf("%s is not valid JSON: %v", ManifestPath, err)
}
if len(raw.Tasks) == 0 {
return Manifest{}, fmt.Errorf("%s names no tasks", ManifestPath)
}
m := Manifest{Tasks: map[string]Task{}}
for name, v := range raw.Tasks {
var t Task
var argv []string
if err := json.Unmarshal(v, &argv); err == nil {
t.Argv = argv
} else if err := json.Unmarshal(v, &t); err != nil {
return Manifest{}, fmt.Errorf("%s: task %q is neither an argv array nor an {\"argv\": [...]} object", ManifestPath, name)
}
if len(t.Argv) == 0 || t.Argv[0] == "" {
return Manifest{}, fmt.Errorf("%s: task %q has no command", ManifestPath, name)
}
m.Tasks[name] = t
}
return m, nil
}
// Names lists the manifest's task types, sorted.
func (m Manifest) Names() []string {
out := make([]string, 0, len(m.Tasks))
for n := range m.Tasks {
out = append(out, n)
}
sort.Strings(out)
return out
}