From f031cfcdc00574cb08a1923e19a31b53b8662e63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:14:38 +0000 Subject: [PATCH 1/6] Initial plan From 7a584088804f504d42378c1620239389060d598d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:47:14 +0000 Subject: [PATCH 2/6] feat(workflow): add copilot custom-config lock metadata signal Co-authored-by: salmanmkc <32169182+salmanmkc@users.noreply.github.com> --- .../workflows/daily-byok-ollama-test.lock.yml | 2 +- .../smoke-copilot-aoai-apikey.lock.yml | 2 +- .../smoke-copilot-aoai-entra.lock.yml | 2 +- pkg/workflow/awf_helpers_test.go | 79 +++++++++++++++++++ pkg/workflow/compiler_yaml_header.go | 3 + pkg/workflow/compiler_yaml_test.go | 52 ++++++++++++ pkg/workflow/copilot_engine_execution.go | 3 +- pkg/workflow/engine_api_targets.go | 15 ++++ pkg/workflow/lock_schema.go | 61 +++++++------- pkg/workflow/lock_schema_test.go | 27 ++++--- 10 files changed, 201 insertions(+), 45 deletions(-) diff --git a/.github/workflows/daily-byok-ollama-test.lock.yml b/.github/workflows/daily-byok-ollama-test.lock.yml index 8906d4d4110..5c699400fb4 100644 --- a/.github/workflows/daily-byok-ollama-test.lock.yml +++ b/.github/workflows/daily-byok-ollama-test.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6d3815161a2692b78e7773b5af1a19c7f550c9a1d3714696eac2c143563c0b1e","body_hash":"bd80ca99e3f4cd56715c7a73bd9f5c56165dc64beee430b859670700764082f0","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6d3815161a2692b78e7773b5af1a19c7f550c9a1d3714696eac2c143563c0b1e","body_hash":"bd80ca99e3f4cd56715c7a73bd9f5c56165dc64beee430b859670700764082f0","strict":true,"engine_base_url_customized":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # diff --git a/.github/workflows/smoke-copilot-aoai-apikey.lock.yml b/.github/workflows/smoke-copilot-aoai-apikey.lock.yml index 17715e7b3a3..29ac055df45 100644 --- a/.github/workflows/smoke-copilot-aoai-apikey.lock.yml +++ b/.github/workflows/smoke-copilot-aoai-apikey.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"867945a51008d22f198693e014374a8e3b54895021ad0de25e8ec249d2a9ec44","body_hash":"c2c94cee8b2b6a422fb4503bb895295ded50290356c2a66036330f383badf341","strict":true,"agent_id":"copilot","agent_model":"o4-mini-aw","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"867945a51008d22f198693e014374a8e3b54895021ad0de25e8ec249d2a9ec44","body_hash":"c2c94cee8b2b6a422fb4503bb895295ded50290356c2a66036330f383badf341","strict":true,"engine_base_url_customized":true,"agent_id":"copilot","agent_model":"o4-mini-aw","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","FOUNDRY_API_KEY","FOUNDRY_OPENAI_ENDPOINT","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"v7.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"53b7df96c91f9c12dcc8a07bcb9ccacbed38856a","version":"v7.3.0"},{"repo":"docker/setup-buildx-action","sha":"bb05f3f5519dd87d3ba754cc423b652a5edd6d2c","version":"v4.2.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.43","digest":"sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.43@sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"ghcr.io/github/serena-mcp-server:sha-2491b68","digest":"sha256:2fc3fab11db8baca5b4fa760ec52f1010425c97edc8bf4c6437f0cdabd17fcb7","pinned_image":"ghcr.io/github/serena-mcp-server:sha-2491b68@sha256:2fc3fab11db8baca5b4fa760ec52f1010425c97edc8bf4c6437f0cdabd17fcb7"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # diff --git a/.github/workflows/smoke-copilot-aoai-entra.lock.yml b/.github/workflows/smoke-copilot-aoai-entra.lock.yml index 27c3af304a4..620f1d44ad8 100644 --- a/.github/workflows/smoke-copilot-aoai-entra.lock.yml +++ b/.github/workflows/smoke-copilot-aoai-entra.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"35204cb454f32a35dc75abed78487afa849f19b65bed2d1d9e4aef686a812cea","body_hash":"321aa2259a81582ee9e292c76542882ed7b7ab61af9efdeb36fd4c47c9a3896c","strict":true,"agent_id":"copilot","agent_model":"o4-mini-aw","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"35204cb454f32a35dc75abed78487afa849f19b65bed2d1d9e4aef686a812cea","body_hash":"321aa2259a81582ee9e292c76542882ed7b7ab61af9efdeb36fd4c47c9a3896c","strict":true,"engine_base_url_customized":true,"agent_id":"copilot","agent_model":"o4-mini-aw","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","FOUNDRY_OPENAI_ENDPOINT","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"v7.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"53b7df96c91f9c12dcc8a07bcb9ccacbed38856a","version":"v7.3.0"},{"repo":"docker/setup-buildx-action","sha":"bb05f3f5519dd87d3ba754cc423b652a5edd6d2c","version":"v4.2.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.43","digest":"sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.43@sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"},{"image":"ghcr.io/github/serena-mcp-server:sha-2491b68","digest":"sha256:2fc3fab11db8baca5b4fa760ec52f1010425c97edc8bf4c6437f0cdabd17fcb7","pinned_image":"ghcr.io/github/serena-mcp-server:sha-2491b68@sha256:2fc3fab11db8baca5b4fa760ec52f1010425c97edc8bf4c6437f0cdabd17fcb7"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # diff --git a/pkg/workflow/awf_helpers_test.go b/pkg/workflow/awf_helpers_test.go index 78f30f1d7ed..6b2836db2ff 100644 --- a/pkg/workflow/awf_helpers_test.go +++ b/pkg/workflow/awf_helpers_test.go @@ -1001,6 +1001,85 @@ func TestGetCopilotAPITarget(t *testing.T) { } } +func TestIsCopilotCustomConfig(t *testing.T) { + tests := []struct { + name string + workflowData *WorkflowData + expected bool + }{ + { + name: "not customized when no custom provider or target is set", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ID: "copilot"}, + }, + expected: false, + }, + { + name: "customized when COPILOT_PROVIDER_BASE_URL is set", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + Env: map[string]string{ + constants.CopilotProviderBaseURL: "https://api.openai.com/v1", + }, + }, + }, + expected: true, + }, + { + name: "customized when model-provider gateway is enabled with firewall", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + LLMProvider: LLMProviderAnthropic, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + }, + expected: true, + }, + { + name: "not customized when model-provider is non-github but firewall is disabled", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + LLMProvider: LLMProviderAnthropic, + }, + }, + expected: false, + }, + { + name: "customized when engine.api-target is set", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + APITarget: "api.acme.ghe.com", + }, + }, + expected: true, + }, + { + name: "customized when GITHUB_COPILOT_BASE_URL is set", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + Env: map[string]string{ + "GITHUB_COPILOT_BASE_URL": "https://copilot-api.contoso-aw.ghe.com", + }, + }, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, isCopilotCustomConfig(tt.workflowData)) + }) + } +} + func TestBuildAWFConfigJSONIncludesCopilotLiteralBYOKTarget(t *testing.T) { config := AWFCommandConfig{ EngineName: "copilot", diff --git a/pkg/workflow/compiler_yaml_header.go b/pkg/workflow/compiler_yaml_header.go index 6ca2ebd3e3c..6fe6648748d 100644 --- a/pkg/workflow/compiler_yaml_header.go +++ b/pkg/workflow/compiler_yaml_header.go @@ -37,6 +37,9 @@ func (c *Compiler) generateWorkflowHeader(yaml *strings.Builder, data *WorkflowD if data.Model != "" { agentInfo.AgentModel = data.Model } + if agentInfo.AgentID == "copilot" { + agentInfo.EngineBaseURLCustomized = isCopilotCustomConfig(data) + } // Detection agent info: only if threat detection has its own engine config if data.SafeOutputs != nil && data.SafeOutputs.ThreatDetection != nil && data.SafeOutputs.ThreatDetection.EngineConfig != nil { agentInfo.DetectionAgentID = data.SafeOutputs.ThreatDetection.EngineConfig.ID diff --git a/pkg/workflow/compiler_yaml_test.go b/pkg/workflow/compiler_yaml_test.go index bb897e9ec2f..2616d7a9443 100644 --- a/pkg/workflow/compiler_yaml_test.go +++ b/pkg/workflow/compiler_yaml_test.go @@ -1762,6 +1762,9 @@ Test prompt. if got := metadata.EngineVersions["copilot-sdk"]; got == "" { t.Fatal("Expected copilot-sdk version in metadata engine_versions when copilot-sdk is enabled") } + if metadata.EngineBaseURLCustomized { + t.Fatal("Expected engine_base_url_customized=false for default copilot configuration") + } if metadata.AgentImageRunner != `["self-hosted","linux"]` { t.Fatalf("Expected serialized array runner identifier, got: %q", metadata.AgentImageRunner) } @@ -1784,6 +1787,55 @@ Test prompt. } } +func TestCompileWorkflowMetadataMarksCopilotCustomConfig(t *testing.T) { + tmpDir := testutil.TempDir(t, "lock-metadata-copilot-custom-config") + + workflowContent := `--- +engine: + id: copilot + api-target: api.acme.ghe.com +on: issues +--- +# Test Workflow + +Test prompt. +` + workflowPath := filepath.Join(tmpDir, "metadata-copilot-custom-config.md") + if err := os.WriteFile(workflowPath, []byte(workflowContent), 0o644); err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + compiler := NewCompiler() + if err := compiler.CompileWorkflow(workflowPath); err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + lockFile := strings.TrimSuffix(workflowPath, ".md") + ".lock.yml" + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + var metadataLine string + for line := range strings.SplitSeq(string(lockContent), "\n") { + if trimmed, ok := strings.CutPrefix(line, "# gh-aw-metadata: "); ok { + metadataLine = trimmed + } + } + if metadataLine == "" { + t.Fatal("Could not find gh-aw-metadata in lock file") + } + + var metadata LockMetadata + if err := json.Unmarshal([]byte(metadataLine), &metadata); err != nil { + t.Fatalf("Failed to parse lock metadata JSON: %v", err) + } + + if !metadata.EngineBaseURLCustomized { + t.Fatal("Expected engine_base_url_customized=true when copilot api-target is customized") + } +} + func TestNormalizeBlankLines(t *testing.T) { tests := []struct { name string diff --git a/pkg/workflow/copilot_engine_execution.go b/pkg/workflow/copilot_engine_execution.go index b48216b58bf..f7492248adc 100644 --- a/pkg/workflow/copilot_engine_execution.go +++ b/pkg/workflow/copilot_engine_execution.go @@ -188,8 +188,7 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st sandboxEnabled := isFirewallEnabled(workflowData) llmProvider := e.ResolveLLMProvider(workflowData) - providerOverrideBYOK := llmProvider != LLMProviderGitHub && sandboxEnabled - isBYOKMode := providerOverrideBYOK || engineEnvHasKey(workflowData, constants.CopilotProviderBaseURL) + isBYOKMode := isCopilotBYOKMode(workflowData, sandboxEnabled) modelConfigured := workflowData.Model != "" copilotArgs := e.buildCopilotArgs(workflowData) mkdirCommands := buildCopilotMkdirCommands(copilotArgs) diff --git a/pkg/workflow/engine_api_targets.go b/pkg/workflow/engine_api_targets.go index 186e0046d37..9cfaa115bf6 100644 --- a/pkg/workflow/engine_api_targets.go +++ b/pkg/workflow/engine_api_targets.go @@ -178,6 +178,21 @@ func GetCopilotAPITarget(workflowData *WorkflowData) string { return extractLiteralEngineEnvHost(workflowData, constants.CopilotProviderBaseURL) } +// isCopilotBYOKMode returns true when Copilot execution is configured to use BYOK routing. +// BYOK mode is active when either: +// - a non-GitHub model-provider gateway is in use (only when sandbox/firewall is enabled), or +// - COPILOT_PROVIDER_BASE_URL is present in engine.env. +func isCopilotBYOKMode(workflowData *WorkflowData, sandboxEnabled bool) bool { + providerOverrideBYOK := resolveEngineLLMProvider(workflowData, LLMProviderGitHub) != LLMProviderGitHub && sandboxEnabled + return providerOverrideBYOK || engineEnvHasKey(workflowData, constants.CopilotProviderBaseURL) +} + +// isCopilotCustomConfig returns true when Copilot is configured away from the default +// GitHub-hosted setup via BYOK routing and/or explicit Copilot API target overrides. +func isCopilotCustomConfig(workflowData *WorkflowData) bool { + return isCopilotBYOKMode(workflowData, isFirewallEnabled(workflowData)) || GetCopilotAPITarget(workflowData) != "" +} + func extractLiteralEngineEnvHost(workflowData *WorkflowData, envVar string) string { env := getEngineEnvOverrides(workflowData) if env == nil { diff --git a/pkg/workflow/lock_schema.go b/pkg/workflow/lock_schema.go index c812f635cb6..9de2560e936 100644 --- a/pkg/workflow/lock_schema.go +++ b/pkg/workflow/lock_schema.go @@ -33,28 +33,30 @@ const ( // LockMetadata represents the structured metadata embedded in lock files type LockMetadata struct { - SchemaVersion LockSchemaVersion `json:"schema_version"` - FrontmatterHash string `json:"frontmatter_hash,omitempty"` - BodyHash string `json:"body_hash,omitempty"` - StopTime string `json:"stop_time,omitempty"` - CompilerVersion string `json:"compiler_version,omitempty"` - Strict bool `json:"strict,omitempty"` - AgentID string `json:"agent_id,omitempty"` - AgentModel string `json:"agent_model,omitempty"` - DetectionAgentID string `json:"detection_agent_id,omitempty"` - DetectionAgentModel string `json:"detection_agent_model,omitempty"` - EngineVersions map[string]string `json:"engine_versions,omitempty"` - AgentImageRunner string `json:"agent_image_runner,omitempty"` + SchemaVersion LockSchemaVersion `json:"schema_version"` + FrontmatterHash string `json:"frontmatter_hash,omitempty"` + BodyHash string `json:"body_hash,omitempty"` + StopTime string `json:"stop_time,omitempty"` + CompilerVersion string `json:"compiler_version,omitempty"` + Strict bool `json:"strict,omitempty"` + EngineBaseURLCustomized bool `json:"engine_base_url_customized,omitempty"` + AgentID string `json:"agent_id,omitempty"` + AgentModel string `json:"agent_model,omitempty"` + DetectionAgentID string `json:"detection_agent_id,omitempty"` + DetectionAgentModel string `json:"detection_agent_model,omitempty"` + EngineVersions map[string]string `json:"engine_versions,omitempty"` + AgentImageRunner string `json:"agent_image_runner,omitempty"` } // AgentMetadataInfo holds agent and detection agent information for embedding in lock file metadata type AgentMetadataInfo struct { - AgentID string - AgentModel string - DetectionAgentID string - DetectionAgentModel string - EngineVersions map[string]string - AgentImageRunner string + AgentID string + AgentModel string + DetectionAgentID string + DetectionAgentModel string + EngineBaseURLCustomized bool + EngineVersions map[string]string + AgentImageRunner string } // SupportedSchemaVersions lists all schema versions this build can consume @@ -121,17 +123,18 @@ func GenerateLockMetadata(hashInfo LockHashInfo, stopTime string, strict bool, a lockSchemaLog.Printf("Generating lock metadata: schema=%s, strict=%t, hasStopTime=%t, hasBodyHash=%t", LockSchemaV4, strict, stopTime != "", hashInfo.BodyHash != "") metadata := &LockMetadata{ - SchemaVersion: LockSchemaV4, - FrontmatterHash: hashInfo.FrontmatterHash, - BodyHash: hashInfo.BodyHash, - StopTime: stopTime, - Strict: strict, - AgentID: agentInfo.AgentID, - AgentModel: agentInfo.AgentModel, - DetectionAgentID: agentInfo.DetectionAgentID, - DetectionAgentModel: agentInfo.DetectionAgentModel, - EngineVersions: agentInfo.EngineVersions, - AgentImageRunner: agentInfo.AgentImageRunner, + SchemaVersion: LockSchemaV4, + FrontmatterHash: hashInfo.FrontmatterHash, + BodyHash: hashInfo.BodyHash, + StopTime: stopTime, + Strict: strict, + EngineBaseURLCustomized: agentInfo.EngineBaseURLCustomized, + AgentID: agentInfo.AgentID, + AgentModel: agentInfo.AgentModel, + DetectionAgentID: agentInfo.DetectionAgentID, + DetectionAgentModel: agentInfo.DetectionAgentModel, + EngineVersions: agentInfo.EngineVersions, + AgentImageRunner: agentInfo.AgentImageRunner, } // Include compiler version only for release builds diff --git a/pkg/workflow/lock_schema_test.go b/pkg/workflow/lock_schema_test.go index 661201baa2e..525b84369a6 100644 --- a/pkg/workflow/lock_schema_test.go +++ b/pkg/workflow/lock_schema_test.go @@ -625,10 +625,11 @@ func TestLockMetadataToJSONWithoutStopTime(t *testing.T) { func TestGenerateLockMetadataWithAgentInfo(t *testing.T) { hash := "abcd1234" agentInfo := AgentMetadataInfo{ - AgentID: "copilot", - AgentModel: "gpt-5", - DetectionAgentID: "copilot", - DetectionAgentModel: "gpt-5.1-codex-mini", + AgentID: "copilot", + AgentModel: "gpt-5", + DetectionAgentID: "copilot", + DetectionAgentModel: "gpt-5.1-codex-mini", + EngineBaseURLCustomized: true, EngineVersions: map[string]string{ "copilot": "1.0.57", "claude": "2.1.160", @@ -643,6 +644,7 @@ func TestGenerateLockMetadataWithAgentInfo(t *testing.T) { assert.Equal(t, "gpt-5", metadata.AgentModel, "Should preserve agent model") assert.Equal(t, "copilot", metadata.DetectionAgentID, "Should preserve detection agent ID") assert.Equal(t, "gpt-5.1-codex-mini", metadata.DetectionAgentModel, "Should preserve detection agent model") + assert.True(t, metadata.EngineBaseURLCustomized, "Should preserve Copilot custom base URL signal") assert.Equal(t, "1.0.57", metadata.EngineVersions["copilot"], "Should preserve engine versions") assert.Equal(t, "ubuntu-latest", metadata.AgentImageRunner, "Should preserve agent image runner") } @@ -657,17 +659,19 @@ func TestGenerateLockMetadataAgentFieldsOmittedWhenEmpty(t *testing.T) { assert.NotContains(t, json, `"agent_model"`, "Empty agent_model should be omitted") assert.NotContains(t, json, `"detection_agent_id"`, "Empty detection_agent_id should be omitted") assert.NotContains(t, json, `"detection_agent_model"`, "Empty detection_agent_model should be omitted") + assert.NotContains(t, json, `"engine_base_url_customized"`, "Empty engine_base_url_customized should be omitted") } func TestLockMetadataToJSONWithAgentFields(t *testing.T) { metadata := &LockMetadata{ - SchemaVersion: LockSchemaV3, - FrontmatterHash: "test123", - Strict: true, - AgentID: "claude", - AgentModel: "claude-sonnet-4.5", - DetectionAgentID: "copilot", - DetectionAgentModel: "gpt-5.1-codex-mini", + SchemaVersion: LockSchemaV3, + FrontmatterHash: "test123", + Strict: true, + EngineBaseURLCustomized: true, + AgentID: "claude", + AgentModel: "claude-sonnet-4.5", + DetectionAgentID: "copilot", + DetectionAgentModel: "gpt-5.1-codex-mini", EngineVersions: map[string]string{ "claude": "2.1.160", "copilot": "1.0.57", @@ -681,6 +685,7 @@ func TestLockMetadataToJSONWithAgentFields(t *testing.T) { assert.Contains(t, json, `"schema_version":"v3"`) assert.Contains(t, json, `"frontmatter_hash":"test123"`) assert.Contains(t, json, `"strict":true`) + assert.Contains(t, json, `"engine_base_url_customized":true`) assert.Contains(t, json, `"agent_id":"claude"`) assert.Contains(t, json, `"agent_model":"claude-sonnet-4.5"`) assert.Contains(t, json, `"detection_agent_id":"copilot"`) From 57f2c82e4b22b42f0b17e4cd4e41771a88157fb1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:05:27 +0000 Subject: [PATCH 3/6] docs(adr): add draft ADR-49991 for Copilot custom-routing signal in lock metadata --- ...-custom-routing-signal-in-lock-metadata.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/adr/49991-emit-copilot-custom-routing-signal-in-lock-metadata.md diff --git a/docs/adr/49991-emit-copilot-custom-routing-signal-in-lock-metadata.md b/docs/adr/49991-emit-copilot-custom-routing-signal-in-lock-metadata.md new file mode 100644 index 00000000000..b1a84ea4984 --- /dev/null +++ b/docs/adr/49991-emit-copilot-custom-routing-signal-in-lock-metadata.md @@ -0,0 +1,50 @@ +# ADR-49991: Emit an authoritative Copilot custom-routing signal in lock file metadata + +**Date**: 2026-08-03 +**Status**: Draft +**Deciders**: pelikhan (PR author, copilot-swe-agent) + +--- + +### Context + +The compiled lock file metadata (`# gh-aw-metadata:`) already records agent identity, schema version, strict mode, and engine versions. However, it did not capture whether the Copilot engine was running with default GitHub-hosted routing or a custom provider/base URL (BYOK mode, a non-GitHub `engine.model-provider`, or an explicit `engine.api-target`). + +Downstream consumers — telemetry pipelines, dashboards, reporting tools — need to classify runs as "default Copilot" vs "custom-routed Copilot." The only existing approach was to infer this from the compiled step `env:` blocks, which has three concrete failure modes: (1) `engine.api-target` is embedded inside the AWF config JSON in the `run:` script and is invisible in `env:`, (2) `GITHUB_COPILOT_BASE_URL` is injected into the DIFC integrity-proxy step even for default configurations, producing false positives, and (3) any env-based inference is silently coupled to the compiled step structure and can drift when the compiler changes. + +The compiler already derives the authoritative answer at compile time via internal predicates. Surfacing it once in the metadata eliminates the fragile inference layer entirely. + +### Decision + +We will add a boolean field `engine_base_url_customized` (with `omitempty`) to `LockMetadata` and `AgentMetadataInfo`, populate it in `generateWorkflowHeader` for Copilot agents by calling a new shared predicate `isCopilotCustomConfig`, and extract the existing inline BYOK predicate into a reusable `isCopilotBYOKMode` helper so that runtime execution and emitted metadata are driven by the same logic. + +`isCopilotCustomConfig` returns `true` when either BYOK routing is active (`isCopilotBYOKMode`: non-GitHub `model-provider` + sandbox enabled, or `COPILOT_PROVIDER_BASE_URL` in `engine.env`) or `GetCopilotAPITarget` returns a non-empty string (covers `engine.api-target` and a literal `GITHUB_COPILOT_BASE_URL` override). The field is omitted (`false`) from the JSON output for default configurations because the `bool` zero-value is `false` and the tag is `omitempty`. + +### Alternatives Considered + +#### Alternative 1: Infer routing mode from compiled step env at read time + +Downstream consumers scan each compiled step's `env:` block for `COPILOT_PROVIDER_BASE_URL`, `GITHUB_COPILOT_BASE_URL`, and related keys to classify the run. Why not chosen: `engine.api-target` is not present in any step env (it lives inside the AWF config JSON string in the `run:` block), `GITHUB_COPILOT_BASE_URL` appears in DIFC proxy steps even in default configurations (false positives), and the inference logic is tightly coupled to step IDs and key placement in compiler output — a silent regression risk on every compiler refactor. + +#### Alternative 2: Bump the lock file schema version alongside the new field + +Introduce `LockSchemaV5` to signal to consumers that the new field is present. Why not chosen: the field is purely additive and uses `omitempty`, so older consumers that do not recognise it ignore it without any schema version signal. A version bump would impose a schema-version negotiation requirement on all consumers for what is a backward-compatible, optional extension. The field's `omitempty` absence in default-config lock files provides enough information to distinguish "field not present" (old compiler) from "field present and false" (impossible — `omitempty` suppresses it) without a version bump. + +### Consequences + +#### Positive +- Downstream consumers get a single drift-free, authoritative boolean covering all custom-routing modes (BYOK via model-provider gateway, BYOK via `COPILOT_PROVIDER_BASE_URL`, explicit `engine.api-target`, literal `GITHUB_COPILOT_BASE_URL` override). +- Runtime BYOK behavior and emitted metadata are now derived from the same `isCopilotBYOKMode` predicate, eliminating a class of behavioral/observability drift bugs. +- The field is backward-compatible: existing lock file consumers ignore the new key; existing lock files compiled without this change simply omit the field. + +#### Negative +- The single boolean does not distinguish *which* form of customization is in use; consumers that need to distinguish BYOK-via-provider-gateway from api-target overrides still need to parse step env or AWF config JSON. +- `engine_base_url_customized` uses `omitempty` on a `bool`, so a `false` value is indistinguishable from "field absent" in JSON — consumers cannot tell whether the compiler that produced the lock file supports the field. A future truthiness migration (if the field ever needs to be `false`-with-meaning) would require a schema bump. + +#### Neutral +- Existing lock files checked into the repository (`.github/workflows/*.lock.yml`) for custom-configured Copilot workflows are updated in the same PR to add `engine_base_url_customized: true`, making the repository state consistent with the new compiler output. +- No schema version was bumped; the lock file format remains `v4`. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* From fd49d623355377c15ec0df82e810fab132583c76 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:19:24 +0000 Subject: [PATCH 4/6] Plan: investigate CI failure Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/skills/agentic-workflows/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/skills/agentic-workflows/SKILL.md b/.github/skills/agentic-workflows/SKILL.md index 6fb19019416..141445630d5 100644 --- a/.github/skills/agentic-workflows/SKILL.md +++ b/.github/skills/agentic-workflows/SKILL.md @@ -71,6 +71,7 @@ Load these files from `github/gh-aw` (they are not available locally). - `.github/aw/test-coverage.md` - `.github/aw/test-expression.md` - `.github/aw/token-optimization-caching-budgets.md` +- `.github/aw/token-optimization-observability.md` - `.github/aw/token-optimization.md` - `.github/aw/triggers.md` - `.github/aw/update-agentic-workflow.md` From 3160bd67def1060f1a49ac0a12921eff70c292a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:37:09 +0000 Subject: [PATCH 5/6] Handle empty Copilot BYOK env values safely Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/workflow/awf_helpers_test.go | 82 +++++++++++++++++++++ pkg/workflow/copilot_engine_installation.go | 8 +- pkg/workflow/engine_api_targets.go | 8 +- pkg/workflow/engine_helpers.go | 10 +++ pkg/workflow/secret_validation_test.go | 16 ++++ 5 files changed, 118 insertions(+), 6 deletions(-) diff --git a/pkg/workflow/awf_helpers_test.go b/pkg/workflow/awf_helpers_test.go index 6b2836db2ff..86d8d6166c3 100644 --- a/pkg/workflow/awf_helpers_test.go +++ b/pkg/workflow/awf_helpers_test.go @@ -1001,6 +1001,76 @@ func TestGetCopilotAPITarget(t *testing.T) { } } +func TestIsCopilotBYOKMode(t *testing.T) { + tests := []struct { + name string + workflowData *WorkflowData + sandboxEnabled bool + expected bool + }{ + { + name: "false when no BYOK signals with sandbox enabled", + workflowData: &WorkflowData{EngineConfig: &EngineConfig{ID: "copilot"}}, + sandboxEnabled: true, + expected: false, + }, + { + name: "true via COPILOT_PROVIDER_BASE_URL when non-empty even with sandbox disabled", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + Env: map[string]string{ + constants.CopilotProviderBaseURL: "https://api.openai.com/v1", + }, + }, + }, + sandboxEnabled: false, + expected: true, + }, + { + name: "false when COPILOT_PROVIDER_BASE_URL is empty", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + Env: map[string]string{ + constants.CopilotProviderBaseURL: "", + }, + }, + }, + sandboxEnabled: true, + expected: false, + }, + { + name: "true for non-github provider when sandbox enabled", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + LLMProvider: LLMProviderAnthropic, + }, + }, + sandboxEnabled: true, + expected: true, + }, + { + name: "false for non-github provider when sandbox disabled", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + LLMProvider: LLMProviderAnthropic, + }, + }, + sandboxEnabled: false, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, isCopilotBYOKMode(tt.workflowData, tt.sandboxEnabled)) + }) + } +} + func TestIsCopilotCustomConfig(t *testing.T) { tests := []struct { name string @@ -1026,6 +1096,18 @@ func TestIsCopilotCustomConfig(t *testing.T) { }, expected: true, }, + { + name: "not customized when COPILOT_PROVIDER_BASE_URL is empty", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + Env: map[string]string{ + constants.CopilotProviderBaseURL: "", + }, + }, + }, + expected: false, + }, { name: "customized when model-provider gateway is enabled with firewall", workflowData: &WorkflowData{ diff --git a/pkg/workflow/copilot_engine_installation.go b/pkg/workflow/copilot_engine_installation.go index 06392dd61d3..74571bc0971 100644 --- a/pkg/workflow/copilot_engine_installation.go +++ b/pkg/workflow/copilot_engine_installation.go @@ -58,7 +58,7 @@ func getWorkspaceCommandPrefixFor(config *EngineConfig) string { // GetSecretValidationStep returns the secret validation step for the Copilot engine. // Returns an empty step if: // - permissions.copilot-requests is set to write (uses GitHub Actions token instead), or -// - COPILOT_PROVIDER_BASE_URL, COPILOT_PROVIDER_API_KEY, or COPILOT_PROVIDER_BEARER_TOKEN is set in engine.env +// - COPILOT_PROVIDER_BASE_URL, COPILOT_PROVIDER_API_KEY, or COPILOT_PROVIDER_BEARER_TOKEN is set to a non-empty value in engine.env // (BYOK mode — the external provider handles authentication, so COPILOT_GITHUB_TOKEN // is not required for model routing). func (e *CopilotEngine) GetSecretValidationStep(workflowData *WorkflowData) GitHubActionStep { @@ -67,9 +67,9 @@ func (e *CopilotEngine) GetSecretValidationStep(workflowData *WorkflowData) GitH copilotInstallLog.Print("Skipping secret validation step: permissions.copilot-requests=write enabled, using GitHub Actions token") return GitHubActionStep{} } - if engineEnvHasKey(workflowData, constants.CopilotProviderBaseURL) || - engineEnvHasKey(workflowData, constants.CopilotProviderAPIKey) || - engineEnvHasKey(workflowData, constants.CopilotProviderBearerToken) { + if engineEnvHasNonEmptyValue(workflowData, constants.CopilotProviderBaseURL) || + engineEnvHasNonEmptyValue(workflowData, constants.CopilotProviderAPIKey) || + engineEnvHasNonEmptyValue(workflowData, constants.CopilotProviderBearerToken) { copilotInstallLog.Print("Skipping COPILOT_GITHUB_TOKEN validation: BYOK provider credentials are configured") return GitHubActionStep{} } diff --git a/pkg/workflow/engine_api_targets.go b/pkg/workflow/engine_api_targets.go index 9cfaa115bf6..92ba80793a8 100644 --- a/pkg/workflow/engine_api_targets.go +++ b/pkg/workflow/engine_api_targets.go @@ -181,10 +181,14 @@ func GetCopilotAPITarget(workflowData *WorkflowData) string { // isCopilotBYOKMode returns true when Copilot execution is configured to use BYOK routing. // BYOK mode is active when either: // - a non-GitHub model-provider gateway is in use (only when sandbox/firewall is enabled), or -// - COPILOT_PROVIDER_BASE_URL is present in engine.env. +// - COPILOT_PROVIDER_BASE_URL is present in engine.env with a non-empty value. +// +// Note that this intentionally checks whether BYOK routing is configured, not whether +// a literal hostname can be extracted. Literal host extraction is handled separately by +// GetCopilotAPITarget(). func isCopilotBYOKMode(workflowData *WorkflowData, sandboxEnabled bool) bool { providerOverrideBYOK := resolveEngineLLMProvider(workflowData, LLMProviderGitHub) != LLMProviderGitHub && sandboxEnabled - return providerOverrideBYOK || engineEnvHasKey(workflowData, constants.CopilotProviderBaseURL) + return providerOverrideBYOK || engineEnvHasNonEmptyValue(workflowData, constants.CopilotProviderBaseURL) } // isCopilotCustomConfig returns true when Copilot is configured away from the default diff --git a/pkg/workflow/engine_helpers.go b/pkg/workflow/engine_helpers.go index 091a6d72153..ba276bfaadb 100644 --- a/pkg/workflow/engine_helpers.go +++ b/pkg/workflow/engine_helpers.go @@ -102,6 +102,16 @@ func engineEnvHasKey(workflowData *WorkflowData, key string) bool { return ok } +// engineEnvHasNonEmptyValue reports whether the given env var key is present in +// engine.env and has a non-empty (after trimming whitespace) value. +func engineEnvHasNonEmptyValue(workflowData *WorkflowData, key string) bool { + if workflowData == nil || workflowData.EngineConfig == nil { + return false + } + value, ok := workflowData.EngineConfig.Env[key] + return ok && strings.TrimSpace(value) != "" +} + // applyEngineCwdEnv sets the GH_AW_ENGINE_CWD environment variable in the given env map // when engine.cwd is configured. This variable is consumed by JS harness processes (via // process_runner.cjs) and by shell-based engine command prefixes so the engine spawns in diff --git a/pkg/workflow/secret_validation_test.go b/pkg/workflow/secret_validation_test.go index 3d20fdc4a0f..e684eb1b868 100644 --- a/pkg/workflow/secret_validation_test.go +++ b/pkg/workflow/secret_validation_test.go @@ -197,6 +197,22 @@ func TestCopilotEngineSkipsSecretValidationWhenBYOKBaseURLOnlySet(t *testing.T) } } +func TestCopilotEngineDoesNotSkipSecretValidationWhenBYOKBaseURLEmpty(t *testing.T) { + engine := NewCopilotEngine() + workflowData := &WorkflowData{ + EngineConfig: &EngineConfig{ + Env: map[string]string{ + "COPILOT_PROVIDER_BASE_URL": "", + }, + }, + } + + step := engine.GetSecretValidationStep(workflowData) + if len(step) == 0 { + t.Fatal("Expected non-empty validation step when COPILOT_PROVIDER_BASE_URL is empty") + } +} + func TestCodexEngineHasSecretValidation(t *testing.T) { engine := NewCodexEngine() workflowData := &WorkflowData{} From c6cc595bb0d104362fffbf74d5c9bac2b6fb4569 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:35:03 +0000 Subject: [PATCH 6/6] refactor: remove unused engineEnvHasKey helper Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/workflow/engine_helpers.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pkg/workflow/engine_helpers.go b/pkg/workflow/engine_helpers.go index ba276bfaadb..762307c8c7e 100644 --- a/pkg/workflow/engine_helpers.go +++ b/pkg/workflow/engine_helpers.go @@ -92,16 +92,6 @@ func ResolveEngineID(workflowData *WorkflowData) string { return workflowData.AI } -// engineEnvHasKey reports whether the given env var key is present in the engine.env map. -// Returns false if workflowData or EngineConfig is nil, or if the key is not in the map. -func engineEnvHasKey(workflowData *WorkflowData, key string) bool { - if workflowData == nil || workflowData.EngineConfig == nil { - return false - } - _, ok := workflowData.EngineConfig.Env[key] - return ok -} - // engineEnvHasNonEmptyValue reports whether the given env var key is present in // engine.env and has a non-empty (after trimming whitespace) value. func engineEnvHasNonEmptyValue(workflowData *WorkflowData, key string) bool {