Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions pkg/workflow/codex_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1294,3 +1294,38 @@ func TestCodexEngineExecutionCustomHarness(t *testing.T) {
t.Errorf("Expected default harness path to be overridden, got:\n%s", stepContent)
}
}

// TestCodexEngineForwardsSafeOutputsInputEnvVars verifies that GH_AW_INPUT_* variables
// from the safe-outputs config are included in the Codex execution step env.
// Codex uses the TOML-based MCP setup; the TOML env_vars list references these vars
// so the safe-outputs container can resolve ${GH_AW_INPUT_…} placeholders in config.json.
// AWF passes them into the sandbox via --env-all, enabling the forwarding chain.
func TestCodexEngineForwardsSafeOutputsInputEnvVars(t *testing.T) {
engine := NewCodexEngine()
workflowData := &WorkflowData{
Name: "test-workflow",
SafeOutputs: &SafeOutputsConfig{
CreatePullRequests: &CreatePullRequestsConfig{
BaseSafeOutputConfig: BaseSafeOutputConfig{Max: strPtr("1")},
TargetRepoSlug: "${{ inputs.owner }}/${{ inputs.repo }}",
},
},
SafeOutputsInputEnvVars: map[string]string{
"GH_AW_INPUT_OWNER": "${{ inputs.owner }}",
"GH_AW_INPUT_REPO": "${{ inputs.repo }}",
},
}

steps := engine.GetExecutionSteps(workflowData, "test-log")
if len(steps) != 1 {
t.Fatalf("Expected 1 execution step, got %d", len(steps))
}
stepContent := strings.Join([]string(steps[0]), "\n")

if !strings.Contains(stepContent, "GH_AW_INPUT_OWNER: ${{ inputs.owner }}") {
t.Errorf("Expected GH_AW_INPUT_OWNER in step env for TOML env_vars forwarding, got:\n%s", stepContent)
}
if !strings.Contains(stepContent, "GH_AW_INPUT_REPO: ${{ inputs.repo }}") {
t.Errorf("Expected GH_AW_INPUT_REPO in step env for TOML env_vars forwarding, got:\n%s", stepContent)
}
}
37 changes: 37 additions & 0 deletions pkg/workflow/copilot_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3096,3 +3096,40 @@ func TestCopilotEngineLLMProviderAnthropicAutoBYOK(t *testing.T) {
t.Errorf("COPILOT_GITHUB_TOKEN should be omitted in auto-BYOK mode, got:\n%s", stepContent)
}
}

// TestCopilotEngineForwardsSafeOutputsInputEnvVars verifies that GH_AW_INPUT_* variables
// extracted from the safe-outputs config are included in the Copilot execution step env.
// These vars must reach the agent step so that the TOML env_vars forwarding chain
// (runner step → AWF sandbox → Copilot CLI → safe-outputs container) can resolve
// ${GH_AW_INPUT_…} placeholders in config.json. Without this, a dynamic target-repo
// like "${{ inputs.owner }}/${{ inputs.repo }}" stays unexpanded in the patch filename,
// causing a mismatch with the consumer's lookup and silently dropping the PR.
func TestCopilotEngineForwardsSafeOutputsInputEnvVars(t *testing.T) {
engine := NewCopilotEngine()
workflowData := &WorkflowData{
Name: "test-workflow",
SafeOutputs: &SafeOutputsConfig{
CreatePullRequests: &CreatePullRequestsConfig{
BaseSafeOutputConfig: BaseSafeOutputConfig{Max: strPtr("1")},
TargetRepoSlug: "${{ inputs.owner }}/${{ inputs.repo }}",
},
},
SafeOutputsInputEnvVars: map[string]string{
"GH_AW_INPUT_OWNER": "${{ inputs.owner }}",
"GH_AW_INPUT_REPO": "${{ inputs.repo }}",
},
}

steps := engine.GetExecutionSteps(workflowData, "/tmp/gh-aw/test.log")
if len(steps) != 1 {
t.Fatalf("Expected 1 execution step, got %d", len(steps))
}
stepContent := strings.Join([]string(steps[0]), "\n")

if !strings.Contains(stepContent, "GH_AW_INPUT_OWNER: ${{ inputs.owner }}") {
t.Errorf("Expected GH_AW_INPUT_OWNER in step env for TOML env_vars forwarding, got:\n%s", stepContent)
}
if !strings.Contains(stepContent, "GH_AW_INPUT_REPO: ${{ inputs.repo }}") {
t.Errorf("Expected GH_AW_INPUT_REPO in step env for TOML env_vars forwarding, got:\n%s", stepContent)
}
}
15 changes: 15 additions & 0 deletions pkg/workflow/safe_output_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,21 @@ func TestApplySafeOutputEnvToMap(t *testing.T) {
"GH_AW_ASSETS_ALLOWED_EXTS": "\".png,.jpg,.jpeg\"",
},
},
{
name: "safe outputs input env vars forwarded to agent step",
workflowData: &WorkflowData{
SafeOutputs: &SafeOutputsConfig{},
SafeOutputsInputEnvVars: map[string]string{
"GH_AW_INPUT_OWNER": "${{ inputs.owner }}",
"GH_AW_INPUT_REPO": "${{ inputs.repo }}",
},
},
expected: map[string]string{
"GH_AW_SAFE_OUTPUTS": "${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}",
"GH_AW_INPUT_OWNER": "${{ inputs.owner }}",
"GH_AW_INPUT_REPO": "${{ inputs.repo }}",
},
},
}

for _, tt := range tests {
Expand Down
11 changes: 11 additions & 0 deletions pkg/workflow/safe_outputs_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package workflow
import (
"encoding/json"
"fmt"
"maps"
"strconv"
"strings"

Expand Down Expand Up @@ -71,6 +72,16 @@ func applySafeOutputEnvToMap(env map[string]string, data *WorkflowData) {
env["GH_AW_ASSETS_MAX_SIZE_KB"] = strconv.Itoa(data.SafeOutputs.UploadAssets.MaxSizeKB)
env["GH_AW_ASSETS_ALLOWED_EXTS"] = fmt.Sprintf("%q", strings.Join(data.SafeOutputs.UploadAssets.AllowedExts, ","))
}

// Forward GH_AW_INPUT_* vars extracted from the safe-outputs config so that
// TOML-based MCP clients (Codex, Copilot) can pass them through to the
// containerised safe-outputs MCP server via the TOML env_vars allowlist.
// Without this, ${GH_AW_INPUT_…} placeholders written into config.json at
// compile time cannot be resolved inside the container, causing a dynamic
// target-repo value (e.g. "${{ inputs.owner }}/${{ inputs.repo }}") to survive
// unexpanded as the patch filename. The consumer's lookup then fails to match,
// silently dropping the pull-request creation.
maps.Copy(env, data.SafeOutputsInputEnvVars)
}

// buildWorkflowMetadataEnvVars builds workflow name and source environment variables
Expand Down
Loading