From da5b494f464f6589ac449943e7886b165d829cf4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:19:26 +0000 Subject: [PATCH 1/2] Initial plan From 87e228126a156982eba341678520933f1c760cad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:36:43 +0000 Subject: [PATCH 2/2] chore: hoist function-body regexp.MustCompile calls to package-level vars Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/copilot_agent.go | 20 +++++++++--------- pkg/cli/copilot_agent_logs.go | 29 +++++++++++++------------- pkg/workflow/action_sha_checker.go | 14 ++++++------- pkg/workflow/concurrency_validation.go | 11 ++++++---- pkg/workflow/lock_schema.go | 11 ++++++---- pkg/workflow/redact_secrets.go | 7 ++++--- pkg/workflow/secrets_validation.go | 4 +++- 7 files changed, 51 insertions(+), 45 deletions(-) diff --git a/pkg/cli/copilot_agent.go b/pkg/cli/copilot_agent.go index 7d867aa4392..16abf419b3f 100644 --- a/pkg/cli/copilot_agent.go +++ b/pkg/cli/copilot_agent.go @@ -13,6 +13,15 @@ import ( var copilotCodingAgentLog = logger.New("cli:copilot_agent") +// agentLogPatterns contains patterns that indicate GitHub Copilot coding agent (not Copilot CLI) +var agentLogPatterns = []*regexp.Regexp{ + regexp.MustCompile(`(?i)github.*copilot.*agent`), + regexp.MustCompile(`(?i)copilot-swe-agent`), + regexp.MustCompile(`(?i)@github/copilot-swe-agent`), + regexp.MustCompile(`(?i)agent.*task.*execution`), + regexp.MustCompile(`(?i)copilot.*agent.*v\d+\.\d+`), +} + // CopilotCodingAgentDetector contains heuristics to detect if a workflow run was executed by GitHub Copilot coding agent type CopilotCodingAgentDetector struct { runDir string @@ -97,15 +106,6 @@ func (d *CopilotCodingAgentDetector) hasAgentWorkflowPath() bool { // hasAgentLogPatterns checks log files for patterns specific to GitHub Copilot coding agent func (d *CopilotCodingAgentDetector) hasAgentLogPatterns() bool { - // Patterns that indicate GitHub Copilot coding agent (not Copilot CLI) - agentPatterns := []*regexp.Regexp{ - regexp.MustCompile(`(?i)github.*copilot.*agent`), - regexp.MustCompile(`(?i)copilot-swe-agent`), - regexp.MustCompile(`(?i)@github/copilot-swe-agent`), - regexp.MustCompile(`(?i)agent.*task.*execution`), - regexp.MustCompile(`(?i)copilot.*agent.*v\d+\.\d+`), - } - found := false // Check log files for agent-specific patterns _ = filepath.Walk(d.runDir, func(path string, info os.FileInfo, err error) error { @@ -121,7 +121,7 @@ func (d *CopilotCodingAgentDetector) hasAgentLogPatterns() bool { return nil } - for _, pattern := range agentPatterns { + for _, pattern := range agentLogPatterns { if pattern.MatchString(content) { if d.verbose { fmt.Fprintln(os.Stderr, console.FormatInfoMessage( diff --git a/pkg/cli/copilot_agent_logs.go b/pkg/cli/copilot_agent_logs.go index e41d2e00096..0bd39af8b9a 100644 --- a/pkg/cli/copilot_agent_logs.go +++ b/pkg/cli/copilot_agent_logs.go @@ -7,6 +7,17 @@ import ( "github.com/github/gh-aw/pkg/workflow" ) +var ( + agentTurnPattern = regexp.MustCompile(`(?i)task.*iteration|agent.*turn|step.*\d+`) + agentToolCallPattern = regexp.MustCompile(`(?i)tool.*call|executing.*tool|calling.*(\w+)`) + toolNamePatterns = []*regexp.Regexp{ + regexp.MustCompile(`(?i)tool[:\s]+([a-zA-Z0-9_-]+)`), + regexp.MustCompile(`(?i)calling[:\s]+([a-zA-Z0-9_-]+)`), + regexp.MustCompile(`(?i)executing[:\s]+([a-zA-Z0-9_-]+)`), + regexp.MustCompile(`(?i)using[:\s]+tool[:\s]+([a-zA-Z0-9_-]+)`), + } +) + // ParseCopilotCodingAgentLogMetrics extracts metrics from GitHub Copilot coding agent logs // This is different from Copilot CLI logs and requires specialized parsing func ParseCopilotCodingAgentLogMetrics(logContent string, verbose bool) workflow.LogMetrics { @@ -20,11 +31,6 @@ func ParseCopilotCodingAgentLogMetrics(logContent string, verbose bool) workflow var currentSequence []string turns := 0 - // GitHub Copilot coding agent log patterns - // These patterns are designed to match the specific log format of the agent - turnPattern := regexp.MustCompile(`(?i)task.*iteration|agent.*turn|step.*\d+`) - toolCallPattern := regexp.MustCompile(`(?i)tool.*call|executing.*tool|calling.*(\w+)`) - for _, line := range lines { // Skip empty lines if strings.TrimSpace(line) == "" { @@ -32,7 +38,7 @@ func ParseCopilotCodingAgentLogMetrics(logContent string, verbose bool) workflow } // Count turns based on agent iteration patterns - if turnPattern.MatchString(line) { + if agentTurnPattern.MatchString(line) { turns++ // Start of a new turn, save previous sequence if any if len(currentSequence) > 0 { @@ -42,7 +48,7 @@ func ParseCopilotCodingAgentLogMetrics(logContent string, verbose bool) workflow } // Extract tool calls from agent logs - if matches := toolCallPattern.FindStringSubmatch(line); len(matches) > 1 { + if matches := agentToolCallPattern.FindStringSubmatch(line); len(matches) > 1 { toolName := extractToolName(line) if toolName != "" { // Track tool call @@ -97,14 +103,7 @@ func ParseCopilotCodingAgentLogMetrics(logContent string, verbose bool) workflow // extractToolName extracts a tool name from a log line func extractToolName(line string) string { // Try to extract tool name from various patterns - patterns := []*regexp.Regexp{ - regexp.MustCompile(`(?i)tool[:\s]+([a-zA-Z0-9_-]+)`), - regexp.MustCompile(`(?i)calling[:\s]+([a-zA-Z0-9_-]+)`), - regexp.MustCompile(`(?i)executing[:\s]+([a-zA-Z0-9_-]+)`), - regexp.MustCompile(`(?i)using[:\s]+tool[:\s]+([a-zA-Z0-9_-]+)`), - } - - for _, pattern := range patterns { + for _, pattern := range toolNamePatterns { if matches := pattern.FindStringSubmatch(line); len(matches) > 1 { return strings.TrimSpace(matches[1]) } diff --git a/pkg/workflow/action_sha_checker.go b/pkg/workflow/action_sha_checker.go index 7705275a8de..118380b108b 100644 --- a/pkg/workflow/action_sha_checker.go +++ b/pkg/workflow/action_sha_checker.go @@ -12,6 +12,10 @@ import ( var actionSHACheckerLog = logger.New("workflow:action_sha_checker") +// actionUsesPattern matches action references in lock files: +// owner/repo@40-char-hex-sha with optional version comment +var actionUsesPattern = regexp.MustCompile(`([a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+(?:/[a-zA-Z0-9_.-]+)*)@([0-9a-f]{40})(?:\s*#\s*([^\s]+))?`) + // ActionUsage represents an action used in a workflow with its SHA type ActionUsage struct { Repo string // e.g., "actions/checkout" @@ -47,16 +51,10 @@ func ExtractActionsFromLockFile(lockFilePath string) ([]ActionUsage, error) { return nil, fmt.Errorf("failed to parse lock file YAML: %w", err) } - // Regular expression to match uses: owner/repo@sha with optional version comment - // This matches: owner/repo@40-char-hex-sha # version - // Captures: (1) repo, (2) sha, (3) version (optional) - usesPattern := regexp.MustCompile(`([a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+(?:/[a-zA-Z0-9_.-]+)*)@([0-9a-f]{40})(?:\s*#\s*([^\s]+))?`) - - actions := make(map[string]ActionUsage) // Use map to deduplicate - // Convert to string and extract all uses fields contentStr := string(content) - matches := usesPattern.FindAllStringSubmatch(contentStr, -1) + actions := make(map[string]ActionUsage) // Use map to deduplicate + matches := actionUsesPattern.FindAllStringSubmatch(contentStr, -1) for _, match := range matches { if len(match) >= 3 { diff --git a/pkg/workflow/concurrency_validation.go b/pkg/workflow/concurrency_validation.go index 423307e38aa..f5a68fcca2b 100644 --- a/pkg/workflow/concurrency_validation.go +++ b/pkg/workflow/concurrency_validation.go @@ -35,6 +35,11 @@ import ( var concurrencyValidationLog = newValidationLogger("concurrency") +var ( + concurrencyExpressionPattern = regexp.MustCompile(`\$\{\{([^}]*)\}\}`) + concurrencyGroupPattern = regexp.MustCompile(`(?m)^\s*group:\s*["']?([^"'\n]+?)["']?\s*$`) +) + // validateConcurrencyGroupExpression validates the syntax of a custom concurrency group expression. // It checks for common syntactic errors that would cause runtime failures: // - Unbalanced ${{ }} braces @@ -125,8 +130,7 @@ func validateBalancedBraces(group string) error { // validateExpressionSyntax validates the syntax of expressions within ${{ }} func validateExpressionSyntax(group string) error { // Pattern to extract content between ${{ and }} - expressionPattern := regexp.MustCompile(`\$\{\{([^}]*)\}\}`) - matches := expressionPattern.FindAllStringSubmatch(group, -1) + matches := concurrencyExpressionPattern.FindAllStringSubmatch(group, -1) concurrencyValidationLog.Printf("Found %d expression(s) to validate", len(matches)) @@ -300,8 +304,7 @@ func containsLogicalOperators(expr string) bool { func extractConcurrencyGroupFromYAML(concurrencyYAML string) string { // First, check if it's object format with explicit "group:" field // Pattern: group: "value" or group: 'value' or group: value (at start of line or after spaces) - groupPattern := regexp.MustCompile(`(?m)^\s*group:\s*["']?([^"'\n]+?)["']?\s*$`) - matches := groupPattern.FindStringSubmatch(concurrencyYAML) + matches := concurrencyGroupPattern.FindStringSubmatch(concurrencyYAML) if len(matches) > 1 { return strings.TrimSpace(matches[1]) } diff --git a/pkg/workflow/lock_schema.go b/pkg/workflow/lock_schema.go index a42485c786f..724063c78d7 100644 --- a/pkg/workflow/lock_schema.go +++ b/pkg/workflow/lock_schema.go @@ -12,6 +12,11 @@ import ( var lockSchemaLog = logger.New("workflow:lock_schema") +var ( + lockMetadataPattern = regexp.MustCompile(`#\s*gh-aw-metadata:\s*(\{.+\})`) + lockHashPattern = regexp.MustCompile(`#\s*frontmatter-hash:\s*([0-9a-f]{64})`) +) + // LockSchemaVersion represents a lock file schema version type LockSchemaVersion string @@ -47,8 +52,7 @@ func IsSchemaVersionSupported(version LockSchemaVersion) bool { func ExtractMetadataFromLockFile(content string) (*LockMetadata, bool, error) { // Look for JSON metadata in comments (format: # gh-aw-metadata: {...}) // Use .+ to capture to end of line since metadata is single-line JSON - metadataPattern := regexp.MustCompile(`#\s*gh-aw-metadata:\s*(\{.+\})`) - matches := metadataPattern.FindStringSubmatch(content) + matches := lockMetadataPattern.FindStringSubmatch(content) if len(matches) >= 2 { jsonStr := matches[1] @@ -61,8 +65,7 @@ func ExtractMetadataFromLockFile(content string) (*LockMetadata, bool, error) { } // Legacy format: look for frontmatter-hash without JSON metadata - hashPattern := regexp.MustCompile(`#\s*frontmatter-hash:\s*([0-9a-f]{64})`) - if matches := hashPattern.FindStringSubmatch(content); len(matches) >= 2 { + if matches := lockHashPattern.FindStringSubmatch(content); len(matches) >= 2 { lockSchemaLog.Print("Legacy lock file detected (no schema version)") // Return a minimal metadata struct with just the hash for legacy files return &LockMetadata{FrontmatterHash: matches[1]}, true, nil diff --git a/pkg/workflow/redact_secrets.go b/pkg/workflow/redact_secrets.go index a799c4ff434..95a3e5a1b05 100644 --- a/pkg/workflow/redact_secrets.go +++ b/pkg/workflow/redact_secrets.go @@ -11,6 +11,9 @@ import ( var secretMaskingLog = logger.New("workflow:secret_masking") +// secretReferencePattern matches ${{ secrets.SECRET_NAME }} or secrets.SECRET_NAME +var secretReferencePattern = regexp.MustCompile(`secrets\.([A-Z][A-Z0-9_]*)`) + // escapeSingleQuote escapes single quotes and backslashes in a string to prevent injection // when embedding data in single-quoted YAML strings func escapeSingleQuote(s string) string { @@ -28,9 +31,7 @@ func CollectSecretReferences(yamlContent string) []string { // Pattern to match ${{ secrets.SECRET_NAME }} or secrets.SECRET_NAME // This matches both with and without the ${{ }} wrapper - secretPattern := regexp.MustCompile(`secrets\.([A-Z][A-Z0-9_]*)`) - - matches := secretPattern.FindAllStringSubmatch(yamlContent, -1) + matches := secretReferencePattern.FindAllStringSubmatch(yamlContent, -1) for _, match := range matches { if len(match) > 1 { secretsMap[match[1]] = true diff --git a/pkg/workflow/secrets_validation.go b/pkg/workflow/secrets_validation.go index 8da858dde1c..f9fafb0bd35 100644 --- a/pkg/workflow/secrets_validation.go +++ b/pkg/workflow/secrets_validation.go @@ -12,6 +12,9 @@ var secretsValidationLog = newValidationLogger("secrets") // This is the same pattern used in the github_token schema definition ($defs/github_token). var secretsExpressionPattern = regexp.MustCompile(`^\$\{\{\s*secrets\.[A-Za-z_][A-Za-z0-9_]*(\s*\|\|\s*secrets\.[A-Za-z_][A-Za-z0-9_]*)*\s*\}\}$`) +// secretNamePattern validates that a secret name follows environment variable naming conventions +var secretNamePattern = regexp.MustCompile(`^[A-Z][A-Z0-9_]*$`) + // validateSecretsExpression validates that a value is a proper GitHub Actions secrets expression. // Returns an error if the value is not in the format: ${{ secrets.NAME }} or ${{ secrets.NAME || secrets.NAME2 }} // Note: This function intentionally does not accept the secret key name as a parameter to prevent @@ -29,7 +32,6 @@ func validateSecretsExpression(value string) error { func validateSecretReferences(secrets []string) error { secretsValidationLog.Printf("Validating secret references: checking %d secrets", len(secrets)) // Secret names must be valid environment variable names - secretNamePattern := regexp.MustCompile(`^[A-Z][A-Z0-9_]*$`) for _, secret := range secrets { if !secretNamePattern.MatchString(secret) {