Skip to content
quarry Artifact [7078b8753e]
Login

Artifact [7078b8753e]

Artifact 7078b8753ec27eb1db327c2a3a71e68448353b94d9931b6e0fb09b5326b74c4e:


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
}