v0.2.0 43088fd

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// runNode is NodeScriptActionHandler.RunAsync (NodeScriptActionHandler.cs:
155// 27-196) on the host of the steps (a job container is phase H): INPUT_
156// variables and the PATH of the previous steps in the environment, the
157// script of the stage, node from the externals, run in the workspace.
158func (j *job) runNode(s *stepRun, def *actionDef, in *inputSet, env *ordered, cancel <-chan struct{}) stepResult {
159	num := s.num
160	for _, k := range in.keys {
161		v, _ := in.Get(k)
162		env.Set(inputEnvName(k), v)
163	}
164	// The PATH of the previous steps is added by procEnv.
165	env.Remove("NODE_ICU_DATA") // https://github.com/actions/runner/issues/795
166	r := def.runs()
167	var target string
168	switch s.stage {
169	case stageMain:
170		target = r.Main
171	case stagePre:
172		target = r.Pre
173	case stagePost:
174		target = r.Post
175	}
176	fail := func(err error) stepResult { j.errorf(num, "%s", err.Error()); return resFailed }
177	if target == "" {
178		return fail(ghErrorf("Value cannot be null or empty. (Parameter 'target')"))
179	}
180	script := pathCombine(def.dir, filepath.FromSlash(target))
181	if st, err := os.Stat(script); err != nil || st.IsDir() {
182		return fail(ghErrorf("File not found: '%s'", script))
183	}
184	if c := j.stepContainer(); c != nil {
185		return j.runNodeInContainer(s, c, script, env, cancel)
186	}
187	j.mu.Lock()
188	wd := j.state.GitHub("workspace")
189	j.mu.Unlock()
190	node := filepath.Join(j.externalsDir(), s.nodeVersion, "bin", "node")
191	s.actEnv = env
192	return j.runProc(s, node, wd, []string{node, script}, "Node Action run completed with exit code %d", cancel)
193}

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