Skip to content
quarry Artifact [937aaf7a59]
Login

Artifact [937aaf7a59]

Artifact 937aaf7a5945d43ab7af4604f10a0e2bfce86ed2d3cbd914e71feee616f03d46:


// workergroup.go — who may publish work into whose queue.
//
// A repository's CI spec lives in that repository, so the person who
// writes it chooses the `worker_group:` its steps are published to. That
// makes the spelling in the file an AUTHORIZATION decision, and the same
// shape as the hole dagnats closed in #695: a caller naming a boundary
// and being believed.
//
// The risk is not reading someone else's work — it is writing into it. A
// spec naming another repository's group publishes ITS steps onto THEIR
// machines, and their runners execute an author they never agreed to
// run. So a group must be absent, or exactly this repository's own.
package api

import (
	"fmt"
	"regexp"
	"sort"
	"strings"

	"craftdesign.group/quarry/internal/ci"
	"craftdesign.group/quarry/internal/ops"
)

// workerGroupDecl finds a worker_group declaration anywhere in a spec —
// any nesting, quoted or bare, block style or inside a flow mapping.
//
// This is a TEXT scan, because quarry has no YAML parser: it delegates
// compilation to the engine (Deps.ValidateCIYAML), and pulling in a YAML
// dependency to read one key is a large answer to a small question.
//
// The trade that makes a text scan acceptable here is the direction of
// its mistakes. Missing a real declaration would leave the hole open;
// flagging a harmless string refuses a save and prints a diagnostic
// saying exactly why. So this matches broadly and errs toward refusing:
// a `worker_group:` inside a quoted string or a comment is caught too,
// and the author's fix is to say which group they meant.
//
// Anchors and merge keys (`<<: *defaults`), which dagnats honours, carry
// the literal key in the block they are defined in, so they are seen.
var workerGroupDecl = regexp.MustCompile(`["']?worker_group["']?\s*:\s*["']?([^"'\s,}#]*)`)

// foreignWorkerGroups names every group a spec publishes to that is not
// own — sorted and deduplicated, so a spec with the same mistake in six
// steps produces one diagnostic rather than six.
//
// An EMPTY value is foreign too. `worker_group:` with nothing after it
// is not "ungrouped": it is a declaration whose meaning the author did
// not state, and guessing which of the two they meant is how a step ends
// up on a queue nobody expected.
func foreignWorkerGroups(content, own string) []string {
	seen := map[string]bool{}
	for _, m := range workerGroupDecl.FindAllStringSubmatch(content, -1) {
		got := strings.TrimSpace(m[1])
		if got == own && own != "" {
			continue
		}
		seen[got] = true
	}
	out := make([]string, 0, len(seen))
	for g := range seen {
		out = append(out, g)
	}
	sort.Strings(out)
	return out
}

// refuseForeignWorkerGroup is the gate ci.pipeline.save applies before a
// spec is written or validated. Absent is fine — that is the ungrouped
// queue, and the forge's own runners.
func refuseForeignWorkerGroup(repo, content string) error {
	own := ci.GroupFor(repo)
	foreign := foreignWorkerGroups(content, own)
	if len(foreign) == 0 {
		return nil
	}
	named := make([]string, 0, len(foreign))
	for _, g := range foreign {
		if g == "" {
			named = append(named, "an empty group")
			continue
		}
		named = append(named, "%q")
		named[len(named)-1] = fmt.Sprintf("%q", g)
	}
	return fmt.Errorf("this pipeline publishes to %s; %s may only use its own worker group %q, or none at all: %w",
		strings.Join(named, ", "), repo, own, ops.ErrInvalid)
}