Skip to content
quarry Artifact [f9159f15d2]
Login

Artifact [f9159f15d2]

Artifact f9159f15d26d9634d09643f5c74ac77305dba46e28a15fb6c27175c27fd88156:


package ci

import (
	"encoding/json"
	"fmt"
	"log"
	"regexp"
	"strings"
)

// A workflow with an environment: job, or one that names any secret,
// deploys or holds something the forge gives to reviewed code only:
// every secret is refused to a merge, a repository's own included. It
// never runs on a merge's tree and never inherits a verdict (RFC 0009
// §once). In a ci.yml an environment: is a job's key, so it is
// indented, and a secret is a ${{ secrets.NAME }} reference; reading
// the text is enough, and a stray match only keeps a workflow on its
// tip, the old behaviour.
var (
	yamlEnvironment = regexp.MustCompile(`(?m)^[ \t]+environment[ \t]*:`)
	secretRef       = regexp.MustCompile(`\$\{\{\s*secrets\.`)
)

// reviewedOnly reports a pipeline that needs what only reviewed code
// gets: an environment, or a secret. In JSON, an "environment" or a
// "secrets" key anywhere (a job's declaration) counts, as does a
// ${{ secrets.NAME }} reference.
func reviewedOnly(def []byte, isJSON bool) bool {
	if secretRef.Match(def) {
		return true
	}
	if !isJSON {
		return yamlEnvironment.Match(def)
	}
	var doc any
	if json.Unmarshal(def, &doc) != nil {
		return false
	}
	var walk func(v any) bool
	walk = func(v any) bool {
		switch x := v.(type) {
		case map[string]any:
			for k, e := range x {
				if (k == "environment" || k == "secrets") && !empty(e) {
					return true
				}
				if walk(e) {
					return true
				}
			}
		case []any:
			for _, e := range x {
				if walk(e) {
					return true
				}
			}
		}
		return false
	}
	return walk(doc)
}

// empty is a JSON value that says nothing: null, "", [] or {}.
func empty(v any) bool {
	switch x := v.(type) {
	case nil:
		return true
	case string:
		return x == ""
	case []any:
		return len(x) == 0
	case map[string]any:
		return len(x) == 0
	}
	return false
}

// yamlInherit is a ci.yml's top-level inherit: line, and its value.
var yamlInherit = regexp.MustCompile(`(?m)^inherit[ \t]*:[ \t]*([^#\n]*?)[ \t]*(?:#[^\n]*)?$`)

// inheritOf reads a pipeline's top-level inherit (RFC 0009
// §inheritance): false makes the default branch run it even when its
// merge was tested as a pair. Absent is true. Anything but true or
// false is an error: guessing would be wrong half the time.
func inheritOf(def []byte, isJSON bool) (bool, error) {
	if isJSON {
		var p struct {
			Inherit json.RawMessage `json:"inherit"`
		}
		if json.Unmarshal(def, &p) != nil || p.Inherit == nil {
			return true, nil // the engine's registration says what is wrong with it
		}
		var b bool
		if err := json.Unmarshal(p.Inherit, &b); err != nil {
			return false, fmt.Errorf("inherit is %s; it may be true or false", p.Inherit)
		}
		return b, nil
	}
	m := yamlInherit.FindSubmatch(def)
	if m == nil {
		return true, nil
	}
	switch v := strings.Trim(string(m[1]), `"'`); v {
	case "true":
		return true, nil
	case "false":
		return false, nil
	default:
		return false, fmt.Errorf("inherit is %q; it may be true or false", v)
	}
}

// withoutInherit blanks a ci.yml's inherit: line before the engine
// compiles it: the key is quarry's, and the compiler, which knows only
// the workflow's own keys, refuses any other. The line stays, empty, so
// the compiler's line numbers still point at the author's lines.
func withoutInherit(spec []byte) []byte {
	return yamlInherit.ReplaceAll(spec, nil)
}

// heir finds what a default-branch check-in may inherit: the runs of
// the pair it merged, when its tree is that pair's. The parents are
// the target tip and the branch tip; which is which is not recorded,
// so both spellings are asked, as carryForward does. Built once per
// launch, on the first pipeline that asks.
type heir struct {
	asked bool
	runs  []RunRecord // the equal pair's, newest first
}

func (h *heir) of(st Store, repo string, r Repo, c Checkin) []RunRecord {
	if h.asked {
		return h.runs
	}
	h.asked = true
	parents, err := r.ParentsOf(c.Hash)
	if err != nil || len(parents) != 2 {
		return nil
	}
	for _, tips := range [][2]string{{parents[1], parents[0]}, {parents[0], parents[1]}} {
		runs, err := st.RunsOf(repo, Pair(tips[0], tips[1]))
		if err != nil || len(runs) == 0 {
			continue
		}
		same, err := r.SameTree(c.Hash, tips[0], tips[1])
		if err != nil {
			log.Printf("ci: %s %.12s: comparing with its pair: %v; it runs", repo, c.Hash, err)
			return nil
		}
		if same {
			h.runs = runs
			return runs
		}
	}
	return nil
}

// verdict is the pair run c inherits for pipeline p, whose definition
// at c is def: the pair's newest run of it, when that ended in a pass
// or a fail and ran def byte for byte. An equal tree reads equal
// definitions, but a run recorded before pairs read the merged tree
// ran the branch tip's, which the target may have changed since. A
// run still going, cancelled, in error, or of another definition is no
// verdict, and p runs.
func (h *heir) verdict(st Store, repo string, r Repo, c Checkin, p string, def []byte) (RunRecord, bool) {
	for _, run := range h.of(st, repo, r, c) {
		if run.Pipeline != p {
			continue
		}
		decided := (Run{Status: run.Status}).Passed() || run.Status == "failed"
		return run, decided && run.Def != "" && run.Def == DefHash(def)
	}
	return RunRecord{}, false
}