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: 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/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/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.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 9358e96..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 } @@ -356,7 +365,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 +484,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 +526,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..7bd829f 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") == "" @@ -314,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) @@ -330,7 +345,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 +398,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 +432,61 @@ 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 != "" { + 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(cleanCwd); 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 +497,7 @@ func RunGraph(ctx context.Context, graphName string, graphContent []byte, opts R ctx, &ag, graphName, - isGitHubActions, + isGitHubWorkflow, debugCb, finalEnv, finalInputs, @@ -1080,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/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" + } +} 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/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 b7774da..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() { @@ -245,7 +252,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 +261,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/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 c172bc9..9e6495c 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) @@ -240,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 + } } } @@ -275,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") @@ -349,7 +349,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 +538,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 +562,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..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,32 +20,43 @@ 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 } -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) + 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, 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{} @@ -63,9 +74,9 @@ 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, 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") @@ -73,26 +84,58 @@ 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, core.CreateErr(c, nil, "unable to read file set in GITHUB_ENV") + 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") } 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") } - err = os.Remove(githubEnv) + err = os.Remove(cleanPath) 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 != "" { + 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") + } + + 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(cleanPath) + 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/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/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) diff --git a/tests_e2e/references/reference_app.sh_l12 b/tests_e2e/references/reference_app.sh_l12 index a7a9425..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:1089 + 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 1359598..41c320c 100644 --- a/tests_e2e/references/reference_dir-walk.sh_l56 +++ b/tests_e2e/references/reference_dir-walk.sh_l56 @@ -32,19 +32,19 @@ 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: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:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + 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 0259b6d..acd9693 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:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + 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 dfef3aa..4612f6f 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:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + 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 e769563..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:657 + graph.go:687 github.com/actionforge/actrun-cli/core.LoadNodes - graph.go:597 + graph.go:627 github.com/actionforge/actrun-cli/core.LoadGraph - graph.go:512 + graph.go:542 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:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + 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 39898cf..c3c7851 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:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + 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 c2d4a20..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:657 + graph.go:687 github.com/actionforge/actrun-cli/core.LoadNodes - graph.go:597 + graph.go:627 github.com/actionforge/actrun-cli/core.LoadGraph - graph.go:512 + graph.go:542 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:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + 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 4042e37..a8d12ad 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:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + 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 4042e37..a8d12ad 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:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + 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 61a06c4..2795e04 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:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + 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_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/references/reference_string-transform.sh_l61 b/tests_e2e/references/reference_string-transform.sh_l61 index f5bc3d2..037c2d7 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:514 github.com/actionforge/actrun-cli/core.RunGraphFromString - graph.go:1074 + graph.go:1104 github.com/actionforge/actrun-cli/core.RunGraphFromFile - graph.go:1092 + 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/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 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 +}