fix(executor): run node12, node16 and node20 actions on Node 24 as GitHub does
Node 24 is required like on github.com today (NodeUtil requireNode24 phase), whatever FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 or ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION say. Adds a test that a matcher removed after 3 timeouts is removed again in the next step, as recorded.
- commit
- 298aaf799765625c68f17d9baccd5bc717f0633c
- author
- brice · · 2026-10-14 07:55 UTC
- committer
- bnema · · 2026-10-14 07:55 UTC
- parent
- f09c6ed
- signature
- SSH Unverified
SHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivAThe key is unknown
6 files · +140 -4
M README.md +1 -0
| @@ -42,6 +42,7 @@ The host provides Node.js to the jobs **outside the image**, like its own execut | |||
| 42 | 42 | | `node20_alpine` | 20.20.2 | `github.com/actions/alpine_nodejs` release `v20.20.2` | x64 | | |
| 43 | 43 | | `node24_alpine` | 24.21.0 | `github.com/actions/alpine_nodejs` release `v24.21.0` | x64 | | |
| 44 | 44 | ||
| 45 | + | - **Node versions of actions** follow github.com today (`RequireNode24` and `Node24ByDefault` of the executor's `ActionsConfig`, both set by the sandbox through `DefaultActionsConfig`): `using: node12`, `node16` and `node20` actions run on `node24`, whatever `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24` and `ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION` say, and `Complete job` ends with `##[warning]Node.js 20 is deprecated. The following actions target Node.js 20 but are being forced to run on Node.js 24: <actions, sorted ignoring case>. For more information see: …` (recordings `act-node-*`). Both flags off give the older phases of the runner. | |
| 45 | 46 | - **Download.** `ermite-runner run` (and `sandbox-check`) fetch what is missing at start, into `$ERMITE_RUNNER_STATE_DIR/externals/<name>-<version>-<arch>/`, in the background; the start waits for them at most 2 minutes (a runner with a network normally takes its first job with everything in place, one without never waits long). The archive is the `.tar.gz` (the Go standard library has no xz); its SHA-256 is checked **before** anything is unpacked. Redirects must stay on https. | |
| 46 | 47 | - **Extraction** keeps the top directory out and refuses absolute names, `..`, devices and fifos, symbolic links that are absolute or climb out of the tree, entries below a link, hard links to anything but an unpacked file, and archives over 200,000 entries or 2 GiB; writes go through `os.Root`. The tree is built in a temporary directory and renamed into place, so a directory under its final name is always complete. | |
| 47 | 48 | - **Only pinned versions are kept**: at every start everything else in `externals/` (other versions, other architectures, interrupted downloads) is removed. | |
| @@ -42,6 +42,7 @@ The host provides Node.js to the jobs **outside the image**, like its own execut | |||||
| 42 | | `node20_alpine` | 20.20.2 | `github.com/actions/alpine_nodejs` release `v20.20.2` | x64 | | 42 | | `node20_alpine` | 20.20.2 | `github.com/actions/alpine_nodejs` release `v20.20.2` | x64 | | ||
| 43 | | `node24_alpine` | 24.21.0 | `github.com/actions/alpine_nodejs` release `v24.21.0` | x64 | | 43 | | `node24_alpine` | 24.21.0 | `github.com/actions/alpine_nodejs` release `v24.21.0` | x64 | | ||
| 44 | 44 | ||||
| 45 | + | - **Node versions of actions** follow github.com today (`RequireNode24` and `Node24ByDefault` of the executor's `ActionsConfig`, both set by the sandbox through `DefaultActionsConfig`): `using: node12`, `node16` and `node20` actions run on `node24`, whatever `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24` and `ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION` say, and `Complete job` ends with `##[warning]Node.js 20 is deprecated. The following actions target Node.js 20 but are being forced to run on Node.js 24: <actions, sorted ignoring case>. For more information see: …` (recordings `act-node-*`). Both flags off give the older phases of the runner. | |||
| 45 | - **Download.** `ermite-runner run` (and `sandbox-check`) fetch what is missing at start, into `$ERMITE_RUNNER_STATE_DIR/externals/<name>-<version>-<arch>/`, in the background; the start waits for them at most 2 minutes (a runner with a network normally takes its first job with everything in place, one without never waits long). The archive is the `.tar.gz` (the Go standard library has no xz); its SHA-256 is checked **before** anything is unpacked. Redirects must stay on https. | 46 | - **Download.** `ermite-runner run` (and `sandbox-check`) fetch what is missing at start, into `$ERMITE_RUNNER_STATE_DIR/externals/<name>-<version>-<arch>/`, in the background; the start waits for them at most 2 minutes (a runner with a network normally takes its first job with everything in place, one without never waits long). The archive is the `.tar.gz` (the Go standard library has no xz); its SHA-256 is checked **before** anything is unpacked. Redirects must stay on https. | ||
| 46 | - **Extraction** keeps the top directory out and refuses absolute names, `..`, devices and fifos, symbolic links that are absolute or climb out of the tree, entries below a link, hard links to anything but an unpacked file, and archives over 200,000 entries or 2 GiB; writes go through `os.Root`. The tree is built in a temporary directory and renamed into place, so a directory under its final name is always complete. | 47 | - **Extraction** keeps the top directory out and refuses absolute names, `..`, devices and fifos, symbolic links that are absolute or climb out of the tree, entries below a link, hard links to anything but an unpacked file, and archives over 200,000 entries or 2 GiB; writes go through `os.Root`. The tree is built in a temporary directory and renamed into place, so a directory under its final name is always complete. | ||
| 47 | - **Only pinned versions are kept**: at every start everything else in `externals/` (other versions, other architectures, interrupted downloads) is removed. | 48 | - **Only pinned versions are kept**: at every start everything else in `externals/` (other versions, other architectures, interrupted downloads) is removed. | ||
M internal/executor/actions_node.go +9 -3
| @@ -51,11 +51,17 @@ func (j *job) nodeEnvFlag(name string, wf *ordered) envFlag { | |||
| 51 | 51 | ||
| 52 | 52 | // nodeVersionDefault is NodeUtil.DetermineActionsNodeVersion | |
| 53 | 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 is | |
| 55 | - | // the server's flag, ActionsConfig.Node24ByDefault here. | |
| 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. | |
| 56 | 57 | func (j *job) nodeVersionDefault(wf *ordered) (version, warning string) { | |
| 57 | 58 | force := j.nodeEnvFlag(forceNode24Var, wf) | |
| 58 | 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 | + | } | |
| 59 | 65 | byDefault := j.cfg.Actions.Node24ByDefault | |
| 60 | 66 | bothWorkflow := force.on && allow.on && force.fromWorkflow && allow.fromWorkflow | |
| 61 | 67 | bothSystem := force.on && allow.on && force.fromSystem && allow.fromSystem | |
| @@ -106,7 +112,7 @@ func (j *job) nodeVersionFor(s *stepRun, def *actionDef) error { | |||
| 106 | 112 | j.mu.Lock() | |
| 107 | 113 | j.upgradedToNode24 = appendUnique(j.upgradedToNode24, actionName(s.n)) | |
| 108 | 114 | j.mu.Unlock() | |
| 109 | - | if j.cfg.Actions.Node24ByDefault { | |
| 115 | + | if j.cfg.Actions.Node24ByDefault && !j.cfg.Actions.RequireNode24 { | |
| 110 | 116 | j.out(s.num, "Node 20 is being deprecated. This workflow is running with Node 24 by default. "+ | |
| 111 | 117 | "If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable. "+ | |
| 112 | 118 | "For more information see: "+node20DeprecationURL) | |
| @@ -51,11 +51,17 @@ func (j *job) nodeEnvFlag(name string, wf *ordered) envFlag { | |||||
| 51 | 51 | ||||
| 52 | // nodeVersionDefault is NodeUtil.DetermineActionsNodeVersion | 52 | // nodeVersionDefault is NodeUtil.DetermineActionsNodeVersion | ||
| 53 | // (NodeUtil.cs:50-110) for an action that says node20: the version the action | 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 is | 54 | + | // runs on and the warning of a conflicting configuration. useNode24ByDefault and |
| 55 | - | // the server's flag, ActionsConfig.Node24ByDefault here. | 55 | + | // requireNode24 are the server's flags, ActionsConfig.Node24ByDefault and |
| 56 | + | // RequireNode24 here. | |||
| 56 | func (j *job) nodeVersionDefault(wf *ordered) (version, warning string) { | 57 | func (j *job) nodeVersionDefault(wf *ordered) (version, warning string) { | ||
| 57 | force := j.nodeEnvFlag(forceNode24Var, wf) | 58 | force := j.nodeEnvFlag(forceNode24Var, wf) | ||
| 58 | allow := j.nodeEnvFlag(allowUnsecureVar, 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 | + | } | |||
| 59 | byDefault := j.cfg.Actions.Node24ByDefault | 65 | byDefault := j.cfg.Actions.Node24ByDefault | ||
| 60 | bothWorkflow := force.on && allow.on && force.fromWorkflow && allow.fromWorkflow | 66 | bothWorkflow := force.on && allow.on && force.fromWorkflow && allow.fromWorkflow | ||
| 61 | bothSystem := force.on && allow.on && force.fromSystem && allow.fromSystem | 67 | bothSystem := force.on && allow.on && force.fromSystem && allow.fromSystem | ||
| @@ -106,7 +112,7 @@ func (j *job) nodeVersionFor(s *stepRun, def *actionDef) error { | |||||
| 106 | j.mu.Lock() | 112 | j.mu.Lock() | ||
| 107 | j.upgradedToNode24 = appendUnique(j.upgradedToNode24, actionName(s.n)) | 113 | j.upgradedToNode24 = appendUnique(j.upgradedToNode24, actionName(s.n)) | ||
| 108 | j.mu.Unlock() | 114 | j.mu.Unlock() | ||
| 109 | - | if j.cfg.Actions.Node24ByDefault { | 115 | + | if j.cfg.Actions.Node24ByDefault && !j.cfg.Actions.RequireNode24 { |
| 110 | j.out(s.num, "Node 20 is being deprecated. This workflow is running with Node 24 by default. "+ | 116 | j.out(s.num, "Node 20 is being deprecated. This workflow is running with Node 24 by default. "+ | ||
| 111 | "If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable. "+ | 117 | "If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable. "+ | ||
| 112 | "For more information see: "+node20DeprecationURL) | 118 | "For more information see: "+node20DeprecationURL) | ||
M internal/executor/actions_test.go +51 -0
| @@ -202,6 +202,57 @@ func TestManifestErrors(t *testing.T) { | |||
| 202 | 202 | } | |
| 203 | 203 | } | |
| 204 | 204 | ||
| 205 | + | // The phase of github.com today (recordings act-node-version, act-node-legacy, | |
| 206 | + | // act-node-force24): node12, node16 and node20 actions run on node24 whatever | |
| 207 | + | // the variables say, and the job ends with one warning that lists them. | |
| 208 | + | func TestNodeVersionGitHubPhase(t *testing.T) { | |
| 209 | + | const tail = ". For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/" | |
| 210 | + | act := func(using string) string { | |
| 211 | + | return "name: a\nruns:\n using: " + using + "\n main: m.sh\n" | |
| 212 | + | } | |
| 213 | + | tree := files{"a/action.yml": act("node20"), "a/m.sh": "echo ran\n", "n12/action.yml": act("node12"), "n12/m.sh": "echo ran\n", | |
| 214 | + | "n16/action.yml": act("node16"), "n16/m.sh": "echo ran\n", "n24/action.yml": act("node24"), "n24/m.sh": "echo ran\n"} | |
| 215 | + | run := func(yaml string) *recorder { | |
| 216 | + | t.Helper() | |
| 217 | + | rec, res := actionRun(t, tree, yaml, func(c *Config) { c.Actions = DefaultActionsConfig(nil); c.Actions.ExternalsDir = fakeNode(t) }) | |
| 218 | + | if res.Conclusion != ConclusionSuccess { | |
| 219 | + | t.Fatalf("%+v\n%s", res, logOf(rec.steps[1])) | |
| 220 | + | } | |
| 221 | + | return rec | |
| 222 | + | } | |
| 223 | + | // Order: case-insensitive, one entry per action; node24 actions are not listed. | |
| 224 | + | rec := run(" - uses: ./n16\n - uses: ./n24\n - uses: ./a\n - uses: ./n12\n - uses: ./a\n") | |
| 225 | + | want := "##[warning]Node.js 20 is deprecated. The following actions target Node.js 20 but are being forced to run on Node.js 24: ./a, ./n12, ./n16" + tail | |
| 226 | + | if got := logOf(rec.steps[len(rec.steps)-1]); !strings.Contains(got, "Cleaning up orphan processes\n"+want) { | |
| 227 | + | t.Errorf("complete job:\n%s", got) | |
| 228 | + | } | |
| 229 | + | for _, name := range []string{"Run ./n12", "Run ./n16", "Run ./a", "Run ./n24"} { | |
| 230 | + | if got := logOf(rec.step(t, name)); !strings.Contains(got, "[node24]") { | |
| 231 | + | t.Errorf("%s: %s", name, got) | |
| 232 | + | } | |
| 233 | + | } | |
| 234 | + | // The variables change nothing: not even ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION, nor both. | |
| 235 | + | for _, env := range []string{ | |
| 236 | + | " ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: 'true'\n", | |
| 237 | + | " FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'\n", | |
| 238 | + | " FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'\n ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: 'true'\n", | |
| 239 | + | } { | |
| 240 | + | rec := run(" - uses: ./a\n env:\n" + env) | |
| 241 | + | got := logOf(rec.step(t, "Run ./a")) | |
| 242 | + | if !strings.Contains(got, "[node24]") || strings.Contains(got, "Both ") || strings.Contains(got, "being deprecated") { | |
| 243 | + | t.Errorf("%s: %s", env, got) | |
| 244 | + | } | |
| 245 | + | if last := logOf(rec.steps[len(rec.steps)-1]); !strings.Contains(last, "forced to run on Node.js 24: ./a"+tail) { | |
| 246 | + | t.Errorf("%s: complete job:\n%s", env, last) | |
| 247 | + | } | |
| 248 | + | } | |
| 249 | + | // Only node24 actions: no warning. | |
| 250 | + | rec = run(" - uses: ./n24\n") | |
| 251 | + | if got := logOf(rec.steps[len(rec.steps)-1]); strings.Contains(got, "Node.js 20 is deprecated") { | |
| 252 | + | t.Errorf("node24 only:\n%s", got) | |
| 253 | + | } | |
| 254 | + | } | |
| 255 | + | ||
| 205 | 256 | func TestNodeVersionRules(t *testing.T) { | |
| 206 | 257 | act := func(using string) files { | |
| 207 | 258 | return files{"a/action.yml": "name: a\nruns:\n using: " + using + "\n main: m.sh\n", "a/m.sh": "echo ran\n"} | |
| @@ -202,6 +202,57 @@ func TestManifestErrors(t *testing.T) { | |||||
| 202 | } | 202 | } | ||
| 203 | } | 203 | } | ||
| 204 | 204 | ||||
| 205 | + | // The phase of github.com today (recordings act-node-version, act-node-legacy, | |||
| 206 | + | // act-node-force24): node12, node16 and node20 actions run on node24 whatever | |||
| 207 | + | // the variables say, and the job ends with one warning that lists them. | |||
| 208 | + | func TestNodeVersionGitHubPhase(t *testing.T) { | |||
| 209 | + | const tail = ". For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/" | |||
| 210 | + | act := func(using string) string { | |||
| 211 | + | return "name: a\nruns:\n using: " + using + "\n main: m.sh\n" | |||
| 212 | + | } | |||
| 213 | + | tree := files{"a/action.yml": act("node20"), "a/m.sh": "echo ran\n", "n12/action.yml": act("node12"), "n12/m.sh": "echo ran\n", | |||
| 214 | + | "n16/action.yml": act("node16"), "n16/m.sh": "echo ran\n", "n24/action.yml": act("node24"), "n24/m.sh": "echo ran\n"} | |||
| 215 | + | run := func(yaml string) *recorder { | |||
| 216 | + | t.Helper() | |||
| 217 | + | rec, res := actionRun(t, tree, yaml, func(c *Config) { c.Actions = DefaultActionsConfig(nil); c.Actions.ExternalsDir = fakeNode(t) }) | |||
| 218 | + | if res.Conclusion != ConclusionSuccess { | |||
| 219 | + | t.Fatalf("%+v\n%s", res, logOf(rec.steps[1])) | |||
| 220 | + | } | |||
| 221 | + | return rec | |||
| 222 | + | } | |||
| 223 | + | // Order: case-insensitive, one entry per action; node24 actions are not listed. | |||
| 224 | + | rec := run(" - uses: ./n16\n - uses: ./n24\n - uses: ./a\n - uses: ./n12\n - uses: ./a\n") | |||
| 225 | + | want := "##[warning]Node.js 20 is deprecated. The following actions target Node.js 20 but are being forced to run on Node.js 24: ./a, ./n12, ./n16" + tail | |||
| 226 | + | if got := logOf(rec.steps[len(rec.steps)-1]); !strings.Contains(got, "Cleaning up orphan processes\n"+want) { | |||
| 227 | + | t.Errorf("complete job:\n%s", got) | |||
| 228 | + | } | |||
| 229 | + | for _, name := range []string{"Run ./n12", "Run ./n16", "Run ./a", "Run ./n24"} { | |||
| 230 | + | if got := logOf(rec.step(t, name)); !strings.Contains(got, "[node24]") { | |||
| 231 | + | t.Errorf("%s: %s", name, got) | |||
| 232 | + | } | |||
| 233 | + | } | |||
| 234 | + | // The variables change nothing: not even ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION, nor both. | |||
| 235 | + | for _, env := range []string{ | |||
| 236 | + | " ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: 'true'\n", | |||
| 237 | + | " FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'\n", | |||
| 238 | + | " FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'\n ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: 'true'\n", | |||
| 239 | + | } { | |||
| 240 | + | rec := run(" - uses: ./a\n env:\n" + env) | |||
| 241 | + | got := logOf(rec.step(t, "Run ./a")) | |||
| 242 | + | if !strings.Contains(got, "[node24]") || strings.Contains(got, "Both ") || strings.Contains(got, "being deprecated") { | |||
| 243 | + | t.Errorf("%s: %s", env, got) | |||
| 244 | + | } | |||
| 245 | + | if last := logOf(rec.steps[len(rec.steps)-1]); !strings.Contains(last, "forced to run on Node.js 24: ./a"+tail) { | |||
| 246 | + | t.Errorf("%s: complete job:\n%s", env, last) | |||
| 247 | + | } | |||
| 248 | + | } | |||
| 249 | + | // Only node24 actions: no warning. | |||
| 250 | + | rec = run(" - uses: ./n24\n") | |||
| 251 | + | if got := logOf(rec.steps[len(rec.steps)-1]); strings.Contains(got, "Node.js 20 is deprecated") { | |||
| 252 | + | t.Errorf("node24 only:\n%s", got) | |||
| 253 | + | } | |||
| 254 | + | } | |||
| 255 | + | ||||
| 205 | func TestNodeVersionRules(t *testing.T) { | 256 | func TestNodeVersionRules(t *testing.T) { | ||
| 206 | act := func(using string) files { | 257 | act := func(using string) files { | ||
| 207 | return files{"a/action.yml": "name: a\nruns:\n using: " + using + "\n main: m.sh\n", "a/m.sh": "echo ran\n"} | 258 | return files{"a/action.yml": "name: a\nruns:\n using: " + using + "\n main: m.sh\n", "a/m.sh": "echo ran\n"} | ||
M internal/executor/actions_types.go +14 -0
| @@ -46,6 +46,13 @@ type ActionsConfig struct { | |||
| 46 | 46 | // (HandlerFactory.cs, NodeUtil.DetermineActionsNodeVersion): ermite has no | |
| 47 | 47 | // feature flags, so the default is pinned here. | |
| 48 | 48 | Node24ByDefault bool | |
| 49 | + | // RequireNode24 is the server flag actions.runner.requirenode24 (phase 3 | |
| 50 | + | // of the migration): every node20 action runs on node24 whatever | |
| 51 | + | // FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 and ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION | |
| 52 | + | // say, without the "being deprecated" notice or the conflict warning. | |
| 53 | + | // github.com does so today (recordings act-node-*). The zero value is the | |
| 54 | + | // runner's phase 1; the guest sets both flags (see DefaultActionsConfig). | |
| 55 | + | RequireNode24 bool | |
| 49 | 56 | // Containers are the time limits of the docker commands of job containers | |
| 50 | 57 | // and services; the zero value means the defaults. | |
| 51 | 58 | Containers ContainerTimeouts | |
| @@ -54,6 +61,13 @@ type ActionsConfig struct { | |||
| 54 | 61 | Backoff func() time.Duration | |
| 55 | 62 | } | |
| 56 | 63 | ||
| 64 | + | // DefaultActionsConfig is the configuration of a job in the sandbox: GitHub's | |
| 65 | + | // current node migration phase, in which node12, node16 and node20 actions run | |
| 66 | + | // on node24 and the job ends with the "forced to run on Node.js 24" warning. | |
| 67 | + | func DefaultActionsConfig(r Resolver) ActionsConfig { | |
| 68 | + | return ActionsConfig{Resolver: r, Node24ByDefault: true, RequireNode24: true} | |
| 69 | + | } | |
| 70 | + | ||
| 57 | 71 | const defaultExternalsDir = "/home/runner/externals" | |
| 58 | 72 | ||
| 59 | 73 | // stage is ActionRunStage. | |
| @@ -46,6 +46,13 @@ type ActionsConfig struct { | |||||
| 46 | // (HandlerFactory.cs, NodeUtil.DetermineActionsNodeVersion): ermite has no | 46 | // (HandlerFactory.cs, NodeUtil.DetermineActionsNodeVersion): ermite has no | ||
| 47 | // feature flags, so the default is pinned here. | 47 | // feature flags, so the default is pinned here. | ||
| 48 | Node24ByDefault bool | 48 | Node24ByDefault bool | ||
| 49 | + | // RequireNode24 is the server flag actions.runner.requirenode24 (phase 3 | |||
| 50 | + | // of the migration): every node20 action runs on node24 whatever | |||
| 51 | + | // FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 and ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION | |||
| 52 | + | // say, without the "being deprecated" notice or the conflict warning. | |||
| 53 | + | // github.com does so today (recordings act-node-*). The zero value is the | |||
| 54 | + | // runner's phase 1; the guest sets both flags (see DefaultActionsConfig). | |||
| 55 | + | RequireNode24 bool | |||
| 49 | // Containers are the time limits of the docker commands of job containers | 56 | // Containers are the time limits of the docker commands of job containers | ||
| 50 | // and services; the zero value means the defaults. | 57 | // and services; the zero value means the defaults. | ||
| 51 | Containers ContainerTimeouts | 58 | Containers ContainerTimeouts | ||
| @@ -54,6 +61,13 @@ type ActionsConfig struct { | |||||
| 54 | Backoff func() time.Duration | 61 | Backoff func() time.Duration | ||
| 55 | } | 62 | } | ||
| 56 | 63 | ||||
| 64 | + | // DefaultActionsConfig is the configuration of a job in the sandbox: GitHub's | |||
| 65 | + | // current node migration phase, in which node12, node16 and node20 actions run | |||
| 66 | + | // on node24 and the job ends with the "forced to run on Node.js 24" warning. | |||
| 67 | + | func DefaultActionsConfig(r Resolver) ActionsConfig { | |||
| 68 | + | return ActionsConfig{Resolver: r, Node24ByDefault: true, RequireNode24: true} | |||
| 69 | + | } | |||
| 70 | + | ||||
| 57 | const defaultExternalsDir = "/home/runner/externals" | 71 | const defaultExternalsDir = "/home/runner/externals" | ||
| 58 | 72 | ||||
| 59 | // stage is ActionRunStage. | 73 | // stage is ActionRunStage. | ||
M internal/executor/results_test.go +64 -0
| @@ -4,6 +4,7 @@ package executor | |||
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | 6 | "fmt" | |
| 7 | + | "slices" | |
| 7 | 8 | "strings" | |
| 8 | 9 | "testing" | |
| 9 | 10 | "time" | |
| @@ -116,6 +117,69 @@ func TestCatastrophicMatcherIsRemovedAfterThreeTimeouts(t *testing.T) { | |||
| 116 | 117 | } | |
| 117 | 118 | } | |
| 118 | 119 | ||
| 120 | + | // Mirror of the recording match-catastrophic: the removal is per step | |
| 121 | + | // (OutputManager.Remove edits the list of the step), so the next step, which | |
| 122 | + | // does not add anything, starts from the matchers of the job and times out and | |
| 123 | + | // removes the matcher again after 3 more timeouts; "good" works in both. | |
| 124 | + | func TestCatastrophicMatcherRemovalIsPerStep(t *testing.T) { | |
| 125 | + | needTools(t, "bash") | |
| 126 | + | rr, res := runResults(t, wfHead+` steps: | |
| 127 | + | - name: prepare | |
| 128 | + | run: | | |
| 129 | + | cat > bad.json <<'JSON' | |
| 130 | + | {"problemMatcher":[{"owner":"catastrophic","pattern":[{"regexp":"^((a+)+)$","message":1}]}]} | |
| 131 | + | JSON | |
| 132 | + | cat > good.json <<'JSON' | |
| 133 | + | {"problemMatcher":[{"owner":"good","severity":"warning","pattern":[{"regexp":"^good: (.+)$","message":1}]}]} | |
| 134 | + | JSON | |
| 135 | + | - name: first | |
| 136 | + | run: | | |
| 137 | + | echo "::add-matcher::bad.json" | |
| 138 | + | echo "::add-matcher::good.json" | |
| 139 | + | echo "good: before the long line" | |
| 140 | + | echo "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!" | |
| 141 | + | echo "good: after the long line" | |
| 142 | + | - name: second | |
| 143 | + | run: | | |
| 144 | + | echo "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!" | |
| 145 | + | echo "aaa" | |
| 146 | + | echo "good: still alive" | |
| 147 | + | `, nil) | |
| 148 | + | if res.Conclusion != ConclusionSuccess { | |
| 149 | + | t.Fatalf("%+v", res) | |
| 150 | + | } | |
| 151 | + | const removing = "##[warning]Removing issue matcher 'catastrophic'. Matcher failed 3 times. Error: The Regex engine has timed out while trying to match a pattern to an input string." | |
| 152 | + | // The lines of the log, in order, with the long text of the removal cut. | |
| 153 | + | lines := func(log string) []string { | |
| 154 | + | var out []string | |
| 155 | + | for _, l := range strings.Split(log, "\n") { | |
| 156 | + | if strings.HasPrefix(l, removing) { | |
| 157 | + | l = removing | |
| 158 | + | } | |
| 159 | + | out = append(out, l) | |
| 160 | + | } | |
| 161 | + | return out | |
| 162 | + | } | |
| 163 | + | has := func(log string, want ...string) bool { | |
| 164 | + | got := lines(log) | |
| 165 | + | for i := range got { | |
| 166 | + | if slices.Equal(got[i:min(i+len(want), len(got))], want) { | |
| 167 | + | return true | |
| 168 | + | } | |
| 169 | + | } | |
| 170 | + | return false | |
| 171 | + | } | |
| 172 | + | const long = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!" | |
| 173 | + | first := logOf(rr.step(t, "first")) | |
| 174 | + | if !has(first, "##[warning]good: before the long line", removing, long, "##[warning]good: after the long line") { | |
| 175 | + | t.Errorf("first:\n%s", first) | |
| 176 | + | } | |
| 177 | + | second := logOf(rr.step(t, "second")) | |
| 178 | + | if !has(second, removing, long, "aaa", "##[warning]good: still alive") { | |
| 179 | + | t.Errorf("second:\n%s", second) | |
| 180 | + | } | |
| 181 | + | } | |
| 182 | + | ||
| 119 | 183 | func TestMatcherTimeoutFromEnvironment(t *testing.T) { | |
| 120 | 184 | needTools(t, "bash") | |
| 121 | 185 | start := time.Now() | |
| @@ -4,6 +4,7 @@ package executor | |||||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | + | "slices" | |||
| 7 | "strings" | 8 | "strings" | ||
| 8 | "testing" | 9 | "testing" | ||
| 9 | "time" | 10 | "time" | ||
| @@ -116,6 +117,69 @@ func TestCatastrophicMatcherIsRemovedAfterThreeTimeouts(t *testing.T) { | |||||
| 116 | } | 117 | } | ||
| 117 | } | 118 | } | ||
| 118 | 119 | ||||
| 120 | + | // Mirror of the recording match-catastrophic: the removal is per step | |||
| 121 | + | // (OutputManager.Remove edits the list of the step), so the next step, which | |||
| 122 | + | // does not add anything, starts from the matchers of the job and times out and | |||
| 123 | + | // removes the matcher again after 3 more timeouts; "good" works in both. | |||
| 124 | + | func TestCatastrophicMatcherRemovalIsPerStep(t *testing.T) { | |||
| 125 | + | needTools(t, "bash") | |||
| 126 | + | rr, res := runResults(t, wfHead+` steps: | |||
| 127 | + | - name: prepare | |||
| 128 | + | run: | | |||
| 129 | + | cat > bad.json <<'JSON' | |||
| 130 | + | {"problemMatcher":[{"owner":"catastrophic","pattern":[{"regexp":"^((a+)+)$","message":1}]}]} | |||
| 131 | + | JSON | |||
| 132 | + | cat > good.json <<'JSON' | |||
| 133 | + | {"problemMatcher":[{"owner":"good","severity":"warning","pattern":[{"regexp":"^good: (.+)$","message":1}]}]} | |||
| 134 | + | JSON | |||
| 135 | + | - name: first | |||
| 136 | + | run: | | |||
| 137 | + | echo "::add-matcher::bad.json" | |||
| 138 | + | echo "::add-matcher::good.json" | |||
| 139 | + | echo "good: before the long line" | |||
| 140 | + | echo "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!" | |||
| 141 | + | echo "good: after the long line" | |||
| 142 | + | - name: second | |||
| 143 | + | run: | | |||
| 144 | + | echo "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!" | |||
| 145 | + | echo "aaa" | |||
| 146 | + | echo "good: still alive" | |||
| 147 | + | `, nil) | |||
| 148 | + | if res.Conclusion != ConclusionSuccess { | |||
| 149 | + | t.Fatalf("%+v", res) | |||
| 150 | + | } | |||
| 151 | + | const removing = "##[warning]Removing issue matcher 'catastrophic'. Matcher failed 3 times. Error: The Regex engine has timed out while trying to match a pattern to an input string." | |||
| 152 | + | // The lines of the log, in order, with the long text of the removal cut. | |||
| 153 | + | lines := func(log string) []string { | |||
| 154 | + | var out []string | |||
| 155 | + | for _, l := range strings.Split(log, "\n") { | |||
| 156 | + | if strings.HasPrefix(l, removing) { | |||
| 157 | + | l = removing | |||
| 158 | + | } | |||
| 159 | + | out = append(out, l) | |||
| 160 | + | } | |||
| 161 | + | return out | |||
| 162 | + | } | |||
| 163 | + | has := func(log string, want ...string) bool { | |||
| 164 | + | got := lines(log) | |||
| 165 | + | for i := range got { | |||
| 166 | + | if slices.Equal(got[i:min(i+len(want), len(got))], want) { | |||
| 167 | + | return true | |||
| 168 | + | } | |||
| 169 | + | } | |||
| 170 | + | return false | |||
| 171 | + | } | |||
| 172 | + | const long = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!" | |||
| 173 | + | first := logOf(rr.step(t, "first")) | |||
| 174 | + | if !has(first, "##[warning]good: before the long line", removing, long, "##[warning]good: after the long line") { | |||
| 175 | + | t.Errorf("first:\n%s", first) | |||
| 176 | + | } | |||
| 177 | + | second := logOf(rr.step(t, "second")) | |||
| 178 | + | if !has(second, removing, long, "aaa", "##[warning]good: still alive") { | |||
| 179 | + | t.Errorf("second:\n%s", second) | |||
| 180 | + | } | |||
| 181 | + | } | |||
| 182 | + | ||||
| 119 | func TestMatcherTimeoutFromEnvironment(t *testing.T) { | 183 | func TestMatcherTimeoutFromEnvironment(t *testing.T) { | ||
| 120 | needTools(t, "bash") | 184 | needTools(t, "bash") | ||
| 121 | start := time.Now() | 185 | start := time.Now() | ||
M internal/guest/jobexec.go +1 -1
| @@ -256,7 +256,7 @@ func runJob(ctx context.Context, conn *execstream.Conn, cfg ExecConfig, spec *ex | |||
| 256 | 256 | Runner: executor.RunnerInfo{Name: spec.GetRunnerName(), Environment: env}, | |
| 257 | 257 | RunnerVersion: spec.GetRunnerVersion(), | |
| 258 | 258 | Home: su.Home, ToolCache: os.Getenv("RUNNER_TOOL_CACHE"), BaseEnv: stepEnv(os.Environ(), su), Credential: su.Credential, | |
| 259 | - | Debug: spec.GetDebug(), Reporter: rep, Actions: executor.ActionsConfig{Resolver: res.resolve}, Log: zerowrap.FromCtx(ctx), | |
| 259 | + | Debug: spec.GetDebug(), Reporter: rep, Actions: executor.DefaultActionsConfig(res.resolve), Log: zerowrap.FromCtx(ctx), | |
| 260 | 260 | SendAnnotations: spec.GetSendAnnotations(), SendSummaries: spec.GetSendSummaries(), | |
| 261 | 261 | TermAfter: cfg.TermAfter, KillAfter: cfg.KillAfter, WaitDelay: cfg.WaitDelay, | |
| 262 | 262 | }) | |
| @@ -256,7 +256,7 @@ func runJob(ctx context.Context, conn *execstream.Conn, cfg ExecConfig, spec *ex | |||||
| 256 | Runner: executor.RunnerInfo{Name: spec.GetRunnerName(), Environment: env}, | 256 | Runner: executor.RunnerInfo{Name: spec.GetRunnerName(), Environment: env}, | ||
| 257 | RunnerVersion: spec.GetRunnerVersion(), | 257 | RunnerVersion: spec.GetRunnerVersion(), | ||
| 258 | Home: su.Home, ToolCache: os.Getenv("RUNNER_TOOL_CACHE"), BaseEnv: stepEnv(os.Environ(), su), Credential: su.Credential, | 258 | Home: su.Home, ToolCache: os.Getenv("RUNNER_TOOL_CACHE"), BaseEnv: stepEnv(os.Environ(), su), Credential: su.Credential, | ||
| 259 | - | Debug: spec.GetDebug(), Reporter: rep, Actions: executor.ActionsConfig{Resolver: res.resolve}, Log: zerowrap.FromCtx(ctx), | 259 | + | Debug: spec.GetDebug(), Reporter: rep, Actions: executor.DefaultActionsConfig(res.resolve), Log: zerowrap.FromCtx(ctx), |
| 260 | SendAnnotations: spec.GetSendAnnotations(), SendSummaries: spec.GetSendSummaries(), | 260 | SendAnnotations: spec.GetSendAnnotations(), SendSummaries: spec.GetSendSummaries(), | ||
| 261 | TermAfter: cfg.TermAfter, KillAfter: cfg.KillAfter, WaitDelay: cfg.WaitDelay, | 261 | TermAfter: cfg.TermAfter, KillAfter: cfg.KillAfter, WaitDelay: cfg.WaitDelay, | ||
| 262 | }) | 262 | }) | ||