Skip to content
quarry Artifact [dad94a833d]
Login

Artifact [dad94a833d]

Artifact dad94a833dc7dc55ebbe969aadabbcaec3f6d9868ed91f0f5fa2a7dd2233c400:


package ci

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

// A workflow with an environment: job deploys, or holds something the
// forge gives to reviewed code only: it never runs on a merge's tree,
// and never inherits a verdict (RFC 0009 §once). In a ci.yml the key is
// a job's, so it is indented; 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]*:`)

// hasEnvironment reports a pipeline that names an environment anywhere:
// a job's environment: in a ci.yml, an "environment" key in JSON.
func hasEnvironment(def []byte, isJSON bool) bool {
	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" && e != nil && e != "" {
					return true
				}
				if walk(e) {
					return true
				}
			}
		case []any:
			for _, e := range x {
				if walk(e) {
					return true
				}
			}
		}
		return false
	}
	return walk(doc)
}

// 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: the pair's newest
// run of it, when that ended in a pass or a fail. A run still going,
// cancelled, or in error is no verdict, and p runs.
func (h *heir) verdict(st Store, repo string, r Repo, c Checkin, p string) (RunRecord, bool) {
	for _, run := range h.of(st, repo, r, c) {
		if run.Pipeline != p {
			continue
		}
		return run, (Run{Status: run.Status}).Passed() || run.Status == "failed"
	}
	return RunRecord{}, false
}