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
4 changes: 2 additions & 2 deletions pkg/cli/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ func TestRemoveWorkflows(t *testing.T) {
}

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

// Should not error since it's a stub implementation
if err != nil {
Expand Down Expand Up @@ -422,7 +422,7 @@ Test workflow for command existence.`
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 StatusWorkflows("nonexistent", false, false, "", "") }, false, "StatusWorkflows"}, // Should handle missing directory gracefully
{func() error { return EnableWorkflows("nonexistent") }, true, "EnableWorkflows"}, // Should now error when no workflows found to enable
{func() error { return DisableWorkflows("nonexistent") }, true, "DisableWorkflows"}, // Should now also error when no workflows found to disable
{func() error { return RunWorkflowOnGitHub("", false, "", "", "", false, false, false, false) }, true, "RunWorkflowOnGitHub"}, // Should error with empty workflow name
Expand Down
7 changes: 5 additions & 2 deletions pkg/cli/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ Examples:
` + constants.CLIExtensionPrefix + ` status # Show all workflow status
` + constants.CLIExtensionPrefix + ` status ci- # Show workflows with 'ci-' in name
` + constants.CLIExtensionPrefix + ` status --json # Output in JSON format
` + constants.CLIExtensionPrefix + ` status --ref main # Show latest run status for main branch`,
` + constants.CLIExtensionPrefix + ` status --ref main # Show latest run status for main branch
` + constants.CLIExtensionPrefix + ` status --label automation # Show workflows with 'automation' label`,
RunE: func(cmd *cobra.Command, args []string) error {
var pattern string
if len(args) > 0 {
Expand All @@ -30,12 +31,14 @@ Examples:
verbose, _ := cmd.Flags().GetBool("verbose")
jsonFlag, _ := cmd.Flags().GetBool("json")
ref, _ := cmd.Flags().GetString("ref")
return StatusWorkflows(pattern, verbose, jsonFlag, ref)
labelFilter, _ := cmd.Flags().GetString("label")
return StatusWorkflows(pattern, verbose, jsonFlag, ref, labelFilter)
},
}

addJSONFlag(cmd)
cmd.Flags().String("ref", "", "Filter runs by branch or tag name (e.g., main, v1.0.0)")
cmd.Flags().String("label", "", "Filter workflows by label")

// Register completions for status command
cmd.ValidArgsFunction = CompleteWorkflowNames
Expand Down
82 changes: 71 additions & 11 deletions pkg/cli/status_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,19 @@ var statusLog = logger.New("cli:status_command")

// WorkflowStatus represents the status of a single workflow for JSON output
type WorkflowStatus struct {
Workflow string `json:"workflow" console:"header:Workflow"`
EngineID string `json:"engine_id" console:"header:Engine"`
Compiled string `json:"compiled" console:"header:Compiled"`
Status string `json:"status" console:"header:Status"`
TimeRemaining string `json:"time_remaining" console:"header:Time Remaining"`
On any `json:"on,omitempty" console:"-"`
RunStatus string `json:"run_status,omitempty" console:"header:Run Status,omitempty"`
RunConclusion string `json:"run_conclusion,omitempty" console:"header:Run Conclusion,omitempty"`
Workflow string `json:"workflow" console:"header:Workflow"`
EngineID string `json:"engine_id" console:"header:Engine"`
Compiled string `json:"compiled" console:"header:Compiled"`
Status string `json:"status" console:"header:Status"`
TimeRemaining string `json:"time_remaining" console:"header:Time Remaining"`
Labels []string `json:"labels,omitempty" console:"header:Labels,omitempty"`
On any `json:"on,omitempty" console:"-"`
RunStatus string `json:"run_status,omitempty" console:"header:Run Status,omitempty"`
RunConclusion string `json:"run_conclusion,omitempty" console:"header:Run Conclusion,omitempty"`
}

func StatusWorkflows(pattern string, verbose bool, jsonOutput bool, ref string) error {
statusLog.Printf("Checking workflow status: pattern=%s, jsonOutput=%v, ref=%s", pattern, jsonOutput, ref)
func StatusWorkflows(pattern string, verbose bool, jsonOutput bool, ref string, labelFilter string) error {
statusLog.Printf("Checking workflow status: pattern=%s, jsonOutput=%v, ref=%s, labelFilter=%s", pattern, jsonOutput, ref, labelFilter)
if verbose && !jsonOutput {
fmt.Printf("Checking status of workflow files\n")
if pattern != "" {
Expand Down Expand Up @@ -154,16 +155,41 @@ func StatusWorkflows(pattern string, verbose bool, jsonOutput bool, ref string)
}
}

// Extract "on" field from frontmatter for JSON output
// Extract "on" field and labels from frontmatter for JSON output
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
}
}
if !hasLabel {
continue
}
}

// Get run status for ref if available
var runStatus, runConclusion string
if latestRunsByWorkflow != nil {
Expand All @@ -180,6 +206,7 @@ func StatusWorkflows(pattern string, verbose bool, jsonOutput bool, ref string)
Compiled: compiled,
Status: status,
TimeRemaining: timeRemaining,
Labels: labels,
On: onField,
RunStatus: runStatus,
RunConclusion: runConclusion,
Expand Down Expand Up @@ -250,13 +277,46 @@ func StatusWorkflows(pattern string, verbose bool, jsonOutput bool, ref string)
}
}

// Extract labels from frontmatter
var labels []string
if content, err := os.ReadFile(file); err == nil {
if result, err := parser.ExtractFrontmatterFromContent(string(content)); err == nil {
if result.Frontmatter != nil {
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
}
}
if !hasLabel {
continue
}
}

// Build status object
statuses = append(statuses, WorkflowStatus{
Workflow: name,
EngineID: agent,
Compiled: compiled,
Status: status,
TimeRemaining: timeRemaining,
Labels: labels,
RunStatus: runStatus,
RunConclusion: runConclusion,
})
Expand Down
72 changes: 70 additions & 2 deletions pkg/cli/status_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,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("", false, true, "", "")
if err != nil {
t.Errorf("StatusWorkflows with JSON flag failed: %v", err)
}
Expand All @@ -38,7 +38,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("smoke", false, true, "", "")
if err != nil {
t.Errorf("StatusWorkflows with JSON flag and pattern failed: %v", err)
}
Expand Down Expand Up @@ -400,6 +400,74 @@ func TestWorkflowStatus_JSONMarshalingWithEmptyRunStatus(t *testing.T) {
}
}

// TestWorkflowStatus_JSONMarshalingWithLabels tests that labels are included in JSON output
func TestWorkflowStatus_JSONMarshalingWithLabels(t *testing.T) {
// Test that WorkflowStatus with labels can be marshaled to JSON
status := WorkflowStatus{
Workflow: "test-workflow",
EngineID: "copilot",
Compiled: "Yes",
Status: "active",
TimeRemaining: "N/A",
Labels: []string{"automation", "testing"},
}

jsonBytes, err := json.Marshal(status)
if err != nil {
t.Fatalf("Failed to marshal WorkflowStatus: %v", err)
}

// Verify JSON contains labels field
var unmarshaled map[string]any
if err := json.Unmarshal(jsonBytes, &unmarshaled); err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}

labels, ok := unmarshaled["labels"].([]any)
if !ok {
t.Fatalf("Expected labels to be an array, got %T", unmarshaled["labels"])
}

if len(labels) != 2 {
t.Errorf("Expected 2 labels, got %d", len(labels))
}

if labels[0] != "automation" {
t.Errorf("Expected first label to be 'automation', got %v", labels[0])
}
if labels[1] != "testing" {
t.Errorf("Expected second label to be 'testing', got %v", labels[1])
}
}

// TestWorkflowStatus_JSONMarshalingWithEmptyLabels tests that empty labels are omitted
func TestWorkflowStatus_JSONMarshalingWithEmptyLabels(t *testing.T) {
// Test that WorkflowStatus without labels omits the field
status := WorkflowStatus{
Workflow: "test-workflow",
EngineID: "copilot",
Compiled: "Yes",
Status: "active",
TimeRemaining: "N/A",
// Labels is empty/nil
}

jsonBytes, err := json.Marshal(status)
if err != nil {
t.Fatalf("Failed to marshal WorkflowStatus: %v", err)
}

// Verify JSON omits empty labels field (due to omitempty)
var unmarshaled map[string]any
if err := json.Unmarshal(jsonBytes, &unmarshaled); err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}

if _, exists := unmarshaled["labels"]; exists {
t.Errorf("Expected labels to be omitted when empty, but it was present with value: %v", unmarshaled["labels"])
}
}

// TestWorkflowStatus_ConsoleRenderingWithRunStatus tests that RunStatus and RunConclusion are rendered when present
func TestWorkflowStatus_ConsoleRenderingWithRunStatus(t *testing.T) {
// Create test data with run status
Expand Down
13 changes: 13 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@
"description": "Optional tracker identifier to tag all created assets (issues, discussions, comments, pull requests). Must be at least 8 characters and contain only alphanumeric characters, hyphens, and underscores. This identifier will be inserted in the body/description of all created assets to enable searching and retrieving assets associated with this workflow.",
"examples": ["workflow-2024-q1", "team-alpha-bot", "security_audit_v2"]
},
"labels": {
"type": "array",
"description": "Optional array of labels to categorize and organize workflows. Labels can be used to filter workflows in status/list commands.",
"items": {
"type": "string",
"minLength": 1
},
"examples": [
["automation", "security"],
["docs", "maintenance"],
["ci", "testing"]
]
},
"imports": {
"type": "array",
"description": "Optional array of workflow specifications to import (similar to @include directives but defined in frontmatter). Format: owner/repo/path@ref (e.g., githubnext/agentics/workflows/shared/common.md@v1.0.0). Can be strings or objects with path and inputs. Any markdown files under .github/agents directory are treated as custom agent files and only one agent file is allowed per workflow.",
Expand Down