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