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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,635 changes: 818 additions & 817 deletions .github/workflows/ci-doctor.lock.yml

Large diffs are not rendered by default.

1,378 changes: 689 additions & 689 deletions .github/workflows/dev.lock.yml

Large diffs are not rendered by default.

1,384 changes: 692 additions & 692 deletions .github/workflows/tidy.lock.yml

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions pkg/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ func GetVersion() string {
return version
}

// GetInstructionsTemplate returns the embedded instructions template
func GetInstructionsTemplate() string {
return copilotInstructionsTemplate
}

// GitHubWorkflow represents a GitHub Actions workflow from the API
// GitHubWorkflowsResponse represents the GitHub API response for workflows
// Note: The API returns an array directly, not wrapped in a workflows field
Expand Down
8 changes: 4 additions & 4 deletions pkg/cli/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ var ErrNoArtifacts = errors.New("no artifacts found for this run")
// fetchJobStatuses gets job information for a workflow run and counts failed jobs
func fetchJobStatuses(runID int64, verbose bool) (int, error) {
args := []string{"api", fmt.Sprintf("repos/{owner}/{repo}/actions/runs/%d/jobs", runID), "--jq", ".jobs[] | {name: .name, status: .status, conclusion: .conclusion}"}

if verbose {
fmt.Println(console.FormatVerboseMessage(fmt.Sprintf("Fetching job statuses for run %d", runID)))
}
Expand All @@ -113,15 +113,15 @@ func fetchJobStatuses(runID int64, verbose bool) (int, error) {
if strings.TrimSpace(line) == "" {
continue
}

var job JobInfo
if err := json.Unmarshal([]byte(line), &job); err != nil {
if verbose {
fmt.Println(console.FormatVerboseMessage(fmt.Sprintf("Failed to parse job info: %s", line)))
}
continue
}

// Count jobs with failure conclusions as errors
if job.Conclusion == "failure" || job.Conclusion == "cancelled" || job.Conclusion == "timed_out" {
failedJobs++
Expand All @@ -130,7 +130,7 @@ func fetchJobStatuses(runID int64, verbose bool) (int, error) {
}
}
}

return failedJobs, nil
}

Expand Down
41 changes: 31 additions & 10 deletions pkg/cli/mcp_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,26 @@ func createMCPServer(verbose bool, allowedTools []string) *mcp.Server {
})
}

// Add docs tool
if isToolAllowed("docs") {
type docsArgs struct {
Verbose bool `json:"verbose,omitempty"`
}
mcp.AddTool(server, &mcp.Tool{
Name: "docs",
Description: "Get GitHub Agentic Workflows documentation and instructions",
}, func(ctx context.Context, req *mcp.CallToolRequest, args docsArgs) (*mcp.CallToolResult, any, error) {
if verbose || args.Verbose {
fmt.Fprintf(os.Stderr, "📚 Retrieving documentation...\n")
}

// Return the embedded instructions template content
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: GetInstructionsTemplate()}},
}, nil, nil
})
}

return server
}

Expand All @@ -447,15 +467,16 @@ func NewMCPServerSubcommand() *cobra.Command {
This command starts an MCP server that can be used by AI assistants and other MCP clients
to interact with GitHub Agentic Workflows functionality. The server exposes the following tools:

compile - Compile markdown workflow files to YAML
logs - Download and analyze agentic workflow logs
mcp_inspect - Inspect MCP servers and list available tools
mcp_list - List MCP servers defined in agentic workflows
mcp_add - Add MCP tools to agentic workflows
run - Run agentic workflows on GitHub Actions
enable - Enable workflows
disable - Disable workflows
status - Show status of natural language action files and workflows
compile - Compile markdown workflow files to YAML
logs - Download and analyze agentic workflow logs
mcp_inspect - Inspect MCP servers and list available tools
mcp_list - List MCP servers defined in agentic workflows
mcp_add - Add MCP tools to agentic workflows
run - Run agentic workflows on GitHub Actions
enable - Enable workflows
disable - Disable workflows
status - Show status of natural language action files and workflows
docs - Get GitHub Agentic Workflows documentation and instructions

The server uses stdio transport by default, making it suitable for use with various MCP clients.

Expand Down Expand Up @@ -487,7 +508,7 @@ Examples:
}

cmd.Flags().BoolP("verbose", "v", false, "Enable verbose output with detailed logging")
cmd.Flags().StringSlice("allowed-tools", []string{}, "Comma-separated list of tools to enable (compile,logs,mcp_inspect,mcp_list,mcp_add,run,enable,disable,status). If not specified, all tools are enabled.")
cmd.Flags().StringSlice("allowed-tools", []string{}, "Comma-separated list of tools to enable (compile,logs,mcp_inspect,mcp_list,mcp_add,run,enable,disable,status,docs). If not specified, all tools are enabled.")

return cmd
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/mcp_server_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ This is a test workflow for status checking.
}

// Check that we have the expected tools
expectedTools := []string{"compile", "logs", "mcp_inspect", "mcp_list", "mcp_add", "run", "enable", "disable", "status"}
expectedTools := []string{"compile", "logs", "mcp_inspect", "mcp_list", "mcp_add", "run", "enable", "disable", "status", "docs"}
if len(toolsResult.Tools) != len(expectedTools) {
t.Errorf("Expected %d tools, got %d", len(expectedTools), len(toolsResult.Tools))
}
Expand Down
33 changes: 33 additions & 0 deletions pkg/cli/mcp_server_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"strings"
"testing"
)

Expand Down Expand Up @@ -46,4 +47,36 @@ func TestMCPServerCommand(t *testing.T) {
// We can't easily test the exact tool count without starting the server,
// but we can verify it was created successfully with the filter
})

t.Run("createMCPServer includes docs tool", func(t *testing.T) {
// Test that docs tool is included when no filter is applied
server := createMCPServer(false, []string{})
if server == nil {
t.Fatal("createMCPServer returned nil")
}

// Test that docs tool is included when specifically allowed
server = createMCPServer(false, []string{"docs"})
if server == nil {
t.Fatal("createMCPServer returned nil")
}
})
}

func TestGetInstructionsTemplate(t *testing.T) {
t.Run("GetInstructionsTemplate returns non-empty content", func(t *testing.T) {
template := GetInstructionsTemplate()
if template == "" {
t.Error("Expected GetInstructionsTemplate to return non-empty content")
}

// Should contain expected markdown content
if !strings.Contains(template, "# GitHub Agentic Workflows") {
t.Error("Expected template to contain main heading")
}

if !strings.Contains(template, "## File Format Overview") {
t.Error("Expected template to contain File Format Overview section")
}
})
}
22 changes: 11 additions & 11 deletions pkg/workflow/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,25 +264,25 @@ type ErrorWarningCounts struct {
// This is more accurate than simple string matching and uses the same logic as validate_errors.cjs
func CountErrorsAndWarningsWithPatterns(logContent string, patterns []ErrorPattern) ErrorWarningCounts {
counts := ErrorWarningCounts{}

if len(patterns) == 0 {
return counts
}

lines := strings.Split(logContent, "\n")

for _, pattern := range patterns {
regex, err := regexp.Compile(pattern.Pattern)
if err != nil {
// Skip invalid patterns
continue
}

for _, line := range lines {
matches := regex.FindAllStringSubmatch(line, -1)
for _, match := range matches {
level := extractLevelFromMatch(match, pattern)

if strings.ToLower(level) == "error" {
counts.ErrorCount++
} else if strings.ToLower(level) == "warning" || strings.ToLower(level) == "warn" {
Expand All @@ -291,7 +291,7 @@ func CountErrorsAndWarningsWithPatterns(logContent string, patterns []ErrorPatte
}
}
}

return counts
}

Expand All @@ -301,26 +301,26 @@ func extractLevelFromMatch(match []string, pattern ErrorPattern) string {
if pattern.LevelGroup > 0 && pattern.LevelGroup < len(match) && match[pattern.LevelGroup] != "" {
levelText := strings.ToLower(match[pattern.LevelGroup])
// Normalize common error/warning keywords
if strings.Contains(levelText, "err") || strings.Contains(levelText, "error") ||
strings.Contains(levelText, "fail") || strings.Contains(levelText, "fatal") {
if strings.Contains(levelText, "err") || strings.Contains(levelText, "error") ||
strings.Contains(levelText, "fail") || strings.Contains(levelText, "fatal") {
return "error"
} else if strings.Contains(levelText, "warn") || strings.Contains(levelText, "warning") {
return "warning"
}
// Return the original level text if it doesn't match common patterns
return match[pattern.LevelGroup]
}

// Try to infer level from the full match content
if len(match) > 0 {
fullMatch := strings.ToLower(match[0])
if strings.Contains(fullMatch, "error") || strings.Contains(fullMatch, "err") ||
strings.Contains(fullMatch, "fail") || strings.Contains(fullMatch, "fatal") {
strings.Contains(fullMatch, "fail") || strings.Contains(fullMatch, "fatal") {
return "error"
} else if strings.Contains(fullMatch, "warn") || strings.Contains(fullMatch, "warning") {
return "warning"
}
}

return "unknown"
}
8 changes: 4 additions & 4 deletions pkg/workflow/pattern_error_counting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ Another warning message`,
},
},
expectedErrors: 2, // npm ERR! + random error
expectedWarns: 2, // npm WARN + warning message
expectedWarns: 2, // npm WARN + warning message
},
{
name: "invalid regex pattern",
Expand Down Expand Up @@ -150,11 +150,11 @@ warning: be careful`,
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
counts := CountErrorsAndWarningsWithPatterns(tt.logContent, tt.patterns)

if counts.ErrorCount != tt.expectedErrors {
t.Errorf("Expected %d errors, got %d", tt.expectedErrors, counts.ErrorCount)
}

if counts.WarningCount != tt.expectedWarns {
t.Errorf("Expected %d warnings, got %d", tt.expectedWarns, counts.WarningCount)
}
Expand Down Expand Up @@ -215,4 +215,4 @@ func TestExtractLevelFromMatch(t *testing.T) {
}
})
}
}
}