Skip to content

Commit 86b5ad1

Browse files
authored
refactor(workflow): deduplicate logic, extract cross-engine helpers, fix interface bypass (#18671)
1 parent 2909018 commit 86b5ad1

19 files changed

Lines changed: 347 additions & 566 deletions

pkg/workflow/agentic_engine.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@ type WorkflowExecutor interface {
155155

156156
// GetExecutionSteps returns the GitHub Actions steps for executing this engine
157157
GetExecutionSteps(workflowData *WorkflowData, logFile string) []GitHubActionStep
158+
159+
// GetFirewallLogsCollectionStep returns steps that collect firewall-related log files
160+
// before secret redaction runs. Engines that copy session or firewall state files should
161+
// override this; the default implementation returns an empty slice.
162+
GetFirewallLogsCollectionStep(workflowData *WorkflowData) []GitHubActionStep
158163
}
159164

160165
// MCPConfigProvider handles MCP (Model Context Protocol) configuration
@@ -317,6 +322,12 @@ func (e *BaseEngine) GetSecretValidationStep(workflowData *WorkflowData) GitHubA
317322
return GitHubActionStep{}
318323
}
319324

325+
// GetFirewallLogsCollectionStep returns an empty slice by default.
326+
// Engines that need to copy session or firewall state files before secret redaction should override this.
327+
func (e *BaseEngine) GetFirewallLogsCollectionStep(workflowData *WorkflowData) []GitHubActionStep {
328+
return []GitHubActionStep{}
329+
}
330+
320331
// ParseLogMetrics provides a default no-op implementation for log parsing
321332
// Engines can override this to provide detailed log parsing and metrics extraction
322333
func (e *BaseEngine) ParseLogMetrics(logContent string, verbose bool) LogMetrics {

pkg/workflow/claude_engine.go

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -468,21 +468,5 @@ func (e *ClaudeEngine) GetFirewallLogsCollectionStep(workflowData *WorkflowData)
468468

469469
// GetSquidLogsSteps returns the steps for uploading and parsing Squid logs (after secret redaction)
470470
func (e *ClaudeEngine) GetSquidLogsSteps(workflowData *WorkflowData) []GitHubActionStep {
471-
var steps []GitHubActionStep
472-
473-
// Only add upload and parsing steps if firewall is enabled
474-
if isFirewallEnabled(workflowData) {
475-
claudeLog.Printf("Adding Squid logs upload and parsing steps for workflow: %s", workflowData.Name)
476-
477-
squidLogsUpload := generateSquidLogsUploadStep(workflowData.Name)
478-
steps = append(steps, squidLogsUpload)
479-
480-
// Add firewall log parsing step to create step summary
481-
firewallLogParsing := generateFirewallLogParsingStep(workflowData.Name)
482-
steps = append(steps, firewallLogParsing)
483-
} else {
484-
claudeLog.Print("Firewall disabled, skipping Squid logs upload")
485-
}
486-
487-
return steps
471+
return defaultGetSquidLogsSteps(workflowData, claudeLog)
488472
}

pkg/workflow/claude_logs.go

Lines changed: 2 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,8 @@ func (e *ClaudeEngine) parseClaudeJSONLog(logContent string, verbose bool) LogMe
308308
if messageMap, ok := message.(map[string]any); ok {
309309
if content, exists := messageMap["content"]; exists {
310310
if contentArray, ok := content.([]any); ok {
311-
e.parseToolCalls(contentArray, toolCallMap)
311+
// Sequence return value intentionally discarded; only toolCallMap is needed here.
312+
e.parseToolCallsWithSequence(contentArray, toolCallMap)
312313
}
313314
}
314315
}
@@ -429,86 +430,6 @@ func (e *ClaudeEngine) parseToolCallsWithSequence(contentArray []any, toolCallMa
429430
return sequence
430431
}
431432

432-
// parseToolCalls extracts tool call information from Claude log content array without sequence tracking
433-
func (e *ClaudeEngine) parseToolCalls(contentArray []any, toolCallMap map[string]*ToolCallInfo) {
434-
for _, contentItem := range contentArray {
435-
if contentMap, ok := contentItem.(map[string]any); ok {
436-
if contentType, exists := contentMap["type"]; exists {
437-
if typeStr, ok := contentType.(string); ok {
438-
switch typeStr {
439-
case "tool_use":
440-
// Extract tool name
441-
if toolName, exists := contentMap["name"]; exists {
442-
if nameStr, ok := toolName.(string); ok {
443-
// Prettify tool name
444-
prettifiedName := PrettifyToolName(nameStr)
445-
446-
// Special handling for bash - each invocation is unique
447-
if nameStr == "Bash" {
448-
if input, exists := contentMap["input"]; exists {
449-
if inputMap, ok := input.(map[string]any); ok {
450-
if command, exists := inputMap["command"]; exists {
451-
if commandStr, ok := command.(string); ok {
452-
// Create unique bash entry with command info, avoiding colons
453-
uniqueBashName := "bash_" + ShortenCommand(commandStr)
454-
prettifiedName = uniqueBashName
455-
}
456-
}
457-
}
458-
}
459-
}
460-
461-
// Calculate input size from the input field
462-
inputSize := 0
463-
if input, exists := contentMap["input"]; exists {
464-
inputSize = e.estimateInputSize(input)
465-
}
466-
467-
// Initialize or update tool call info
468-
if toolInfo, exists := toolCallMap[prettifiedName]; exists {
469-
toolInfo.CallCount++
470-
if inputSize > toolInfo.MaxInputSize {
471-
toolInfo.MaxInputSize = inputSize
472-
}
473-
} else {
474-
toolCallMap[prettifiedName] = &ToolCallInfo{
475-
Name: prettifiedName,
476-
CallCount: 1,
477-
MaxInputSize: inputSize,
478-
MaxOutputSize: 0, // Will be updated when we find tool results
479-
MaxDuration: 0, // Will be updated when we find execution timing
480-
}
481-
}
482-
}
483-
}
484-
case "tool_result":
485-
// Extract output size for tool results
486-
if content, exists := contentMap["content"]; exists {
487-
if contentStr, ok := content.(string); ok {
488-
// Estimate token count (rough approximation: 1 token = ~4 characters)
489-
outputSize := len(contentStr) / 4
490-
491-
// Find corresponding tool call to update max output size
492-
if toolUseID, exists := contentMap["tool_use_id"]; exists {
493-
if _, ok := toolUseID.(string); ok {
494-
// This is simplified - in a full implementation we'd track tool_use_id to tool name mapping
495-
// For now, we'll update the max output size for all tools (conservative estimate)
496-
for _, toolInfo := range toolCallMap {
497-
if outputSize > toolInfo.MaxOutputSize {
498-
toolInfo.MaxOutputSize = outputSize
499-
}
500-
}
501-
}
502-
}
503-
}
504-
}
505-
}
506-
}
507-
}
508-
}
509-
}
510-
}
511-
512433
// estimateInputSize estimates the input size in tokens from a tool input object
513434
func (e *ClaudeEngine) estimateInputSize(input any) int {
514435
// Convert input to JSON string to get approximate size

pkg/workflow/claude_mcp.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func (e *ClaudeEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]a
4545
renderer := createRenderer(isLast)
4646
renderer.RenderSerenaMCP(yaml, serenaTool)
4747
},
48-
RenderCacheMemory: e.renderCacheMemoryMCPConfig,
48+
RenderCacheMemory: noOpCacheMemoryRenderer,
4949
RenderAgenticWorkflows: func(yaml *strings.Builder, isLast bool) {
5050
renderer := createRenderer(isLast)
5151
renderer.RenderAgenticWorkflowsMCP(yaml)
@@ -73,12 +73,3 @@ func (e *ClaudeEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]a
7373
func (e *ClaudeEngine) renderClaudeMCPConfigWithContext(yaml *strings.Builder, toolName string, toolConfig map[string]any, isLast bool, workflowData *WorkflowData) error {
7474
return renderCustomMCPConfigWrapperWithContext(yaml, toolName, toolConfig, isLast, workflowData)
7575
}
76-
77-
// renderCacheMemoryMCPConfig handles cache-memory configuration without MCP server mounting
78-
// Cache-memory is now a simple file share, not an MCP server
79-
func (e *ClaudeEngine) renderCacheMemoryMCPConfig(yaml *strings.Builder, isLast bool, workflowData *WorkflowData) {
80-
// Cache-memory no longer uses MCP server mounting
81-
// The cache folder is available as a simple file share at /tmp/gh-aw/cache-memory/
82-
// The folder is created by the cache step and is accessible to all tools
83-
// No MCP configuration is needed for simple file access
84-
}

pkg/workflow/codex_engine.go

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -352,23 +352,7 @@ func (e *CodexEngine) GetFirewallLogsCollectionStep(workflowData *WorkflowData)
352352

353353
// GetSquidLogsSteps returns the steps for uploading and parsing Squid logs (after secret redaction)
354354
func (e *CodexEngine) GetSquidLogsSteps(workflowData *WorkflowData) []GitHubActionStep {
355-
var steps []GitHubActionStep
356-
357-
// Only add upload and parsing steps if firewall is enabled
358-
if isFirewallEnabled(workflowData) {
359-
codexEngineLog.Printf("Adding Squid logs upload and parsing steps for workflow: %s", workflowData.Name)
360-
361-
squidLogsUpload := generateSquidLogsUploadStep(workflowData.Name)
362-
steps = append(steps, squidLogsUpload)
363-
364-
// Add firewall log parsing step to create step summary
365-
firewallLogParsing := generateFirewallLogParsingStep(workflowData.Name)
366-
steps = append(steps, firewallLogParsing)
367-
} else {
368-
codexEngineLog.Print("Firewall disabled, skipping Squid logs upload")
369-
}
370-
371-
return steps
355+
return defaultGetSquidLogsSteps(workflowData, codexEngineLog)
372356
}
373357

374358
// expandNeutralToolsToCodexTools converts neutral tools to Codex-specific tools format

pkg/workflow/codex_mcp.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,7 @@ func (e *CodexEngine) renderCodexMCPConfigWithContext(yaml *strings.Builder, too
169169

170170
// Determine if localhost URLs should be rewritten to host.docker.internal
171171
// This is needed when firewall is enabled (agent is not disabled)
172-
rewriteLocalhost := workflowData != nil && (workflowData.SandboxConfig == nil ||
173-
workflowData.SandboxConfig.Agent == nil ||
174-
!workflowData.SandboxConfig.Agent.Disabled)
172+
rewriteLocalhost := shouldRewriteLocalhostToDocker(workflowData)
175173

176174
// Use the shared MCP config renderer with TOML format
177175
renderer := MCPConfigRenderer{
@@ -192,9 +190,7 @@ func (e *CodexEngine) renderCodexMCPConfigWithContext(yaml *strings.Builder, too
192190
// This is used to generate the JSON config file that the MCP gateway reads
193191
func (e *CodexEngine) renderCodexJSONMCPConfigWithContext(yaml *strings.Builder, toolName string, toolConfig map[string]any, isLast bool, workflowData *WorkflowData) error {
194192
// Determine if localhost URLs should be rewritten to host.docker.internal
195-
rewriteLocalhost := workflowData != nil && (workflowData.SandboxConfig == nil ||
196-
workflowData.SandboxConfig.Agent == nil ||
197-
!workflowData.SandboxConfig.Agent.Disabled)
193+
rewriteLocalhost := shouldRewriteLocalhostToDocker(workflowData)
198194

199195
// Use the shared renderer with JSON format for gateway
200196
renderer := MCPConfigRenderer{

pkg/workflow/compiler_yaml_main_job.go

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -276,36 +276,9 @@ func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowDat
276276
}
277277

278278
// Collect firewall logs BEFORE secret redaction so secrets in logs can be redacted
279-
if copilotEngine, ok := engine.(*CopilotEngine); ok {
280-
collectionSteps := copilotEngine.GetFirewallLogsCollectionStep(data)
281-
for _, step := range collectionSteps {
282-
for _, line := range step {
283-
yaml.WriteString(line + "\n")
284-
}
285-
}
286-
}
287-
if codexEngine, ok := engine.(*CodexEngine); ok {
288-
collectionSteps := codexEngine.GetFirewallLogsCollectionStep(data)
289-
for _, step := range collectionSteps {
290-
for _, line := range step {
291-
yaml.WriteString(line + "\n")
292-
}
293-
}
294-
}
295-
if claudeEngine, ok := engine.(*ClaudeEngine); ok {
296-
collectionSteps := claudeEngine.GetFirewallLogsCollectionStep(data)
297-
for _, step := range collectionSteps {
298-
for _, line := range step {
299-
yaml.WriteString(line + "\n")
300-
}
301-
}
302-
}
303-
if codexEngine, ok := engine.(*CodexEngine); ok {
304-
collectionSteps := codexEngine.GetFirewallLogsCollectionStep(data)
305-
for _, step := range collectionSteps {
306-
for _, line := range step {
307-
yaml.WriteString(line + "\n")
308-
}
279+
for _, step := range engine.GetFirewallLogsCollectionStep(data) {
280+
for _, line := range step {
281+
yaml.WriteString(line + "\n")
309282
}
310283
}
311284

pkg/workflow/copilot_installer.go

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package workflow
22

33
import (
4-
"fmt"
5-
"strings"
6-
74
"github.com/github/gh-aw/pkg/constants"
85
"github.com/github/gh-aw/pkg/logger"
96
)
@@ -30,50 +27,3 @@ func GenerateCopilotInstallerSteps(version, stepName string) []GitHubActionStep
3027

3128
return []GitHubActionStep{GitHubActionStep(stepLines)}
3229
}
33-
34-
// generateSquidLogsUploadStep creates a GitHub Actions step to upload Squid logs as artifact.
35-
func generateSquidLogsUploadStep(workflowName string) GitHubActionStep {
36-
sanitizedName := strings.ToLower(SanitizeWorkflowName(workflowName))
37-
artifactName := "firewall-logs-" + sanitizedName
38-
// Firewall logs are now at a known location in the sandbox folder structure
39-
firewallLogsDir := "/tmp/gh-aw/sandbox/firewall/logs/"
40-
41-
stepLines := []string{
42-
" - name: Upload Firewall Logs",
43-
" if: always()",
44-
" continue-on-error: true",
45-
" uses: " + GetActionPin("actions/upload-artifact"),
46-
" with:",
47-
" name: " + artifactName,
48-
" path: " + firewallLogsDir,
49-
" if-no-files-found: ignore",
50-
}
51-
52-
return GitHubActionStep(stepLines)
53-
}
54-
55-
// generateFirewallLogParsingStep creates a GitHub Actions step to parse firewall logs and create step summary.
56-
func generateFirewallLogParsingStep(workflowName string) GitHubActionStep {
57-
// Firewall logs are at a known location in the sandbox folder structure
58-
firewallLogsDir := "/tmp/gh-aw/sandbox/firewall/logs"
59-
60-
stepLines := []string{
61-
" - name: Print firewall logs",
62-
" if: always()",
63-
" continue-on-error: true",
64-
" env:",
65-
" AWF_LOGS_DIR: " + firewallLogsDir,
66-
" run: |",
67-
" # Fix permissions on firewall logs so they can be uploaded as artifacts",
68-
" # AWF runs with sudo, creating files owned by root",
69-
fmt.Sprintf(" sudo chmod -R a+r %s 2>/dev/null || true", firewallLogsDir),
70-
" # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step)",
71-
" if command -v awf &> /dev/null; then",
72-
" awf logs summary | tee -a \"$GITHUB_STEP_SUMMARY\"",
73-
" else",
74-
" echo 'AWF binary not installed, skipping firewall log summary'",
75-
" fi",
76-
}
77-
78-
return GitHubActionStep(stepLines)
79-
}

pkg/workflow/copilot_logs.go

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -448,23 +448,7 @@ func (e *CopilotEngine) GetFirewallLogsCollectionStep(workflowData *WorkflowData
448448

449449
// GetSquidLogsSteps returns the steps for uploading and parsing Squid logs (after secret redaction)
450450
func (e *CopilotEngine) GetSquidLogsSteps(workflowData *WorkflowData) []GitHubActionStep {
451-
var steps []GitHubActionStep
452-
453-
// Only add upload and parsing steps if firewall is enabled
454-
if isFirewallEnabled(workflowData) {
455-
copilotLogsLog.Printf("Adding Squid logs upload and parsing steps for workflow: %s", workflowData.Name)
456-
457-
squidLogsUpload := generateSquidLogsUploadStep(workflowData.Name)
458-
steps = append(steps, squidLogsUpload)
459-
460-
// Add firewall log parsing step to create step summary
461-
firewallLogParsing := generateFirewallLogParsingStep(workflowData.Name)
462-
steps = append(steps, firewallLogParsing)
463-
} else {
464-
copilotLogsLog.Print("Firewall disabled, skipping Squid logs upload")
465-
}
466-
467-
return steps
451+
return defaultGetSquidLogsSteps(workflowData, copilotLogsLog)
468452
}
469453

470454
// GetCleanupStep returns the post-execution cleanup step (currently empty)

pkg/workflow/copilot_mcp.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,7 @@ func (e *CopilotEngine) renderCopilotMCPConfigWithContext(yaml *strings.Builder,
8787

8888
// Determine if localhost URLs should be rewritten to host.docker.internal
8989
// This is needed when firewall is enabled (agent is not disabled)
90-
rewriteLocalhost := workflowData != nil && (workflowData.SandboxConfig == nil ||
91-
workflowData.SandboxConfig.Agent == nil ||
92-
!workflowData.SandboxConfig.Agent.Disabled)
90+
rewriteLocalhost := shouldRewriteLocalhostToDocker(workflowData)
9391

9492
// Use the shared renderer with copilot-specific requirements
9593
renderer := MCPConfigRenderer{

0 commit comments

Comments
 (0)