env.go

  1package engine
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"sort"
  7	"strconv"
  8	"strings"
  9)
 10
 11// actionEnv builds an action's environment (DESIGN.md ยง9): the engine's own
 12// environment without any CI_*/TANGLED_* left over from an enclosing run,
 13// then the manifest's environment, then the contract.
 14func (e *Engine) actionEnv(p Pending, n int, resultFile, inputsFile, triggerFile string) []string {
 15	env := map[string]string{}
 16	for _, kv := range e.BaseEnv {
 17		k, v, ok := strings.Cut(kv, "=")
 18		if !ok || strings.HasPrefix(k, "CI_") || strings.HasPrefix(k, "TANGLED_") {
 19			continue
 20		}
 21		env[k] = v
 22	}
 23	for k, v := range e.M.Environment {
 24		env[k] = v
 25	}
 26	// ci-result (and ci) of the engine running this action come first, so
 27	// recipes can report results even in repos that don't pin the engine.
 28	if dir := helperDir(); dir != "" {
 29		env["PATH"] = dir + string(os.PathListSeparator) + env["PATH"]
 30	}
 31	m := e.st.Meta
 32	set := func(k, v string) { env[k] = v }
 33
 34	set("CI", "true")
 35	set("CI_RUN_ID", m.Run)
 36	set("CI_RUN_DIR", e.Dir.Path())
 37	set("CI_WORKFLOW", m.Workflow)
 38	set("CI_ACTION", p.Action)
 39	set("CI_ATTEMPT", strconv.Itoa(n))
 40	set("CI_RESULT", resultFile)
 41	set("CI_STATE", e.stateDir())
 42	set("CI_PARENT_RUN", m.Parent)
 43	set("CI_ARTIFACTS", e.Dir.Artifacts())
 44	set("CI_INPUTS", inputsFile)
 45	set("CI_TRIGGER_RESULT", triggerFile)
 46	set("CI_SHA", m.Sha)
 47	set("CI_REF", m.Ref)
 48	set("CI_REPO", m.Repo)
 49	set("CI_ROOT", m.Root)
 50	set("CI_HOST", m.Host)
 51	for _, c := range m.Checkouts {
 52		if c.Env != "" {
 53			set(c.Env, c.Path)
 54		}
 55	}
 56
 57	// Upstream spindle compatibility (TANGLED_* as documented upstream).
 58	set("TANGLED_SHA", m.Sha)
 59	set("TANGLED_COMMIT_SHA", m.Sha)
 60	set("TANGLED_PIPELINE_KIND", m.Kind)
 61	set("TANGLED_PIPELINE_ID", m.Pipeline)
 62	set("TANGLED_REPO_NAME", m.Repo)
 63	set("TANGLED_REPO_DID", m.RepoDid)
 64	set("TANGLED_REPO_KNOT", m.Knot)
 65	set("TANGLED_REPO_DEFAULT_BRANCH", m.DefaultBranch)
 66	if m.Ref != "" {
 67		set("TANGLED_REF", m.Ref)
 68		switch {
 69		case strings.HasPrefix(m.Ref, "refs/heads/"):
 70			set("TANGLED_REF_NAME", strings.TrimPrefix(m.Ref, "refs/heads/"))
 71			set("TANGLED_REF_TYPE", "branch")
 72		case strings.HasPrefix(m.Ref, "refs/tags/"):
 73			set("TANGLED_REF_NAME", strings.TrimPrefix(m.Ref, "refs/tags/"))
 74			set("TANGLED_REF_TYPE", "tag")
 75		}
 76	}
 77
 78	out := make([]string, 0, len(env))
 79	for k, v := range env {
 80		out = append(out, k+"="+v)
 81	}
 82	sort.Strings(out)
 83	return out
 84}
 85
 86// stateDir is $CI_STATE: the run's own, or for a manual action run against
 87// an earlier run (ci action --run), that run's, so rollback sees what deploy
 88// recorded.
 89func (e *Engine) stateDir() string {
 90	if p := e.st.Meta.Parent; p != "" {
 91		return Dir(p).State()
 92	}
 93	return e.Dir.State()
 94}
 95
 96// helperDir is the directory of the running engine binary if ci-result is
 97// next to it (the Nix package installs it as a symlink), else "".
 98func helperDir() string {
 99	exe, err := os.Executable()
100	if err != nil {
101		return ""
102	}
103	dir := filepath.Dir(exe)
104	if _, err := os.Stat(filepath.Join(dir, "ci-result")); err != nil {
105		return ""
106	}
107	return dir
108}
109
110// osEnviron is os.Environ, replaceable in tests.
111var osEnviron = os.Environ