From 4f9a75c73c12aed2a96f677dfd4da4d4756ac73f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 06:24:19 +0000 Subject: [PATCH 1/7] Initial plan From dafd92366a8a972a8b4f5e747fcd9032cadc88eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 06:29:54 +0000 Subject: [PATCH 2/7] Initial plan for Serena toolchain mappings Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/data/action_pins.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json index 705bdeb0b6..b8a317833e 100644 --- a/pkg/workflow/data/action_pins.json +++ b/pkg/workflow/data/action_pins.json @@ -75,6 +75,11 @@ "version": "v4.8.0", "sha": "c1e323688fd81a25caa38c78aa6df2d33d3e20d9" }, + "actions/setup-node@v6": { + "repo": "actions/setup-node", + "version": "v6", + "sha": "6044e13b5dc448c55e2357c09f80417699197238" + }, "actions/setup-node@v6.1.0": { "repo": "actions/setup-node", "version": "v6.1.0", From 423ff726638b041fe9fcebb4089bcbe0258d27ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 06:37:11 +0000 Subject: [PATCH 3/7] Add runtime toolchain mappings for Serena MCP server Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/claude_mcp.go | 4 +- pkg/workflow/codex_mcp.go | 6 +- pkg/workflow/compiler_types.go | 1 + pkg/workflow/compiler_yaml_main_job.go | 9 + pkg/workflow/copilot_mcp.go | 4 +- pkg/workflow/custom_engine.go | 4 +- pkg/workflow/mcp-config.go | 66 +++++- pkg/workflow/mcp_config_comprehensive_test.go | 2 +- pkg/workflow/mcp_renderer.go | 57 +++++- pkg/workflow/runtime_toolchain_mappings.go | 188 ++++++++++++++++++ 10 files changed, 320 insertions(+), 21 deletions(-) create mode 100644 pkg/workflow/runtime_toolchain_mappings.go diff --git a/pkg/workflow/claude_mcp.go b/pkg/workflow/claude_mcp.go index 018aa8e2f8..35f76735f9 100644 --- a/pkg/workflow/claude_mcp.go +++ b/pkg/workflow/claude_mcp.go @@ -46,9 +46,9 @@ func (e *ClaudeEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]a renderer := createRenderer(isLast) renderer.RenderPlaywrightMCP(yaml, playwrightTool) }, - RenderSerena: func(yaml *strings.Builder, serenaTool any, isLast bool) { + RenderSerena: func(yaml *strings.Builder, serenaTool any, isLast bool, workflowData *WorkflowData) { renderer := createRenderer(isLast) - renderer.RenderSerenaMCP(yaml, serenaTool) + renderer.RenderSerenaMCP(yaml, serenaTool, workflowData) }, RenderCacheMemory: e.renderCacheMemoryMCPConfig, RenderAgenticWorkflows: func(yaml *strings.Builder, isLast bool) { diff --git a/pkg/workflow/codex_mcp.go b/pkg/workflow/codex_mcp.go index b6b3e79f5b..7feaf42719 100644 --- a/pkg/workflow/codex_mcp.go +++ b/pkg/workflow/codex_mcp.go @@ -51,7 +51,7 @@ func (e *CodexEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]an renderer.RenderPlaywrightMCP(yaml, playwrightTool) case "serena": serenaTool := expandedTools["serena"] - renderer.RenderSerenaMCP(yaml, serenaTool) + renderer.RenderSerenaMCP(yaml, serenaTool, workflowData) case "agentic-workflows": renderer.RenderAgenticWorkflowsMCP(yaml) case "safe-outputs": @@ -130,9 +130,9 @@ func (e *CodexEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]an renderer := createJSONRenderer(isLast) renderer.RenderPlaywrightMCP(yaml, playwrightTool) }, - RenderSerena: func(yaml *strings.Builder, serenaTool any, isLast bool) { + RenderSerena: func(yaml *strings.Builder, serenaTool any, isLast bool, workflowData *WorkflowData) { renderer := createJSONRenderer(isLast) - renderer.RenderSerenaMCP(yaml, serenaTool) + renderer.RenderSerenaMCP(yaml, serenaTool, workflowData) }, RenderCacheMemory: func(yaml *strings.Builder, isLast bool, workflowData *WorkflowData) { // Cache-memory is not used as MCP server diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index a990808289..2b9f16b625 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -304,6 +304,7 @@ type WorkflowData struct { SecretMasking *SecretMaskingConfig // secret masking configuration CompilerSkipValidation *bool // compiler's skipValidation flag (passed from compiler to engines for MCP gateway schema validation) CompilerWarningCallback func() // callback to increment compiler warning count (passed from compiler to engines for MCP gateway schema validation warnings) + ToolchainMappings *ToolchainMappings // runtime toolchain environment variables and mounts to pass to agent containers } // BaseSafeOutputConfig holds common configuration fields for all safe output types diff --git a/pkg/workflow/compiler_yaml_main_job.go b/pkg/workflow/compiler_yaml_main_job.go index 1934173089..7b2cf01dd7 100644 --- a/pkg/workflow/compiler_yaml_main_job.go +++ b/pkg/workflow/compiler_yaml_main_job.go @@ -53,6 +53,15 @@ func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowDat } } + // Collect toolchain mappings for detected runtimes + // These mappings will be used to configure agent containers (e.g., Serena MCP server) + // with the necessary environment variables and mounts for toolchains to work + data.ToolchainMappings = CollectToolchainMappings(runtimeRequirements) + compilerYamlLog.Printf("Collected toolchain mappings: %d runtimes, %d env vars, %d mounts", + len(data.ToolchainMappings.Mappings), + len(data.ToolchainMappings.GetAllEnvVars()), + len(data.ToolchainMappings.GetAllMounts())) + // Generate runtime setup steps (after filtering out user-customized ones) runtimeSetupSteps := GenerateRuntimeSetupSteps(runtimeRequirements) compilerYamlLog.Printf("Detected runtime requirements: %d runtimes, %d setup steps", len(runtimeRequirements), len(runtimeSetupSteps)) diff --git a/pkg/workflow/copilot_mcp.go b/pkg/workflow/copilot_mcp.go index e95263e8e4..7f30daa5c2 100644 --- a/pkg/workflow/copilot_mcp.go +++ b/pkg/workflow/copilot_mcp.go @@ -49,9 +49,9 @@ func (e *CopilotEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string] renderer := createRenderer(isLast) renderer.RenderPlaywrightMCP(yaml, playwrightTool) }, - RenderSerena: func(yaml *strings.Builder, serenaTool any, isLast bool) { + RenderSerena: func(yaml *strings.Builder, serenaTool any, isLast bool, workflowData *WorkflowData) { renderer := createRenderer(isLast) - renderer.RenderSerenaMCP(yaml, serenaTool) + renderer.RenderSerenaMCP(yaml, serenaTool, workflowData) }, RenderCacheMemory: func(yaml *strings.Builder, isLast bool, workflowData *WorkflowData) { // Cache-memory is not used for Copilot (filtered out) diff --git a/pkg/workflow/custom_engine.go b/pkg/workflow/custom_engine.go index 946cc90373..6f6bd074a1 100644 --- a/pkg/workflow/custom_engine.go +++ b/pkg/workflow/custom_engine.go @@ -194,9 +194,9 @@ func (e *CustomEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]a renderer := createRenderer(isLast) renderer.RenderPlaywrightMCP(yaml, playwrightTool) }, - RenderSerena: func(yaml *strings.Builder, serenaTool any, isLast bool) { + RenderSerena: func(yaml *strings.Builder, serenaTool any, isLast bool, workflowData *WorkflowData) { renderer := createRenderer(isLast) - renderer.RenderSerenaMCP(yaml, serenaTool) + renderer.RenderSerenaMCP(yaml, serenaTool, workflowData) }, RenderCacheMemory: e.renderCacheMemoryMCPConfig, RenderAgenticWorkflows: func(yaml *strings.Builder, isLast bool) { diff --git a/pkg/workflow/mcp-config.go b/pkg/workflow/mcp-config.go index eb1851ef58..f763b4d451 100644 --- a/pkg/workflow/mcp-config.go +++ b/pkg/workflow/mcp-config.go @@ -148,7 +148,7 @@ func renderPlaywrightMCPConfigWithOptions(yaml *strings.Builder, playwrightTool // renderSerenaMCPConfigWithOptions generates the Serena MCP server configuration with engine-specific options // Per MCP Gateway Specification v1.0.0 section 3.2.1, stdio-based MCP servers MUST be containerized. // Uses Docker container format as specified by Serena: ghcr.io/oraios/serena:latest -func renderSerenaMCPConfigWithOptions(yaml *strings.Builder, serenaTool any, isLast bool, includeCopilotFields bool, inlineArgs bool) { +func renderSerenaMCPConfigWithOptions(yaml *strings.Builder, serenaTool any, isLast bool, includeCopilotFields bool, inlineArgs bool, workflowData *WorkflowData) { customArgs := getSerenaCustomArgs(serenaTool) yaml.WriteString(" \"serena\": {\n") @@ -193,8 +193,68 @@ func renderSerenaMCPConfigWithOptions(yaml *strings.Builder, serenaTool any, isL yaml.WriteString(" ],\n") } - // Add volume mount for workspace access - yaml.WriteString(" \"mounts\": [\"${{ github.workspace }}:${{ github.workspace }}:rw\"]\n") + // Collect environment variables from toolchain mappings + var envVars map[string]string + if workflowData != nil && workflowData.ToolchainMappings != nil { + envVars = workflowData.ToolchainMappings.GetAllEnvVars() + } + + // Add environment variables if any + if len(envVars) > 0 { + yaml.WriteString(" \"env\": {\n") + + // Sort env var names for deterministic output + var envNames []string + for name := range envVars { + envNames = append(envNames, name) + } + sort.Strings(envNames) + + for i, name := range envNames { + value := envVars[name] + if i < len(envNames)-1 { + fmt.Fprintf(yaml, " \"%s\": \"%s\",\n", name, value) + } else { + fmt.Fprintf(yaml, " \"%s\": \"%s\"\n", name, value) + } + } + yaml.WriteString(" },\n") + } + + // Collect mounts: base mount + toolchain mounts + baseMounts := []string{ + "${{ github.workspace }}:${{ github.workspace }}:rw", + } + + var allMounts []string + if workflowData != nil && workflowData.ToolchainMappings != nil { + toolchainMounts := workflowData.ToolchainMappings.GetAllMounts() + allMounts = MergeMountsWithDedup(baseMounts, toolchainMounts) + } else { + allMounts = baseMounts + } + + // Add volume mounts + if inlineArgs { + yaml.WriteString(" \"mounts\": [") + for i, mount := range allMounts { + if i > 0 { + yaml.WriteString(", ") + } + fmt.Fprintf(yaml, "\"%s\"", mount) + } + yaml.WriteString("]\n") + } else { + yaml.WriteString(" \"mounts\": [\n") + for i, mount := range allMounts { + if i < len(allMounts)-1 { + fmt.Fprintf(yaml, " \"%s\",\n", mount) + } else { + fmt.Fprintf(yaml, " \"%s\"\n", mount) + } + } + yaml.WriteString(" ]\n") + } // Note: tools field is NOT included here - the converter script adds it back // for Copilot. This keeps the gateway config compatible with the schema. diff --git a/pkg/workflow/mcp_config_comprehensive_test.go b/pkg/workflow/mcp_config_comprehensive_test.go index 12c2aa8f57..a5ae8b1652 100644 --- a/pkg/workflow/mcp_config_comprehensive_test.go +++ b/pkg/workflow/mcp_config_comprehensive_test.go @@ -651,7 +651,7 @@ func TestRenderSerenaMCPConfigWithOptions(t *testing.T) { t.Run(tt.name, func(t *testing.T) { var output strings.Builder - renderSerenaMCPConfigWithOptions(&output, tt.serenaTool, tt.isLast, tt.includeCopilotFields, tt.inlineArgs) + renderSerenaMCPConfigWithOptions(&output, tt.serenaTool, tt.isLast, tt.includeCopilotFields, tt.inlineArgs, nil) result := output.String() diff --git a/pkg/workflow/mcp_renderer.go b/pkg/workflow/mcp_renderer.go index 1eb9a57514..a3ab941fb4 100644 --- a/pkg/workflow/mcp_renderer.go +++ b/pkg/workflow/mcp_renderer.go @@ -176,22 +176,22 @@ func (r *MCPConfigRendererUnified) renderPlaywrightTOML(yaml *strings.Builder, p } // RenderSerenaMCP generates Serena MCP server configuration -func (r *MCPConfigRendererUnified) RenderSerenaMCP(yaml *strings.Builder, serenaTool any) { +func (r *MCPConfigRendererUnified) RenderSerenaMCP(yaml *strings.Builder, serenaTool any, workflowData *WorkflowData) { mcpRendererLog.Printf("Rendering Serena MCP: format=%s, inline_args=%t", r.options.Format, r.options.InlineArgs) if r.options.Format == "toml" { - r.renderSerenaTOML(yaml, serenaTool) + r.renderSerenaTOML(yaml, serenaTool, workflowData) return } // JSON format - renderSerenaMCPConfigWithOptions(yaml, serenaTool, r.options.IsLast, r.options.IncludeCopilotFields, r.options.InlineArgs) + renderSerenaMCPConfigWithOptions(yaml, serenaTool, r.options.IsLast, r.options.IncludeCopilotFields, r.options.InlineArgs, workflowData) } // renderSerenaTOML generates Serena MCP configuration in TOML format // Per MCP Gateway Specification v1.0.0 section 3.2.1, stdio-based MCP servers MUST be containerized. // Uses Docker container format as specified by Serena: ghcr.io/oraios/serena:latest -func (r *MCPConfigRendererUnified) renderSerenaTOML(yaml *strings.Builder, serenaTool any) { +func (r *MCPConfigRendererUnified) renderSerenaTOML(yaml *strings.Builder, serenaTool any, workflowData *WorkflowData) { customArgs := getSerenaCustomArgs(serenaTool) yaml.WriteString(" \n") @@ -221,8 +221,49 @@ func (r *MCPConfigRendererUnified) renderSerenaTOML(yaml *strings.Builder, seren yaml.WriteString("\n") yaml.WriteString(" ]\n") - // Add volume mount for workspace access - yaml.WriteString(" mounts = [\"${{ github.workspace }}:${{ github.workspace }}:rw\"]\n") + // Collect environment variables from toolchain mappings + var envVars map[string]string + if workflowData != nil && workflowData.ToolchainMappings != nil { + envVars = workflowData.ToolchainMappings.GetAllEnvVars() + } + + // Add environment variables if any + if len(envVars) > 0 { + // Sort env var names for deterministic output + var envNames []string + for name := range envVars { + envNames = append(envNames, name) + } + sort.Strings(envNames) + + for _, name := range envNames { + value := envVars[name] + fmt.Fprintf(yaml, " env.%s = \"%s\"\n", name, value) + } + } + + // Collect mounts: base mount + toolchain mounts + baseMounts := []string{ + "${{ github.workspace }}:${{ github.workspace }}:rw", + } + + var allMounts []string + if workflowData != nil && workflowData.ToolchainMappings != nil { + toolchainMounts := workflowData.ToolchainMappings.GetAllMounts() + allMounts = MergeMountsWithDedup(baseMounts, toolchainMounts) + } else { + allMounts = baseMounts + } + + // Add volume mounts + yaml.WriteString(" mounts = [") + for i, mount := range allMounts { + if i > 0 { + yaml.WriteString(", ") + } + fmt.Fprintf(yaml, "\"%s\"", mount) + } + yaml.WriteString("]\n") } // RenderSafeOutputsMCP generates the Safe Outputs MCP server configuration @@ -471,7 +512,7 @@ func HandleCustomMCPToolInSwitch( type MCPToolRenderers struct { RenderGitHub func(yaml *strings.Builder, githubTool any, isLast bool, workflowData *WorkflowData) RenderPlaywright func(yaml *strings.Builder, playwrightTool any, isLast bool) - RenderSerena func(yaml *strings.Builder, serenaTool any, isLast bool) + RenderSerena func(yaml *strings.Builder, serenaTool any, isLast bool, workflowData *WorkflowData) RenderCacheMemory func(yaml *strings.Builder, isLast bool, workflowData *WorkflowData) RenderAgenticWorkflows func(yaml *strings.Builder, isLast bool) RenderSafeOutputs func(yaml *strings.Builder, isLast bool) @@ -754,7 +795,7 @@ func RenderJSONMCPConfig( options.Renderers.RenderPlaywright(&configBuilder, playwrightTool, isLast) case "serena": serenaTool := tools["serena"] - options.Renderers.RenderSerena(&configBuilder, serenaTool, isLast) + options.Renderers.RenderSerena(&configBuilder, serenaTool, isLast, workflowData) case "cache-memory": options.Renderers.RenderCacheMemory(&configBuilder, isLast, workflowData) case "agentic-workflows": diff --git a/pkg/workflow/runtime_toolchain_mappings.go b/pkg/workflow/runtime_toolchain_mappings.go new file mode 100644 index 0000000000..6a95558b8b --- /dev/null +++ b/pkg/workflow/runtime_toolchain_mappings.go @@ -0,0 +1,188 @@ +package workflow + +import ( + "sort" +) + +// RuntimeToolchainMapping represents environment variables and mounts that need to be +// passed to the agent container (e.g., Serena MCP server) so that the toolchain works properly. +// These mappings are collected from runtime setup actions like actions/setup-go, actions/setup-node, etc. +type RuntimeToolchainMapping struct { + // RuntimeID identifies which runtime this mapping is for (e.g., "go", "node", "python") + RuntimeID string + + // EnvVars maps environment variable names to their values/expressions + // Example: {"GOPATH": "${{ env.GOPATH }}", "GOCACHE": "$HOME/go/cache"} + EnvVars map[string]string + + // Mounts lists volume mounts needed for the toolchain to work + // Format: "source:dest:mode" (e.g., "/home/runner/go:/home/runner/go:rw") + Mounts []string +} + +// ToolchainMappings holds all runtime toolchain mappings +type ToolchainMappings struct { + // Mappings by runtime ID + Mappings map[string]*RuntimeToolchainMapping +} + +// NewToolchainMappings creates a new ToolchainMappings instance +func NewToolchainMappings() *ToolchainMappings { + return &ToolchainMappings{ + Mappings: make(map[string]*RuntimeToolchainMapping), + } +} + +// AddMapping adds or updates a runtime toolchain mapping +func (tm *ToolchainMappings) AddMapping(runtimeID string, envVars map[string]string, mounts []string) { + if tm.Mappings == nil { + tm.Mappings = make(map[string]*RuntimeToolchainMapping) + } + + if _, exists := tm.Mappings[runtimeID]; !exists { + tm.Mappings[runtimeID] = &RuntimeToolchainMapping{ + RuntimeID: runtimeID, + EnvVars: make(map[string]string), + Mounts: []string{}, + } + } + + // Merge environment variables + for key, value := range envVars { + tm.Mappings[runtimeID].EnvVars[key] = value + } + + // Merge mounts (will be deduplicated later) + tm.Mappings[runtimeID].Mounts = append(tm.Mappings[runtimeID].Mounts, mounts...) +} + +// GetAllEnvVars returns all environment variables from all runtime mappings +func (tm *ToolchainMappings) GetAllEnvVars() map[string]string { + result := make(map[string]string) + for _, mapping := range tm.Mappings { + for key, value := range mapping.EnvVars { + result[key] = value + } + } + return result +} + +// GetAllMounts returns all mounts from all runtime mappings, sorted and deduplicated +func (tm *ToolchainMappings) GetAllMounts() []string { + mountSet := make(map[string]bool) + for _, mapping := range tm.Mappings { + for _, mount := range mapping.Mounts { + mountSet[mount] = true + } + } + + // Convert to slice and sort + var mounts []string + for mount := range mountSet { + mounts = append(mounts, mount) + } + sort.Strings(mounts) + + return mounts +} + +// CollectToolchainMappings collects environment variables and mounts from detected runtime requirements +// This function determines what needs to be passed to the agent container for toolchains to work +func CollectToolchainMappings(requirements []RuntimeRequirement) *ToolchainMappings { + mappings := NewToolchainMappings() + + for _, req := range requirements { + runtime := req.Runtime + runtimeID := runtime.ID + + envVars := make(map[string]string) + var mounts []string + + // Collect runtime-specific environment variables and mounts + switch runtimeID { + case "go": + // Go requires GOPATH, GOCACHE, and GOMODCACHE to be accessible + envVars["GOPATH"] = "/home/runner/go" + envVars["GOCACHE"] = "/home/runner/.cache/go-build" + envVars["GOMODCACHE"] = "/home/runner/go/pkg/mod" + + // Mount Go directories + mounts = append(mounts, "/home/runner/go:/home/runner/go:rw") + mounts = append(mounts, "/home/runner/.cache/go-build:/home/runner/.cache/go-build:rw") + + case "node": + // Node.js requires npm cache and node_modules + envVars["NPM_CONFIG_CACHE"] = "/home/runner/.npm" + + // Mount npm cache + mounts = append(mounts, "/home/runner/.npm:/home/runner/.npm:rw") + + case "python": + // Python requires pip cache + envVars["PIP_CACHE_DIR"] = "/home/runner/.cache/pip" + + // Mount pip cache + mounts = append(mounts, "/home/runner/.cache/pip:/home/runner/.cache/pip:rw") + + case "uv": + // uv uses its own cache directory + envVars["UV_CACHE_DIR"] = "/home/runner/.cache/uv" + + // Mount uv cache + mounts = append(mounts, "/home/runner/.cache/uv:/home/runner/.cache/uv:rw") + + case "ruby": + // Ruby requires gem home + envVars["GEM_HOME"] = "/home/runner/.gem" + + // Mount gem directory + mounts = append(mounts, "/home/runner/.gem:/home/runner/.gem:rw") + + case "java": + // Java/Maven requires M2 repository + envVars["MAVEN_OPTS"] = "-Dmaven.repo.local=/home/runner/.m2/repository" + + // Mount Maven repository + mounts = append(mounts, "/home/runner/.m2:/home/runner/.m2:rw") + + case "dotnet": + // .NET requires NuGet packages directory + envVars["NUGET_PACKAGES"] = "/home/runner/.nuget/packages" + + // Mount NuGet packages + mounts = append(mounts, "/home/runner/.nuget:/home/runner/.nuget:rw") + + // Other runtimes can be added here as needed + } + + if len(envVars) > 0 || len(mounts) > 0 { + mappings.AddMapping(runtimeID, envVars, mounts) + } + } + + return mappings +} + +// MergeMountsWithDedup merges two lists of mounts, removes duplicates, and sorts them +func MergeMountsWithDedup(existingMounts []string, newMounts []string) []string { + mountSet := make(map[string]bool) + + // Add existing mounts + for _, mount := range existingMounts { + mountSet[mount] = true + } + + // Add new mounts + for _, mount := range newMounts { + mountSet[mount] = true + } + + // Convert to slice and sort + var result []string + for mount := range mountSet { + result = append(result, mount) + } + sort.Strings(result) + + return result +} From dad7531b42945ee2af97cbff836a5cff28bdd0b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 06:46:54 +0000 Subject: [PATCH 4/7] Add tests for runtime toolchain mappings Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/cloclo.lock.yml | 4 +- .../developer-docs-consolidator.lock.yml | 4 +- .../duplicate-code-detector.lock.yml | 4 +- .github/workflows/go-fan.lock.yml | 4 +- .github/workflows/jsweep.lock.yml | 5 +- .github/workflows/mcp-inspector.lock.yml | 7 +- .github/workflows/smoke-claude.lock.yml | 4 +- .github/workflows/smoke-codex.lock.yml | 4 +- .github/workflows/typist.lock.yml | 4 +- .../runtime_toolchain_mappings_test.go | 290 ++++++++++++++++++ 10 files changed, 321 insertions(+), 9 deletions(-) create mode 100644 pkg/workflow/runtime_toolchain_mappings_test.go diff --git a/.github/workflows/cloclo.lock.yml b/.github/workflows/cloclo.lock.yml index 1bce6a73fd..27415dfd72 100644 --- a/.github/workflows/cloclo.lock.yml +++ b/.github/workflows/cloclo.lock.yml @@ -572,7 +572,9 @@ jobs: "--project", "${{ github.workspace }}" ], - "mounts": ["${{ github.workspace }}:${{ github.workspace }}:rw"] + "mounts": [ + "${{ github.workspace }}:${{ github.workspace }}:rw" + ] } }, "gateway": { diff --git a/.github/workflows/developer-docs-consolidator.lock.yml b/.github/workflows/developer-docs-consolidator.lock.yml index 3092a21efa..0d084fa310 100644 --- a/.github/workflows/developer-docs-consolidator.lock.yml +++ b/.github/workflows/developer-docs-consolidator.lock.yml @@ -481,7 +481,9 @@ jobs: "--project", "${{ github.workspace }}" ], - "mounts": ["${{ github.workspace }}:${{ github.workspace }}:rw"] + "mounts": [ + "${{ github.workspace }}:${{ github.workspace }}:rw" + ] } }, "gateway": { diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml index fc7f4fe9a9..4d535b2d13 100644 --- a/.github/workflows/duplicate-code-detector.lock.yml +++ b/.github/workflows/duplicate-code-detector.lock.yml @@ -463,7 +463,9 @@ jobs: "--project", "${{ github.workspace }}" ], - "mounts": ["${{ github.workspace }}:${{ github.workspace }}:rw"] + "mounts": [ + "${{ github.workspace }}:${{ github.workspace }}:rw" + ] } }, "gateway": { diff --git a/.github/workflows/go-fan.lock.yml b/.github/workflows/go-fan.lock.yml index 199028fc4e..a922c01c02 100644 --- a/.github/workflows/go-fan.lock.yml +++ b/.github/workflows/go-fan.lock.yml @@ -417,7 +417,9 @@ jobs: "--project", "${{ github.workspace }}" ], - "mounts": ["${{ github.workspace }}:${{ github.workspace }}:rw"] + "mounts": [ + "${{ github.workspace }}:${{ github.workspace }}:rw" + ] } }, "gateway": { diff --git a/.github/workflows/jsweep.lock.yml b/.github/workflows/jsweep.lock.yml index fefddc318d..ff933072eb 100644 --- a/.github/workflows/jsweep.lock.yml +++ b/.github/workflows/jsweep.lock.yml @@ -433,7 +433,10 @@ jobs: "args": ["--network", "host"], "entrypoint": "serena", "entrypointArgs": ["start-mcp-server", "--context", "codex", "--project", "${{ github.workspace }}"], - "mounts": ["${{ github.workspace }}:${{ github.workspace }}:rw"] + "env": { + "NPM_CONFIG_CACHE": "/home/runner/.npm" + }, + "mounts": ["${{ github.workspace }}:${{ github.workspace }}:rw", "/home/runner/.npm:/home/runner/.npm:rw"] } }, "gateway": { diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index 658913642a..9c96672818 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -731,7 +731,12 @@ jobs: "args": ["--network", "host"], "entrypoint": "serena", "entrypointArgs": ["start-mcp-server", "--context", "codex", "--project", "${{ github.workspace }}"], - "mounts": ["${{ github.workspace }}:${{ github.workspace }}:rw"] + "env": { + "NPM_CONFIG_CACHE": "/home/runner/.npm", + "PIP_CACHE_DIR": "/home/runner/.cache/pip", + "UV_CACHE_DIR": "/home/runner/.cache/uv" + }, + "mounts": ["${{ github.workspace }}:${{ github.workspace }}:rw", "/home/runner/.cache/pip:/home/runner/.cache/pip:rw", "/home/runner/.cache/uv:/home/runner/.cache/uv:rw", "/home/runner/.npm:/home/runner/.npm:rw"] }, "tavily": { "type": "http", diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 152788a0ca..5388011c4c 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -666,7 +666,9 @@ jobs: "--project", "${{ github.workspace }}" ], - "mounts": ["${{ github.workspace }}:${{ github.workspace }}:rw"] + "mounts": [ + "${{ github.workspace }}:${{ github.workspace }}:rw" + ] }, "tavily": { "type": "http", diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index d720ca3617..15557d474e 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -760,7 +760,9 @@ jobs: "--project", "${{ github.workspace }}" ], - "mounts": ["${{ github.workspace }}:${{ github.workspace }}:rw"] + "mounts": [ + "${{ github.workspace }}:${{ github.workspace }}:rw" + ] }, "tavily": { "type": "http", diff --git a/.github/workflows/typist.lock.yml b/.github/workflows/typist.lock.yml index 0f3a58f402..1504714528 100644 --- a/.github/workflows/typist.lock.yml +++ b/.github/workflows/typist.lock.yml @@ -404,7 +404,9 @@ jobs: "--project", "${{ github.workspace }}" ], - "mounts": ["${{ github.workspace }}:${{ github.workspace }}:rw"] + "mounts": [ + "${{ github.workspace }}:${{ github.workspace }}:rw" + ] } }, "gateway": { diff --git a/pkg/workflow/runtime_toolchain_mappings_test.go b/pkg/workflow/runtime_toolchain_mappings_test.go new file mode 100644 index 0000000000..72e2cc8261 --- /dev/null +++ b/pkg/workflow/runtime_toolchain_mappings_test.go @@ -0,0 +1,290 @@ +package workflow + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCollectToolchainMappings(t *testing.T) { + tests := []struct { + name string + requirements []RuntimeRequirement + expectedRuntimes []string + expectedEnvVarKeys []string + expectedMountCount int + hasGoCacheMount bool + hasNodeCacheMount bool + hasPythonCacheMount bool + }{ + { + name: "single Go runtime", + requirements: []RuntimeRequirement{ + { + Runtime: &Runtime{ + ID: "go", + Name: "Go", + }, + }, + }, + expectedRuntimes: []string{"go"}, + expectedEnvVarKeys: []string{"GOCACHE", "GOMODCACHE", "GOPATH"}, + expectedMountCount: 2, // GOPATH and GOCACHE + hasGoCacheMount: true, + }, + { + name: "multiple runtimes - Go and Node", + requirements: []RuntimeRequirement{ + { + Runtime: &Runtime{ + ID: "go", + Name: "Go", + }, + }, + { + Runtime: &Runtime{ + ID: "node", + Name: "Node.js", + }, + }, + }, + expectedRuntimes: []string{"go", "node"}, + expectedEnvVarKeys: []string{"GOCACHE", "GOMODCACHE", "GOPATH", "NPM_CONFIG_CACHE"}, + expectedMountCount: 3, // GOPATH, GOCACHE, NPM_CONFIG_CACHE + hasGoCacheMount: true, + hasNodeCacheMount: true, + }, + { + name: "Python runtime", + requirements: []RuntimeRequirement{ + { + Runtime: &Runtime{ + ID: "python", + Name: "Python", + }, + }, + }, + expectedRuntimes: []string{"python"}, + expectedEnvVarKeys: []string{"PIP_CACHE_DIR"}, + expectedMountCount: 1, + hasPythonCacheMount: true, + }, + { + name: "no requirements", + requirements: []RuntimeRequirement{}, + expectedRuntimes: []string{}, + expectedEnvVarKeys: nil, // nil for empty case + expectedMountCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mappings := CollectToolchainMappings(tt.requirements) + + // Check runtime count + assert.Len(t, mappings.Mappings, len(tt.expectedRuntimes), "Expected %d runtime mappings", len(tt.expectedRuntimes)) + + // Check that expected runtimes are present + for _, runtimeID := range tt.expectedRuntimes { + _, exists := mappings.Mappings[runtimeID] + assert.True(t, exists, "Expected runtime %s to be present", runtimeID) + } + + // Check environment variables + allEnvVars := mappings.GetAllEnvVars() + if tt.expectedEnvVarKeys == nil { + // For empty case, just check count + assert.Empty(t, allEnvVars, "Expected no environment variables") + } else { + var envVarKeys []string + for key := range allEnvVars { + envVarKeys = append(envVarKeys, key) + } + sort.Strings(envVarKeys) + sort.Strings(tt.expectedEnvVarKeys) + assert.Equal(t, tt.expectedEnvVarKeys, envVarKeys, "Environment variable keys don't match") + } + + // Check mount count + allMounts := mappings.GetAllMounts() + assert.Len(t, allMounts, tt.expectedMountCount, "Expected %d mounts", tt.expectedMountCount) + + // Check specific mounts if needed + if tt.hasGoCacheMount { + found := false + for _, mount := range allMounts { + if mount == "/home/runner/.cache/go-build:/home/runner/.cache/go-build:rw" { + found = true + break + } + } + assert.True(t, found, "Expected Go cache mount to be present") + } + + if tt.hasNodeCacheMount { + found := false + for _, mount := range allMounts { + if mount == "/home/runner/.npm:/home/runner/.npm:rw" { + found = true + break + } + } + assert.True(t, found, "Expected Node cache mount to be present") + } + + if tt.hasPythonCacheMount { + found := false + for _, mount := range allMounts { + if mount == "/home/runner/.cache/pip:/home/runner/.cache/pip:rw" { + found = true + break + } + } + assert.True(t, found, "Expected Python cache mount to be present") + } + + // Verify mounts are sorted (only check if we have mounts) + if len(allMounts) > 0 { + sortedMounts := make([]string, len(allMounts)) + copy(sortedMounts, allMounts) + sort.Strings(sortedMounts) + assert.Equal(t, sortedMounts, allMounts, "Mounts should be sorted") + } + }) + } +} + +func TestMergeMountsWithDedup(t *testing.T) { + tests := []struct { + name string + existing []string + new []string + expected []string + expectedCount int + }{ + { + name: "no duplicates", + existing: []string{"/path/a:/path/a:rw"}, + new: []string{"/path/b:/path/b:rw"}, + expected: []string{"/path/a:/path/a:rw", "/path/b:/path/b:rw"}, + expectedCount: 2, + }, + { + name: "with duplicates", + existing: []string{"/path/a:/path/a:rw", "/path/b:/path/b:rw"}, + new: []string{"/path/b:/path/b:rw", "/path/c:/path/c:rw"}, + expected: []string{"/path/a:/path/a:rw", "/path/b:/path/b:rw", "/path/c:/path/c:rw"}, + expectedCount: 3, + }, + { + name: "all same", + existing: []string{"/path/a:/path/a:rw"}, + new: []string{"/path/a:/path/a:rw"}, + expected: []string{"/path/a:/path/a:rw"}, + expectedCount: 1, + }, + { + name: "empty existing", + existing: []string{}, + new: []string{"/path/a:/path/a:rw", "/path/b:/path/b:rw"}, + expected: []string{"/path/a:/path/a:rw", "/path/b:/path/b:rw"}, + expectedCount: 2, + }, + { + name: "empty new", + existing: []string{"/path/a:/path/a:rw", "/path/b:/path/b:rw"}, + new: []string{}, + expected: []string{"/path/a:/path/a:rw", "/path/b:/path/b:rw"}, + expectedCount: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := MergeMountsWithDedup(tt.existing, tt.new) + + // Check count + assert.Len(t, result, tt.expectedCount, "Expected %d mounts after merge", tt.expectedCount) + + // Verify no duplicates + seen := make(map[string]bool) + for _, mount := range result { + assert.False(t, seen[mount], "Found duplicate mount: %s", mount) + seen[mount] = true + } + + // Verify sorted + sortedExpected := make([]string, len(tt.expected)) + copy(sortedExpected, tt.expected) + sort.Strings(sortedExpected) + assert.Equal(t, sortedExpected, result, "Result should be sorted and match expected") + }) + } +} + +func TestToolchainMappings_AddMapping(t *testing.T) { + mappings := NewToolchainMappings() + + // Add first mapping + envVars1 := map[string]string{"VAR1": "value1"} + mounts1 := []string{"/mount1:/mount1:rw"} + mappings.AddMapping("go", envVars1, mounts1) + + require.NotNil(t, mappings.Mappings["go"]) + assert.Equal(t, "go", mappings.Mappings["go"].RuntimeID) + assert.Equal(t, "value1", mappings.Mappings["go"].EnvVars["VAR1"]) + assert.Contains(t, mappings.Mappings["go"].Mounts, "/mount1:/mount1:rw") + + // Add second mapping to same runtime (should merge) + envVars2 := map[string]string{"VAR2": "value2"} + mounts2 := []string{"/mount2:/mount2:rw"} + mappings.AddMapping("go", envVars2, mounts2) + + assert.Equal(t, "value1", mappings.Mappings["go"].EnvVars["VAR1"]) + assert.Equal(t, "value2", mappings.Mappings["go"].EnvVars["VAR2"]) + assert.Contains(t, mappings.Mappings["go"].Mounts, "/mount1:/mount1:rw") + assert.Contains(t, mappings.Mappings["go"].Mounts, "/mount2:/mount2:rw") +} + +func TestToolchainMappings_GetAllEnvVars(t *testing.T) { + mappings := NewToolchainMappings() + + mappings.AddMapping("go", map[string]string{"GOPATH": "/go"}, nil) + mappings.AddMapping("node", map[string]string{"NPM_CONFIG_CACHE": "/npm"}, nil) + + allEnvVars := mappings.GetAllEnvVars() + + assert.Len(t, allEnvVars, 2) + assert.Equal(t, "/go", allEnvVars["GOPATH"]) + assert.Equal(t, "/npm", allEnvVars["NPM_CONFIG_CACHE"]) +} + +func TestToolchainMappings_GetAllMounts(t *testing.T) { + mappings := NewToolchainMappings() + + mappings.AddMapping("go", nil, []string{"/mount1:/mount1:rw", "/mount2:/mount2:rw"}) + mappings.AddMapping("node", nil, []string{"/mount2:/mount2:rw", "/mount3:/mount3:rw"}) + + allMounts := mappings.GetAllMounts() + + // Should have 3 unique mounts + assert.Len(t, allMounts, 3) + + // Should be sorted + sortedMounts := make([]string, len(allMounts)) + copy(sortedMounts, allMounts) + sort.Strings(sortedMounts) + assert.Equal(t, sortedMounts, allMounts) + + // Should contain all three unique mounts + mountSet := make(map[string]bool) + for _, mount := range allMounts { + mountSet[mount] = true + } + assert.True(t, mountSet["/mount1:/mount1:rw"]) + assert.True(t, mountSet["/mount2:/mount2:rw"]) + assert.True(t, mountSet["/mount3:/mount3:rw"]) +} From f6c397f96c5e1a8375c6dba46e2257c96002bda4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 07:03:24 +0000 Subject: [PATCH 5/7] Use runtime variable resolution for toolchain paths Changed from hardcoded paths like "/home/runner/go" to shell variables like "$GOPATH", "$GOCACHE", "$HOME/.npm" so values are resolved at runtime by the GitHub Actions environment, not at compile time. Addresses feedback from @pelikhan to resolve variables at runtime in the action. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/runtime_toolchain_mappings.go | 39 ++++++++++--------- .../runtime_toolchain_mappings_test.go | 6 +-- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/pkg/workflow/runtime_toolchain_mappings.go b/pkg/workflow/runtime_toolchain_mappings.go index 6a95558b8b..dd17f09ba1 100644 --- a/pkg/workflow/runtime_toolchain_mappings.go +++ b/pkg/workflow/runtime_toolchain_mappings.go @@ -99,58 +99,61 @@ func CollectToolchainMappings(requirements []RuntimeRequirement) *ToolchainMappi var mounts []string // Collect runtime-specific environment variables and mounts + // Use shell variable expansion syntax so values are resolved at runtime by the action switch runtimeID { case "go": // Go requires GOPATH, GOCACHE, and GOMODCACHE to be accessible - envVars["GOPATH"] = "/home/runner/go" - envVars["GOCACHE"] = "/home/runner/.cache/go-build" - envVars["GOMODCACHE"] = "/home/runner/go/pkg/mod" + // These are set by actions/setup-go and resolved at runtime + envVars["GOPATH"] = "$GOPATH" + envVars["GOCACHE"] = "$GOCACHE" + envVars["GOMODCACHE"] = "$GOMODCACHE" - // Mount Go directories - mounts = append(mounts, "/home/runner/go:/home/runner/go:rw") - mounts = append(mounts, "/home/runner/.cache/go-build:/home/runner/.cache/go-build:rw") + // Mount Go directories using shell expansion + mounts = append(mounts, "$GOPATH:$GOPATH:rw") + mounts = append(mounts, "$GOCACHE:$GOCACHE:rw") case "node": // Node.js requires npm cache and node_modules - envVars["NPM_CONFIG_CACHE"] = "/home/runner/.npm" + // Use $HOME for runtime resolution + envVars["NPM_CONFIG_CACHE"] = "$HOME/.npm" // Mount npm cache - mounts = append(mounts, "/home/runner/.npm:/home/runner/.npm:rw") + mounts = append(mounts, "$HOME/.npm:$HOME/.npm:rw") case "python": // Python requires pip cache - envVars["PIP_CACHE_DIR"] = "/home/runner/.cache/pip" + envVars["PIP_CACHE_DIR"] = "$HOME/.cache/pip" // Mount pip cache - mounts = append(mounts, "/home/runner/.cache/pip:/home/runner/.cache/pip:rw") + mounts = append(mounts, "$HOME/.cache/pip:$HOME/.cache/pip:rw") case "uv": // uv uses its own cache directory - envVars["UV_CACHE_DIR"] = "/home/runner/.cache/uv" + envVars["UV_CACHE_DIR"] = "$HOME/.cache/uv" // Mount uv cache - mounts = append(mounts, "/home/runner/.cache/uv:/home/runner/.cache/uv:rw") + mounts = append(mounts, "$HOME/.cache/uv:$HOME/.cache/uv:rw") case "ruby": // Ruby requires gem home - envVars["GEM_HOME"] = "/home/runner/.gem" + envVars["GEM_HOME"] = "$HOME/.gem" // Mount gem directory - mounts = append(mounts, "/home/runner/.gem:/home/runner/.gem:rw") + mounts = append(mounts, "$HOME/.gem:$HOME/.gem:rw") case "java": // Java/Maven requires M2 repository - envVars["MAVEN_OPTS"] = "-Dmaven.repo.local=/home/runner/.m2/repository" + envVars["MAVEN_OPTS"] = "-Dmaven.repo.local=$HOME/.m2/repository" // Mount Maven repository - mounts = append(mounts, "/home/runner/.m2:/home/runner/.m2:rw") + mounts = append(mounts, "$HOME/.m2:$HOME/.m2:rw") case "dotnet": // .NET requires NuGet packages directory - envVars["NUGET_PACKAGES"] = "/home/runner/.nuget/packages" + envVars["NUGET_PACKAGES"] = "$HOME/.nuget/packages" // Mount NuGet packages - mounts = append(mounts, "/home/runner/.nuget:/home/runner/.nuget:rw") + mounts = append(mounts, "$HOME/.nuget:$HOME/.nuget:rw") // Other runtimes can be added here as needed } diff --git a/pkg/workflow/runtime_toolchain_mappings_test.go b/pkg/workflow/runtime_toolchain_mappings_test.go index 72e2cc8261..1132b2ebeb 100644 --- a/pkg/workflow/runtime_toolchain_mappings_test.go +++ b/pkg/workflow/runtime_toolchain_mappings_test.go @@ -116,7 +116,7 @@ func TestCollectToolchainMappings(t *testing.T) { if tt.hasGoCacheMount { found := false for _, mount := range allMounts { - if mount == "/home/runner/.cache/go-build:/home/runner/.cache/go-build:rw" { + if mount == "$GOCACHE:$GOCACHE:rw" { found = true break } @@ -127,7 +127,7 @@ func TestCollectToolchainMappings(t *testing.T) { if tt.hasNodeCacheMount { found := false for _, mount := range allMounts { - if mount == "/home/runner/.npm:/home/runner/.npm:rw" { + if mount == "$HOME/.npm:$HOME/.npm:rw" { found = true break } @@ -138,7 +138,7 @@ func TestCollectToolchainMappings(t *testing.T) { if tt.hasPythonCacheMount { found := false for _, mount := range allMounts { - if mount == "/home/runner/.cache/pip:/home/runner/.cache/pip:rw" { + if mount == "$HOME/.cache/pip:$HOME/.cache/pip:rw" { found = true break } From f04b0d070ff9bb087402c293222aa11a6aff7f48 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 07:11:31 +0000 Subject: [PATCH 6/7] Use SortStrings helper function for sorting mounts Replaced direct calls to sort.Strings with the SortStrings helper function in GetAllMounts and MergeMountsWithDedup, following the pattern used in GetAllowedDomains. Addresses feedback from @pelikhan to use runtime function helper. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/jsweep.lock.yml | 4 ++-- .github/workflows/mcp-inspector.lock.yml | 8 ++++---- pkg/workflow/runtime_toolchain_mappings.go | 8 ++------ 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/jsweep.lock.yml b/.github/workflows/jsweep.lock.yml index ff933072eb..37a5b607e0 100644 --- a/.github/workflows/jsweep.lock.yml +++ b/.github/workflows/jsweep.lock.yml @@ -434,9 +434,9 @@ jobs: "entrypoint": "serena", "entrypointArgs": ["start-mcp-server", "--context", "codex", "--project", "${{ github.workspace }}"], "env": { - "NPM_CONFIG_CACHE": "/home/runner/.npm" + "NPM_CONFIG_CACHE": "$HOME/.npm" }, - "mounts": ["${{ github.workspace }}:${{ github.workspace }}:rw", "/home/runner/.npm:/home/runner/.npm:rw"] + "mounts": ["$HOME/.npm:$HOME/.npm:rw", "${{ github.workspace }}:${{ github.workspace }}:rw"] } }, "gateway": { diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index 9c96672818..d281fd665a 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -732,11 +732,11 @@ jobs: "entrypoint": "serena", "entrypointArgs": ["start-mcp-server", "--context", "codex", "--project", "${{ github.workspace }}"], "env": { - "NPM_CONFIG_CACHE": "/home/runner/.npm", - "PIP_CACHE_DIR": "/home/runner/.cache/pip", - "UV_CACHE_DIR": "/home/runner/.cache/uv" + "NPM_CONFIG_CACHE": "$HOME/.npm", + "PIP_CACHE_DIR": "$HOME/.cache/pip", + "UV_CACHE_DIR": "$HOME/.cache/uv" }, - "mounts": ["${{ github.workspace }}:${{ github.workspace }}:rw", "/home/runner/.cache/pip:/home/runner/.cache/pip:rw", "/home/runner/.cache/uv:/home/runner/.cache/uv:rw", "/home/runner/.npm:/home/runner/.npm:rw"] + "mounts": ["$HOME/.cache/pip:$HOME/.cache/pip:rw", "$HOME/.cache/uv:$HOME/.cache/uv:rw", "$HOME/.npm:$HOME/.npm:rw", "${{ github.workspace }}:${{ github.workspace }}:rw"] }, "tavily": { "type": "http", diff --git a/pkg/workflow/runtime_toolchain_mappings.go b/pkg/workflow/runtime_toolchain_mappings.go index dd17f09ba1..60bdb9c722 100644 --- a/pkg/workflow/runtime_toolchain_mappings.go +++ b/pkg/workflow/runtime_toolchain_mappings.go @@ -1,9 +1,5 @@ package workflow -import ( - "sort" -) - // RuntimeToolchainMapping represents environment variables and mounts that need to be // passed to the agent container (e.g., Serena MCP server) so that the toolchain works properly. // These mappings are collected from runtime setup actions like actions/setup-go, actions/setup-node, etc. @@ -81,7 +77,7 @@ func (tm *ToolchainMappings) GetAllMounts() []string { for mount := range mountSet { mounts = append(mounts, mount) } - sort.Strings(mounts) + SortStrings(mounts) return mounts } @@ -185,7 +181,7 @@ func MergeMountsWithDedup(existingMounts []string, newMounts []string) []string for mount := range mountSet { result = append(result, mount) } - sort.Strings(result) + SortStrings(result) return result } From 8702797e79334a0ff1992c1b512a94c4bb35138e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 07:36:56 +0000 Subject: [PATCH 7/7] Document Go environment variables source from actions/setup-go Added detailed comments explaining how Go toolchain environment variables (GOPATH, GOCACHE, GOMODCACHE) are determined by running `go env` commands after actions/setup-go installs Go. These values are resolved at runtime using shell variable expansion. Research findings: - actions/setup-go uses `go env GOPATH`, `go env GOCACHE`, and `go env GOMODCACHE` to get actual paths - Default values: GOPATH=$HOME/go, GOCACHE=$HOME/.cache/go-build, GOMODCACHE=$GOPATH/pkg/mod - actions/setup-node uses `npm config get cache` for npm cache path - See: https://github.com/actions/setup-go/blob/main/src/package-managers.ts Addresses deep research request from @pelikhan. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/runtime_toolchain_mappings.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/runtime_toolchain_mappings.go b/pkg/workflow/runtime_toolchain_mappings.go index 60bdb9c722..a32ddf63a0 100644 --- a/pkg/workflow/runtime_toolchain_mappings.go +++ b/pkg/workflow/runtime_toolchain_mappings.go @@ -98,13 +98,20 @@ func CollectToolchainMappings(requirements []RuntimeRequirement) *ToolchainMappi // Use shell variable expansion syntax so values are resolved at runtime by the action switch runtimeID { case "go": - // Go requires GOPATH, GOCACHE, and GOMODCACHE to be accessible - // These are set by actions/setup-go and resolved at runtime + // Go toolchain requires access to three key directories: + // - GOPATH: workspace for Go code ($HOME/go by default) + // - GOCACHE: build cache ($HOME/.cache/go-build by default) + // - GOMODCACHE: module cache ($GOPATH/pkg/mod by default) + // + // These variables are determined by running `go env` commands after + // actions/setup-go installs Go. The actual paths are resolved at runtime. + // See: https://github.com/actions/setup-go/blob/main/src/package-managers.ts envVars["GOPATH"] = "$GOPATH" envVars["GOCACHE"] = "$GOCACHE" envVars["GOMODCACHE"] = "$GOMODCACHE" // Mount Go directories using shell expansion + // These mounts allow the container to access the Go workspace and caches mounts = append(mounts, "$GOPATH:$GOPATH:rw") mounts = append(mounts, "$GOCACHE:$GOCACHE:rw")