From 25c9930f47e692bb8973f5f23be33aeedae4f1cb Mon Sep 17 00:00:00 2001 From: Sebastian Rath Date: Mon, 2 Feb 2026 00:55:00 -0500 Subject: [PATCH 1/7] Remove some output from e2e tests as they are too noisy for reference files --- .github/workflows/workflow.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index fb5298a..c25a5eb 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -4,6 +4,7 @@ on: - '*' branches: - main + - "feature/docker-run-node" pull_request: branches: From 92ce7356db2d7605a618380f909feb0863aeea11 Mon Sep 17 00:00:00 2001 From: Sebatian Rath Date: Sun, 1 Feb 2026 21:03:01 -0500 Subject: [PATCH 2/7] Add steps context for tracking execution state --- core/context.go | 109 ++++++++++++++++++++++++++++++++++++++- core/executions.go | 12 +++++ core/github_evaluator.go | 18 ++++++- core/graph.go | 86 ++++++++++++++++++------------ core/inputs.go | 7 ++- core/outputs.go | 76 ++++++++++++++++++++------- 6 files changed, 252 insertions(+), 56 deletions(-) diff --git a/core/context.go b/core/context.go index f79105f..c86da1a 100644 --- a/core/context.go +++ b/core/context.go @@ -11,6 +11,8 @@ import ( "github.com/google/uuid" ) +const MAX_STEP_CACHE_ITEM_SIZE = 64 * 1024 // 64KB + type ContextVisit struct { Node NodeBaseInterface `json:"-"` NodeID string `json:"node_id"` @@ -30,6 +32,82 @@ const ( Ephemeral ) +type StepCacheEntry struct { + Conclusion string `json:"conclusion"` + Outputs map[string]any `json:"outputs"` +} + +// HierarchicalMap is a memory-efficient map that chains to parent contexts. +// Each context only stores its own entries; lookups traverse the parent chain. +type HierarchicalMap[K comparable, V any] struct { + data map[K]V + parent *HierarchicalMap[K, V] +} + +// NewHierarchicalMap creates a new map, optionally chained to a parent. +func NewHierarchicalMap[K comparable, V any](parent *HierarchicalMap[K, V]) *HierarchicalMap[K, V] { + return &HierarchicalMap[K, V]{ + data: make(map[K]V), + parent: parent, + } +} + +// Get retrieves a value by key, traversing up the parent chain if not found locally. +func (m *HierarchicalMap[K, V]) Get(key K) (V, bool) { + if val, ok := m.data[key]; ok { + return val, true + } + if m.parent != nil { + return m.parent.Get(key) + } + var zero V + return zero, false +} + +// GetLocal retrieves a value only from local data, not traversing parents. +func (m *HierarchicalMap[K, V]) GetLocal(key K) (V, bool) { + val, ok := m.data[key] + return val, ok +} + +// Set stores a value in the current context only (does not affect parent). +func (m *HierarchicalMap[K, V]) Set(key K, value V) { + m.data[key] = value +} + +// All returns all entries merged from the entire chain (child entries override parent). +func (m *HierarchicalMap[K, V]) All() map[K]V { + result := make(map[K]V) + m.collectAll(result) + return result +} + +func (m *HierarchicalMap[K, V]) collectAll(result map[K]V) { + if m.parent != nil { + m.parent.collectAll(result) + } + maps.Copy(result, m.data) +} + +// StepCache is a type alias for the step output cache. +type StepCache = HierarchicalMap[string, *StepCacheEntry] + +// NewStepCache creates a new step cache, optionally chained to a parent. +func NewStepCache(parent *StepCache) *StepCache { + return NewHierarchicalMap(parent) +} + +// GetOrCreateStepEntry retrieves an existing local entry or creates a new one. +// Only checks local cache to avoid races on shared parent entries. +func GetOrCreateStepEntry(cache *StepCache, key string) *StepCacheEntry { + if entry, ok := cache.GetLocal(key); ok { + return entry + } + entry := &StepCacheEntry{Outputs: make(map[string]any)} + cache.Set(key, entry) + return entry +} + // ExecutionState is a structure whose main purpose is to provide the correct output values // and environment variables requested by nodes that were executed in subsequent goroutines. // @@ -85,6 +163,7 @@ type ExecutionState struct { OutputCacheLock *sync.RWMutex `json:"-"` DataOutputCache map[string]any `json:"dataOutputCache"` ExecutionOutputCache map[string]any `json:"executionOutputCache"` + StepCache *StepCache `json:"stepCache"` DebugCallback DebugCallback `json:"-"` } @@ -157,6 +236,7 @@ func (c *ExecutionState) PushNewExecutionState(parentNode NodeBaseInterface) *Ex OutputCacheLock: &sync.RWMutex{}, DataOutputCache: make(map[string]any), ExecutionOutputCache: make(map[string]any), + StepCache: NewStepCache(c.StepCache), Visited: visited, DebugCallback: c.DebugCallback, @@ -203,19 +283,44 @@ func (c *ExecutionState) GetDataFromOutputCache(nodeCacheId string, outputId str return nil, false } -func (c *ExecutionState) CacheDataOutput(nodeCacheId string, outputId string, value any, ct CacheType) { +func (c *ExecutionState) CacheDataOutput(node NodeBaseInterface, outputId string, value any, outputType string, ct CacheType) { c.ContextStackLock.RLock() defer c.ContextStackLock.RUnlock() c.OutputCacheLock.Lock() defer c.OutputCacheLock.Unlock() - cacheId := nodeCacheId + ":" + outputId + cacheId := node.GetCacheId() + ":" + outputId if ct == Permanent { c.ExecutionOutputCache[cacheId] = value } else { c.DataOutputCache[cacheId] = value } + + // Only cache primitive types under 64KB + if isPrimitiveType(outputType) && isUnderCacheLimit(value) { + stepEntry := GetOrCreateStepEntry(c.StepCache, node.GetId()) + stepEntry.Outputs[outputId] = value + } +} + +// isPrimitiveType returns true if the output type is a primitive type that should be cached. +func isPrimitiveType(outputType string) bool { + switch outputType { + case "string", "number", "bool": + return true + default: + return false + } +} + +// isUnderCacheLimit checks if a string value is under the cache size limit. +// Non-string primitives always return true as they are small. +func isUnderCacheLimit(value any) bool { + if s, ok := value.(string); ok { + return len(s) <= MAX_STEP_CACHE_ITEM_SIZE + } + return true } func (c *ExecutionState) EmptyDataOutputCache() { diff --git a/core/executions.go b/core/executions.go index a79e2fe..ca2b66a 100644 --- a/core/executions.go +++ b/core/executions.go @@ -30,6 +30,18 @@ func (n *Executions) Execute(outputPort OutputId, ec *ExecutionState, err error) dest, hasDest := n.GetExecutionTarget(outputPort) + // Set the step conclusion for the SOURCE node based on which output port is being executed. + // This enables ${{ steps.X.conclusion }} syntax similar to GitHub Actions. + // The conclusion is set BEFORE downstream nodes execute so they can read it. + if hasDest && dest.SrcNode != nil { + srcStepEntry := GetOrCreateStepEntry(ec.StepCache, dest.SrcNode.GetId()) + if err != nil { + srcStepEntry.Conclusion = "failure" + } else { + srcStepEntry.Conclusion = "success" + } + } + // if this is the error path, and the error port is not connected // we return the error so it won't be silently ignored if err != nil { diff --git a/core/github_evaluator.go b/core/github_evaluator.go index 9358e96..5db761e 100644 --- a/core/github_evaluator.go +++ b/core/github_evaluator.go @@ -356,7 +356,7 @@ func (e *Evaluator) resolveRootVar(name string) (any, error) { case "needs": return &GhNeedsProxy{GhNeeds: e.ctx.GhNeeds}, nil case "steps": - return e.ctx.DataOutputCache, nil + return e.ctx.StepCache, nil case "inputs": return &InputsProxy{ctx: e.ctx}, nil case "matrix": @@ -475,6 +475,9 @@ func (e *Evaluator) evaluateArrayDeref(node *actionlint.ArrayDerefNode) (any, er receiverVal = v.Secrets case *InputsProxy: receiverVal = v.ctx.Inputs + case *StepCache: + // Only flatten for iteration (steps.*) + return toSortedValues(v.All()), nil } switch m := receiverVal.(type) { @@ -514,6 +517,19 @@ func (e *Evaluator) evaluateObjectDeref(node *actionlint.ObjectDerefNode) (any, return val, nil } + case *StepCache: + if entry, ok := v.Get(propName); ok { + return entry, nil + } + + case *StepCacheEntry: + switch propName { + case "outputs": + return v.Outputs, nil + case "conclusion": + return v.Conclusion, nil + } + case map[string]any: return lookupCaseInsensitive(v, propName), nil diff --git a/core/graph.go b/core/graph.go index c970114..a865ad0 100644 --- a/core/graph.go +++ b/core/graph.go @@ -267,6 +267,7 @@ func NewExecutionState( DataOutputCache: make(map[string]any), ExecutionOutputCache: make(map[string]any), + StepCache: NewStepCache(nil), } } @@ -288,19 +289,29 @@ func RunGraph(ctx context.Context, graphName string, graphContent []byte, opts R entryNode, isBaseNode := entry.(NodeBaseInterface) - // isGitHubActions: Determines if this run should behave as a GitHub Action. + // isGitHubWorkflow: Determines if this run should behave as a GitHub Action. // True when either running on actual GitHub Actions (system env), or an external // caller (e.g., web app) explicitly requests GitHub Actions behavior via OverrideEnv. // This affects input variable handling, context loading, and other GitHub-specific behavior. // // **Important** we haven't loaded the config file yet, so we can only look at overriden envs, // .env (already set in os.GetEnv) or shell. - isGitHubActions := opts.OverrideEnv["GITHUB_ACTIONS"] == "true" || os.Getenv("GITHUB_ACTIONS") == "true" || entryNode.GetNodeTypeId() == "core/gh-start@v1" + isGitHubWorkflow := false + if opts.OverrideEnv["GITHUB_ACTIONS"] == "true" { + isGitHubWorkflow = true + utils.LogOut.Infof("GitHub workflow detected via OverrideEnv") + } else if os.Getenv("GITHUB_ACTIONS") == "true" { + isGitHubWorkflow = true + utils.LogOut.Infof("GitHub workflow detected via GITHUB_ACTIONS environment variable (.env or shell)") + } else if entryNode.GetNodeTypeId() == "core/gh-start@v1" { + isGitHubWorkflow = true + utils.LogOut.Infof("GitHub workflow detected via entry node type: core/gh-start@v1") + } // mimickGitHubEnv: Determines if we need to set up a simulated GitHub environment. The easiest // approach for now is to just check a bunch of env vars. The user may have set one or the other // (through .env or shell) but unlikely all of them but they are by a real GitHub Actions runner. - mimickGitHubEnv := isGitHubActions && os.Getenv("GITHUB_RUN_ID") == "" && + mimickGitHubEnv := isGitHubWorkflow && os.Getenv("GITHUB_RUN_ID") == "" && os.Getenv("RUNNER_TEMP") == "" && os.Getenv("GITHUB_API_URL") == "" && os.Getenv("GITHUB_RETENTION_DAYS") == "" @@ -330,7 +341,7 @@ func RunGraph(ctx context.Context, graphName string, graphContent []byte, opts R rawEnv := utils.GetAllEnvMapCopy() // normalize all inputs/secrets with ACT_* iif we're in GitHub - if isGitHubActions { + if isGitHubWorkflow { prefixedRawEnv := make(map[string]utils.EnvKV) for k, v := range rawEnv { prefixedKey := k @@ -383,15 +394,15 @@ func RunGraph(ctx context.Context, graphName string, graphContent []byte, opts R secretTracker.setSingle(key, v.Value, fmt.Sprintf("%s (%s)", source, k), true, true) // GitHub specifics - case isGitHubActions && k == "ACT_INPUT_MATRIX": + case isGitHubWorkflow && k == "ACT_INPUT_MATRIX": if m, err := decodeJsonFromEnvValue[any](v.Value); err == nil { matrixTracker.set(m, source, true, true) } - case isGitHubActions && k == "ACT_INPUT_NEEDS": + case isGitHubWorkflow && k == "ACT_INPUT_NEEDS": if m, err := decodeJsonFromEnvValue[any](v.Value); err == nil { needsTracker.set(m, source, true, true) } - case isGitHubActions && k == "ACT_INPUT_TOKEN": + case isGitHubWorkflow && k == "ACT_INPUT_TOKEN": secretTracker.setSingle("GITHUB_TOKEN", v.Value, source, true, true) default: @@ -417,46 +428,57 @@ func RunGraph(ctx context.Context, graphName string, graphContent []byte, opts R printExplicit(envTracker, false) } - if cwd := finalEnv["ACT_CWD"]; cwd != "" { - originalCwd, err := os.Getwd() - if err != nil { - return CreateErr(nil, err, "failed to get current working directory") - } - if err := os.Chdir(cwd); err != nil { - return CreateErr(nil, err, "failed to change working directory to ACT_CWD") - } - defer func() { - _ = os.Chdir(originalCwd) - }() + var newCwd string + + if cwd, ok := finalEnv["ACT_CWD"]; ok { + newCwd = cwd + utils.LogOut.Debugf("changing working directory to ACT_CWD: %s\n", newCwd) } if mimickGitHubEnv { + if cwd, ok := finalEnv["GITHUB_WORKSPACE"]; ok { + newCwd = cwd + utils.LogOut.Debugf("changing working directory to GITHUB_WORKSPACE: %s\n", newCwd) + } + // If we are running a github actions workflow, then mimic a GitHub Actions environment // But only do is if we are NOT already in GitHub Actions err = SetupGitHubActionsEnv(finalEnv) if err != nil { return CreateErr(nil, err, "failed to setup GitHub Actions environment") } + } else if debugCb != nil && newCwd == "" { + // for debug sessions, always create a temp working directory if none is set + tmpDir, tmpErr := os.MkdirTemp("", "actrun-debug-*") + if tmpErr != nil { + return CreateErr(nil, tmpErr, "failed to create temp working directory for debug session") + } - // set cwd for current process. `ACT_CWD` above is used for non GitHub workflows - if cwd := finalEnv["GITHUB_WORKSPACE"]; cwd != "" { - originalCwd, err := os.Getwd() - if err != nil { - return CreateErr(nil, err, "failed to get current working directory") - } - if err := os.Chdir(cwd); err != nil { - return CreateErr(nil, err, "failed to change working directory to GITHUB_WORKSPACE") - } - defer func() { - _ = os.Chdir(originalCwd) - }() + newCwd = tmpDir + utils.LogOut.Infof("created temp working directory for debug session: %s\n", newCwd) + + defer func() { + _ = os.RemoveAll(tmpDir) + }() + } + + if newCwd != "" { + originalCwd, err := os.Getwd() + if err != nil { + return CreateErr(nil, err, "failed to get current working directory") + } + if err := os.Chdir(newCwd); err != nil { + return CreateErr(nil, err, "failed to change working directory to ACT_CWD/GITHUB_WORKSPACE") } + defer func() { + _ = os.Chdir(originalCwd) + }() } // construct the `github` context var ghContext map[string]any var errGh error - if isGitHubActions { + if isGitHubWorkflow { ghContext, errGh = LoadGitHubContext(finalEnv, finalInputs, finalSecrets) if errGh != nil { return CreateErr(nil, errGh, "failed to load github context") @@ -467,7 +489,7 @@ func RunGraph(ctx context.Context, graphName string, graphContent []byte, opts R ctx, &ag, graphName, - isGitHubActions, + isGitHubWorkflow, debugCb, finalEnv, finalInputs, diff --git a/core/inputs.go b/core/inputs.go index c95de47..b8eefb3 100644 --- a/core/inputs.go +++ b/core/inputs.go @@ -388,7 +388,12 @@ func (n *Inputs) InputValueById(ec *ExecutionState, host NodeWithInputs, inputId valueFound = true if regardCache { - ec.CacheDataOutput(dataSource.SrcNode.GetCacheId(), outputCacheIdForCache, finalValue, cacheType) + // Get the output type from the source node's output definition + outputType := "unknown" + if outputDef, _, ok := dataSource.SrcNodeOutputs.OutputDefByPortId(outputCacheId); ok { + outputType = outputDef.Type + } + ec.CacheDataOutput(dataSource.SrcNode, outputCacheIdForCache, finalValue, outputType, cacheType) } } } else { diff --git a/core/outputs.go b/core/outputs.go index 2d7ce39..759219a 100644 --- a/core/outputs.go +++ b/core/outputs.go @@ -8,6 +8,8 @@ import ( type SetOutputValueOpts struct { NotExistsIsNoError bool + ForceSet bool + StringTypeHint bool // If true, treat value as string type; otherwise determine from value } // HasOutputsInterface is a representation for all outputs of a node. @@ -116,9 +118,11 @@ func (n *Outputs) OutputValueById(c *ExecutionState, outputId OutputId) (any, er // The value type must match the output type, otherwise an error // is returned. func (n *Outputs) SetOutputValue(ec *ExecutionState, outputId OutputId, value any, opts SetOutputValueOpts) error { + var outputType string outputDef, outputExists := n.outputDefs[outputId] if outputExists { - expectedType := outputDef.Type + outputType = outputDef.Type + expectedType := outputType if outputDef.Array { expectedType = "[]" + expectedType } @@ -127,39 +131,50 @@ func (n *Outputs) SetOutputValue(ec *ExecutionState, outputId OutputId, value an return CreateErr(ec, nil, "output '%s' (%s): expected %v, but got %T", outputDef.Name, outputId, outputDef.Type, value) } } else { - // if the output could not be found, - // check if it is a sub port instead - groupPortId, _, isIndexPort := IsValidIndexPortId(string(outputId)) - if !isIndexPort { - if opts.NotExistsIsNoError { - return nil + if !opts.ForceSet { + // if the output could not be found, + // check if it is a sub port instead + groupPortId, _, isIndexPort := IsValidIndexPortId(string(outputId)) + if !isIndexPort { + if opts.NotExistsIsNoError { + return nil + } + return CreateErr(ec, nil, "failed to set a value to an unknown port '%s'", outputId) } - return CreateErr(ec, nil, "failed to set a value to an unknown port '%s'", outputId) - } - outputDef, outputExists = n.outputDefs[OutputId(groupPortId)] - if !outputExists { - if opts.NotExistsIsNoError { - return nil + outputDef, outputExists = n.outputDefs[OutputId(groupPortId)] + if !outputExists { + if opts.NotExistsIsNoError { + return nil + } + // If still nothing found, return an error + return CreateErr(ec, nil, "failed to set a value to an unknown port '%s'", outputId) } - // If still nothing found, return an error - return CreateErr(ec, nil, "failed to set a value to an unknown port '%s'", outputId) - } - if !isValueValidForOutput(value, outputDef.Type) { - return CreateErr(ec, nil, "output '%s' (%s): expected %v, but got %T", outputDef.Name, outputId, outputDef.Type, value) + outputType = outputDef.Type + if !isValueValidForOutput(value, outputType) { + return CreateErr(ec, nil, "output '%s' (%s): expected %v, but got %T", outputDef.Name, outputId, outputDef.Type, value) + } + } else { + // ForceSet without known output definition - use provided type or determine from value + if opts.StringTypeHint { + outputType = "string" + } else { + outputType = determineOutputType(value) + } } } // If the output is not connected, there's no need to keep the value. It can be discarded, unless // for debug sessions where we always keep the output value, as it will be transmitted to the client for inspection connectionCounter := n.outputConnectionCounter[outputId] - if connectionCounter == 0 && !ec.IsDebugSession { + if connectionCounter == 0 && !ec.IsDebugSession && !opts.ForceSet { // TODO: (Seb) If the value is a stream, we should close it here return nil } - ec.CacheDataOutput(n.owner.GetCacheId(), string(outputId), value, Permanent) + ec.CacheDataOutput(n.owner, string(outputId), value, outputType, Permanent) + return nil } @@ -236,3 +251,24 @@ var validKindsForExpectedType = map[string]map[reflect.Kind]struct{}{ reflect.Bool: {}, }, } + +// determineOutputType returns the primitive type name for a value, or "unknown" if not a primitive. +func determineOutputType(value any) string { + if value == nil { + return "unknown" + } + + kind := reflect.TypeOf(value).Kind() + switch kind { + case reflect.String: + return "string" + case reflect.Bool: + return "bool" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return "number" + default: + return "unknown" + } +} From a9bcb21b49531bb55310e14595b4897591dfdee9 Mon Sep 17 00:00:00 2001 From: Sebatian Rath Date: Sun, 1 Feb 2026 22:53:01 -0500 Subject: [PATCH 3/7] Update nodes to support steps context --- nodes/concurrent-exec@v1.go | 5 ++++ nodes/gh-action@v1.go | 54 +++++++++++++------------------------ nodes/gh-context-parser.go | 41 ++++++++++++++++++++++------ nodes/run-exec@v1.go | 13 ++++++++- nodes/run@v1.go | 17 +++++++++--- 5 files changed, 82 insertions(+), 48 deletions(-) diff --git a/nodes/concurrent-exec@v1.go b/nodes/concurrent-exec@v1.go index 825b598..0b2c256 100644 --- a/nodes/concurrent-exec@v1.go +++ b/nodes/concurrent-exec@v1.go @@ -31,6 +31,11 @@ func (n *ConcurrentExecNode) ExecuteImpl(c *core.ExecutionState, inputId core.In continue } + // Skip exec-completed - it's triggered after all branches complete + if outputId == ni.Core_concurrent_exec_v1_Output_exec_completed { + continue + } + outputIdCopy := outputId fn := func() { diff --git a/nodes/gh-action@v1.go b/nodes/gh-action@v1.go index c172bc9..bdf4d09 100644 --- a/nodes/gh-action@v1.go +++ b/nodes/gh-action@v1.go @@ -192,8 +192,8 @@ func (n *GhActionNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId, return nil } - // process fiel commands post-execution (GITHUB_ENV, GITHUB_OUTPUT, GITHUB_PATH) - ghEnvs, err = ghContextParser.Parse(c, currentEnvMap) + // process file commands post-execution (GITHUB_ENV, GITHUB_OUTPUT, GITHUB_PATH) + ghEnvs, ghOutputs, err := ghContextParser.Parse(c, currentEnvMap) if err != nil { return err } @@ -203,28 +203,15 @@ func (n *GhActionNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId, maps.Copy(nextEnvMap, ghEnvs) c.SetContextEnvironMap(nextEnvMap) - // parse GITHUB_OUTPUT - githubOutput := currentEnvMap["GITHUB_OUTPUT"] - if githubOutput != "" { - b, err := os.ReadFile(githubOutput) - if err != nil { - return core.CreateErr(c, err, "unable to read github output file") - } - - outputs, err := parseOutputFile(string(b)) + for key, value := range ghOutputs { + err = n.SetOutputValue(c, core.OutputId(key), value, core.SetOutputValueOpts{ + NotExistsIsNoError: true, + ForceSet: true, + StringTypeHint: true, + }) if err != nil { return err } - for key, value := range outputs { - err = n.SetOutputValue(c, core.OutputId(key), strings.TrimRight(value, "\t\n"), core.SetOutputValueOpts{ - NotExistsIsNoError: true, - }) - if err != nil { - return err - } - } - - _ = os.Remove(githubOutput) } err = n.Execute(ni.Core_gh_action_v1_Output_exec_success, c, nil) @@ -349,7 +336,15 @@ func (n *GhActionNode) ExecuteDocker(c *core.ExecutionState, workingDirectory st ReadOnly: false, }) - exitCode, err := core.DockerRun(context.Background(), n.Data.DockerInstanceLabel, ci, workingDirectory, nil, nil) + // Convert to SDK-based config and run + dockerClient, err := core.NewDockerClient() + if err != nil { + return core.CreateErr(c, err, "failed to create Docker client") + } + defer dockerClient.Close() + + config := core.ContainerInfo2DockerRunConfig(ci) + exitCode, err := dockerClient.RunContainer(context.Background(), config) if err != nil { return err } @@ -530,13 +525,10 @@ func init() { node.Data.Image = dockerUrl if !validate { - exitCode, err := core.DockerPull(context.Background(), dockerUrl, sysWorkspaceDir) + err := core.SDKDockerPull(context.Background(), dockerUrl) if err != nil { return nil, []error{err} } - if exitCode != 0 { - return nil, []error{core.CreateErr(nil, nil, "docker pull failed with exit code %d", exitCode)} - } } } else { @@ -557,23 +549,15 @@ func init() { node.Data.ExecutionStateId = executionContextId.String() if !validate { - utils.LogOut.Infof("%sBuild container for action use '%s'.\n", u.LogGhStartGroup, "") - // resolve Dockerfile path relative to the action directory dockerFilePath := filepath.Join(actionDir, action.Runs.Image) // Build context is usually the action directory, but we pass actionDir. // If the Dockerfile is "../../Dockerfile", this logic handles the location of the file. - exitCode, err := core.DockerBuild(context.Background(), actionDir, dockerFilePath, actionDir, imageName) + err := core.SDKDockerBuild(context.Background(), dockerFilePath, actionDir, imageName) if err != nil { return nil, []error{err} } - - if exitCode != 0 { - return nil, []error{core.CreateErr(nil, nil, "docker build failed with exit code %d", exitCode)} - } - - utils.LogOut.Infof(u.LogGhEndGroup) } } case "node12", "node14", "node16", "node20", "node24": diff --git a/nodes/gh-context-parser.go b/nodes/gh-context-parser.go index 00abbc4..50592fa 100644 --- a/nodes/gh-context-parser.go +++ b/nodes/gh-context-parser.go @@ -36,16 +36,17 @@ func (p *GhContextParser) Init(c *core.ExecutionState, sysRunnerTempDir string) return envs, nil } -func (p *GhContextParser) Parse(c *core.ExecutionState, contextEnvironMap map[string]string) (map[string]string, error) { +func (p *GhContextParser) Parse(c *core.ExecutionState, contextEnvironMap map[string]string) (envs map[string]string, outputs map[string]string, err error) { - envs := map[string]string{} + envs = map[string]string{} + outputs = map[string]string{} githubPath := contextEnvironMap["GITHUB_PATH"] // load all paths from the github path file and append them to the PATH if githubPath != "" { p, err := os.ReadFile(githubPath) if err != nil { - return nil, core.CreateErr(c, err, "unable to read file set in GITHUB_PATH") + return nil, nil, core.CreateErr(c, err, "unable to read file set in GITHUB_PATH") } newPaths := []string{} @@ -65,7 +66,7 @@ func (p *GhContextParser) Parse(c *core.ExecutionState, contextEnvironMap map[st err = os.Remove(githubPath) if err != nil { - return nil, core.CreateErr(c, nil, "unable to remove file set in GITHUB_PATH") + return nil, nil, core.CreateErr(c, nil, "unable to remove file set in GITHUB_PATH") } delete(contextEnvironMap, "GITHUB_PATH") @@ -75,11 +76,11 @@ func (p *GhContextParser) Parse(c *core.ExecutionState, contextEnvironMap map[st if githubEnv != "" { b, err := os.ReadFile(githubEnv) if err != nil { - return nil, core.CreateErr(c, nil, "unable to read file set in GITHUB_ENV") + return nil, nil, core.CreateErr(c, nil, "unable to read file set in GITHUB_ENV") } ghEnvs, err := parseOutputFile(string(b)) if err != nil { - return nil, err + return nil, nil, err } for envName, envValue := range ghEnvs { envs[envName] = strings.TrimRight(envValue, " \t\n\r") @@ -87,12 +88,36 @@ func (p *GhContextParser) Parse(c *core.ExecutionState, contextEnvironMap map[st err = os.Remove(githubEnv) if err != nil { - return nil, core.CreateErr(c, err, "unable to remove file set in GITHUB_ENV") + return nil, nil, core.CreateErr(c, err, "unable to remove file set in GITHUB_ENV") } delete(contextEnvironMap, "GITHUB_ENV") } - return envs, nil + + githubOutput := contextEnvironMap["GITHUB_OUTPUT"] + if githubOutput != "" { + b, err := os.ReadFile(githubOutput) + if err != nil { + return nil, nil, core.CreateErr(c, err, "unable to read file set in GITHUB_OUTPUT") + } + + ghOutputs, err := parseOutputFile(string(b)) + if err != nil { + return nil, nil, err + } + for key, value := range ghOutputs { + outputs[key] = strings.TrimRight(value, "\t\n") + } + + err = os.Remove(githubOutput) + if err != nil { + return nil, nil, core.CreateErr(c, err, "unable to remove file set in GITHUB_OUTPUT") + } + + delete(contextEnvironMap, "GITHUB_OUTPUT") + } + + return envs, outputs, nil } func parseOutputFile(input string) (map[string]string, error) { diff --git a/nodes/run-exec@v1.go b/nodes/run-exec@v1.go index 576b043..3312211 100644 --- a/nodes/run-exec@v1.go +++ b/nodes/run-exec@v1.go @@ -102,7 +102,7 @@ func (n *RunExecNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId, } else { if c.IsGitHubWorkflow { ghContextParser := GhContextParser{} - ghEnvs, err := ghContextParser.Parse(c, currentEnvMap) + ghEnvs, ghOutputs, err := ghContextParser.Parse(c, currentEnvMap) if err != nil { return err } @@ -110,6 +110,17 @@ func (n *RunExecNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId, nextEnvMap := c.GetContextEnvironMapCopy() maps.Copy(nextEnvMap, ghEnvs) c.SetContextEnvironMap(nextEnvMap) + + for key, value := range ghOutputs { + err = n.SetOutputValue(c, core.OutputId(key), value, core.SetOutputValueOpts{ + NotExistsIsNoError: true, + ForceSet: true, + StringTypeHint: true, + }) + if err != nil { + return err + } + } } err = n.Execute(ni.Core_run_exec_v1_Output_exec_success, c, nil) diff --git a/nodes/run@v1.go b/nodes/run@v1.go index c4511b4..b921d5c 100644 --- a/nodes/run@v1.go +++ b/nodes/run@v1.go @@ -104,9 +104,7 @@ func (n *RunNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId, prev if err != nil { return core.CreateErr(c, err, "failed to initialize GitHub context parser") } - for envName, path := range ghEnvs { - currentEnvMap[envName] = path - } + maps.Copy(currentEnvMap, ghEnvs) } output, exitCode, runErr := runCommand(c, shell, &script, args, print, stdin, currentEnvMap) @@ -133,7 +131,7 @@ func (n *RunNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId, prev } else { if c.IsGitHubWorkflow { ghContextParser := GhContextParser{} - ghEnvs, err := ghContextParser.Parse(c, currentEnvMap) + ghEnvs, ghOutputs, err := ghContextParser.Parse(c, currentEnvMap) if err != nil { return err } @@ -141,6 +139,17 @@ func (n *RunNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId, prev nextEnvMap := c.GetContextEnvironMapCopy() maps.Copy(nextEnvMap, ghEnvs) c.SetContextEnvironMap(nextEnvMap) + + for key, value := range ghOutputs { + err = n.SetOutputValue(c, core.OutputId(key), value, core.SetOutputValueOpts{ + NotExistsIsNoError: true, + ForceSet: true, + StringTypeHint: true, + }) + if err != nil { + return err + } + } } err = n.Execute(ni.Core_run_v1_Output_exec_success, c, nil) From ea8be2b17da2993f15369dde97bf46d996734dc2 Mon Sep 17 00:00:00 2001 From: Sebatian Rath Date: Sun, 1 Feb 2026 22:03:01 -0500 Subject: [PATCH 4/7] Add e2e tests for steps context functionality --- tests_e2e/references/reference_steps.sh_l8 | 26 ++ .../references/reference_steps_gh.sh_l11 | 98 +++++++ .../reference_steps_gh_conclusion.sh_l11 | 26 ++ .../references/reference_steps_gh_env.sh_l11 | 27 ++ .../reference_steps_gh_forloop.sh_l11 | 66 +++++ .../reference_steps_gh_heredoc.sh_l11 | 31 +++ .../references/reference_steps_gh_path.sh_l11 | 27 ++ .../reference_steps_gh_sequential.sh_l11 | 29 ++ tests_e2e/scripts/steps.act | 69 +++++ tests_e2e/scripts/steps.sh | 8 + tests_e2e/scripts/steps_gh.act | 248 ++++++++++++++++++ tests_e2e/scripts/steps_gh.sh | 11 + tests_e2e/scripts/steps_gh_conclusion.act | 69 +++++ tests_e2e/scripts/steps_gh_conclusion.sh | 11 + tests_e2e/scripts/steps_gh_env.act | 71 +++++ tests_e2e/scripts/steps_gh_env.sh | 11 + tests_e2e/scripts/steps_gh_forloop.act | 117 +++++++++ tests_e2e/scripts/steps_gh_forloop.sh | 11 + tests_e2e/scripts/steps_gh_heredoc.act | 91 +++++++ tests_e2e/scripts/steps_gh_heredoc.sh | 11 + tests_e2e/scripts/steps_gh_path.act | 76 ++++++ tests_e2e/scripts/steps_gh_path.sh | 11 + tests_e2e/scripts/steps_gh_sequential.act | 85 ++++++ tests_e2e/scripts/steps_gh_sequential.sh | 11 + 24 files changed, 1241 insertions(+) create mode 100644 tests_e2e/references/reference_steps.sh_l8 create mode 100644 tests_e2e/references/reference_steps_gh.sh_l11 create mode 100644 tests_e2e/references/reference_steps_gh_conclusion.sh_l11 create mode 100644 tests_e2e/references/reference_steps_gh_env.sh_l11 create mode 100644 tests_e2e/references/reference_steps_gh_forloop.sh_l11 create mode 100644 tests_e2e/references/reference_steps_gh_heredoc.sh_l11 create mode 100644 tests_e2e/references/reference_steps_gh_path.sh_l11 create mode 100644 tests_e2e/references/reference_steps_gh_sequential.sh_l11 create mode 100644 tests_e2e/scripts/steps.act create mode 100644 tests_e2e/scripts/steps.sh create mode 100644 tests_e2e/scripts/steps_gh.act create mode 100644 tests_e2e/scripts/steps_gh.sh create mode 100644 tests_e2e/scripts/steps_gh_conclusion.act create mode 100644 tests_e2e/scripts/steps_gh_conclusion.sh create mode 100644 tests_e2e/scripts/steps_gh_env.act create mode 100644 tests_e2e/scripts/steps_gh_env.sh create mode 100644 tests_e2e/scripts/steps_gh_forloop.act create mode 100644 tests_e2e/scripts/steps_gh_forloop.sh create mode 100644 tests_e2e/scripts/steps_gh_heredoc.act create mode 100644 tests_e2e/scripts/steps_gh_heredoc.sh create mode 100644 tests_e2e/scripts/steps_gh_path.act create mode 100644 tests_e2e/scripts/steps_gh_path.sh create mode 100644 tests_e2e/scripts/steps_gh_sequential.act create mode 100644 tests_e2e/scripts/steps_gh_sequential.sh diff --git a/tests_e2e/references/reference_steps.sh_l8 b/tests_e2e/references/reference_steps.sh_l8 new file mode 100644 index 0000000..3c54a05 --- /dev/null +++ b/tests_e2e/references/reference_steps.sh_l8 @@ -0,0 +1,26 @@ +build hasn't expired yet +looking for value: 'env_file' + no value (is optional) found for: 'env_file' +looking for value: 'config_file' + no value (is optional) found for: 'config_file' +looking for value: 'concurrency' + no value (is optional) found for: 'concurrency' +looking for value: 'graph_file' + found value in: 'env (shell)' + evaluated to: 'steps.act' +looking for value: 'session_token' + no value (is optional) found for: 'session_token' +looking for value: 'create_debug_session' + found value in flags + evaluated to: 'false' +PushNodeVisit: start, execute: true +🟢 Execute 'Run Script (step1)' +PushNodeVisit: step1, execute: true +step1 executed +🟢 Execute 'Run Script (step2)' +PushNodeVisit: step2, execute: true +step2 executed +🟢 Execute 'Print (final-print)' +PushNodeVisit: final-print, execute: true +PushNodeVisit: print-conclusions, execute: false +step1.conclusion: success, step2.conclusion: success diff --git a/tests_e2e/references/reference_steps_gh.sh_l11 b/tests_e2e/references/reference_steps_gh.sh_l11 new file mode 100644 index 0000000..de91528 --- /dev/null +++ b/tests_e2e/references/reference_steps_gh.sh_l11 @@ -0,0 +1,98 @@ + evaluated to: 'false' + evaluated to: 'steps_gh.act' + found value in flags + found value in: 'env (shell)' + no value (is optional) found for: 'concurrency' + no value (is optional) found for: 'config_file' + no value (is optional) found for: 'env_file' + no value (is optional) found for: 'session_token' +GitHub workflow detected via GITHUB_ACTIONS environment variable (.env or shell)changing working directory to GITHUB_WORKSPACE: [REDACTED]/scripts +PushNodeVisit: core-concurrent-exec-v1-gold-raspberry-jellyfish, execute: true +PushNodeVisit: core-concurrent-exec-v1-plum-parrot-tangerine, execute: true +PushNodeVisit: core-const-string-v1-black-snake-violet, execute: false +PushNodeVisit: core-const-string-v1-black-snake-violet, execute: false +PushNodeVisit: core-const-string-v1-black-snake-violet, execute: false +PushNodeVisit: core-const-string-v1-black-snake-violet, execute: false +PushNodeVisit: core-const-string-v1-black-snake-violet, execute: false +PushNodeVisit: core-const-string-v1-grape-jellyfish-shark, execute: false +PushNodeVisit: core-const-string-v1-grape-jellyfish-shark, execute: false +PushNodeVisit: core-const-string-v1-grape-jellyfish-shark, execute: false +PushNodeVisit: core-const-string-v1-grape-jellyfish-shark, execute: false +PushNodeVisit: core-const-string-v1-grape-jellyfish-shark, execute: false +PushNodeVisit: core-const-string-v1-lion-gray-blackberry, execute: false +PushNodeVisit: core-const-string-v1-lion-gray-blackberry, execute: false +PushNodeVisit: core-const-string-v1-lion-gray-blackberry, execute: false +PushNodeVisit: core-const-string-v1-lion-gray-blackberry, execute: false +PushNodeVisit: core-const-string-v1-lion-gray-blackberry, execute: false +PushNodeVisit: core-const-string-v1-nectarine-jellyfish-rabbit, execute: false +PushNodeVisit: core-const-string-v1-nectarine-jellyfish-rabbit, execute: false +PushNodeVisit: core-const-string-v1-nectarine-jellyfish-rabbit, execute: false +PushNodeVisit: core-const-string-v1-nectarine-jellyfish-rabbit, execute: false +PushNodeVisit: core-const-string-v1-nectarine-jellyfish-rabbit, execute: false +PushNodeVisit: core-const-string-v1-squirrel-octopus-pineapple, execute: false +PushNodeVisit: core-const-string-v1-squirrel-octopus-pineapple, execute: false +PushNodeVisit: core-const-string-v1-squirrel-octopus-pineapple, execute: false +PushNodeVisit: core-const-string-v1-squirrel-octopus-pineapple, execute: false +PushNodeVisit: core-const-string-v1-squirrel-octopus-pineapple, execute: false +PushNodeVisit: core-print-v1-blackberry-teal-fig, execute: true +PushNodeVisit: core-print-v1-blackberry-teal-fig, execute: true +PushNodeVisit: core-print-v1-blackberry-teal-fig, execute: true +PushNodeVisit: core-print-v1-blackberry-teal-fig, execute: true +PushNodeVisit: core-print-v1-blackberry-teal-fig, execute: true +PushNodeVisit: core-sequence-v1-giraffe-fox-jackfruit, execute: true +PushNodeVisit: left, execute: true +PushNodeVisit: left-2, execute: true +PushNodeVisit: right, execute: true +PushNodeVisit: right-2, execute: true +PushNodeVisit: root, execute: true +PushNodeVisit: start, execute: true +build hasn't expired yet +left-2.LEFT_2: +left-2.LEFT_2: +left-2.LEFT_2: +left-2.LEFT_2: +left-2.LEFT_2: Left 2 Value +left.LEFT: +left.LEFT: +left.LEFT: +left.LEFT: Left Value +left.LEFT: Left Value +looking for value: 'concurrency' +looking for value: 'config_file' +looking for value: 'create_debug_session' +looking for value: 'env_file' +looking for value: 'graph_file' +looking for value: 'session_token' +right-2.RIGHT_2: +right-2.RIGHT_2: +right-2.RIGHT_2: +right-2.RIGHT_2: +right-2.RIGHT_2: Right Value +right.RIGHT: +right.RIGHT: +right.RIGHT: +right.RIGHT: +right.RIGHT: Right Value +root.ROOT: Im the root value +root.ROOT: Im the root value +root.ROOT: Im the root value +root.ROOT: Im the root value +root.ROOT: Im the root value +set left-2.LEFT_2 +set left.LEFT +set right-2.RIGHT_2 +set right.RIGHT +set root.ROOT +🟢 Execute 'Concurrent Execution (core-concurrent-exec-v1-gold-raspberry-jellyfish)' +🟢 Execute 'Concurrent Execution (core-concurrent-exec-v1-plum-parrot-tangerine)' +🟢 Execute 'Print (core-print-v1-blackberry-teal-fig)' +🟢 Execute 'Print (core-print-v1-blackberry-teal-fig)' +🟢 Execute 'Print (core-print-v1-blackberry-teal-fig)' +🟢 Execute 'Print (core-print-v1-blackberry-teal-fig)' +🟢 Execute 'Print (core-print-v1-blackberry-teal-fig)' +🟢 Execute 'Run Script (left)' +🟢 Execute 'Run Script (left-2)' +🟢 Execute 'Run Script (right)' +🟢 Execute 'Run Script (right-2)' +🟢 Execute 'Run Script (root)' +🟢 Execute 'Sequence (core-sequence-v1-giraffe-fox-jackfruit)' diff --git a/tests_e2e/references/reference_steps_gh_conclusion.sh_l11 b/tests_e2e/references/reference_steps_gh_conclusion.sh_l11 new file mode 100644 index 0000000..c928fc4 --- /dev/null +++ b/tests_e2e/references/reference_steps_gh_conclusion.sh_l11 @@ -0,0 +1,26 @@ +build hasn't expired yet +looking for value: 'env_file' + no value (is optional) found for: 'env_file' +looking for value: 'config_file' + no value (is optional) found for: 'config_file' +looking for value: 'concurrency' + no value (is optional) found for: 'concurrency' +looking for value: 'graph_file' + found value in: 'env (shell)' + evaluated to: 'steps_gh_conclusion.act' +looking for value: 'session_token' + no value (is optional) found for: 'session_token' +looking for value: 'create_debug_session' + found value in flags + evaluated to: 'false' +GitHub workflow detected via GITHUB_ACTIONS environment variable (.env or shell)changing working directory to GITHUB_WORKSPACE: [REDACTED]/scripts +PushNodeVisit: start, execute: true +🟢 Execute 'Run Script (step-success)' +PushNodeVisit: step-success, execute: true +This step will succeed +🟢 Execute 'Print (final-print)' +PushNodeVisit: final-print, execute: true +PushNodeVisit: print-conclusion-success, execute: false +PushNodeVisit: print-output, execute: false +step-success.conclusion: success +step-success.outputs.SUCCESS_OUTPUT: worked diff --git a/tests_e2e/references/reference_steps_gh_env.sh_l11 b/tests_e2e/references/reference_steps_gh_env.sh_l11 new file mode 100644 index 0000000..c9e8c4a --- /dev/null +++ b/tests_e2e/references/reference_steps_gh_env.sh_l11 @@ -0,0 +1,27 @@ +build hasn't expired yet +looking for value: 'env_file' + no value (is optional) found for: 'env_file' +looking for value: 'config_file' + no value (is optional) found for: 'config_file' +looking for value: 'concurrency' + no value (is optional) found for: 'concurrency' +looking for value: 'graph_file' + found value in: 'env (shell)' + evaluated to: 'steps_gh_env.act' +looking for value: 'session_token' + no value (is optional) found for: 'session_token' +looking for value: 'create_debug_session' + found value in flags + evaluated to: 'false' +GitHub workflow detected via GITHUB_ACTIONS environment variable (.env or shell)changing working directory to GITHUB_WORKSPACE: [REDACTED]/scripts +PushNodeVisit: start, execute: true +🟢 Execute 'Run Script (step-set-env)' +PushNodeVisit: step-set-env, execute: true +Setting MY_VAR via GITHUB_ENV +🟢 Execute 'Run Script (step-read-env)' +PushNodeVisit: step-read-env, execute: true +Reading MY_VAR: hello_from_env +🟢 Execute 'Print (final-print)' +PushNodeVisit: final-print, execute: true +PushNodeVisit: print-value, execute: false +Read from step output: hello_from_env diff --git a/tests_e2e/references/reference_steps_gh_forloop.sh_l11 b/tests_e2e/references/reference_steps_gh_forloop.sh_l11 new file mode 100644 index 0000000..3771da8 --- /dev/null +++ b/tests_e2e/references/reference_steps_gh_forloop.sh_l11 @@ -0,0 +1,66 @@ +build hasn't expired yet +looking for value: 'env_file' + no value (is optional) found for: 'env_file' +looking for value: 'config_file' + no value (is optional) found for: 'config_file' +looking for value: 'concurrency' + no value (is optional) found for: 'concurrency' +looking for value: 'graph_file' + found value in: 'env (shell)' + evaluated to: 'steps_gh_forloop.act' +looking for value: 'session_token' + no value (is optional) found for: 'session_token' +looking for value: 'create_debug_session' + found value in flags + evaluated to: 'false' +GitHub workflow detected via GITHUB_ACTIONS environment variable (.env or shell)changing working directory to GITHUB_WORKSPACE: [REDACTED]/scripts +PushNodeVisit: start, execute: true +🟢 Execute 'Run Script (init-step)' +PushNodeVisit: init-step, execute: true +Setting INIT_VALUE +🟢 Execute 'For Loop (for-loop)' +PushNodeVisit: for-loop, execute: true +🟢 Execute 'Run Script (loop-step)' +PushNodeVisit: loop-step, execute: true +PushNodeVisit: index-to-array, execute: false +PushNodeVisit: (cached) for-loop, execute: false +Loop iteration 0 +🟢 Execute 'Print (loop-print)' +PushNodeVisit: loop-print, execute: true +PushNodeVisit: print-init, execute: false +PushNodeVisit: print-loop, execute: false +init-step.INIT_VALUE: initialized +loop-step.LOOP_OUTPUT: iteration_0 +🟢 Execute 'Run Script (loop-step)' +PushNodeVisit: loop-step, execute: true +PushNodeVisit: index-to-array, execute: false +PushNodeVisit: (cached) for-loop, execute: false +Loop iteration 1 +🟢 Execute 'Print (loop-print)' +PushNodeVisit: loop-print, execute: true +PushNodeVisit: print-init, execute: false +PushNodeVisit: print-loop, execute: false +init-step.INIT_VALUE: initialized +loop-step.LOOP_OUTPUT: iteration_1 +🟢 Execute 'Run Script (loop-step)' +PushNodeVisit: loop-step, execute: true +PushNodeVisit: index-to-array, execute: false +PushNodeVisit: (cached) for-loop, execute: false +Loop iteration 2 +🟢 Execute 'Print (loop-print)' +PushNodeVisit: loop-print, execute: true +PushNodeVisit: print-init, execute: false +PushNodeVisit: print-loop, execute: false +init-step.INIT_VALUE: initialized +loop-step.LOOP_OUTPUT: iteration_2 +🟢 Execute 'Run Script (loop-step)' +PushNodeVisit: loop-step, execute: true +PushNodeVisit: index-to-array, execute: false +PushNodeVisit: (cached) for-loop, execute: false +Loop iteration 3 +🟢 Execute 'Print (loop-print)' +PushNodeVisit: loop-print, execute: true +PushNodeVisit: print-init, execute: false +PushNodeVisit: print-loop, execute: false +init-step.INIT_VALUE: initialized +loop-step.LOOP_OUTPUT: iteration_3 diff --git a/tests_e2e/references/reference_steps_gh_heredoc.sh_l11 b/tests_e2e/references/reference_steps_gh_heredoc.sh_l11 new file mode 100644 index 0000000..69c59ae --- /dev/null +++ b/tests_e2e/references/reference_steps_gh_heredoc.sh_l11 @@ -0,0 +1,31 @@ +build hasn't expired yet +looking for value: 'env_file' + no value (is optional) found for: 'env_file' +looking for value: 'config_file' + no value (is optional) found for: 'config_file' +looking for value: 'concurrency' + no value (is optional) found for: 'concurrency' +looking for value: 'graph_file' + found value in: 'env (shell)' + evaluated to: 'steps_gh_heredoc.act' +looking for value: 'session_token' + no value (is optional) found for: 'session_token' +looking for value: 'create_debug_session' + found value in flags + evaluated to: 'false' +GitHub workflow detected via GITHUB_ACTIONS environment variable (.env or shell)changing working directory to GITHUB_WORKSPACE: [REDACTED]/scripts +PushNodeVisit: start, execute: true +🟢 Execute 'Run Script (step-heredoc)' +PushNodeVisit: step-heredoc, execute: true +Setting MULTILINE_OUTPUT using heredoc +🟢 Execute 'Run Script (step-simple)' +PushNodeVisit: step-simple, execute: true +Setting SIMPLE_OUTPUT +🟢 Execute 'Print (final-print)' +PushNodeVisit: final-print, execute: true +PushNodeVisit: print-heredoc, execute: false +PushNodeVisit: print-simple, execute: false +heredoc output: [Line 1 +Line 2 +Line 3] +simple output: [simple_value] diff --git a/tests_e2e/references/reference_steps_gh_path.sh_l11 b/tests_e2e/references/reference_steps_gh_path.sh_l11 new file mode 100644 index 0000000..eee59a7 --- /dev/null +++ b/tests_e2e/references/reference_steps_gh_path.sh_l11 @@ -0,0 +1,27 @@ +build hasn't expired yet +looking for value: 'env_file' + no value (is optional) found for: 'env_file' +looking for value: 'config_file' + no value (is optional) found for: 'config_file' +looking for value: 'concurrency' + no value (is optional) found for: 'concurrency' +looking for value: 'graph_file' + found value in: 'env (shell)' + evaluated to: 'steps_gh_path.act' +looking for value: 'session_token' + no value (is optional) found for: 'session_token' +looking for value: 'create_debug_session' + found value in flags + evaluated to: 'false' +GitHub workflow detected via GITHUB_ACTIONS environment variable (.env or shell)changing working directory to GITHUB_WORKSPACE: [REDACTED]/scripts +PushNodeVisit: start, execute: true +🟢 Execute 'Run Script (step-add-path)' +PushNodeVisit: step-add-path, execute: true +Adding [REDACTED]/bin to PATH via GITHUB_PATH +🟢 Execute 'Run Script (step-check-path)' +PushNodeVisit: step-check-path, execute: true +SUCCESS: PATH contains [REDACTED]/bin +🟢 Execute 'Print (final-print)' +PushNodeVisit: final-print, execute: true +PushNodeVisit: print-result, execute: false +Path modification result: path_modified diff --git a/tests_e2e/references/reference_steps_gh_sequential.sh_l11 b/tests_e2e/references/reference_steps_gh_sequential.sh_l11 new file mode 100644 index 0000000..d56b934 --- /dev/null +++ b/tests_e2e/references/reference_steps_gh_sequential.sh_l11 @@ -0,0 +1,29 @@ +build hasn't expired yet +looking for value: 'env_file' + no value (is optional) found for: 'env_file' +looking for value: 'config_file' + no value (is optional) found for: 'config_file' +looking for value: 'concurrency' + no value (is optional) found for: 'concurrency' +looking for value: 'graph_file' + found value in: 'env (shell)' + evaluated to: 'steps_gh_sequential.act' +looking for value: 'session_token' + no value (is optional) found for: 'session_token' +looking for value: 'create_debug_session' + found value in flags + evaluated to: 'false' +GitHub workflow detected via GITHUB_ACTIONS environment variable (.env or shell)changing working directory to GITHUB_WORKSPACE: [REDACTED]/scripts +PushNodeVisit: start, execute: true +🟢 Execute 'Run Script (step1)' +PushNodeVisit: step1, execute: true +Setting STEP1_OUTPUT +🟢 Execute 'Run Script (step2)' +PushNodeVisit: step2, execute: true +Setting STEP2_OUTPUT +🟢 Execute 'Print (final-print)' +PushNodeVisit: final-print, execute: true +PushNodeVisit: print-step1, execute: false +PushNodeVisit: print-step2, execute: false +step1.STEP1_OUTPUT: value_from_step1 +step2.STEP2_OUTPUT: value_from_step2 diff --git a/tests_e2e/scripts/steps.act b/tests_e2e/scripts/steps.act new file mode 100644 index 0000000..321557d --- /dev/null +++ b/tests_e2e/scripts/steps.act @@ -0,0 +1,69 @@ +editor: + version: + created: v1.34.0 +entry: start +type: standard +nodes: + - id: start + type: core/start@v1 + position: + x: 0 + y: 0 + - id: step1 + type: core/run@v1 + position: + x: 200 + y: 0 + inputs: + script: | + echo "step1 executed" + shell: bash + - id: step2 + type: core/run@v1 + position: + x: 400 + y: 0 + inputs: + script: | + echo "step2 executed" + shell: bash + - id: print-conclusions + type: core/const-string@v1 + position: + x: 600 + y: 100 + inputs: + value: "step1.conclusion: ${{ steps.step1.conclusion }}, step2.conclusion: ${{ steps.step2.conclusion }}" + - id: final-print + type: core/print@v1 + position: + x: 800 + y: 0 + inputs: + values[0]: null +connections: + - src: + node: print-conclusions + port: result + dst: + node: final-print + port: values[0] +executions: + - src: + node: start + port: exec + dst: + node: step1 + port: exec + - src: + node: step1 + port: exec-success + dst: + node: step2 + port: exec + - src: + node: step2 + port: exec-success + dst: + node: final-print + port: exec diff --git a/tests_e2e/scripts/steps.sh b/tests_e2e/scripts/steps.sh new file mode 100644 index 0000000..9f3474b --- /dev/null +++ b/tests_e2e/scripts/steps.sh @@ -0,0 +1,8 @@ +echo "Test Steps - step references using node port outputs (non-GitHub)" + +TEST_NAME=steps +GRAPH_FILE="${ACT_GRAPH_FILES_DIR}${PATH_SEPARATOR}${TEST_NAME}.act" +cp $GRAPH_FILE $TEST_NAME.act +export ACT_GRAPH_FILE=$TEST_NAME.act + +#! test actrun diff --git a/tests_e2e/scripts/steps_gh.act b/tests_e2e/scripts/steps_gh.act new file mode 100644 index 0000000..752ccc4 --- /dev/null +++ b/tests_e2e/scripts/steps_gh.act @@ -0,0 +1,248 @@ +editor: + version: + created: v1.34.0 + updated: v1.58.0 +entry: start +type: standard +nodes: + - id: start + type: core/start@v1 + position: + x: -1230 + y: 1530 + - id: core-concurrent-exec-v1-plum-parrot-tangerine + type: core/concurrent-exec@v1 + position: + x: 160 + y: 1390 + outputs: + exec[0]: null + exec[1]: null + - id: core-print-v1-blackberry-teal-fig + type: core/print@v1 + position: + x: 2460 + y: 1430 + inputs: + values[0]: null + values[1]: null + values[2]: null + values[3]: null + values[4]: null + - id: core-const-string-v1-black-snake-violet + type: core/const-string@v1 + position: + x: 2010 + y: 1650 + inputs: + value: 'left.LEFT: ${{ steps.left.outputs.LEFT }}' + - id: core-const-string-v1-lion-gray-blackberry + type: core/const-string@v1 + position: + x: 2010 + y: 1710 + inputs: + value: 'right.RIGHT: ${{ steps.right.outputs.RIGHT }}' + - id: core-const-string-v1-squirrel-octopus-pineapple + type: core/const-string@v1 + position: + x: 2030 + y: 1570 + inputs: + value: 'root.ROOT: ${{ steps.root.outputs.ROOT }}' + - id: left + type: core/run@v1 + position: + x: 650 + y: 1050 + inputs: + script: |- + echo "set left.LEFT" + echo "LEFT=Left Value" >> $GITHUB_OUTPUT + shell: bash + label: '' + - id: right + type: core/run@v1 + position: + x: 740 + y: 1650 + inputs: + script: |- + sleep 1 + echo "set right.RIGHT" + echo "RIGHT=Right Value" >> $GITHUB_OUTPUT + shell: bash + label: '' + - id: root + type: core/run@v1 + position: + x: -620 + y: 1630 + inputs: + script: |- + echo "set root.ROOT" + echo "ROOT=Im the root value" >> $GITHUB_OUTPUT + label: '' + - id: core-concurrent-exec-v1-gold-raspberry-jellyfish + type: core/concurrent-exec@v1 + position: + x: 1130 + y: 1120 + outputs: + exec[0]: null + exec[1]: null + - id: left-2 + type: core/run@v1 + position: + x: 1750 + y: 980 + inputs: + script: |- + sleep 0.5 + echo "set left-2.LEFT_2" + echo "LEFT_2=Left 2 Value" >> $GITHUB_OUTPUT + shell: bash + label: '' + - id: right-2 + type: core/run@v1 + position: + x: 1540 + y: 1580 + inputs: + script: |- + echo "set right-2.RIGHT_2" + echo "RIGHT_2=Right Value" >> $GITHUB_OUTPUT + shell: bash + label: '' + - id: core-const-string-v1-grape-jellyfish-shark + type: core/const-string@v1 + position: + x: 2010 + y: 1760 + inputs: + value: 'left-2.LEFT_2: ${{ steps.left-2.outputs.LEFT_2 }}' + - id: core-const-string-v1-nectarine-jellyfish-rabbit + type: core/const-string@v1 + position: + x: 2010 + y: 1810 + inputs: + value: 'right-2.RIGHT_2: ${{ steps.right-2.outputs.RIGHT_2 }}' + - id: core-sequence-v1-giraffe-fox-jackfruit + type: core/sequence@v1 + position: + x: -150 + y: 1570 + outputs: + exec[0]: null + exec[1]: null +connections: + - src: + node: core-const-string-v1-black-snake-violet + port: result + dst: + node: core-print-v1-blackberry-teal-fig + port: values[1] + - src: + node: core-const-string-v1-lion-gray-blackberry + port: result + dst: + node: core-print-v1-blackberry-teal-fig + port: values[2] + - src: + node: core-const-string-v1-squirrel-octopus-pineapple + port: result + dst: + node: core-print-v1-blackberry-teal-fig + port: values[0] + - src: + node: core-const-string-v1-grape-jellyfish-shark + port: result + dst: + node: core-print-v1-blackberry-teal-fig + port: values[3] + - src: + node: core-const-string-v1-nectarine-jellyfish-rabbit + port: result + dst: + node: core-print-v1-blackberry-teal-fig + port: values[4] +executions: + - src: + node: core-concurrent-exec-v1-plum-parrot-tangerine + port: exec[0] + dst: + node: left + port: exec + - src: + node: start + port: exec + dst: + node: root + port: exec + - src: + node: core-concurrent-exec-v1-plum-parrot-tangerine + port: exec-completed + dst: + node: core-print-v1-blackberry-teal-fig + port: exec + - src: + node: core-concurrent-exec-v1-gold-raspberry-jellyfish + port: exec[1] + dst: + node: right-2 + port: exec + - src: + node: left-2 + port: exec-success + dst: + node: core-print-v1-blackberry-teal-fig + port: exec + - src: + node: right + port: exec-success + dst: + node: core-print-v1-blackberry-teal-fig + port: exec + - src: + node: right-2 + port: exec-success + dst: + node: core-print-v1-blackberry-teal-fig + port: exec + - src: + node: left + port: exec-success + dst: + node: core-concurrent-exec-v1-gold-raspberry-jellyfish + port: exec + - src: + node: root + port: exec-success + dst: + node: core-sequence-v1-giraffe-fox-jackfruit + port: exec + - src: + node: core-sequence-v1-giraffe-fox-jackfruit + port: exec[0] + dst: + node: core-print-v1-blackberry-teal-fig + port: exec + - src: + node: core-sequence-v1-giraffe-fox-jackfruit + port: exec[1] + dst: + node: core-concurrent-exec-v1-plum-parrot-tangerine + port: exec + - src: + node: core-concurrent-exec-v1-plum-parrot-tangerine + port: exec[1] + dst: + node: right + port: exec + - src: + node: core-concurrent-exec-v1-gold-raspberry-jellyfish + port: exec[0] + dst: + node: left-2 + port: exec diff --git a/tests_e2e/scripts/steps_gh.sh b/tests_e2e/scripts/steps_gh.sh new file mode 100644 index 0000000..f78bf22 --- /dev/null +++ b/tests_e2e/scripts/steps_gh.sh @@ -0,0 +1,11 @@ +echo "Test Steps - GITHUB_OUTPUT and step references" + +TEST_NAME=steps_gh +GRAPH_FILE="${ACT_GRAPH_FILES_DIR}${PATH_SEPARATOR}${TEST_NAME}.act" +cp $GRAPH_FILE $TEST_NAME.act +export ACT_GRAPH_FILE=$TEST_NAME.act +export GITHUB_ACTIONS=true +export GITHUB_WORKSPACE=$ACT_GRAPH_FILES_DIR +export GITHUB_EVENT_NAME=push + +#! test actrun | sort diff --git a/tests_e2e/scripts/steps_gh_conclusion.act b/tests_e2e/scripts/steps_gh_conclusion.act new file mode 100644 index 0000000..300b6ca --- /dev/null +++ b/tests_e2e/scripts/steps_gh_conclusion.act @@ -0,0 +1,69 @@ +editor: + version: + created: v1.34.0 +entry: start +type: standard +nodes: + - id: start + type: core/start@v1 + position: + x: 0 + y: 0 + - id: step-success + type: core/run@v1 + position: + x: 200 + y: 0 + inputs: + script: | + echo "This step will succeed" + echo "SUCCESS_OUTPUT=worked" >> $GITHUB_OUTPUT + shell: bash + - id: print-conclusion-success + type: core/const-string@v1 + position: + x: 400 + y: 100 + inputs: + value: "step-success.conclusion: ${{ steps.step-success.conclusion }}" + - id: print-output + type: core/const-string@v1 + position: + x: 400 + y: 200 + inputs: + value: "step-success.outputs.SUCCESS_OUTPUT: ${{ steps.step-success.outputs.SUCCESS_OUTPUT }}" + - id: final-print + type: core/print@v1 + position: + x: 600 + y: 0 + inputs: + values[0]: null + values[1]: null +connections: + - src: + node: print-conclusion-success + port: result + dst: + node: final-print + port: values[0] + - src: + node: print-output + port: result + dst: + node: final-print + port: values[1] +executions: + - src: + node: start + port: exec + dst: + node: step-success + port: exec + - src: + node: step-success + port: exec-success + dst: + node: final-print + port: exec diff --git a/tests_e2e/scripts/steps_gh_conclusion.sh b/tests_e2e/scripts/steps_gh_conclusion.sh new file mode 100644 index 0000000..5b45546 --- /dev/null +++ b/tests_e2e/scripts/steps_gh_conclusion.sh @@ -0,0 +1,11 @@ +echo "Test Steps Conclusion - step conclusion (success/failure) via steps.X.conclusion" + +TEST_NAME=steps_gh_conclusion +GRAPH_FILE="${ACT_GRAPH_FILES_DIR}${PATH_SEPARATOR}${TEST_NAME}.act" +cp $GRAPH_FILE $TEST_NAME.act +export ACT_GRAPH_FILE=$TEST_NAME.act +export GITHUB_ACTIONS=true +export GITHUB_WORKSPACE=$ACT_GRAPH_FILES_DIR +export GITHUB_EVENT_NAME=push + +#! test actrun diff --git a/tests_e2e/scripts/steps_gh_env.act b/tests_e2e/scripts/steps_gh_env.act new file mode 100644 index 0000000..2c75f66 --- /dev/null +++ b/tests_e2e/scripts/steps_gh_env.act @@ -0,0 +1,71 @@ +editor: + version: + created: v1.34.0 +entry: start +type: standard +nodes: + - id: start + type: core/start@v1 + position: + x: 0 + y: 0 + - id: step-set-env + type: core/run@v1 + position: + x: 200 + y: 0 + inputs: + script: | + echo "Setting MY_VAR via GITHUB_ENV" + echo "MY_VAR=hello_from_env" >> $GITHUB_ENV + shell: bash + - id: step-read-env + type: core/run@v1 + position: + x: 400 + y: 0 + inputs: + script: | + echo "Reading MY_VAR: ${MY_VAR}" + echo "READ_VALUE=${MY_VAR}" >> $GITHUB_OUTPUT + shell: bash + - id: print-value + type: core/const-string@v1 + position: + x: 600 + y: 100 + inputs: + value: "Read from step output: ${{ steps.step-read-env.outputs.READ_VALUE }}" + - id: final-print + type: core/print@v1 + position: + x: 800 + y: 0 + inputs: + values[0]: null +connections: + - src: + node: print-value + port: result + dst: + node: final-print + port: values[0] +executions: + - src: + node: start + port: exec + dst: + node: step-set-env + port: exec + - src: + node: step-set-env + port: exec-success + dst: + node: step-read-env + port: exec + - src: + node: step-read-env + port: exec-success + dst: + node: final-print + port: exec diff --git a/tests_e2e/scripts/steps_gh_env.sh b/tests_e2e/scripts/steps_gh_env.sh new file mode 100644 index 0000000..97df031 --- /dev/null +++ b/tests_e2e/scripts/steps_gh_env.sh @@ -0,0 +1,11 @@ +echo "Test Steps Env - GITHUB_ENV for setting environment variables" + +TEST_NAME=steps_gh_env +GRAPH_FILE="${ACT_GRAPH_FILES_DIR}${PATH_SEPARATOR}${TEST_NAME}.act" +cp $GRAPH_FILE $TEST_NAME.act +export ACT_GRAPH_FILE=$TEST_NAME.act +export GITHUB_ACTIONS=true +export GITHUB_WORKSPACE=$ACT_GRAPH_FILES_DIR +export GITHUB_EVENT_NAME=push + +#! test actrun diff --git a/tests_e2e/scripts/steps_gh_forloop.act b/tests_e2e/scripts/steps_gh_forloop.act new file mode 100644 index 0000000..cf54678 --- /dev/null +++ b/tests_e2e/scripts/steps_gh_forloop.act @@ -0,0 +1,117 @@ +editor: + version: + created: v1.34.0 +entry: start +type: standard +nodes: + - id: start + type: core/start@v1 + position: + x: 0 + y: 0 + - id: init-step + type: core/run@v1 + position: + x: 200 + y: 0 + inputs: + script: | + echo "Setting INIT_VALUE" + echo "INIT_VALUE=initialized" >> $GITHUB_OUTPUT + shell: bash + - id: for-loop + type: core/for-loop@v1 + position: + x: 400 + y: 0 + inputs: + last_index: 3 + - id: index-to-array + type: core/string-array@v1 + position: + x: 600 + y: 100 + inputs: + inputs[0]: null + - id: loop-step + type: core/run@v1 + position: + x: 700 + y: 0 + inputs: + script: | + echo "Loop iteration $1" + echo "LOOP_OUTPUT=iteration_$1" >> $GITHUB_OUTPUT + shell: bash + - id: print-init + type: core/const-string@v1 + position: + x: 900 + y: 100 + inputs: + value: "init-step.INIT_VALUE: ${{ steps.init-step.outputs.INIT_VALUE }}" + - id: print-loop + type: core/const-string@v1 + position: + x: 900 + y: 200 + inputs: + value: "loop-step.LOOP_OUTPUT: ${{ steps.loop-step.outputs.LOOP_OUTPUT }}" + - id: loop-print + type: core/print@v1 + position: + x: 1100 + y: 0 + inputs: + values[0]: null + values[1]: null +connections: + - src: + node: for-loop + port: index + dst: + node: index-to-array + port: inputs[0] + - src: + node: index-to-array + port: array + dst: + node: loop-step + port: args + - src: + node: print-init + port: result + dst: + node: loop-print + port: values[0] + - src: + node: print-loop + port: result + dst: + node: loop-print + port: values[1] +executions: + - src: + node: start + port: exec + dst: + node: init-step + port: exec + - src: + node: init-step + port: exec-success + dst: + node: for-loop + port: exec + - src: + node: for-loop + port: exec-body + dst: + node: loop-step + port: exec + - src: + node: loop-step + port: exec-success + dst: + node: loop-print + port: exec diff --git a/tests_e2e/scripts/steps_gh_forloop.sh b/tests_e2e/scripts/steps_gh_forloop.sh new file mode 100644 index 0000000..75689f2 --- /dev/null +++ b/tests_e2e/scripts/steps_gh_forloop.sh @@ -0,0 +1,11 @@ +echo "Test Steps For Loop - GITHUB_OUTPUT in for-loop iterations" + +TEST_NAME=steps_gh_forloop +GRAPH_FILE="${ACT_GRAPH_FILES_DIR}${PATH_SEPARATOR}${TEST_NAME}.act" +cp $GRAPH_FILE $TEST_NAME.act +export ACT_GRAPH_FILE=$TEST_NAME.act +export GITHUB_ACTIONS=true +export GITHUB_WORKSPACE=$ACT_GRAPH_FILES_DIR +export GITHUB_EVENT_NAME=push + +#! test actrun diff --git a/tests_e2e/scripts/steps_gh_heredoc.act b/tests_e2e/scripts/steps_gh_heredoc.act new file mode 100644 index 0000000..3707064 --- /dev/null +++ b/tests_e2e/scripts/steps_gh_heredoc.act @@ -0,0 +1,91 @@ +editor: + version: + created: v1.34.0 +entry: start +type: standard +nodes: + - id: start + type: core/start@v1 + position: + x: 0 + y: 0 + - id: step-heredoc + type: core/run@v1 + position: + x: 200 + y: 0 + inputs: + script: | + echo "Setting MULTILINE_OUTPUT using heredoc" + { + echo "MULTILINE_OUTPUT<> $GITHUB_OUTPUT + shell: bash + - id: step-simple + type: core/run@v1 + position: + x: 400 + y: 0 + inputs: + script: | + echo "Setting SIMPLE_OUTPUT" + echo "SIMPLE_OUTPUT=simple_value" >> $GITHUB_OUTPUT + shell: bash + - id: print-heredoc + type: core/const-string@v1 + position: + x: 600 + y: 100 + inputs: + value: "heredoc output: [${{ steps.step-heredoc.outputs.MULTILINE_OUTPUT }}]" + - id: print-simple + type: core/const-string@v1 + position: + x: 600 + y: 200 + inputs: + value: "simple output: [${{ steps.step-simple.outputs.SIMPLE_OUTPUT }}]" + - id: final-print + type: core/print@v1 + position: + x: 800 + y: 0 + inputs: + values[0]: null + values[1]: null +connections: + - src: + node: print-heredoc + port: result + dst: + node: final-print + port: values[0] + - src: + node: print-simple + port: result + dst: + node: final-print + port: values[1] +executions: + - src: + node: start + port: exec + dst: + node: step-heredoc + port: exec + - src: + node: step-heredoc + port: exec-success + dst: + node: step-simple + port: exec + - src: + node: step-simple + port: exec-success + dst: + node: final-print + port: exec diff --git a/tests_e2e/scripts/steps_gh_heredoc.sh b/tests_e2e/scripts/steps_gh_heredoc.sh new file mode 100644 index 0000000..aa9a719 --- /dev/null +++ b/tests_e2e/scripts/steps_gh_heredoc.sh @@ -0,0 +1,11 @@ +echo "Test Heredoc Steps - GITHUB_OUTPUT with multiline values" + +TEST_NAME=steps_gh_heredoc +GRAPH_FILE="${ACT_GRAPH_FILES_DIR}${PATH_SEPARATOR}${TEST_NAME}.act" +cp $GRAPH_FILE $TEST_NAME.act +export ACT_GRAPH_FILE=$TEST_NAME.act +export GITHUB_ACTIONS=true +export GITHUB_WORKSPACE=$ACT_GRAPH_FILES_DIR +export GITHUB_EVENT_NAME=push + +#! test actrun diff --git a/tests_e2e/scripts/steps_gh_path.act b/tests_e2e/scripts/steps_gh_path.act new file mode 100644 index 0000000..583e5c5 --- /dev/null +++ b/tests_e2e/scripts/steps_gh_path.act @@ -0,0 +1,76 @@ +editor: + version: + created: v1.34.0 +entry: start +type: standard +nodes: + - id: start + type: core/start@v1 + position: + x: 0 + y: 0 + - id: step-add-path + type: core/run@v1 + position: + x: 200 + y: 0 + inputs: + script: | + echo "Adding /custom/bin to PATH via GITHUB_PATH" + echo "/custom/bin" >> $GITHUB_PATH + shell: bash + - id: step-check-path + type: core/run@v1 + position: + x: 400 + y: 0 + inputs: + script: | + if [[ "$PATH" == *"/custom/bin"* ]]; then + echo "SUCCESS: PATH contains /custom/bin" + echo "RESULT=path_modified" >> $GITHUB_OUTPUT + else + echo "FAIL: PATH does not contain /custom/bin" + echo "RESULT=path_not_modified" >> $GITHUB_OUTPUT + fi + shell: bash + - id: print-result + type: core/const-string@v1 + position: + x: 600 + y: 100 + inputs: + value: "Path modification result: ${{ steps.step-check-path.outputs.RESULT }}" + - id: final-print + type: core/print@v1 + position: + x: 800 + y: 0 + inputs: + values[0]: null +connections: + - src: + node: print-result + port: result + dst: + node: final-print + port: values[0] +executions: + - src: + node: start + port: exec + dst: + node: step-add-path + port: exec + - src: + node: step-add-path + port: exec-success + dst: + node: step-check-path + port: exec + - src: + node: step-check-path + port: exec-success + dst: + node: final-print + port: exec diff --git a/tests_e2e/scripts/steps_gh_path.sh b/tests_e2e/scripts/steps_gh_path.sh new file mode 100644 index 0000000..eef0fa5 --- /dev/null +++ b/tests_e2e/scripts/steps_gh_path.sh @@ -0,0 +1,11 @@ +echo "Test Steps Path - GITHUB_PATH for modifying PATH" + +TEST_NAME=steps_gh_path +GRAPH_FILE="${ACT_GRAPH_FILES_DIR}${PATH_SEPARATOR}${TEST_NAME}.act" +cp $GRAPH_FILE $TEST_NAME.act +export ACT_GRAPH_FILE=$TEST_NAME.act +export GITHUB_ACTIONS=true +export GITHUB_WORKSPACE=$ACT_GRAPH_FILES_DIR +export GITHUB_EVENT_NAME=push + +#! test actrun diff --git a/tests_e2e/scripts/steps_gh_sequential.act b/tests_e2e/scripts/steps_gh_sequential.act new file mode 100644 index 0000000..1461b38 --- /dev/null +++ b/tests_e2e/scripts/steps_gh_sequential.act @@ -0,0 +1,85 @@ +editor: + version: + created: v1.34.0 +entry: start +type: standard +nodes: + - id: start + type: core/start@v1 + position: + x: 0 + y: 0 + - id: step1 + type: core/run@v1 + position: + x: 200 + y: 0 + inputs: + script: | + echo "Setting STEP1_OUTPUT" + echo "STEP1_OUTPUT=value_from_step1" >> $GITHUB_OUTPUT + shell: bash + - id: step2 + type: core/run@v1 + position: + x: 400 + y: 0 + inputs: + script: | + echo "Setting STEP2_OUTPUT" + echo "STEP2_OUTPUT=value_from_step2" >> $GITHUB_OUTPUT + shell: bash + - id: print-step1 + type: core/const-string@v1 + position: + x: 600 + y: 100 + inputs: + value: "step1.STEP1_OUTPUT: ${{ steps.step1.outputs.STEP1_OUTPUT }}" + - id: print-step2 + type: core/const-string@v1 + position: + x: 600 + y: 200 + inputs: + value: "step2.STEP2_OUTPUT: ${{ steps.step2.outputs.STEP2_OUTPUT }}" + - id: final-print + type: core/print@v1 + position: + x: 800 + y: 0 + inputs: + values[0]: null + values[1]: null +connections: + - src: + node: print-step1 + port: result + dst: + node: final-print + port: values[0] + - src: + node: print-step2 + port: result + dst: + node: final-print + port: values[1] +executions: + - src: + node: start + port: exec + dst: + node: step1 + port: exec + - src: + node: step1 + port: exec-success + dst: + node: step2 + port: exec + - src: + node: step2 + port: exec-success + dst: + node: final-print + port: exec diff --git a/tests_e2e/scripts/steps_gh_sequential.sh b/tests_e2e/scripts/steps_gh_sequential.sh new file mode 100644 index 0000000..5a0acb5 --- /dev/null +++ b/tests_e2e/scripts/steps_gh_sequential.sh @@ -0,0 +1,11 @@ +echo "Test Sequential Steps - GITHUB_OUTPUT and step references" + +TEST_NAME=steps_gh_sequential +GRAPH_FILE="${ACT_GRAPH_FILES_DIR}${PATH_SEPARATOR}${TEST_NAME}.act" +cp $GRAPH_FILE $TEST_NAME.act +export ACT_GRAPH_FILE=$TEST_NAME.act +export GITHUB_ACTIONS=true +export GITHUB_WORKSPACE=$ACT_GRAPH_FILES_DIR +export GITHUB_EVENT_NAME=push + +#! test actrun From b4d520778544c198f80da83fe0225cd256cc6ad1 Mon Sep 17 00:00:00 2001 From: Sebatian Rath Date: Mon, 2 Feb 2026 14:00:02 -0500 Subject: [PATCH 5/7] Update e2e tests --- nodes/docker-run@v1.go | 27 ++----- tests_e2e/references/reference_app.sh_l12 | 2 +- .../references/reference_dir-walk.sh_l56 | 8 +- .../reference_error_no_output.sh_l8 | 16 ++-- .../references/reference_group-error.sh_l8 | 74 +++++++++---------- .../reference_group-port-collision.sh_l13 | 12 +-- tests_e2e/references/reference_index.sh_l20 | 16 ++-- .../reference_run-python-embedded.sh_l13 | 12 +-- .../references/reference_s3_aws_walk.sh_l22 | 8 +- .../references/reference_s3_aws_walk.sh_l44 | 8 +- .../references/reference_select-data.sh_l9 | 16 ++-- .../reference_string-transform.sh_l61 | 14 ++-- 12 files changed, 100 insertions(+), 113 deletions(-) diff --git a/nodes/docker-run@v1.go b/nodes/docker-run@v1.go index b7774da..7a02ed3 100644 --- a/nodes/docker-run@v1.go +++ b/nodes/docker-run@v1.go @@ -245,7 +245,7 @@ func (n *DockerNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId, p // Handle GITHUB_ENV and GITHUB_OUTPUT for GitHub workflows if c.IsGitHubWorkflow { ghContextParser := GhContextParser{} - ghEnvs, err := ghContextParser.Parse(c, currentEnvMap) + ghEnvs, ghOutputs, err := ghContextParser.Parse(c, currentEnvMap) if err != nil { return err } @@ -254,28 +254,15 @@ func (n *DockerNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId, p maps.Copy(nextEnvMap, ghEnvs) c.SetContextEnvironMap(nextEnvMap) - // Parse GITHUB_OUTPUT file if it exists - githubOutput := currentEnvMap["GITHUB_OUTPUT"] - if githubOutput != "" { - b, err := os.ReadFile(githubOutput) - if err != nil { - return core.CreateErr(c, err, "unable to read github output file") - } - - outputs, err := parseOutputFile(string(b)) + for key, value := range ghOutputs { + err = n.SetOutputValue(c, core.OutputId(key), value, core.SetOutputValueOpts{ + NotExistsIsNoError: true, + ForceSet: true, + StringTypeHint: true, + }) if err != nil { return err } - for key, value := range outputs { - err = n.SetOutputValue(c, core.OutputId(key), strings.TrimRight(value, "\t\n"), core.SetOutputValueOpts{ - NotExistsIsNoError: true, - }) - if err != nil { - return err - } - } - - _ = os.Remove(githubOutput) } } diff --git a/tests_e2e/references/reference_app.sh_l12 b/tests_e2e/references/reference_app.sh_l12 index a7a9425..d9fd55e 100644 --- a/tests_e2e/references/reference_app.sh_l12 +++ b/tests_e2e/references/reference_app.sh_l12 @@ -23,7 +23,7 @@ hint: stack trace: github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1089 + graph.go:1111 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_dir-walk.sh_l56 b/tests_e2e/references/reference_dir-walk.sh_l56 index 1359598..4ed3c68 100644 --- a/tests_e2e/references/reference_dir-walk.sh_l56 +++ b/tests_e2e/references/reference_dir-walk.sh_l56 @@ -34,17 +34,17 @@ stack trace: github.com/actionforge/actrun-cli/nodes.(*WalkNode).ExecuteImpl dir-walk@v1.go:61 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl start@v1.go:50 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:484 + graph.go:506 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1096 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + graph.go:1114 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_error_no_output.sh_l8 b/tests_e2e/references/reference_error_no_output.sh_l8 index 0259b6d..57a2131 100644 --- a/tests_e2e/references/reference_error_no_output.sh_l8 +++ b/tests_e2e/references/reference_error_no_output.sh_l8 @@ -33,29 +33,29 @@ hint: stack trace: github.com/actionforge/actrun-cli/core.(*Outputs).OutputValueById - outputs.go:112 + outputs.go:114 github.com/actionforge/actrun-cli/core.(*Inputs).InputValueById inputs.go:364 github.com/actionforge/actrun-cli/core.inputValueById[...] - inputs.go:478 + inputs.go:483 github.com/actionforge/actrun-cli/core.InputValueFromSubInputs[...] - inputs.go:473 + inputs.go:478 github.com/actionforge/actrun-cli/core.InputArrayValueById[...] - inputs.go:555 + inputs.go:560 github.com/actionforge/actrun-cli/nodes.(*PrintNode).ExecuteImpl print@v1.go:27 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl start@v1.go:50 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:484 + graph.go:506 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1096 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + graph.go:1114 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_group-error.sh_l8 b/tests_e2e/references/reference_group-error.sh_l8 index dfef3aa..c00aabb 100644 --- a/tests_e2e/references/reference_group-error.sh_l8 +++ b/tests_e2e/references/reference_group-error.sh_l8 @@ -126,131 +126,131 @@ error: stack trace: github.com/actionforge/actrun-cli/nodes.runAndCaptureOutput - run@v1.go:384 + run@v1.go:393 github.com/actionforge/actrun-cli/nodes.runCommand - run@v1.go:260 + run@v1.go:269 github.com/actionforge/actrun-cli/nodes.(*RunNode).ExecuteImpl - run@v1.go:112 + run@v1.go:110 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupInputsNode).ExecuteImpl group-inputs@v1.go:39 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupNode).ExecuteImpl group@v1.go:102 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*PrintNode).ExecuteImpl print@v1.go:103 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupNode).ExecuteImpl group@v1.go:102 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupOutputsNode).ExecuteImpl group-outputs@v1.go:30 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*RunNode).ExecuteImpl - run@v1.go:129 + run@v1.go:127 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupInputsNode).ExecuteImpl group-inputs@v1.go:39 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupNode).ExecuteImpl group@v1.go:102 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*PrintNode).ExecuteImpl print@v1.go:103 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupNode).ExecuteImpl group@v1.go:102 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupOutputsNode).ExecuteImpl group-outputs@v1.go:30 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupNode).ExecuteImpl group@v1.go:102 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupOutputsNode).ExecuteImpl group-outputs@v1.go:30 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*RunNode).ExecuteImpl - run@v1.go:129 + run@v1.go:127 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupInputsNode).ExecuteImpl group-inputs@v1.go:39 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupNode).ExecuteImpl group@v1.go:102 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupInputsNode).ExecuteImpl group-inputs@v1.go:39 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupNode).ExecuteImpl group@v1.go:102 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*PrintNode).ExecuteImpl print@v1.go:103 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupNode).ExecuteImpl group@v1.go:102 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupOutputsNode).ExecuteImpl group-outputs@v1.go:30 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupNode).ExecuteImpl group@v1.go:102 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupOutputsNode).ExecuteImpl group-outputs@v1.go:30 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*RunNode).ExecuteImpl - run@v1.go:146 + run@v1.go:155 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupInputsNode).ExecuteImpl group-inputs@v1.go:39 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupNode).ExecuteImpl group@v1.go:102 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupInputsNode).ExecuteImpl group-inputs@v1.go:39 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*GroupNode).ExecuteImpl group@v1.go:102 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl start@v1.go:50 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:484 + graph.go:506 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1096 diff --git a/tests_e2e/references/reference_group-port-collision.sh_l13 b/tests_e2e/references/reference_group-port-collision.sh_l13 index e769563..a131a4f 100644 --- a/tests_e2e/references/reference_group-port-collision.sh_l13 +++ b/tests_e2e/references/reference_group-port-collision.sh_l13 @@ -25,17 +25,17 @@ github.com/actionforge/actrun-cli/nodes.init.41.func1 github.com/actionforge/actrun-cli/core.NewNodeInstance base.go:619 github.com/actionforge/actrun-cli/core.LoadNode - graph.go:657 + graph.go:679 github.com/actionforge/actrun-cli/core.LoadNodes - graph.go:597 + graph.go:619 github.com/actionforge/actrun-cli/core.LoadGraph - graph.go:512 + graph.go:534 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:279 + graph.go:280 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1096 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + graph.go:1114 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_index.sh_l20 b/tests_e2e/references/reference_index.sh_l20 index 39898cf..1f2e4bb 100644 --- a/tests_e2e/references/reference_index.sh_l20 +++ b/tests_e2e/references/reference_index.sh_l20 @@ -66,29 +66,29 @@ github.com/actionforge/actrun-cli/nodes.(*ArrayGet).OutputValueById github.com/actionforge/actrun-cli/core.(*Inputs).InputValueById inputs.go:364 github.com/actionforge/actrun-cli/core.inputValueById[...] - inputs.go:478 + inputs.go:483 github.com/actionforge/actrun-cli/core.InputValueFromSubInputs[...] - inputs.go:473 + inputs.go:478 github.com/actionforge/actrun-cli/core.InputArrayValueById[...] - inputs.go:555 + inputs.go:560 github.com/actionforge/actrun-cli/nodes.(*PrintNode).ExecuteImpl print@v1.go:27 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*LoopNode).ExecuteImpl for-loop@v1.go:54 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl start@v1.go:50 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:484 + graph.go:506 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1096 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + graph.go:1114 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_run-python-embedded.sh_l13 b/tests_e2e/references/reference_run-python-embedded.sh_l13 index c2d4a20..0b31eda 100644 --- a/tests_e2e/references/reference_run-python-embedded.sh_l13 +++ b/tests_e2e/references/reference_run-python-embedded.sh_l13 @@ -29,17 +29,17 @@ github.com/actionforge/actrun-cli/nodes.init.52.func1 github.com/actionforge/actrun-cli/core.NewNodeInstance base.go:619 github.com/actionforge/actrun-cli/core.LoadNode - graph.go:657 + graph.go:679 github.com/actionforge/actrun-cli/core.LoadNodes - graph.go:597 + graph.go:619 github.com/actionforge/actrun-cli/core.LoadGraph - graph.go:512 + graph.go:534 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:279 + graph.go:280 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1096 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + graph.go:1114 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_s3_aws_walk.sh_l22 b/tests_e2e/references/reference_s3_aws_walk.sh_l22 index 4042e37..313feac 100644 --- a/tests_e2e/references/reference_s3_aws_walk.sh_l22 +++ b/tests_e2e/references/reference_s3_aws_walk.sh_l22 @@ -38,17 +38,17 @@ stack trace: github.com/actionforge/actrun-cli/nodes.(*StorageListNode).ExecuteImpl storage-walk@v1.go:45 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl start@v1.go:50 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:484 + graph.go:506 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1096 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + graph.go:1114 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_s3_aws_walk.sh_l44 b/tests_e2e/references/reference_s3_aws_walk.sh_l44 index 4042e37..313feac 100644 --- a/tests_e2e/references/reference_s3_aws_walk.sh_l44 +++ b/tests_e2e/references/reference_s3_aws_walk.sh_l44 @@ -38,17 +38,17 @@ stack trace: github.com/actionforge/actrun-cli/nodes.(*StorageListNode).ExecuteImpl storage-walk@v1.go:45 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl start@v1.go:50 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:484 + graph.go:506 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1096 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + graph.go:1114 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_select-data.sh_l9 b/tests_e2e/references/reference_select-data.sh_l9 index 61a06c4..289f2d8 100644 --- a/tests_e2e/references/reference_select-data.sh_l9 +++ b/tests_e2e/references/reference_select-data.sh_l9 @@ -73,29 +73,29 @@ github.com/actionforge/actrun-cli/nodes.(*SelectDataNode).OutputValueById github.com/actionforge/actrun-cli/core.(*Inputs).InputValueById inputs.go:364 github.com/actionforge/actrun-cli/core.inputValueById[...] - inputs.go:478 + inputs.go:483 github.com/actionforge/actrun-cli/core.InputValueFromSubInputs[...] - inputs.go:473 + inputs.go:478 github.com/actionforge/actrun-cli/core.InputArrayValueById[...] - inputs.go:555 + inputs.go:560 github.com/actionforge/actrun-cli/nodes.(*PrintNode).ExecuteImpl print@v1.go:27 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*LoopNode).ExecuteImpl for-loop@v1.go:54 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl start@v1.go:50 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:484 + graph.go:506 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1096 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + graph.go:1114 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_string-transform.sh_l61 b/tests_e2e/references/reference_string-transform.sh_l61 index f5bc3d2..3465078 100644 --- a/tests_e2e/references/reference_string-transform.sh_l61 +++ b/tests_e2e/references/reference_string-transform.sh_l61 @@ -37,25 +37,25 @@ github.com/actionforge/actrun-cli/nodes.(*StringTransform).OutputValueById github.com/actionforge/actrun-cli/core.(*Inputs).InputValueById inputs.go:364 github.com/actionforge/actrun-cli/core.inputValueById[...] - inputs.go:478 + inputs.go:483 github.com/actionforge/actrun-cli/core.InputValueFromSubInputs[...] - inputs.go:473 + inputs.go:478 github.com/actionforge/actrun-cli/core.InputArrayValueById[...] - inputs.go:555 + inputs.go:560 github.com/actionforge/actrun-cli/nodes.(*PrintNode).ExecuteImpl print@v1.go:27 github.com/actionforge/actrun-cli/core.(*Executions).Execute - executions.go:56 + executions.go:68 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl start@v1.go:50 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:484 + graph.go:506 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1096 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + graph.go:1114 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute From a2a3f58202fe6d8d9fde94d2af575f4db9316e28 Mon Sep 17 00:00:00 2001 From: Sebatian Rath Date: Mon, 2 Feb 2026 20:21:55 -0500 Subject: [PATCH 6/7] Address security issues and update e2e tests --- core/docker.go | 4 +- core/github.go | 15 ++++-- core/github_evaluator.go | 15 ++++-- core/graph.go | 24 ++++++--- nodes/credentials-ssh@v1.go | 17 ++++--- nodes/dir-create@v1.go | 10 +++- nodes/dir-walk@v1.go | 6 +++ nodes/docker-run@v1.go | 13 +++-- nodes/file-compress@v1.go | 17 ++++--- nodes/file-read@v1.go | 14 ++++-- nodes/file-write@v1.go | 7 ++- nodes/gh-action@v1.go | 10 ++-- nodes/gh-context-parser.go | 42 +++++++++++----- nodes/hash@v1.go | 16 +++--- nodes/item-stats@v1.go | 8 ++- tests_e2e/references/reference_app.sh_l12 | 2 +- .../references/reference_dir-walk.sh_l56 | 8 +-- .../reference_error_no_output.sh_l8 | 6 +-- .../references/reference_group-error.sh_l8 | 4 +- .../reference_group-port-collision.sh_l13 | 10 ++-- tests_e2e/references/reference_index.sh_l20 | 6 +-- .../reference_run-python-embedded.sh_l13 | 10 ++-- .../references/reference_s3_aws_walk.sh_l22 | 6 +-- .../references/reference_s3_aws_walk.sh_l44 | 6 +-- .../references/reference_select-data.sh_l9 | 6 +-- .../reference_string-transform.sh_l61 | 6 +-- tests_e2e/tests_e2e.py | 2 +- utils/path.go | 49 +++++++++++++++++++ utils/utils.go | 13 +++++ 29 files changed, 255 insertions(+), 97 deletions(-) diff --git a/core/docker.go b/core/docker.go index 4584116..d23b97e 100644 --- a/core/docker.go +++ b/core/docker.go @@ -100,7 +100,7 @@ func (d *DockerClient) PullImage(ctx context.Context, imageRef string) error { verbose := !IsTestE2eRunning() if verbose { - utils.LogOut.Infof("%sPulling image '%s'\n", utils.LogGhStartGroup, imageRef) + utils.LogOut.Infof("%sPulling image '%s'\n", utils.LogGhStartGroup, utils.SanitizeImageRef(imageRef)) defer utils.LogOut.Infof(utils.LogGhEndGroup) } @@ -138,7 +138,7 @@ func (d *DockerClient) BuildImage(ctx context.Context, dockerfilePath, contextPa verbose := !IsTestE2eRunning() if verbose { - utils.LogOut.Infof("%sBuilding image '%s' from %s\n", utils.LogGhStartGroup, tag, dockerfilePath) + utils.LogOut.Infof("%sBuilding image '%s' from %s\n", utils.LogGhStartGroup, utils.SanitizeImageRef(tag), utils.SanitizeImageRef(dockerfilePath)) defer utils.LogOut.Infof(utils.LogGhEndGroup) } diff --git a/core/github.go b/core/github.go index fcf7710..c1b6f10 100644 --- a/core/github.go +++ b/core/github.go @@ -213,9 +213,12 @@ func LoadGitHubContext(env map[string]string, inputs map[string]any, secrets map // Support for github.event.pull_request, github.event.commits, etc. eventData := make(map[string]any) if eventPath, ok := env["GITHUB_EVENT_PATH"]; ok && eventPath != "" { - fileContent, err := os.ReadFile(eventPath) + cleanPath, err := utils.ValidatePath(eventPath) if err == nil { - _ = json.Unmarshal(fileContent, &eventData) + fileContent, err := os.ReadFile(cleanPath) + if err == nil { + _ = json.Unmarshal(fileContent, &eventData) + } } } @@ -439,9 +442,13 @@ func SetupGitHubActionsEnv(finalEnv map[string]string) error { for _, filePath := range fileCommandFiles { if filePath != "" { - f, err := os.Create(filePath) + cleanPath, err := utils.ValidatePath(filePath) + if err != nil { + return CreateErr(nil, err, "invalid file command path %s", filePath) + } + f, err := os.Create(cleanPath) if err != nil { - return CreateErr(nil, err, "failed to create file command file %s", filePath). + return CreateErr(nil, err, "failed to create file command file %s", cleanPath). SetHint("Check that you have write permissions to the runner temp directory.") } f.Close() diff --git a/core/github_evaluator.go b/core/github_evaluator.go index 5db761e..bca8415 100644 --- a/core/github_evaluator.go +++ b/core/github_evaluator.go @@ -16,6 +16,7 @@ import ( "strconv" "strings" + "github.com/actionforge/actrun-cli/utils" "github.com/rhysd/actionlint" ) @@ -318,9 +319,13 @@ func (e *Evaluator) hashFiles(patterns ...string) (string, error) { } var uniqueFiles []string for f := range fileSet { - info, err := os.Stat(f) + cleanPath, err := utils.ValidatePath(f) + if err != nil { + continue + } + info, err := os.Stat(cleanPath) if err == nil && info.Mode().IsRegular() { - uniqueFiles = append(uniqueFiles, f) + uniqueFiles = append(uniqueFiles, cleanPath) } } sort.Strings(uniqueFiles) @@ -331,7 +336,11 @@ func (e *Evaluator) hashFiles(patterns ...string) (string, error) { hasher := sha256.New() for _, filePath := range uniqueFiles { - file, err := os.Open(filePath) + cleanPath, err := utils.ValidatePath(filePath) + if err != nil { + continue + } + file, err := os.Open(cleanPath) if err != nil { continue } diff --git a/core/graph.go b/core/graph.go index a865ad0..7bd829f 100644 --- a/core/graph.go +++ b/core/graph.go @@ -325,13 +325,17 @@ func RunGraph(ctx context.Context, graphName string, graphContent []byte, opts R // Priority 1 (Lowest): Config file if opts.ConfigFile != "" { - if _, err := os.Stat(opts.ConfigFile); err == nil { - localConfig, err := utils.LoadConfig(opts.ConfigFile) + cleanConfigPath, err := utils.ValidatePath(opts.ConfigFile) + if err != nil { + return CreateErr(nil, err, "invalid config file path") + } + if _, err := os.Stat(cleanConfigPath); err == nil { + localConfig, err := utils.LoadConfig(cleanConfigPath) if err != nil { return CreateErr(nil, err, "failed to load config file") } - configName := filepath.Base(opts.ConfigFile) + configName := filepath.Base(cleanConfigPath) envTracker.set(localConfig.Env, configName, true, false) inputTracker.set(localConfig.Inputs, configName, true, false) secretTracker.set(localConfig.Secrets, configName, true, true) @@ -463,11 +467,15 @@ func RunGraph(ctx context.Context, graphName string, graphContent []byte, opts R } if newCwd != "" { + cleanCwd, err := utils.ValidatePath(newCwd) + if err != nil { + return CreateErr(nil, err, "invalid working directory path") + } originalCwd, err := os.Getwd() if err != nil { return CreateErr(nil, err, "failed to get current working directory") } - if err := os.Chdir(newCwd); err != nil { + if err := os.Chdir(cleanCwd); err != nil { return CreateErr(nil, err, "failed to change working directory to ACT_CWD/GITHUB_WORKSPACE") } defer func() { @@ -1102,10 +1110,14 @@ func RunGraphFromString(ctx context.Context, graphName string, graphContent stri } func RunGraphFromFile(ctx context.Context, graphFile string, opts RunOpts, debugCb DebugCallback) error { - graphContent, err := os.ReadFile(graphFile) + cleanPath, err := utils.ValidatePath(graphFile) + if err != nil { + return CreateErr(nil, err, "invalid graph file path") + } + graphContent, err := os.ReadFile(cleanPath) if err != nil { if os.IsNotExist(err) { - err = fmt.Errorf("open %s: no such file or directory", graphFile) + err = fmt.Errorf("open %s: no such file or directory", cleanPath) } return CreateErr(nil, err, "failed loading graph") diff --git a/nodes/credentials-ssh@v1.go b/nodes/credentials-ssh@v1.go index 86b6c93..1d54bfe 100644 --- a/nodes/credentials-ssh@v1.go +++ b/nodes/credentials-ssh@v1.go @@ -1,12 +1,12 @@ package nodes import ( - "github.com/actionforge/actrun-cli/core" - _ "embed" "os" + "github.com/actionforge/actrun-cli/core" ni "github.com/actionforge/actrun-cli/node_interfaces" + "github.com/actionforge/actrun-cli/utils" ) //go:embed credentials-ssh@v1.yml @@ -48,19 +48,22 @@ func (n *SshCredentialNode) OutputValueById(c *core.ExecutionState, outputId cor } expandedPath = homeDir + expandedPath[1:] } - _, err = os.Stat(expandedPath) - privateKeyInput = expandedPath + cleanPath, pathErr := utils.ValidatePath(expandedPath) + if pathErr != nil { + return nil, core.CreateErr(c, pathErr, "invalid private key path") + } + _, err = os.Stat(cleanPath) if err == nil { - keyBytes, readErr := os.ReadFile(privateKeyInput) + keyBytes, readErr := os.ReadFile(cleanPath) if readErr != nil { - return nil, core.CreateErr(c, readErr, "failed to read private key from path '%s'", privateKeyInput) + return nil, core.CreateErr(c, readErr, "failed to read private key from path '%s'", cleanPath) } keyContent = string(keyBytes) if keyContent == "" { return nil, core.CreateErr(c, nil, "private key content is empty") } } else { - return nil, core.CreateErr(c, err, "error checking path for private key '%s'", privateKeyInput) + return nil, core.CreateErr(c, err, "error checking path for private key '%s'", cleanPath) } credential := SshCredentials{ diff --git a/nodes/dir-create@v1.go b/nodes/dir-create@v1.go index ba3f16c..a4b7e67 100644 --- a/nodes/dir-create@v1.go +++ b/nodes/dir-create@v1.go @@ -6,6 +6,7 @@ import ( "github.com/actionforge/actrun-cli/core" ni "github.com/actionforge/actrun-cli/node_interfaces" + "github.com/actionforge/actrun-cli/utils" ) //go:embed dir-create@v1.yml @@ -29,11 +30,16 @@ func (n *DirCreateNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId return err } + cleanPath, pathErr := utils.ValidatePath(path) + if pathErr != nil { + return core.CreateErr(c, pathErr, "invalid directory path") + } + var mkdirErr error if mkdirAll { - mkdirErr = os.MkdirAll(path, 0755) + mkdirErr = os.MkdirAll(cleanPath, 0755) } else { - mkdirErr = os.Mkdir(path, 0755) + mkdirErr = os.Mkdir(cleanPath, 0755) } if mkdirErr != nil { diff --git a/nodes/dir-walk@v1.go b/nodes/dir-walk@v1.go index 52f8f97..bcd775b 100644 --- a/nodes/dir-walk@v1.go +++ b/nodes/dir-walk@v1.go @@ -9,6 +9,7 @@ import ( "github.com/actionforge/actrun-cli/core" ni "github.com/actionforge/actrun-cli/node_interfaces" + "github.com/actionforge/actrun-cli/utils" "golang.org/x/exp/maps" ) @@ -118,6 +119,11 @@ func walk(root string, opts walkOpts, pattern []string, items map[string]os.File } } + root, err = utils.ValidatePath(root) + if err != nil { + return "", core.CreateErr(nil, err, "invalid path") + } + root, err = filepath.Abs(root) if err != nil { return "", core.CreateErr(nil, err, "failed to get absolute path") diff --git a/nodes/docker-run@v1.go b/nodes/docker-run@v1.go index 7a02ed3..d26114e 100644 --- a/nodes/docker-run@v1.go +++ b/nodes/docker-run@v1.go @@ -11,6 +11,7 @@ import ( "github.com/actionforge/actrun-cli/core" ni "github.com/actionforge/actrun-cli/node_interfaces" + "github.com/actionforge/actrun-cli/utils" "github.com/google/uuid" ) @@ -130,13 +131,19 @@ func (n *DockerNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId, p dockerfilePath = filepath.Join(cwd, dockerfilePath) } - if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) { + cleanPath, pathErr := utils.ValidatePath(dockerfilePath) + if pathErr != nil { + return core.CreateErr(c, pathErr, "invalid Dockerfile path") + } + + if _, err := os.Stat(cleanPath); os.IsNotExist(err) { // check if this looks like an image reference (user forgot docker:// prefix) if looksLikeImageReference(imageRef) { - return core.CreateErr(c, nil, "Dockerfile not found: %s. Did you mean 'docker://%s' to pull from a registry?", dockerfilePath, imageRef) + return core.CreateErr(c, nil, "Dockerfile not found: %s. Did you mean 'docker://%s' to pull from a registry?", cleanPath, imageRef) } - return core.CreateErr(c, nil, "Dockerfile not found: %s", dockerfilePath) + return core.CreateErr(c, nil, "Dockerfile not found: %s", cleanPath) } + dockerfilePath = cleanPath var containerIdSuffix string if core.IsTestE2eRunning() { diff --git a/nodes/file-compress@v1.go b/nodes/file-compress@v1.go index d0cab9a..32d1ffc 100644 --- a/nodes/file-compress@v1.go +++ b/nodes/file-compress@v1.go @@ -16,6 +16,7 @@ import ( "github.com/actionforge/actrun-cli/core" ni "github.com/actionforge/actrun-cli/node_interfaces" + "github.com/actionforge/actrun-cli/utils" "golang.org/x/exp/maps" ) @@ -126,25 +127,29 @@ func createArchiveStreamFromPaths(c *core.ExecutionState, itemPaths []string, co dirSet := make(map[string]struct{}) for _, path := range itemPaths { - stats, err := os.Lstat(path) + cleanPath, pathErr := utils.ValidatePath(path) + if pathErr != nil { + return nil, core.CreateErr(c, pathErr, "invalid path: '%s'", path) + } + stats, err := os.Lstat(cleanPath) if err != nil { - return nil, core.CreateErr(c, err, "failed to stat file: '%s'", path) + return nil, core.CreateErr(c, err, "failed to stat file: '%s'", cleanPath) } if stats.IsDir() { // ignore walking a directory that has already been walked - if _, ok := dirSet[path]; ok { + if _, ok := dirSet[cleanPath]; ok { continue } tmpItemSet := make(map[string]os.FileInfo) - path, err = walk(path, walkOpts{ + walkPath, err := walk(cleanPath, walkOpts{ recursive: true, files: true, dirs: false, }, nil, tmpItemSet) if err != nil { - return nil, core.CreateErr(c, err, "failed to walk directory: '%s'", path) + return nil, core.CreateErr(c, err, "failed to walk directory: '%s'", walkPath) } for k, v := range tmpItemSet { @@ -158,7 +163,7 @@ func createArchiveStreamFromPaths(c *core.ExecutionState, itemPaths []string, co } } else if stats.Mode().IsRegular() { - itemSet[path] = stats + itemSet[cleanPath] = stats } } diff --git a/nodes/file-read@v1.go b/nodes/file-read@v1.go index ba5fdc3..d477661 100644 --- a/nodes/file-read@v1.go +++ b/nodes/file-read@v1.go @@ -7,6 +7,7 @@ import ( "github.com/actionforge/actrun-cli/core" ni "github.com/actionforge/actrun-cli/node_interfaces" + "github.com/actionforge/actrun-cli/utils" ) //go:embed file-read@v1.yml @@ -25,13 +26,18 @@ func (n *FileReadNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId, return err } - fp, err := os.Open(path) + cleanPath, pathErr := utils.ValidatePath(path) + if pathErr != nil { + return core.CreateErr(c, pathErr, "invalid file path") + } + + fp, err := os.Open(cleanPath) if err != nil { return core.CreateErr(c, err) } dsf := core.DataStreamFactory{ - SourcePath: path, + SourcePath: cleanPath, Reader: fp, Length: core.GetReaderLength(fp), } @@ -42,10 +48,10 @@ func (n *FileReadNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId, return err } - st, statErr := os.Stat(path) + st, statErr := os.Stat(cleanPath) if statErr == nil { if st.IsDir() { - statErr = core.CreateErr(c, nil, "stat %s: is a directory", path) + statErr = core.CreateErr(c, nil, "stat %s: is a directory", cleanPath) } } diff --git a/nodes/file-write@v1.go b/nodes/file-write@v1.go index 86b088a..3745751 100644 --- a/nodes/file-write@v1.go +++ b/nodes/file-write@v1.go @@ -33,7 +33,12 @@ func (n *FileWriteNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId return err } - fw, err := os.Create(p) + cleanPath, pathErr := utils.ValidatePath(p) + if pathErr != nil { + return core.CreateErr(c, pathErr, "invalid file path") + } + + fw, err := os.Create(cleanPath) if err != nil { return core.CreateErr(c, err, "error creating file") } diff --git a/nodes/gh-action@v1.go b/nodes/gh-action@v1.go index bdf4d09..4b36ab9 100644 --- a/nodes/gh-action@v1.go +++ b/nodes/gh-action@v1.go @@ -227,10 +227,12 @@ func (n *GhActionNode) ExecuteNode(c *core.ExecutionState, workspace string, env runners, err := getRunnersDir() if err == nil { // Look for external node binary bundled with the runner - externalNodeBin := filepath.Join(runners, "externals", n.actionRuns.Using, "bin", "node") - _, err := os.Stat(nodeBin) - if err == nil { - nodeBin = externalNodeBin + externalNodeBin, pathErr := utils.SafeJoinPath(runners, "externals", n.actionRuns.Using, "bin", "node") + if pathErr == nil { + _, err := os.Stat(externalNodeBin) + if err == nil { + nodeBin = externalNodeBin + } } } diff --git a/nodes/gh-context-parser.go b/nodes/gh-context-parser.go index 50592fa..a7c25c5 100644 --- a/nodes/gh-context-parser.go +++ b/nodes/gh-context-parser.go @@ -3,10 +3,10 @@ package nodes import ( "fmt" "os" - "path/filepath" "strings" "github.com/actionforge/actrun-cli/core" + "github.com/actionforge/actrun-cli/utils" "github.com/google/uuid" ) @@ -20,18 +20,24 @@ func (p *GhContextParser) Init(c *core.ExecutionState, sysRunnerTempDir string) for fileCommand, envName := range contextEnvList { fname := fmt.Sprintf("%s_%s", fileCommand, fileCommandUuid) - path := filepath.Join(sysRunnerTempDir, "_runner_file_commands") - err := os.MkdirAll(path, 0755) + dirPath, err := utils.SafeJoinPath(sysRunnerTempDir, "_runner_file_commands") + if err != nil { + return nil, core.CreateErr(c, err, "invalid directory path") + } + err = os.MkdirAll(dirPath, 0755) if err != nil { return nil, core.CreateErr(c, err, "unable to create directory") } - path = filepath.Join(sysRunnerTempDir, "_runner_file_commands", fname) - err = os.WriteFile(path, []byte(""), 0644) + filePath, err := utils.SafeJoinPath(sysRunnerTempDir, "_runner_file_commands", fname) + if err != nil { + return nil, core.CreateErr(c, err, "invalid file path") + } + err = os.WriteFile(filePath, []byte(""), 0644) if err != nil { return nil, core.CreateErr(c, err, "unable to create file") } - envs[envName] = path + envs[envName] = filePath } return envs, nil } @@ -44,7 +50,11 @@ func (p *GhContextParser) Parse(c *core.ExecutionState, contextEnvironMap map[st githubPath := contextEnvironMap["GITHUB_PATH"] // load all paths from the github path file and append them to the PATH if githubPath != "" { - p, err := os.ReadFile(githubPath) + cleanPath, err := utils.ValidatePath(githubPath) + if err != nil { + return nil, nil, core.CreateErr(c, err, "invalid GITHUB_PATH") + } + p, err := os.ReadFile(cleanPath) if err != nil { return nil, nil, core.CreateErr(c, err, "unable to read file set in GITHUB_PATH") } @@ -64,7 +74,7 @@ func (p *GhContextParser) Parse(c *core.ExecutionState, contextEnvironMap map[st envs["PATH"] = strings.Join(newPaths, string(os.PathListSeparator)) + string(os.PathListSeparator) + contextEnvironMap["PATH"] } - err = os.Remove(githubPath) + err = os.Remove(cleanPath) if err != nil { return nil, nil, core.CreateErr(c, nil, "unable to remove file set in GITHUB_PATH") } @@ -74,7 +84,11 @@ func (p *GhContextParser) Parse(c *core.ExecutionState, contextEnvironMap map[st githubEnv := contextEnvironMap["GITHUB_ENV"] if githubEnv != "" { - b, err := os.ReadFile(githubEnv) + cleanPath, err := utils.ValidatePath(githubEnv) + if err != nil { + return nil, nil, core.CreateErr(c, err, "invalid GITHUB_ENV path") + } + b, err := os.ReadFile(cleanPath) if err != nil { return nil, nil, core.CreateErr(c, nil, "unable to read file set in GITHUB_ENV") } @@ -86,7 +100,7 @@ func (p *GhContextParser) Parse(c *core.ExecutionState, contextEnvironMap map[st envs[envName] = strings.TrimRight(envValue, " \t\n\r") } - err = os.Remove(githubEnv) + err = os.Remove(cleanPath) if err != nil { return nil, nil, core.CreateErr(c, err, "unable to remove file set in GITHUB_ENV") } @@ -96,7 +110,11 @@ func (p *GhContextParser) Parse(c *core.ExecutionState, contextEnvironMap map[st githubOutput := contextEnvironMap["GITHUB_OUTPUT"] if githubOutput != "" { - b, err := os.ReadFile(githubOutput) + cleanPath, err := utils.ValidatePath(githubOutput) + if err != nil { + return nil, nil, core.CreateErr(c, err, "invalid GITHUB_OUTPUT path") + } + b, err := os.ReadFile(cleanPath) if err != nil { return nil, nil, core.CreateErr(c, err, "unable to read file set in GITHUB_OUTPUT") } @@ -109,7 +127,7 @@ func (p *GhContextParser) Parse(c *core.ExecutionState, contextEnvironMap map[st outputs[key] = strings.TrimRight(value, "\t\n") } - err = os.Remove(githubOutput) + err = os.Remove(cleanPath) if err != nil { return nil, nil, core.CreateErr(c, err, "unable to remove file set in GITHUB_OUTPUT") } diff --git a/nodes/hash@v1.go b/nodes/hash@v1.go index a2baf48..e9b1168 100644 --- a/nodes/hash@v1.go +++ b/nodes/hash@v1.go @@ -32,8 +32,6 @@ type HashNode struct { func getHashFunction(c *core.ExecutionState, algorithm string) (hash.Hash, error) { switch strings.ToLower(algorithm) { - case "sha1": - return sha1.New(), nil case "sha224": return sha256.New224(), nil case "sha256": @@ -48,16 +46,20 @@ func getHashFunction(c *core.ExecutionState, algorithm string) (hash.Hash, error return sha3.New384(), nil case "sha3_512": return sha3.New512(), nil - case "md5": - return md5.New(), nil - case "crc32": - return crc32.New(crc32.MakeTable(crc32.IEEE)), nil case "blake2b256": return blake2b.New256(nil) case "blake2b384": return blake2b.New384(nil) case "blake2b512": return blake2b.New512(nil) + // legacy algorithms for file checksums and compatibility only + // codeql[go/weak-sensitive-data-hashing]: intentionally available for non-security use cases + case "sha1": + return sha1.New(), nil + case "md5": + return md5.New(), nil + case "crc32": + return crc32.New(crc32.MakeTable(crc32.IEEE)), nil default: return nil, core.CreateErr(c, nil, "unsupported hash algorithm: %s", algorithm) } @@ -81,6 +83,8 @@ func (n *HashNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId, pre return err } + // user explicitly selects the hash algorithm. Legacy algorithms like md5, sha1, crc32 + // codeql[go/weak-sensitive-data-hashing] _, copyErr := io.Copy(hashFunc, input) if copyErr != nil { copyErr = core.CreateErr(c, copyErr, "error reading stream") diff --git a/nodes/item-stats@v1.go b/nodes/item-stats@v1.go index 1163aba..062c944 100644 --- a/nodes/item-stats@v1.go +++ b/nodes/item-stats@v1.go @@ -7,6 +7,7 @@ import ( "github.com/actionforge/actrun-cli/core" ni "github.com/actionforge/actrun-cli/node_interfaces" + "github.com/actionforge/actrun-cli/utils" ) //go:embed item-stats@v1.yml @@ -25,6 +26,11 @@ func (n *ItemStatsNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId return err } + cleanPath, pathErr := utils.ValidatePath(path) + if pathErr != nil { + return core.CreateErr(c, pathErr, "invalid path") + } + exists := true var ( @@ -34,7 +40,7 @@ func (n *ItemStatsNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId isRegular := false isDir := false - stats, err := os.Stat(path) + stats, err := os.Stat(cleanPath) if err != nil { if errors.Is(err, os.ErrNotExist) { exists = false diff --git a/tests_e2e/references/reference_app.sh_l12 b/tests_e2e/references/reference_app.sh_l12 index d9fd55e..b755ac3 100644 --- a/tests_e2e/references/reference_app.sh_l12 +++ b/tests_e2e/references/reference_app.sh_l12 @@ -23,7 +23,7 @@ hint: stack trace: github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1111 + graph.go:1123 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_dir-walk.sh_l56 b/tests_e2e/references/reference_dir-walk.sh_l56 index 4ed3c68..41c320c 100644 --- a/tests_e2e/references/reference_dir-walk.sh_l56 +++ b/tests_e2e/references/reference_dir-walk.sh_l56 @@ -32,7 +32,7 @@ error: stack trace: github.com/actionforge/actrun-cli/nodes.(*WalkNode).ExecuteImpl - dir-walk@v1.go:61 + dir-walk@v1.go:62 github.com/actionforge/actrun-cli/core.(*Executions).Execute executions.go:68 github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl @@ -40,11 +40,11 @@ github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:506 + graph.go:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1096 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1114 + graph.go:1126 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_error_no_output.sh_l8 b/tests_e2e/references/reference_error_no_output.sh_l8 index 57a2131..acd9693 100644 --- a/tests_e2e/references/reference_error_no_output.sh_l8 +++ b/tests_e2e/references/reference_error_no_output.sh_l8 @@ -51,11 +51,11 @@ github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:506 + graph.go:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1096 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1114 + graph.go:1126 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_group-error.sh_l8 b/tests_e2e/references/reference_group-error.sh_l8 index c00aabb..4612f6f 100644 --- a/tests_e2e/references/reference_group-error.sh_l8 +++ b/tests_e2e/references/reference_group-error.sh_l8 @@ -250,7 +250,7 @@ github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:506 + graph.go:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1096 + graph.go:1104 diff --git a/tests_e2e/references/reference_group-port-collision.sh_l13 b/tests_e2e/references/reference_group-port-collision.sh_l13 index a131a4f..d633c83 100644 --- a/tests_e2e/references/reference_group-port-collision.sh_l13 +++ b/tests_e2e/references/reference_group-port-collision.sh_l13 @@ -25,17 +25,17 @@ github.com/actionforge/actrun-cli/nodes.init.41.func1 github.com/actionforge/actrun-cli/core.NewNodeInstance base.go:619 github.com/actionforge/actrun-cli/core.LoadNode - graph.go:679 + graph.go:687 github.com/actionforge/actrun-cli/core.LoadNodes - graph.go:619 + graph.go:627 github.com/actionforge/actrun-cli/core.LoadGraph - graph.go:534 + graph.go:542 github.com/actionforge/actrun-cli/core.RunGraph graph.go:280 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1096 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1114 + graph.go:1126 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_index.sh_l20 b/tests_e2e/references/reference_index.sh_l20 index 1f2e4bb..c3c7851 100644 --- a/tests_e2e/references/reference_index.sh_l20 +++ b/tests_e2e/references/reference_index.sh_l20 @@ -84,11 +84,11 @@ github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:506 + graph.go:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1096 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1114 + graph.go:1126 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_run-python-embedded.sh_l13 b/tests_e2e/references/reference_run-python-embedded.sh_l13 index 0b31eda..7af4dc2 100644 --- a/tests_e2e/references/reference_run-python-embedded.sh_l13 +++ b/tests_e2e/references/reference_run-python-embedded.sh_l13 @@ -29,17 +29,17 @@ github.com/actionforge/actrun-cli/nodes.init.52.func1 github.com/actionforge/actrun-cli/core.NewNodeInstance base.go:619 github.com/actionforge/actrun-cli/core.LoadNode - graph.go:679 + graph.go:687 github.com/actionforge/actrun-cli/core.LoadNodes - graph.go:619 + graph.go:627 github.com/actionforge/actrun-cli/core.LoadGraph - graph.go:534 + graph.go:542 github.com/actionforge/actrun-cli/core.RunGraph graph.go:280 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1096 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1114 + graph.go:1126 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_s3_aws_walk.sh_l22 b/tests_e2e/references/reference_s3_aws_walk.sh_l22 index 313feac..a8d12ad 100644 --- a/tests_e2e/references/reference_s3_aws_walk.sh_l22 +++ b/tests_e2e/references/reference_s3_aws_walk.sh_l22 @@ -44,11 +44,11 @@ github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:506 + graph.go:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1096 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1114 + graph.go:1126 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_s3_aws_walk.sh_l44 b/tests_e2e/references/reference_s3_aws_walk.sh_l44 index 313feac..a8d12ad 100644 --- a/tests_e2e/references/reference_s3_aws_walk.sh_l44 +++ b/tests_e2e/references/reference_s3_aws_walk.sh_l44 @@ -44,11 +44,11 @@ github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:506 + graph.go:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1096 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1114 + graph.go:1126 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_select-data.sh_l9 b/tests_e2e/references/reference_select-data.sh_l9 index 289f2d8..2795e04 100644 --- a/tests_e2e/references/reference_select-data.sh_l9 +++ b/tests_e2e/references/reference_select-data.sh_l9 @@ -91,11 +91,11 @@ github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:506 + graph.go:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1096 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1114 + graph.go:1126 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/references/reference_string-transform.sh_l61 b/tests_e2e/references/reference_string-transform.sh_l61 index 3465078..037c2d7 100644 --- a/tests_e2e/references/reference_string-transform.sh_l61 +++ b/tests_e2e/references/reference_string-transform.sh_l61 @@ -51,11 +51,11 @@ github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteImpl github.com/actionforge/actrun-cli/nodes.(*StartNode).ExecuteEntry start@v1.go:45 github.com/actionforge/actrun-cli/core.RunGraph - graph.go:506 + graph.go:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1096 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1114 + graph.go:1126 github.com/actionforge/actrun-cli/cmd.cmdRootRun cmd_root.go:186 github.com/spf13/cobra.(*Command).execute diff --git a/tests_e2e/tests_e2e.py b/tests_e2e/tests_e2e.py index 2e84616..e390386 100644 --- a/tests_e2e/tests_e2e.py +++ b/tests_e2e/tests_e2e.py @@ -376,7 +376,7 @@ def main(): # excludes reference files from other platforms (e.g., _linux files when running on darwin) try: git_cmd = ['git', '-c', 'core.autocrlf=input', '-c', 'core.safecrlf=false', - '--no-pager', 'diff', ref_dir, '--'] + '--no-pager', 'diff', '--', ref_dir] for plat in ALL_PLATFORMS: if plat != CURRENT_PLATFORM: diff --git a/utils/path.go b/utils/path.go index 2404264..b29a381 100644 --- a/utils/path.go +++ b/utils/path.go @@ -1,7 +1,9 @@ package utils import ( + "fmt" "os" + "path/filepath" "runtime" "strings" ) @@ -42,3 +44,50 @@ func CreateAndWriteTempFile(script, tmpfileName string, opts WriteOptions) (stri return tmpfilePath, nil } + +// SafeJoinPath safely joins path elements and validates the result stays within the base directory. +// It prevents path traversal attacks by cleaning the path and verifying containment. +// Returns the joined path and an error if the path would escape the base directory. +func SafeJoinPath(base string, elem ...string) (string, error) { + absBase, err := filepath.Abs(filepath.Clean(base)) + if err != nil { + return "", fmt.Errorf("invalid base path: %w", err) + } + + allElems := append([]string{absBase}, elem...) + joined := filepath.Join(allElems...) + + absJoined, err := filepath.Abs(filepath.Clean(joined)) + if err != nil { + return "", fmt.Errorf("invalid path: %w", err) + } + + // check for null bytes + if strings.ContainsRune(absJoined, 0) { + return "", fmt.Errorf("path contains null byte") + } + + rel, err := filepath.Rel(absBase, absJoined) + if err != nil { + return "", fmt.Errorf("path validation failed: %w", err) + } + + // If relative path starts with "..", it escapes the base directory + if strings.HasPrefix(rel, ".."+string(filepath.Separator)) || rel == ".." { + return "", fmt.Errorf("path escapes base directory") + } + + return absJoined, nil +} + +// ValidatePath validates that a path is safe to use. +// It cleans the path and checks for dangerous patterns +func ValidatePath(path string) (string, error) { + cleaned := filepath.Clean(path) + + if strings.ContainsRune(cleaned, 0) { + return "", fmt.Errorf("path contains null byte") + } + + return cleaned, nil +} diff --git a/utils/utils.go b/utils/utils.go index f74a5be..b1c112f 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -319,3 +319,16 @@ func Ordinal(i int) string { return fmt.Sprintf("%d%s", n, suffix) } + +// SanitizeImageRef removes any embedded credentials from a Docker image reference. +// Docker image refs can contain credentials like user:pass@registry/image:tag +// This function just removes them +func SanitizeImageRef(imageRef string) string { + if idx := strings.Index(imageRef, "@"); idx != -1 { + prefix := imageRef[:idx] + if strings.Contains(prefix, ":") && !strings.Contains(prefix, "/") { + return "***@" + imageRef[idx+1:] + } + } + return imageRef +} From 91a93bd0fdbd8de7650f6c7b2b0170c57d30206c Mon Sep 17 00:00:00 2001 From: Sebatian Rath Date: Tue, 3 Feb 2026 00:12:23 -0500 Subject: [PATCH 7/7] Create temporary directories for GitHub workflow execution This is now needed since the switch from the Docker CLI to the Docker Go lib (some defaults were turned on by the CLI) --- nodes/gh-action@v1.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nodes/gh-action@v1.go b/nodes/gh-action@v1.go index 4b36ab9..9e6495c 100644 --- a/nodes/gh-action@v1.go +++ b/nodes/gh-action@v1.go @@ -264,6 +264,17 @@ func (n *GhActionNode) ExecuteDocker(c *core.ExecutionState, workingDirectory st return core.CreateErr(c, nil, "RUNNER_TEMP is not set") } + tempDirs := []string{ + filepath.Join(sysRunnerTempDir, "_github_workflow"), + filepath.Join(sysRunnerTempDir, "_github_home"), + filepath.Join(sysRunnerTempDir, "_runner_file_commands"), + } + for _, dir := range tempDirs { + if err := os.MkdirAll(dir, 0755); err != nil { + return core.CreateErr(c, err, "failed to create directory %s", dir) + } + } + sysGithubWorkspace := env["GITHUB_WORKSPACE"] if sysGithubWorkspace == "" { return core.CreateErr(c, nil, "GITHUB_WORKSPACE is not set")