From dfb7f2f230ed1b246dc17f1e0f20ee8a49c784d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 18:46:39 +0000 Subject: [PATCH 01/10] Initial plan From 0d88d71f60a3d81d152850f57d1cc39aa7611817 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 19:02:24 +0000 Subject: [PATCH 02/10] fix: resolve safe-outputs workflow input expressions in config generation Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6340fd05-1d07-4c32-ba41-126e4046144c Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/mcp_setup_generator.go | 18 ++++- ...safe_outputs_dynamic_allowed_repos_test.go | 75 +++++++++++++++++++ pkg/workflow/secret_extraction.go | 46 ++++++++++++ pkg/workflow/secret_extraction_test.go | 36 +++++++++ 4 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 pkg/workflow/safe_outputs_dynamic_allowed_repos_test.go diff --git a/pkg/workflow/mcp_setup_generator.go b/pkg/workflow/mcp_setup_generator.go index 74ac527c2ae..f44e0c87844 100644 --- a/pkg/workflow/mcp_setup_generator.go +++ b/pkg/workflow/mcp_setup_generator.go @@ -229,15 +229,22 @@ func generateSafeOutputsSetup(c *Compiler, yaml *strings.Builder, safeOutputConf yaml.WriteString(" - name: Generate Safe Outputs Config\n") configSecrets := ExtractSecretsFromValue(safeOutputConfig) configContextVars := ExtractGitHubContextExpressionsFromValue(safeOutputConfig) - hasEnvVars := len(configSecrets) > 0 || len(configContextVars) > 0 + configWorkflowInputs := ExtractWorkflowInputExpressionsFromValue(safeOutputConfig) + hasEnvVars := len(configSecrets) > 0 || len(configContextVars) > 0 || len(configWorkflowInputs) > 0 if hasEnvVars { yaml.WriteString(" env:\n") - envKeys := make([]string, 0, len(configSecrets)+len(configContextVars)) - envValues := make(map[string]string, len(configSecrets)+len(configContextVars)) - for k, v := range configContextVars { + envKeys := make([]string, 0, len(configSecrets)+len(configContextVars)+len(configWorkflowInputs)) + envValues := make(map[string]string, len(configSecrets)+len(configContextVars)+len(configWorkflowInputs)) + for k, v := range configWorkflowInputs { envKeys = append(envKeys, k) envValues[k] = v } + for k, v := range configContextVars { + if _, exists := envValues[k]; !exists { + envKeys = append(envKeys, k) + } + envValues[k] = v + } for k, v := range configSecrets { if _, exists := envValues[k]; !exists { envKeys = append(envKeys, k) @@ -267,6 +274,9 @@ func generateSafeOutputsSetup(c *Compiler, yaml *strings.Builder, safeOutputConf for varName, ctxExpr := range configContextVars { sanitizedConfig = strings.ReplaceAll(sanitizedConfig, ctxExpr, "${"+varName+"}") } + for varName, inputExpr := range configWorkflowInputs { + sanitizedConfig = strings.ReplaceAll(sanitizedConfig, inputExpr, "${"+varName+"}") + } yaml.WriteString(" cat > \"${RUNNER_TEMP}/gh-aw/safeoutputs/config.json\" << " + delimiter + "\n") yaml.WriteString(" " + sanitizedConfig + "\n") yaml.WriteString(" " + delimiter + "\n") diff --git a/pkg/workflow/safe_outputs_dynamic_allowed_repos_test.go b/pkg/workflow/safe_outputs_dynamic_allowed_repos_test.go new file mode 100644 index 00000000000..9a3960b4b9e --- /dev/null +++ b/pkg/workflow/safe_outputs_dynamic_allowed_repos_test.go @@ -0,0 +1,75 @@ +//go:build !integration + +package workflow + +import ( + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/github/gh-aw/pkg/stringutil" + "github.com/github/gh-aw/pkg/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSafeOutputsConfigUsesWorkflowInputEnvVarsForDynamicAllowedRepos(t *testing.T) { + tmpDir := testutil.TempDir(t, "safe-outputs-dynamic-allowed-repos") + mdFile := filepath.Join(tmpDir, "dynamic-safe-outputs.md") + + content := `--- +name: Dynamic Safe Outputs +on: + workflow_dispatch: + inputs: + target_repo: + required: true + type: string + base_branch: + required: true + type: string +engine: copilot +safe-outputs: + create-pull-request: + allowed-repos: + - ${{ inputs.target_repo }} + allowed-base-branches: + - ${{ inputs.base_branch }} +--- + +Test workflow +` + + err := os.WriteFile(mdFile, []byte(content), 0600) + require.NoError(t, err, "Failed to write test workflow markdown") + + compiler := NewCompiler() + err = compiler.CompileWorkflow(mdFile) + require.NoError(t, err, "Failed to compile workflow") + + lockFile := stringutil.MarkdownToLockFile(mdFile) + compiledBytes, err := os.ReadFile(lockFile) + require.NoError(t, err, "Failed to read compiled workflow") + compiled := string(compiledBytes) + + assert.Contains(t, compiled, "GH_AW_INPUT_TARGET_REPO: ${{ inputs.target_repo }}", + "Generate Safe Outputs Config step should map inputs.target_repo to an env var") + assert.Contains(t, compiled, "GH_AW_INPUT_BASE_BRANCH: ${{ inputs.base_branch }}", + "Generate Safe Outputs Config step should map inputs.base_branch to an env var") + assert.Contains(t, compiled, `"allowed_repos":"${GH_AW_INPUT_TARGET_REPO}"`, + "config.json payload should use shell env var for allowed_repos") + assert.Contains(t, compiled, `"allowed_base_branches":"${GH_AW_INPUT_BASE_BRANCH}"`, + "config.json payload should use shell env var for allowed_base_branches") + + quotedHeredocPattern := regexp.MustCompile(`cat > "\$\{RUNNER_TEMP\}/gh-aw/safeoutputs/config\.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_[0-9a-f]{16}_EOF'`) + assert.False(t, quotedHeredocPattern.MatchString(compiled), + "Safe outputs config heredoc should not be single-quoted when using dynamic input expressions") + + unquotedHeredocPattern := regexp.MustCompile(`cat > "\$\{RUNNER_TEMP\}/gh-aw/safeoutputs/config\.json" << GH_AW_SAFE_OUTPUTS_CONFIG_[0-9a-f]{16}_EOF`) + assert.True(t, unquotedHeredocPattern.MatchString(compiled), + "Safe outputs config heredoc should be unquoted when dynamic input expressions are present") + assert.NotContains(t, strings.ReplaceAll(compiled, `\"`, `"`), `"allowed_repos":["${{ inputs.target_repo }}"]`, + "config.json payload should not keep unresolved workflow input expression") +} diff --git a/pkg/workflow/secret_extraction.go b/pkg/workflow/secret_extraction.go index e366bb597d9..358251646c1 100644 --- a/pkg/workflow/secret_extraction.go +++ b/pkg/workflow/secret_extraction.go @@ -200,6 +200,10 @@ func ExtractEnvExpressionsFromValue(value string) map[string]string { // accepted later if they are present in gitHubContextEnvVarMap. var gitHubContextExprPattern = regexp.MustCompile(`\$\{\{\s*github\.([a-z][a-z0-9_.]*)\s*\}\}`) +// workflowInputExprPattern matches simple ${{ inputs.NAME }} expressions. +// NAME may contain letters, numbers, underscores, and dashes. +var workflowInputExprPattern = regexp.MustCompile(`\$\{\{\s*inputs\.([a-zA-Z_][a-zA-Z0-9_-]*)\s*\}\}`) + // gitHubContextEnvVarMap maps common github.* context properties to their corresponding // GitHub Actions runner environment variables (always available on all runners). // See: https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables @@ -258,6 +262,48 @@ func ExtractGitHubContextExpressionsFromValue(value string) map[string]string { return result } +// ExtractWorkflowInputExpressionsFromValue extracts all simple ${{ inputs.X }} expressions from a +// string value and maps them to deterministic environment variable names. +// Returns a map of env var name -> full expression. +// +// Examples: +// - "${{ inputs.target_repo }}" -> {"GH_AW_INPUT_TARGET_REPO": "${{ inputs.target_repo }}"} +// - "${{ inputs.base-branch }}" -> {"GH_AW_INPUT_BASE_BRANCH": "${{ inputs.base-branch }}"} +func ExtractWorkflowInputExpressionsFromValue(value string) map[string]string { + result := make(map[string]string) + + matches := workflowInputExprPattern.FindAllStringSubmatch(value, -1) + for _, match := range matches { + if len(match) < 2 { + continue + } + + inputName := match[1] + fullExpr := match[0] + envVar := normalizeInputNameToEnvVar(inputName) + result[envVar] = fullExpr + secretLog.Printf("Extracted workflow input expression: %s -> %s", fullExpr, envVar) + } + + return result +} + +func normalizeInputNameToEnvVar(inputName string) string { + var b strings.Builder + b.Grow(len(inputName)) + for _, r := range inputName { + switch { + case r >= 'a' && r <= 'z': + b.WriteRune(r - ('a' - 'A')) + case (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9'): + b.WriteRune(r) + default: + b.WriteRune('_') + } + } + return "GH_AW_INPUT_" + b.String() +} + // ReplaceTemplateExpressionsWithEnvVars replaces all template expressions with environment variable references // Handles: secrets.*, env.*, and github.workspace // Examples: diff --git a/pkg/workflow/secret_extraction_test.go b/pkg/workflow/secret_extraction_test.go index b2590d7457c..9b9b0f6dc56 100644 --- a/pkg/workflow/secret_extraction_test.go +++ b/pkg/workflow/secret_extraction_test.go @@ -451,3 +451,39 @@ func TestExtractGitHubContextExpressionsFromValue(t *testing.T) { }) } } + +func TestExtractWorkflowInputExpressionsFromValue(t *testing.T) { + tests := []struct { + name string + value string + expected map[string]string + }{ + { + name: "single input expression", + value: `"repo":"${{ inputs.target_repo }}"`, + expected: map[string]string{ + "GH_AW_INPUT_TARGET_REPO": "${{ inputs.target_repo }}", + }, + }, + { + name: "multiple input expressions with dash and underscore", + value: `"repo":"${{ inputs.target-repo }}","base":"${{ inputs.base_branch }}"`, + expected: map[string]string{ + "GH_AW_INPUT_TARGET_REPO": "${{ inputs.target-repo }}", + "GH_AW_INPUT_BASE_BRANCH": "${{ inputs.base_branch }}", + }, + }, + { + name: "no input expressions", + value: `"repo":"${{ github.repository }}"`, + expected: map[string]string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ExtractWorkflowInputExpressionsFromValue(tt.value) + assert.Equal(t, tt.expected, result, "Should extract expected workflow input expressions") + }) + } +} From 1067955f2e6e4f91645050534313d0b9fb532b36 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 19:05:48 +0000 Subject: [PATCH 03/10] test: tighten env key dedupe and readability for dynamic safe outputs regression Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6340fd05-1d07-4c32-ba41-126e4046144c Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/mcp_setup_generator.go | 19 +++++++++---------- ...safe_outputs_dynamic_allowed_repos_test.go | 3 ++- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/workflow/mcp_setup_generator.go b/pkg/workflow/mcp_setup_generator.go index f44e0c87844..c754c3285b6 100644 --- a/pkg/workflow/mcp_setup_generator.go +++ b/pkg/workflow/mcp_setup_generator.go @@ -235,21 +235,20 @@ func generateSafeOutputsSetup(c *Compiler, yaml *strings.Builder, safeOutputConf yaml.WriteString(" env:\n") envKeys := make([]string, 0, len(configSecrets)+len(configContextVars)+len(configWorkflowInputs)) envValues := make(map[string]string, len(configSecrets)+len(configContextVars)+len(configWorkflowInputs)) + addEnvValue := func(key, value string) { + if _, exists := envValues[key]; !exists { + envKeys = append(envKeys, key) + } + envValues[key] = value + } for k, v := range configWorkflowInputs { - envKeys = append(envKeys, k) - envValues[k] = v + addEnvValue(k, v) } for k, v := range configContextVars { - if _, exists := envValues[k]; !exists { - envKeys = append(envKeys, k) - } - envValues[k] = v + addEnvValue(k, v) } for k, v := range configSecrets { - if _, exists := envValues[k]; !exists { - envKeys = append(envKeys, k) - } - envValues[k] = v + addEnvValue(k, v) } sort.Strings(envKeys) for _, varName := range envKeys { diff --git a/pkg/workflow/safe_outputs_dynamic_allowed_repos_test.go b/pkg/workflow/safe_outputs_dynamic_allowed_repos_test.go index 9a3960b4b9e..3cef3c899a4 100644 --- a/pkg/workflow/safe_outputs_dynamic_allowed_repos_test.go +++ b/pkg/workflow/safe_outputs_dynamic_allowed_repos_test.go @@ -70,6 +70,7 @@ Test workflow unquotedHeredocPattern := regexp.MustCompile(`cat > "\$\{RUNNER_TEMP\}/gh-aw/safeoutputs/config\.json" << GH_AW_SAFE_OUTPUTS_CONFIG_[0-9a-f]{16}_EOF`) assert.True(t, unquotedHeredocPattern.MatchString(compiled), "Safe outputs config heredoc should be unquoted when dynamic input expressions are present") - assert.NotContains(t, strings.ReplaceAll(compiled, `\"`, `"`), `"allowed_repos":["${{ inputs.target_repo }}"]`, + normalizedCompiled := strings.ReplaceAll(compiled, `\"`, `"`) + assert.NotContains(t, normalizedCompiled, `"allowed_repos":["${{ inputs.target_repo }}"]`, "config.json payload should not keep unresolved workflow input expression") } From aa50985c0e87a5dbe7b73e5fa95220ec8e42dae0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 19:08:55 +0000 Subject: [PATCH 04/10] refactor: clarify env var formatter naming and env dedupe behavior Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6340fd05-1d07-4c32-ba41-126e4046144c Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/mcp_setup_generator.go | 2 ++ pkg/workflow/secret_extraction.go | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/mcp_setup_generator.go b/pkg/workflow/mcp_setup_generator.go index c754c3285b6..44cc32da76b 100644 --- a/pkg/workflow/mcp_setup_generator.go +++ b/pkg/workflow/mcp_setup_generator.go @@ -235,6 +235,8 @@ func generateSafeOutputsSetup(c *Compiler, yaml *strings.Builder, safeOutputConf yaml.WriteString(" env:\n") envKeys := make([]string, 0, len(configSecrets)+len(configContextVars)+len(configWorkflowInputs)) envValues := make(map[string]string, len(configSecrets)+len(configContextVars)+len(configWorkflowInputs)) + // Keep first-seen key ordering stable in envKeys while allowing later sources + // to overwrite the value in envValues when the same key appears again. addEnvValue := func(key, value string) { if _, exists := envValues[key]; !exists { envKeys = append(envKeys, key) diff --git a/pkg/workflow/secret_extraction.go b/pkg/workflow/secret_extraction.go index 358251646c1..d4d8fde8bb3 100644 --- a/pkg/workflow/secret_extraction.go +++ b/pkg/workflow/secret_extraction.go @@ -280,7 +280,7 @@ func ExtractWorkflowInputExpressionsFromValue(value string) map[string]string { inputName := match[1] fullExpr := match[0] - envVar := normalizeInputNameToEnvVar(inputName) + envVar := formatInputNameAsEnvVar(inputName) result[envVar] = fullExpr secretLog.Printf("Extracted workflow input expression: %s -> %s", fullExpr, envVar) } @@ -288,7 +288,7 @@ func ExtractWorkflowInputExpressionsFromValue(value string) map[string]string { return result } -func normalizeInputNameToEnvVar(inputName string) string { +func formatInputNameAsEnvVar(inputName string) string { var b strings.Builder b.Grow(len(inputName)) for _, r := range inputName { From a71911d198351c7f6992dc19c2745118cfd803f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 19:12:05 +0000 Subject: [PATCH 05/10] style: simplify input env var formatting and document env merge helper Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6340fd05-1d07-4c32-ba41-126e4046144c Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/mcp_setup_generator.go | 4 ++-- pkg/workflow/secret_extraction.go | 16 ++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/pkg/workflow/mcp_setup_generator.go b/pkg/workflow/mcp_setup_generator.go index 44cc32da76b..cafd221643b 100644 --- a/pkg/workflow/mcp_setup_generator.go +++ b/pkg/workflow/mcp_setup_generator.go @@ -235,8 +235,8 @@ func generateSafeOutputsSetup(c *Compiler, yaml *strings.Builder, safeOutputConf yaml.WriteString(" env:\n") envKeys := make([]string, 0, len(configSecrets)+len(configContextVars)+len(configWorkflowInputs)) envValues := make(map[string]string, len(configSecrets)+len(configContextVars)+len(configWorkflowInputs)) - // Keep first-seen key ordering stable in envKeys while allowing later sources - // to overwrite the value in envValues when the same key appears again. + // addEnvValue keeps first-seen key ordering stable in envKeys while allowing + // later sources to override the value in envValues for duplicate keys. addEnvValue := func(key, value string) { if _, exists := envValues[key]; !exists { envKeys = append(envKeys, key) diff --git a/pkg/workflow/secret_extraction.go b/pkg/workflow/secret_extraction.go index d4d8fde8bb3..e201f8584fc 100644 --- a/pkg/workflow/secret_extraction.go +++ b/pkg/workflow/secret_extraction.go @@ -289,19 +289,15 @@ func ExtractWorkflowInputExpressionsFromValue(value string) map[string]string { } func formatInputNameAsEnvVar(inputName string) string { - var b strings.Builder - b.Grow(len(inputName)) - for _, r := range inputName { + normalized := strings.Map(func(r rune) rune { switch { - case r >= 'a' && r <= 'z': - b.WriteRune(r - ('a' - 'A')) - case (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9'): - b.WriteRune(r) + case (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'): + return r default: - b.WriteRune('_') + return '_' } - } - return "GH_AW_INPUT_" + b.String() + }, inputName) + return "GH_AW_INPUT_" + strings.ToUpper(normalized) } // ReplaceTemplateExpressionsWithEnvVars replaces all template expressions with environment variable references From 2d4c686ff32c77bbb0729e44a3cc71e69d2b80cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 19:15:11 +0000 Subject: [PATCH 06/10] test: add direct coverage for workflow input env var formatting Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6340fd05-1d07-4c32-ba41-126e4046144c Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/mcp_setup_generator.go | 2 ++ pkg/workflow/secret_extraction_test.go | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/pkg/workflow/mcp_setup_generator.go b/pkg/workflow/mcp_setup_generator.go index cafd221643b..72146831739 100644 --- a/pkg/workflow/mcp_setup_generator.go +++ b/pkg/workflow/mcp_setup_generator.go @@ -237,6 +237,8 @@ func generateSafeOutputsSetup(c *Compiler, yaml *strings.Builder, safeOutputConf envValues := make(map[string]string, len(configSecrets)+len(configContextVars)+len(configWorkflowInputs)) // addEnvValue keeps first-seen key ordering stable in envKeys while allowing // later sources to override the value in envValues for duplicate keys. + // Example: if key K appears first in workflow inputs and later in secrets, + // K is emitted once in envKeys and its final value comes from secrets. addEnvValue := func(key, value string) { if _, exists := envValues[key]; !exists { envKeys = append(envKeys, key) diff --git a/pkg/workflow/secret_extraction_test.go b/pkg/workflow/secret_extraction_test.go index 9b9b0f6dc56..be3f8a8e333 100644 --- a/pkg/workflow/secret_extraction_test.go +++ b/pkg/workflow/secret_extraction_test.go @@ -487,3 +487,23 @@ func TestExtractWorkflowInputExpressionsFromValue(t *testing.T) { }) } } + +func TestFormatInputNameAsEnvVar(t *testing.T) { + tests := []struct { + name string + inputName string + expected string + }{ + {name: "underscore", inputName: "target_repo", expected: "GH_AW_INPUT_TARGET_REPO"}, + {name: "dash", inputName: "base-branch", expected: "GH_AW_INPUT_BASE_BRANCH"}, + {name: "consecutive separators", inputName: "my--input__name", expected: "GH_AW_INPUT_MY__INPUT__NAME"}, + {name: "mixed case and numeric", inputName: "Repo2Name", expected: "GH_AW_INPUT_REPO2NAME"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := formatInputNameAsEnvVar(tt.inputName) + assert.Equal(t, tt.expected, actual, "Input name should be converted to the expected env var") + }) + } +} From a889661bc4aa4a14ffb9f21c7de1bee96369474b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 20:46:39 +0000 Subject: [PATCH 07/10] fix: support bracket notation for workflow input extraction Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6eced4f9-a33c-4277-9e7a-d4ad15f26ff7 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/mcp_setup_generator.go | 6 ++---- pkg/workflow/secret_extraction.go | 27 +++++++++++++++++++------- pkg/workflow/secret_extraction_test.go | 13 ++++++++++--- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/pkg/workflow/mcp_setup_generator.go b/pkg/workflow/mcp_setup_generator.go index 72146831739..da96a7dfc54 100644 --- a/pkg/workflow/mcp_setup_generator.go +++ b/pkg/workflow/mcp_setup_generator.go @@ -235,10 +235,8 @@ func generateSafeOutputsSetup(c *Compiler, yaml *strings.Builder, safeOutputConf yaml.WriteString(" env:\n") envKeys := make([]string, 0, len(configSecrets)+len(configContextVars)+len(configWorkflowInputs)) envValues := make(map[string]string, len(configSecrets)+len(configContextVars)+len(configWorkflowInputs)) - // addEnvValue keeps first-seen key ordering stable in envKeys while allowing - // later sources to override the value in envValues for duplicate keys. - // Example: if key K appears first in workflow inputs and later in secrets, - // K is emitted once in envKeys and its final value comes from secrets. + // addEnvValue deduplicates envKeys while allowing later sources to override + // the value in envValues for duplicate keys. addEnvValue := func(key, value string) { if _, exists := envValues[key]; !exists { envKeys = append(envKeys, key) diff --git a/pkg/workflow/secret_extraction.go b/pkg/workflow/secret_extraction.go index e201f8584fc..8dfc31a2b93 100644 --- a/pkg/workflow/secret_extraction.go +++ b/pkg/workflow/secret_extraction.go @@ -200,9 +200,13 @@ func ExtractEnvExpressionsFromValue(value string) map[string]string { // accepted later if they are present in gitHubContextEnvVarMap. var gitHubContextExprPattern = regexp.MustCompile(`\$\{\{\s*github\.([a-z][a-z0-9_.]*)\s*\}\}`) -// workflowInputExprPattern matches simple ${{ inputs.NAME }} expressions. -// NAME may contain letters, numbers, underscores, and dashes. -var workflowInputExprPattern = regexp.MustCompile(`\$\{\{\s*inputs\.([a-zA-Z_][a-zA-Z0-9_-]*)\s*\}\}`) +// workflowInputDotExprPattern matches simple ${{ inputs.NAME }} expressions. +// NAME supports alphanumeric and underscore characters. +var workflowInputDotExprPattern = regexp.MustCompile(`\$\{\{\s*inputs\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}`) + +// workflowInputBracketExprPattern matches bracket-notation input expressions: +// ${{ inputs['NAME'] }} and ${{ inputs["NAME"] }}. NAME may include dashes. +var workflowInputBracketExprPattern = regexp.MustCompile(`\$\{\{\s*inputs\[\s*['"]([a-zA-Z_][a-zA-Z0-9_-]*)['"]\s*\]\s*\}\}`) // gitHubContextEnvVarMap maps common github.* context properties to their corresponding // GitHub Actions runner environment variables (always available on all runners). @@ -262,22 +266,31 @@ func ExtractGitHubContextExpressionsFromValue(value string) map[string]string { return result } -// ExtractWorkflowInputExpressionsFromValue extracts all simple ${{ inputs.X }} expressions from a +// ExtractWorkflowInputExpressionsFromValue extracts simple workflow input expressions from a // string value and maps them to deterministic environment variable names. // Returns a map of env var name -> full expression. // // Examples: // - "${{ inputs.target_repo }}" -> {"GH_AW_INPUT_TARGET_REPO": "${{ inputs.target_repo }}"} -// - "${{ inputs.base-branch }}" -> {"GH_AW_INPUT_BASE_BRANCH": "${{ inputs.base-branch }}"} +// - "${{ inputs['base-branch'] }}" -> {"GH_AW_INPUT_BASE_BRANCH": "${{ inputs['base-branch'] }}"} func ExtractWorkflowInputExpressionsFromValue(value string) map[string]string { result := make(map[string]string) - matches := workflowInputExprPattern.FindAllStringSubmatch(value, -1) - for _, match := range matches { + for _, match := range workflowInputDotExprPattern.FindAllStringSubmatch(value, -1) { if len(match) < 2 { continue } + inputName := match[1] + fullExpr := match[0] + envVar := formatInputNameAsEnvVar(inputName) + result[envVar] = fullExpr + secretLog.Printf("Extracted workflow input expression: %s -> %s", fullExpr, envVar) + } + for _, match := range workflowInputBracketExprPattern.FindAllStringSubmatch(value, -1) { + if len(match) < 2 { + continue + } inputName := match[1] fullExpr := match[0] envVar := formatInputNameAsEnvVar(inputName) diff --git a/pkg/workflow/secret_extraction_test.go b/pkg/workflow/secret_extraction_test.go index be3f8a8e333..2fc4776dfdc 100644 --- a/pkg/workflow/secret_extraction_test.go +++ b/pkg/workflow/secret_extraction_test.go @@ -466,13 +466,20 @@ func TestExtractWorkflowInputExpressionsFromValue(t *testing.T) { }, }, { - name: "multiple input expressions with dash and underscore", - value: `"repo":"${{ inputs.target-repo }}","base":"${{ inputs.base_branch }}"`, + name: "multiple input expressions with bracket dash and underscore", + value: `"repo":"${{ inputs['target-repo'] }}","base":"${{ inputs.base_branch }}"`, expected: map[string]string{ - "GH_AW_INPUT_TARGET_REPO": "${{ inputs.target-repo }}", + "GH_AW_INPUT_TARGET_REPO": "${{ inputs['target-repo'] }}", "GH_AW_INPUT_BASE_BRANCH": "${{ inputs.base_branch }}", }, }, + { + name: "double-quote bracket notation", + value: `"base":"${{ inputs["base-branch"] }}"`, + expected: map[string]string{ + "GH_AW_INPUT_BASE_BRANCH": `${{ inputs["base-branch"] }}`, + }, + }, { name: "no input expressions", value: `"repo":"${{ github.repository }}"`, From 7a8e483060a4da1bd951cd326205a94b0551c457 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 20:49:20 +0000 Subject: [PATCH 08/10] docs: clarify workflow input regex identifier constraints Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6eced4f9-a33c-4277-9e7a-d4ad15f26ff7 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/secret_extraction.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/secret_extraction.go b/pkg/workflow/secret_extraction.go index 8dfc31a2b93..82921bda3ab 100644 --- a/pkg/workflow/secret_extraction.go +++ b/pkg/workflow/secret_extraction.go @@ -201,11 +201,13 @@ func ExtractEnvExpressionsFromValue(value string) map[string]string { var gitHubContextExprPattern = regexp.MustCompile(`\$\{\{\s*github\.([a-z][a-z0-9_.]*)\s*\}\}`) // workflowInputDotExprPattern matches simple ${{ inputs.NAME }} expressions. -// NAME supports alphanumeric and underscore characters. +// NAME must start with a letter or underscore, followed by alphanumeric +// characters or underscores. var workflowInputDotExprPattern = regexp.MustCompile(`\$\{\{\s*inputs\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}`) // workflowInputBracketExprPattern matches bracket-notation input expressions: -// ${{ inputs['NAME'] }} and ${{ inputs["NAME"] }}. NAME may include dashes. +// ${{ inputs['NAME'] }} and ${{ inputs["NAME"] }}. NAME must start with a +// letter or underscore, followed by alphanumeric characters, underscores, or dashes. var workflowInputBracketExprPattern = regexp.MustCompile(`\$\{\{\s*inputs\[\s*['"]([a-zA-Z_][a-zA-Z0-9_-]*)['"]\s*\]\s*\}\}`) // gitHubContextEnvVarMap maps common github.* context properties to their corresponding From 5efa7c55f312001e34664e0533fa2846b80b642f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 20:52:03 +0000 Subject: [PATCH 09/10] refactor: deduplicate workflow input expression extraction loop logic Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6eced4f9-a33c-4277-9e7a-d4ad15f26ff7 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/secret_extraction.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pkg/workflow/secret_extraction.go b/pkg/workflow/secret_extraction.go index 82921bda3ab..7d4354fc33c 100644 --- a/pkg/workflow/secret_extraction.go +++ b/pkg/workflow/secret_extraction.go @@ -278,9 +278,9 @@ func ExtractGitHubContextExpressionsFromValue(value string) map[string]string { func ExtractWorkflowInputExpressionsFromValue(value string) map[string]string { result := make(map[string]string) - for _, match := range workflowInputDotExprPattern.FindAllStringSubmatch(value, -1) { + processMatch := func(match []string) { if len(match) < 2 { - continue + return } inputName := match[1] fullExpr := match[0] @@ -289,15 +289,12 @@ func ExtractWorkflowInputExpressionsFromValue(value string) map[string]string { secretLog.Printf("Extracted workflow input expression: %s -> %s", fullExpr, envVar) } + for _, match := range workflowInputDotExprPattern.FindAllStringSubmatch(value, -1) { + processMatch(match) + } + for _, match := range workflowInputBracketExprPattern.FindAllStringSubmatch(value, -1) { - if len(match) < 2 { - continue - } - inputName := match[1] - fullExpr := match[0] - envVar := formatInputNameAsEnvVar(inputName) - result[envVar] = fullExpr - secretLog.Printf("Extracted workflow input expression: %s -> %s", fullExpr, envVar) + processMatch(match) } return result From 9f782f6f3367edcdefb2e5dd1558d11f853cd085 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 20:54:52 +0000 Subject: [PATCH 10/10] fix: keep dash-compatible dot notation while adding bracket support Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6eced4f9-a33c-4277-9e7a-d4ad15f26ff7 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/secret_extraction.go | 4 ++-- pkg/workflow/secret_extraction_test.go | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/secret_extraction.go b/pkg/workflow/secret_extraction.go index 7d4354fc33c..750c4c5bb32 100644 --- a/pkg/workflow/secret_extraction.go +++ b/pkg/workflow/secret_extraction.go @@ -202,8 +202,8 @@ var gitHubContextExprPattern = regexp.MustCompile(`\$\{\{\s*github\.([a-z][a-z0- // workflowInputDotExprPattern matches simple ${{ inputs.NAME }} expressions. // NAME must start with a letter or underscore, followed by alphanumeric -// characters or underscores. -var workflowInputDotExprPattern = regexp.MustCompile(`\$\{\{\s*inputs\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}`) +// characters, underscores, or dashes. +var workflowInputDotExprPattern = regexp.MustCompile(`\$\{\{\s*inputs\.([a-zA-Z_][a-zA-Z0-9_-]*)\s*\}\}`) // workflowInputBracketExprPattern matches bracket-notation input expressions: // ${{ inputs['NAME'] }} and ${{ inputs["NAME"] }}. NAME must start with a diff --git a/pkg/workflow/secret_extraction_test.go b/pkg/workflow/secret_extraction_test.go index 2fc4776dfdc..0c20a75dd57 100644 --- a/pkg/workflow/secret_extraction_test.go +++ b/pkg/workflow/secret_extraction_test.go @@ -473,6 +473,13 @@ func TestExtractWorkflowInputExpressionsFromValue(t *testing.T) { "GH_AW_INPUT_BASE_BRANCH": "${{ inputs.base_branch }}", }, }, + { + name: "dot notation with dash remains supported", + value: `"repo":"${{ inputs.target-repo }}"`, + expected: map[string]string{ + "GH_AW_INPUT_TARGET_REPO": "${{ inputs.target-repo }}", + }, + }, { name: "double-quote bracket notation", value: `"base":"${{ inputs["base-branch"] }}"`,