diff --git a/pkg/runner/expression.go b/pkg/runner/expression.go index b83d2f2d1c1..dd1e2bad591 100644 --- a/pkg/runner/expression.go +++ b/pkg/runner/expression.go @@ -23,6 +23,9 @@ import ( type ExpressionEvaluator interface { evaluate(context.Context, string, exprparser.DefaultStatusCheck) (interface{}, error) EvaluateYamlNode(context.Context, *yaml.Node) error + // EvaluateYamlNodeGetResult evaluates the yaml node and returns the result without modifying the original node. + // This is useful when multiple goroutines need to evaluate the same shared yaml.Node concurrently. + EvaluateYamlNodeGetResult(context.Context, *yaml.Node) (*yaml.Node, error) Interpolate(context.Context, string) string } @@ -377,6 +380,13 @@ func (ee expressionEvaluator) EvaluateYamlNode(ctx context.Context, node *yaml.N return nil } +// EvaluateYamlNodeGetResult evaluates the yaml node and returns the result without modifying the original node. +// This is useful when multiple goroutines need to evaluate the same shared yaml.Node concurrently, +// such as when evaluating matrix jobs in parallel. +func (ee expressionEvaluator) EvaluateYamlNodeGetResult(ctx context.Context, node *yaml.Node) (*yaml.Node, error) { + return ee.evaluateYamlNodeInternal(ctx, node) +} + func (ee expressionEvaluator) Interpolate(ctx context.Context, in string) string { if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") { return in diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 5d4277123ed..06d62d4ddab 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -26,6 +26,7 @@ import ( "github.com/nektos/act/pkg/exprparser" "github.com/nektos/act/pkg/model" "github.com/opencontainers/selinux/go-selinux" + "gopkg.in/yaml.v3" ) // RunContext contains info about current job @@ -229,6 +230,10 @@ func (rc *RunContext) startHostEnvironment() common.Executor { rc.Env[fmt.Sprintf("RUNNER_%s", strings.ToUpper(k))] = v } } + // Set AGENT_TOOLSDIRECTORY to match RUNNER_TOOL_CACHE for self-hosted mode + // This ensures that actions like setup-python use the correct tool cache path + // instead of potentially using a host system's AGENT_TOOLSDIRECTORY value + rc.Env["AGENT_TOOLSDIRECTORY"] = toolCache for _, env := range os.Environ() { if k, v, ok := strings.Cut(env, "="); ok { // don't override @@ -764,12 +769,66 @@ func (rc *RunContext) runsOnPlatformNames(ctx context.Context) []string { return []string{} } - if err := rc.ExprEval.EvaluateYamlNode(ctx, &job.RawRunsOn); err != nil { + // Use EvaluateYamlNodeGetResult to avoid modifying the shared yaml.Node. + // This is important when multiple matrix jobs run in parallel - they share + // the same Job object, and modifying RawRunsOn would cause race conditions. + evaluatedNode, err := rc.ExprEval.EvaluateYamlNodeGetResult(ctx, &job.RawRunsOn) + if err != nil { common.Logger(ctx).Errorf("Error while evaluating runs-on: %v", err) return []string{} } - return job.RunsOn() + if evaluatedNode == nil { + return []string{} + } + + return extractRunsOnFromNode(*evaluatedNode) +} + +// extractRunsOnFromNode extracts the runs-on labels from a yaml.Node. +// This mirrors the logic in (*Job).RunsOn() but works on any yaml.Node. +func extractRunsOnFromNode(node yaml.Node) []string { + switch node.Kind { + case yaml.MappingNode: + var val struct { + Group string + Labels yaml.Node + } + + if err := node.Decode(&val); err != nil { + return nil + } + + labels := extractRunsOnFromNode(val.Labels) + + if val.Group != "" { + labels = append(labels, val.Group) + } + + return labels + default: + return nodeAsStringSlice(node) + } +} + +// nodeAsStringSlice converts a yaml.Node to a []string. +// This is a copy of the function from model/workflow.go to avoid export issues. +func nodeAsStringSlice(node yaml.Node) []string { + switch node.Kind { + case yaml.ScalarNode: + var val string + if err := node.Decode(&val); err != nil { + return nil + } + return []string{val} + case yaml.SequenceNode: + var val []string + if err := node.Decode(&val); err != nil { + return nil + } + return val + } + return nil } func (rc *RunContext) platformImage(ctx context.Context) string {