Skip to content
Merged
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 change: 1 addition & 0 deletions .github/skills/agentic-workflows/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Repository overlay (optional):
Read only the files you need:
Load these files from `github/gh-aw` (they are not available locally).
- `.github/aw/action-container-substitutions.md`
- `.github/aw/agent-runtime-instructions.md`
- `.github/aw/agentic-chat.md`
- `.github/aw/agentic-workflows-mcp.md`
- `.github/aw/asciicharts.md`
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ The `cli` package is intentionally large and command-oriented. The tables below
| `RunUpdateWorkflows` | `func RunUpdateWorkflows(ctx context.Context, opts UpdateWorkflowsOptions) error` | CLI wrapper for updating sourced workflows. |
| `UpdateWorkflows` | `func UpdateWorkflows(ctx context.Context, opts UpdateWorkflowsOptions) error` | Updates workflows with `source:` frontmatter from upstream definitions. |
| `NewStatusCommand` | `func NewStatusCommand() *cobra.Command` | Constructs the `gh aw status` command. |
| `GetWorkflowStatuses` | `func GetWorkflowStatuses(pattern string, ref string, labelFilter string, repoOverride string) ([]WorkflowStatus, error)` | Returns workflow status data for programmatic callers. |
| `StatusWorkflows` | `func StatusWorkflows(pattern string, verbose bool, jsonOutput bool, ref string, labelFilter string, repoOverride string) error` | Renders workflow status to terminal or JSON. |
| `GetWorkflowStatuses` | `func GetWorkflowStatuses(ctx context.Context, pattern string, ref string, labelFilter string, repoOverride string) ([]WorkflowStatus, error)` | Returns workflow status data for programmatic callers. |
| `StatusWorkflows` | `func StatusWorkflows(ctx context.Context, pattern string, verbose bool, jsonOutput bool, ref string, labelFilter string, repoOverride string) error` | Renders workflow status to terminal or JSON. |
| `InitRepository` | `func InitRepository(opts InitOptions) error` | Initializes repository-local gh-aw support files. |
| `CreateWorkflowMarkdownFile` | `func CreateWorkflowMarkdownFile(workflowName string, verbose bool, force bool, engine string) error` | Creates a new workflow markdown file. |
| `ResolveWorkflowPath` | `func ResolveWorkflowPath(workflowFile string) (string, error)` | Resolves a workflow identifier to a local file path. |
Expand Down
10 changes: 5 additions & 5 deletions pkg/cli/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ func TestRemoveWorkflows(t *testing.T) {
}

func TestStatusWorkflows(t *testing.T) {
err := StatusWorkflows("test-pattern", false, false, "", "", "")
err := StatusWorkflows(t.Context(), "test-pattern", false, false, "", "", "")

// Should not error since it's a stub implementation
if err != nil {
Expand Down Expand Up @@ -385,8 +385,8 @@ Test workflow for command existence.`
_, err := CompileWorkflows(context.Background(), config)
return err
}, false, "CompileWorkflows"},
{func() error { return RemoveWorkflows("nonexistent", false, "") }, false, "RemoveWorkflows"}, // Should handle missing directory gracefully
{func() error { return StatusWorkflows("nonexistent", false, false, "", "", "") }, false, "StatusWorkflows"}, // Should handle missing directory gracefully
{func() error { return RemoveWorkflows("nonexistent", false, "") }, false, "RemoveWorkflows"}, // Should handle missing directory gracefully
{func() error { return StatusWorkflows(t.Context(), "nonexistent", false, false, "", "", "") }, false, "StatusWorkflows"}, // Should handle missing directory gracefully
{func() error {
return RunWorkflowOnGitHub(context.Background(), "", RunOptions{})
}, true, "RunWorkflowOnGitHub"}, // Should error with empty workflow name
Expand Down Expand Up @@ -985,13 +985,13 @@ func TestRunWorkflowOnGitHubWithEnable(t *testing.T) {
func TestGetWorkflowStatus(t *testing.T) {

// Test with non-existent workflow
_, err := getWorkflowStatus("nonexistent-workflow", "", false)
_, err := getWorkflowStatus(t.Context(), "nonexistent-workflow", "", false)
if err == nil {
t.Error("getWorkflowStatus should return error for non-existent workflow")
}

// Test with empty workflow name
_, err = getWorkflowStatus("", "", false)
_, err = getWorkflowStatus(t.Context(), "", "", false)
if err == nil {
t.Error("getWorkflowStatus should return error for empty workflow name")
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/enable.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func toggleWorkflowsByNames(ctx context.Context, workflowNames []string, enable

// Get GitHub workflows status for comparison; warn but continue if unavailable
enableLog.Print("Fetching GitHub workflows status for comparison")
githubWorkflows, err := fetchGitHubWorkflows(repoOverride, false)
githubWorkflows, err := fetchGitHubWorkflows(ctx, repoOverride, false)
if err != nil {
enableLog.Printf("Failed to fetch GitHub workflows: %v", err)
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Unable to fetch GitHub workflows (gh CLI may not be authenticated): %v", err)))
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/forecast_montecarlo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ func TestResolveForecastWorkflowsFromRemote_RateLimitFallsBackToPartialResults(t

attempts := 0
var backoffs []time.Duration
forecastFetchGitHubWorkflows = func(repoOverride string, verbose bool) (map[string]*GitHubWorkflow, error) {
forecastFetchGitHubWorkflows = func(_ context.Context, repoOverride string, verbose bool) (map[string]*GitHubWorkflow, error) {
attempts++
return nil, errors.New("API rate limit exceeded")
}
Expand Down
6 changes: 4 additions & 2 deletions pkg/cli/forecast_resolution.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ const (
)

var (
forecastFetchGitHubWorkflows = fetchGitHubWorkflows
forecastFetchGitHubWorkflows = func(ctx context.Context, repoOverride string, verbose bool) (map[string]*GitHubWorkflow, error) {
return fetchGitHubWorkflows(ctx, repoOverride, verbose)
}
forecastListWorkflowRunsPaginated = listWorkflowRunsWithPagination
forecastRateLimitSleep = func(ctx context.Context, delay time.Duration) error {
timer := time.NewTimer(delay)
Expand Down Expand Up @@ -110,7 +112,7 @@ func fetchWorkflowsWithBackoff(ctx context.Context, ids []string, repoOverride s
var lastErr error

for attempt := 1; attempt <= forecastRateLimitMaxAttempts; attempt++ {
githubWorkflows, err := forecastFetchGitHubWorkflows(repoOverride, verbose)
githubWorkflows, err := forecastFetchGitHubWorkflows(ctx, repoOverride, verbose)
if err == nil {
return githubWorkflows, nil
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/mcp_tools_readonly.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Returns a JSON array where each element has the following structure:
mcpLog.Printf("Executing status tool: pattern=%s", args.Pattern)

// Call GetWorkflowStatuses directly instead of spawning subprocess
statuses, err := GetWorkflowStatuses(args.Pattern, "", "", "")
statuses, err := GetWorkflowStatuses(ctx, args.Pattern, "", "", "")
if err != nil {
return nil, nil, newMCPError(jsonrpc.CodeInternalError, "failed to get workflow statuses", map[string]any{"error": err.Error()})
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/cli/run_workflow_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func prepareWorkflowRun(ctx context.Context, workflowIdOrName string, opts RunOp
if err := validateWorkflowForRun(workflowIdOrName, opts); err != nil {
return nil, err
}
enableState, err := handleWorkflowEnablement(workflowIdOrName, opts)
enableState, err := handleWorkflowEnablement(ctx, workflowIdOrName, opts)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -214,11 +214,11 @@ func warnLocalWorkflowStatus(workflowFile string) {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Consider pushing your changes before running the workflow"))
}

func handleWorkflowEnablement(workflowIdOrName string, opts RunOptions) (workflowEnableState, error) {
func handleWorkflowEnablement(ctx context.Context, workflowIdOrName string, opts RunOptions) (workflowEnableState, error) {
if !opts.Enable {
return workflowEnableState{}, nil
}
wf, err := getWorkflowStatus(workflowIdOrName, opts.RepoOverride, opts.Verbose)
wf, err := getWorkflowStatus(ctx, workflowIdOrName, opts.RepoOverride, opts.Verbose)
if err != nil {
if opts.Verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Could not check workflow status: %v", err)))
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ It accepts workflow IDs (basename without .md) or full filenames.`,
labelFilter, _ := cmd.Flags().GetString("label")
repoOverride, _ := cmd.Flags().GetString("repo")
statusLog.Printf("Status command invoked: pattern=%q, json=%v, ref=%q, label=%q, repo=%q", pattern, jsonFlag, ref, labelFilter, repoOverride)
return StatusWorkflows(pattern, verbose, jsonFlag, ref, labelFilter, repoOverride)
return StatusWorkflows(cmd.Context(), pattern, verbose, jsonFlag, ref, labelFilter, repoOverride)
},
}

Expand Down
23 changes: 15 additions & 8 deletions pkg/cli/status_command.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"context"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -40,14 +41,17 @@ type WorkflowStatus struct {
// GetWorkflowStatuses retrieves workflow status information and returns it as a slice.
// This function is designed for programmatic access (e.g., from MCP server).
// For CLI usage, use StatusWorkflows which handles output formatting.
func GetWorkflowStatuses(pattern string, ref string, labelFilter string, repoOverride string) ([]WorkflowStatus, error) {
func GetWorkflowStatuses(ctx context.Context, pattern string, ref string, labelFilter string, repoOverride string) ([]WorkflowStatus, error) {
statusLog.Printf("Getting workflow statuses: pattern=%s, ref=%s, labelFilter=%s, repo=%s", pattern, ref, labelFilter, repoOverride)

// Get GitHub workflows data
statusLog.Print("Fetching GitHub workflow status")
githubWorkflows, err := fetchGitHubWorkflows(repoOverride, false)
githubWorkflows, err := fetchGitHubWorkflows(ctx, repoOverride, false)
if err != nil {
statusLog.Printf("Failed to fetch GitHub workflows: %v", err)
if ctx.Err() != nil {
return nil, ctx.Err()
}
githubWorkflows = make(map[string]*GitHubWorkflow)
Comment on lines +49 to 55
} else {
statusLog.Printf("Successfully fetched %d GitHub workflows", len(githubWorkflows))
Expand All @@ -56,9 +60,12 @@ func GetWorkflowStatuses(pattern string, ref string, labelFilter string, repoOve
// Fetch latest workflow runs for ref if specified
var latestRunsByWorkflow map[string]*WorkflowRun
if ref != "" {
latestRunsByWorkflow, err = fetchLatestRunsByRef(ref, repoOverride, false)
latestRunsByWorkflow, err = fetchLatestRunsByRef(ctx, ref, repoOverride, false)
if err != nil {
statusLog.Printf("Failed to fetch workflow runs for ref %s: %v", ref, err)
if ctx.Err() != nil {
return nil, ctx.Err()
}
latestRunsByWorkflow = make(map[string]*WorkflowRun)
} else {
statusLog.Printf("Successfully fetched %d workflow runs for ref %s", len(latestRunsByWorkflow), ref)
Expand Down Expand Up @@ -240,7 +247,7 @@ func buildRemoteWorkflowStatuses(pattern string, githubWorkflows map[string]*Git
return statuses
}

func StatusWorkflows(pattern string, verbose bool, jsonOutput bool, ref string, labelFilter string, repoOverride string) error {
func StatusWorkflows(ctx context.Context, pattern string, verbose bool, jsonOutput bool, ref string, labelFilter string, repoOverride string) error {
statusLog.Printf("Checking workflow status: pattern=%s, jsonOutput=%v, ref=%s, labelFilter=%s, repo=%s", pattern, jsonOutput, ref, labelFilter, repoOverride)
if verbose && !jsonOutput {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Checking status of workflow files"))
Expand All @@ -255,11 +262,11 @@ func StatusWorkflows(pattern string, verbose bool, jsonOutput bool, ref string,
}

// Get workflow statuses
statuses, err := GetWorkflowStatuses(pattern, ref, labelFilter, repoOverride)
statuses, err := GetWorkflowStatuses(ctx, pattern, ref, labelFilter, repoOverride)
if err != nil {
statusLog.Printf("Failed to get workflow statuses: %v", err)
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
return nil
return err
}

// Additional verbose output after successful fetch
Expand Down Expand Up @@ -482,7 +489,7 @@ func isCompiledUpToDateWithCache(workflowPath, lockFilePath string, cache *parse
}

// fetchLatestRunsByRef fetches the latest workflow run for each workflow from a specific ref (branch or tag)
func fetchLatestRunsByRef(ref string, repoOverride string, verbose bool) (map[string]*WorkflowRun, error) {
func fetchLatestRunsByRef(ctx context.Context, ref string, repoOverride string, verbose bool) (map[string]*WorkflowRun, error) {
statusLog.Printf("Fetching latest workflow runs for ref: %s, repo: %s", ref, repoOverride)

// Start spinner for network operation (only if not in verbose mode)
Expand All @@ -496,7 +503,7 @@ func fetchLatestRunsByRef(ref string, repoOverride string, verbose bool) (map[st
if repoOverride != "" {
args = append(args, "--repo", repoOverride)
}
cmd := workflow.ExecGH(args...)
cmd := workflow.ExecGHContext(ctx, args...)
output, err := cmd.Output()

if err != nil {
Expand Down
8 changes: 4 additions & 4 deletions pkg/cli/status_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestStatusWorkflows_JSONOutput(t *testing.T) {

// Test JSON output without pattern
t.Run("JSON output without pattern", func(t *testing.T) {
err := StatusWorkflows("", false, true, "", "", "")
err := StatusWorkflows(t.Context(), "", false, true, "", "", "")
if err != nil {
t.Errorf("StatusWorkflows with JSON flag failed: %v", err)
}
Expand All @@ -41,7 +41,7 @@ func TestStatusWorkflows_JSONOutput(t *testing.T) {

// Test JSON output with pattern
t.Run("JSON output with pattern", func(t *testing.T) {
err := StatusWorkflows("smoke", false, true, "", "", "")
err := StatusWorkflows(t.Context(), "smoke", false, true, "", "", "")
if err != nil {
t.Errorf("StatusWorkflows with JSON flag and pattern failed: %v", err)
}
Expand Down Expand Up @@ -547,14 +547,14 @@ func TestWorkflowStatus_ConsoleRenderingWithRunStatus(t *testing.T) {
func TestStatusWorkflows_WithRepoOverride(t *testing.T) {
// This test verifies that the function accepts the repoOverride parameter
// and doesn't error out. It should work in the current repository context.
err := StatusWorkflows("", false, true, "", "", "")
err := StatusWorkflows(t.Context(), "", false, true, "", "", "")
if err != nil {
t.Errorf("StatusWorkflows with empty repoOverride should not error: %v", err)
}

// Test with a non-empty repo override (will fail gracefully if repo doesn't exist)
// We expect this to either succeed or fail gracefully without panicking
_ = StatusWorkflows("", false, true, "", "", "nonexistent/repo")
_ = StatusWorkflows(t.Context(), "", false, true, "", "", "nonexistent/repo")
// Note: We don't check error here because it's expected to fail for a nonexistent repo
// The important part is that the parameter is accepted and used
}
8 changes: 4 additions & 4 deletions pkg/cli/status_mcp_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
func TestGetWorkflowStatuses_MCPIntegration(t *testing.T) {
// This test requires being run from the repository root
// since it needs .github/workflows directory
statuses, err := GetWorkflowStatuses("", "", "", "")
statuses, err := GetWorkflowStatuses(t.Context(), "", "", "", "")

// We expect either:
// - No error and a valid (possibly empty) slice
Expand Down Expand Up @@ -45,7 +45,7 @@ func TestGetWorkflowStatuses_MCPIntegration(t *testing.T) {
// TestGetWorkflowStatuses_WithPattern tests filtering by pattern
func TestGetWorkflowStatuses_WithPattern(t *testing.T) {
// Get all statuses first
allStatuses, err := GetWorkflowStatuses("", "", "", "")
allStatuses, err := GetWorkflowStatuses(t.Context(), "", "", "", "")
if err != nil {
t.Skipf("Skipping test: not in a repository with workflows: %v", err)
return
Expand All @@ -61,7 +61,7 @@ func TestGetWorkflowStatuses_WithPattern(t *testing.T) {
pattern := firstWorkflowName[:min(3, len(firstWorkflowName))] // Use first 3 chars as pattern

// Get filtered statuses
filteredStatuses, err := GetWorkflowStatuses(pattern, "", "", "")
filteredStatuses, err := GetWorkflowStatuses(t.Context(), pattern, "", "", "")
require.NoError(t, err, "GetWorkflowStatuses with pattern should not error")

// Verify that filtered results are a subset
Expand All @@ -79,7 +79,7 @@ func TestGetWorkflowStatuses_WithPattern(t *testing.T) {

// TestGetWorkflowStatuses_MCPJSONStructure verifies the JSON structure
func TestGetWorkflowStatuses_MCPJSONStructure(t *testing.T) {
statuses, err := GetWorkflowStatuses("", "", "", "")
statuses, err := GetWorkflowStatuses(t.Context(), "", "", "", "")
if err != nil {
t.Skipf("Skipping test: not in a repository with workflows: %v", err)
return
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/status_remote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func TestGetWorkflowStatuses_WithRepoFlag_SkipsLocalFiles(t *testing.T) {
// there is no local .github/workflows directory. The GitHub API call will
// fail (no real token in tests) but that is swallowed and an empty result
// is returned rather than a "no .github/workflows directory found" error.
statuses, err := GetWorkflowStatuses("", "", "", "owner/repo")
statuses, err := GetWorkflowStatuses(t.Context(), "", "", "", "owner/repo")
require.NoError(t, err, "GetWorkflowStatuses with --repo should not propagate a 'missing local dir' error")
// statuses may be nil (no API mock) or an empty slice; either is acceptable.
_ = statuses
Expand All @@ -107,7 +107,7 @@ func TestGetWorkflowStatuses_WithRepoFlag_SkipsLocalFiles(t *testing.T) {
// with --repo returns a clear error, since label information is not exposed by
// the GitHub Actions workflow API.
func TestGetWorkflowStatuses_LabelFilterWithRepo(t *testing.T) {
_, err := GetWorkflowStatuses("", "", "my-label", "owner/repo")
_, err := GetWorkflowStatuses(t.Context(), "", "", "my-label", "owner/repo")
require.Error(t, err)
require.ErrorContains(t, err, "--label filter is not supported with --repo")
}
9 changes: 5 additions & 4 deletions pkg/cli/workflows.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cli
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -68,7 +69,7 @@ type GitHubWorkflow struct {
}

// fetchGitHubWorkflows fetches workflow information from GitHub
func fetchGitHubWorkflows(repoOverride string, verbose bool) (map[string]*GitHubWorkflow, error) {
func fetchGitHubWorkflows(ctx context.Context, repoOverride string, verbose bool) (map[string]*GitHubWorkflow, error) {
workflowsLog.Printf("Fetching GitHub workflows: repoOverride=%s", repoOverride)

// Start spinner for network operation (only if not in verbose mode)
Expand All @@ -81,7 +82,7 @@ func fetchGitHubWorkflows(repoOverride string, verbose bool) (map[string]*GitHub
if repoOverride != "" {
args = append(args, "--repo", repoOverride)
}
cmd := workflow.ExecGH(args...)
cmd := workflow.ExecGHContext(ctx, args...)
output, err := cmd.Output()

if err != nil {
Expand Down Expand Up @@ -176,14 +177,14 @@ func extractWorkflowNameFromPath(path string) string {
}

// getWorkflowStatus gets the status of a single workflow by name
func getWorkflowStatus(workflowIdOrName string, repoOverride string, verbose bool) (*GitHubWorkflow, error) {
func getWorkflowStatus(ctx context.Context, workflowIdOrName string, repoOverride string, verbose bool) (*GitHubWorkflow, error) {
workflowsLog.Printf("Getting workflow status: workflow=%s", workflowIdOrName)

// Extract workflow name for lookup
filename := normalizeWorkflowID(workflowIdOrName)

// Get all GitHub workflows
githubWorkflows, err := fetchGitHubWorkflows(repoOverride, verbose)
githubWorkflows, err := fetchGitHubWorkflows(ctx, repoOverride, verbose)
if err != nil {
return nil, fmt.Errorf("failed to fetch GitHub workflows: %w", err)
}
Expand Down
Loading
Loading