Skip to content
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2142,6 +2142,57 @@ jobs:
echo "$WORKFLOWS" > /tmp/workflow-list.txt
echo "workflow_count=$WORKFLOW_COUNT" >> $GITHUB_OUTPUT

- name: Compare gh aw list with git clone
run: |
set -e
echo "## Comparing 'gh aw list' output with git clone results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

# List workflows using gh aw list command with custom path
echo "Running: ./gh-aw list --repo githubnext/agentics --path workflows --json"
./gh-aw list --repo githubnext/agentics --path workflows --json > /tmp/gh-aw-list.json

# Extract workflow names from JSON output
echo "Extracting workflow names from gh aw list output..."
jq -r '.[].workflow' /tmp/gh-aw-list.json | sort > /tmp/gh-aw-workflows.txt

# Get workflow names from git clone (already in /tmp/workflow-list.txt)
echo "Sorting git clone workflow list..."
sort /tmp/workflow-list.txt > /tmp/git-workflows-sorted.txt

# Display both lists
echo "### Workflows from 'gh aw list --repo githubnext/agentics --path workflows'" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
cat /tmp/gh-aw-workflows.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

echo "### Workflows from git clone" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
cat /tmp/git-workflows-sorted.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

# Compare the two lists
if diff -u /tmp/git-workflows-sorted.txt /tmp/gh-aw-workflows.txt > /tmp/diff-output.txt; then
echo "✅ **SUCCESS**: Workflow lists match!" >> $GITHUB_STEP_SUMMARY
echo "The 'gh aw list' command returned the same workflows as the git clone." >> $GITHUB_STEP_SUMMARY
echo ""
echo "✅ Workflow lists match!"
else
echo "❌ **FAILURE**: Workflow lists do not match!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Differences" >> $GITHUB_STEP_SUMMARY
echo '```diff' >> $GITHUB_STEP_SUMMARY
cat /tmp/diff-output.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo ""
echo "❌ Workflow lists do not match!"
echo "Differences:"
cat /tmp/diff-output.txt
exit 1
fi

- name: Add workflows one by one
id: add-workflows
env:
Expand Down
193 changes: 131 additions & 62 deletions pkg/cli/list_workflows_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,32 +30,40 @@ func NewListCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list [pattern]",
Short: "List agentic workflows in the repository",
Long: `List all agentic workflows in the repository without checking their status.
Long: `List all agentic workflows in a repository without checking their status.

Displays a simplified table with workflow name, AI engine, and compilation status.
Unlike 'status', this command does not check GitHub workflow state or time remaining.

The optional pattern argument filters workflows by name (case-insensitive substring match).

Examples:
` + string(constants.CLIExtensionPrefix) + ` list # List all workflows
` + string(constants.CLIExtensionPrefix) + ` list ci- # List workflows with 'ci-' in name
` + string(constants.CLIExtensionPrefix) + ` list --json # Output in JSON format
` + string(constants.CLIExtensionPrefix) + ` list --label automation # List workflows with 'automation' label`,
` + string(constants.CLIExtensionPrefix) + ` list # List all workflows in current repo
` + string(constants.CLIExtensionPrefix) + ` list --repo github/gh-aw # List workflows from github/gh-aw repo
` + string(constants.CLIExtensionPrefix) + ` list --repo org/repo --path workflows # List from custom path
` + string(constants.CLIExtensionPrefix) + ` list ci- # List workflows with 'ci-' in name
` + string(constants.CLIExtensionPrefix) + ` list --repo github/gh-aw ci- # List workflows from github/gh-aw with 'ci-' in name
` + string(constants.CLIExtensionPrefix) + ` list --json # Output in JSON format
` + string(constants.CLIExtensionPrefix) + ` list --label automation # List workflows with 'automation' label`,
RunE: func(cmd *cobra.Command, args []string) error {
var pattern string
if len(args) > 0 {
pattern = args[0]
}

repo, _ := cmd.Flags().GetString("repo")
path, _ := cmd.Flags().GetString("path")
verbose, _ := cmd.Flags().GetBool("verbose")
jsonFlag, _ := cmd.Flags().GetBool("json")
labelFilter, _ := cmd.Flags().GetString("label")
return RunListWorkflows(pattern, verbose, jsonFlag, labelFilter)
return RunListWorkflows(repo, path, pattern, verbose, jsonFlag, labelFilter)
},
}

addRepoFlag(cmd)
addJSONFlag(cmd)
cmd.Flags().String("label", "", "Filter workflows by label")
cmd.Flags().String("path", ".github/workflows", "Path to workflows directory in the repository")

// Register completions for list command
cmd.ValidArgsFunction = CompleteWorkflowNames
Expand All @@ -64,16 +72,31 @@ Examples:
}

// RunListWorkflows lists workflows without checking GitHub status
func RunListWorkflows(pattern string, verbose bool, jsonOutput bool, labelFilter string) error {
listWorkflowsLog.Printf("Listing workflows: pattern=%s, jsonOutput=%v, labelFilter=%s", pattern, jsonOutput, labelFilter)
if verbose && !jsonOutput {
fmt.Fprintf(os.Stderr, "Listing workflow files\n")
if pattern != "" {
fmt.Fprintf(os.Stderr, "Filtering by pattern: %s\n", pattern)
func RunListWorkflows(repo, path, pattern string, verbose bool, jsonOutput bool, labelFilter string) error {
listWorkflowsLog.Printf("Listing workflows: repo=%s, path=%s, pattern=%s, jsonOutput=%v, labelFilter=%s", repo, path, pattern, jsonOutput, labelFilter)

var mdFiles []string
var err error
var isRemote bool

if repo != "" {
// List workflows from remote repository
isRemote = true
if verbose && !jsonOutput {
fmt.Fprintf(os.Stderr, "Listing workflow files from %s\n", repo)
}
mdFiles, err = getRemoteWorkflowFiles(repo, path, verbose, jsonOutput)
} else {
// List workflows from local repository
if verbose && !jsonOutput {
fmt.Fprintf(os.Stderr, "Listing workflow files\n")
if pattern != "" {
fmt.Fprintf(os.Stderr, "Filtering by pattern: %s\n", pattern)
}
}
mdFiles, err = getMarkdownWorkflowFiles("")
}

mdFiles, err := getMarkdownWorkflowFiles("")
if err != nil {
listWorkflowsLog.Printf("Failed to get markdown workflow files: %v", err)
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
Expand Down Expand Up @@ -108,67 +131,80 @@ func RunListWorkflows(pattern string, verbose bool, jsonOutput bool, labelFilter
continue
}

// Extract engine ID from workflow file
agent := extractEngineIDFromFile(file)

// Check if compiled (.lock.yml file is in .github/workflows)
lockFile := stringutil.MarkdownToLockFile(file)
compiled := "N/A"

if _, err := os.Stat(lockFile); err == nil {
// Check if up to date
mdStat, _ := os.Stat(file)
lockStat, _ := os.Stat(lockFile)
if mdStat.ModTime().After(lockStat.ModTime()) {
compiled = "No"
} else {
compiled = "Yes"
// For remote repos, we can't check compilation status or read local files
if isRemote {
// For remote repos, skip fetching individual file metadata to avoid slowness
// Just show file name with minimal info
workflows = append(workflows, WorkflowListItem{
Workflow: name,
EngineID: "N/A", // Skip fetching to avoid slow API/git calls
Compiled: "N/A", // Cannot determine for remote repos
Labels: nil,
On: nil,
})
Comment on lines +135 to +144
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The label filter (--label flag) does not work for remote repositories. When isRemote is true, all workflows are added with Labels: nil and the label filtering check at line 183-194 is skipped. This means workflows from remote repositories will be shown even when they don't have the requested label.

Consider either:

  1. Documenting in the command help that --label filtering is not supported for remote repositories
  2. Checking the label filter early and returning an error if used with --repo
  3. Implementing label extraction for remote repositories (though this would require fetching each file)

This issue also appears on line 264 of the same file.

Copilot uses AI. Check for mistakes.
} else {
// Extract engine ID from workflow file
agent := extractEngineIDFromFile(file)

// Check if compiled (.lock.yml file is in .github/workflows)
lockFile := stringutil.MarkdownToLockFile(file)
compiled := "N/A"

if _, err := os.Stat(lockFile); err == nil {
// Check if up to date
mdStat, _ := os.Stat(file)
lockStat, _ := os.Stat(lockFile)
if mdStat.ModTime().After(lockStat.ModTime()) {
compiled = "No"
} else {
compiled = "Yes"
}
}
}

// Extract "on" field and labels from frontmatter
var onField any
var labels []string
if content, err := os.ReadFile(file); err == nil {
if result, err := parser.ExtractFrontmatterFromContent(string(content)); err == nil {
if result.Frontmatter != nil {
onField = result.Frontmatter["on"]
// Extract labels field if present
if labelsField, ok := result.Frontmatter["labels"]; ok {
if labelsArray, ok := labelsField.([]any); ok {
for _, label := range labelsArray {
if labelStr, ok := label.(string); ok {
labels = append(labels, labelStr)
// Extract "on" field and labels from frontmatter
var onField any
var labels []string
if content, err := os.ReadFile(file); err == nil {
if result, err := parser.ExtractFrontmatterFromContent(string(content)); err == nil {
if result.Frontmatter != nil {
onField = result.Frontmatter["on"]
// Extract labels field if present
if labelsField, ok := result.Frontmatter["labels"]; ok {
if labelsArray, ok := labelsField.([]any); ok {
for _, label := range labelsArray {
if labelStr, ok := label.(string); ok {
labels = append(labels, labelStr)
}
}
}
}
}
}
}
}

// Skip if label filter specified and workflow doesn't have the label
if labelFilter != "" {
hasLabel := false
for _, label := range labels {
if strings.EqualFold(label, labelFilter) {
hasLabel = true
break
// Skip if label filter specified and workflow doesn't have the label
if labelFilter != "" {
hasLabel := false
for _, label := range labels {
if strings.EqualFold(label, labelFilter) {
hasLabel = true
break
}
}
if !hasLabel {
continue
}
}
if !hasLabel {
continue
}
}

// Build workflow list item
workflows = append(workflows, WorkflowListItem{
Workflow: name,
EngineID: agent,
Compiled: compiled,
Labels: labels,
On: onField,
})
// Build workflow list item
workflows = append(workflows, WorkflowListItem{
Workflow: name,
EngineID: agent,
Compiled: compiled,
Labels: labels,
On: onField,
})
}
}

// Output results
Expand All @@ -194,3 +230,36 @@ func RunListWorkflows(pattern string, verbose bool, jsonOutput bool, labelFilter

return nil
}

// getRemoteWorkflowFiles fetches the list of workflow files from a remote repository
func getRemoteWorkflowFiles(repoSpec, workflowPath string, verbose bool, jsonOutput bool) ([]string, error) {
// Parse repo spec: owner/repo[@ref]
var owner, repo, ref string
parts := strings.SplitN(repoSpec, "@", 2)
repoPart := parts[0]
if len(parts) == 2 {
ref = parts[1]
} else {
ref = "main" // default to main branch
}

// Parse owner/repo
repoParts := strings.Split(repoPart, "/")
if len(repoParts) != 2 {
return nil, fmt.Errorf("invalid repository format: %s (expected owner/repo or owner/repo@ref)", repoSpec)
}
owner = repoParts[0]
repo = repoParts[1]

if verbose && !jsonOutput {
fmt.Fprintf(os.Stderr, "Fetching workflow files from %s/%s@%s (path: %s)\n", owner, repo, ref, workflowPath)
}

// Use the parser package to list workflow files
files, err := parser.ListWorkflowFiles(owner, repo, ref, workflowPath)
if err != nil {
return nil, fmt.Errorf("failed to list workflow files from %s/%s: %w", owner, repo, err)
}

return files, nil
}
10 changes: 5 additions & 5 deletions pkg/cli/list_workflows_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,19 @@ func TestRunListWorkflows_JSONOutput(t *testing.T) {

// Test JSON output without pattern
t.Run("JSON output without pattern", func(t *testing.T) {
err := RunListWorkflows("", false, true, "")
err := RunListWorkflows("", ".github/workflows", "", false, true, "")
assert.NoError(t, err, "RunListWorkflows with JSON flag should not error")
})

// Test JSON output with pattern
t.Run("JSON output with pattern", func(t *testing.T) {
err := RunListWorkflows("smoke", false, true, "")
err := RunListWorkflows("", ".github/workflows", "smoke", false, true, "")
assert.NoError(t, err, "RunListWorkflows with JSON flag and pattern should not error")
})

// Test JSON output with label filter
t.Run("JSON output with label filter", func(t *testing.T) {
err := RunListWorkflows("", false, true, "test")
err := RunListWorkflows("", ".github/workflows", "", false, true, "test")
assert.NoError(t, err, "RunListWorkflows with JSON flag and label filter should not error")
})
}
Expand Down Expand Up @@ -93,13 +93,13 @@ func TestRunListWorkflows_TextOutput(t *testing.T) {

// Test text output
t.Run("Text output without pattern", func(t *testing.T) {
err := RunListWorkflows("", false, false, "")
err := RunListWorkflows("", ".github/workflows", "", false, false, "")
assert.NoError(t, err, "RunListWorkflows without JSON flag should not error")
})

// Test text output with pattern
t.Run("Text output with pattern", func(t *testing.T) {
err := RunListWorkflows("ci-", false, false, "")
err := RunListWorkflows("", ".github/workflows", "ci-", false, false, "")
assert.NoError(t, err, "RunListWorkflows with pattern should not error")
})
}
Expand Down
Loading
Loading