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
}