v0.4.2 257b319

actions_node.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6	"fmt"
7	"os"
8	"path/filepath"
9	"strings"
10
11	"bnema.dev/ermite-actions/action"
12)
13
14const (
15	node20Name = "node20"
16	node24Name = "node24"
17	// Node20DeprecationURL is Constants.Runner.NodeMigration.Node20DeprecationUrl.
18	node20DeprecationURL = "https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/"
19	forceNode24Var       = "FORCE_JAVASCRIPT_ACTIONS_TO_NODE24"
20	allowUnsecureVar     = "ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION"
21)
22
23// convertToBool is StringUtil.ConvertToBoolean with a false default.
24func convertToBool(v string) bool {
25	switch strings.ToLower(v) {
26	case "1", "true", "$true":
27		return true
28	}
29	return false
30}
31
32// envFlag is NodeUtil.GetEnvironmentVariableDetails: the value comes from the
33// workflow's environment (the env context of the step) when it is set there,
34// from the process of the runner otherwise.
35type envFlag struct{ on, fromWorkflow, fromSystem bool }
36
37func (j *job) nodeEnvFlag(name string, wf *ordered) envFlag {
38	var f envFlag
39	v, found := wf.Get(name)
40	if found {
41		f.fromWorkflow = true
42		f.on = convertToBool(v)
43	}
44	sys := envValue(j.baseEnv, name)
45	f.fromSystem = sys != ""
46	if !found {
47		f.on = convertToBool(sys)
48	}
49	return f
50}
51
52// nodeVersionDefault is NodeUtil.DetermineActionsNodeVersion
53// (NodeUtil.cs:50-110) for an action that says node20: the version the action
54// runs on and the warning of a conflicting configuration. useNode24ByDefault and
55// requireNode24 are the server's flags, ActionsConfig.Node24ByDefault and
56// RequireNode24 here.
57func (j *job) nodeVersionDefault(wf *ordered) (version, warning string) {
58	force := j.nodeEnvFlag(forceNode24Var, wf)
59	allow := j.nodeEnvFlag(allowUnsecureVar, wf)
60	if j.cfg.Actions.RequireNode24 {
61		// Phase 3 (NodeUtil.cs:60-63): node24 whatever the variables say, and
62		// no warning about them.
63		return node24Name, ""
64	}
65	byDefault := j.cfg.Actions.Node24ByDefault
66	bothWorkflow := force.on && allow.on && force.fromWorkflow && allow.fromWorkflow
67	bothSystem := force.on && allow.on && force.fromSystem && allow.fromSystem
68	if bothWorkflow || bothSystem {
69		source := "system"
70		if bothWorkflow {
71			source = "workflow"
72		}
73		def := node20Name
74		if byDefault {
75			def = node24Name
76		}
77		return def, fmt.Sprintf("Both %s and %s environment variables are set to true in the %s environment. This is likely a configuration error. Using the default Node version: %s.",
78			forceNode24Var, allowUnsecureVar, source, def)
79	}
80	if byDefault {
81		if allow.on {
82			return node20Name, ""
83		}
84		return node24Name, ""
85	}
86	if force.on {
87		return node24Name, ""
88	}
89	return node20Name, ""
90}
91
92// nodeVersionFor is the node part of HandlerFactory.Create
93// (HandlerFactory.cs:56-170): node12 and node16 run on node20, node20 on
94// whatever DetermineActionsNodeVersion says, node24 on node24. The result is
95// kept in the step for the handler.
96func (j *job) nodeVersionFor(s *stepRun, def *actionDef) error {
97	r := def.runs()
98	if r == nil || def.kind != kindNode {
99		return nil
100	}
101	v := r.Using
102	if v == "node12" || v == "node16" {
103		v = node20Name
104	}
105	if v == node20Name {
106		final, warn := j.nodeVersionDefault(s.env)
107		v = final
108		if warn != "" {
109			j.tagged(s.num, "##[warning]", warn)
110		}
111		if final == node24Name {
112			j.mu.Lock()
113			j.upgradedToNode24 = appendUnique(j.upgradedToNode24, actionName(s.n))
114			j.mu.Unlock()
115			if j.cfg.Actions.Node24ByDefault && !j.cfg.Actions.RequireNode24 {
116				j.out(s.num, "Node 20 is being deprecated. This workflow is running with Node 24 by default. "+
117					"If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable. "+
118					"For more information see: "+node20DeprecationURL)
119			}
120		}
121	}
122	s.nodeVersion = v
123	return nil
124}
125
126func appendUnique(l []string, v string) []string {
127	if v == "" {
128		return l
129	}
130	for _, x := range l {
131		if x == v {
132			return l
133		}
134	}
135	return append(l, v)
136}
137
138// actionName is HandlerFactory.GetActionName: "Name[/Path]@Ref".
139func actionName(n *node) string {
140	if n.uses.Kind == action.UsesLocal {
141		return n.uses.Path // no name: HandlerFactory.cs:200-208 uses the path alone
142	}
143	return registeredRef(n.uses)
144}
145
146// externalsDir is where the node versions live.
147func (j *job) externalsDir() string {
148	if d := j.cfg.Actions.ExternalsDir; d != "" {
149		return d
150	}
151	return defaultExternalsDir
152}
153
154// addRuntimeEnv is the "Add Actions Runtime server info" part of the handlers
155// (NodeScriptActionHandler.cs:55-84, ContainerActionHandler.cs:219-245): the
156// variables @actions/cache and @actions/artifact read. ermite's runtime service
157// is the channel of the runner behind one URL, which serves the cache, the
158// results service (artifacts) or both: ACTIONS_RUNTIME_URL and
159// ACTIONS_RUNTIME_TOKEN, then, when the channel serves the cache,
160// ACTIONS_CACHE_URL (the base of "_apis/artifactcache/…": the toolkit appends
161// to it, cacheHttpClient.ts:47, so it ends with a slash) and ACTIONS_CACHE_MODE,
162// and, when it serves results, ACTIONS_RESULTS_URL (the handlers set it from
163// ResultsServiceUrl, NodeScriptActionHandler.cs:71-84, ContainerActionHandler.cs:
164// 237-245; the toolkit keeps only its origin, artifact shared/config.ts).
165// ACTIONS_CACHE_SERVICE_V2 is never set: the toolkit would then ignore
166// ACTIONS_CACHE_URL (config.ts:42-58) and, on a server URL that is not
167// github.com, uses v1 anyway (config.ts:22-27).
168//
169// inContainer says the process runs in a container (a step in the job
170// container, a docker action): it has its own network namespace and reaches
171// the relay at ContainerCacheURL, not on the loopback. Without a URL for where
172// the step runs, or without a token, nothing is set.
173func (j *job) addRuntimeEnv(env *ordered, inContainer bool) {
174	url := j.cfg.CacheURL
175	if inContainer {
176		url = j.cfg.ContainerCacheURL
177	}
178	if url == "" || j.cfg.CacheToken == "" {
179		return
180	}
181	env.Set("ACTIONS_RUNTIME_URL", url)
182	env.Set("ACTIONS_RUNTIME_TOKEN", j.cfg.CacheToken)
183	if !j.cfg.CacheOff {
184		env.Set("ACTIONS_CACHE_URL", url)
185		// ACTIONS_CACHE_MODE: NodeScriptActionHandler.cs:71-84, ContainerActionHandler.cs:
186		// 237-245; the toolkit skips uploads in "read" mode (cache config.ts:27-40).
187		if j.cfg.CacheMode != "" {
188			env.Set("ACTIONS_CACHE_MODE", j.cfg.CacheMode)
189		}
190	}
191	if j.cfg.Results {
192		env.Set("ACTIONS_RESULTS_URL", url)
193	}
194}
195
196// runNode is NodeScriptActionHandler.RunAsync (NodeScriptActionHandler.cs:
197// 27-196) on the host of the steps (a job container is phase H): INPUT_
198// variables and the PATH of the previous steps in the environment, the
199// script of the stage, node from the externals, run in the workspace.
200func (j *job) runNode(s *stepRun, def *actionDef, in *inputSet, env *ordered, cancel <-chan struct{}) stepResult {
201	num := s.num
202	for _, k := range in.keys {
203		v, _ := in.Get(k)
204		env.Set(inputEnvName(k), v)
205	}
206	j.addRuntimeEnv(env, j.stepContainer() != nil)
207	// The PATH of the previous steps is added by procEnv.
208	env.Remove("NODE_ICU_DATA") // https://github.com/actions/runner/issues/795
209	r := def.runs()
210	var target string
211	switch s.stage {
212	case stageMain:
213		target = r.Main
214	case stagePre:
215		target = r.Pre
216	case stagePost:
217		target = r.Post
218	}
219	fail := func(err error) stepResult { j.errorf(num, "%s", err.Error()); return resFailed }
220	if target == "" {
221		return fail(ghErrorf("Value cannot be null or empty. (Parameter 'target')"))
222	}
223	script := pathCombine(def.dir, filepath.FromSlash(target))
224	if st, err := os.Stat(script); err != nil || st.IsDir() {
225		return fail(ghErrorf("File not found: '%s'", script))
226	}
227	j.setArtifactsServerURL(s, script)
228	if c := j.stepContainer(); c != nil {
229		return j.runNodeInContainer(s, c, script, env, cancel)
230	}
231	j.mu.Lock()
232	wd := j.state.GitHub("workspace")
233	j.mu.Unlock()
234	node := filepath.Join(j.externalsDir(), s.nodeVersion, "bin", "node")
235	s.actEnv = env
236	return j.runProc(s, node, wd, []string{node, script}, "Node Action run completed with exit code %d", cancel)
237}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close