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
76 changes: 76 additions & 0 deletions cmd/workflow_approve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cmd

import (
"fmt"
"strings"

"github.com/nudgebee/nbctl/pkg/client"
"github.com/nudgebee/nbctl/pkg/format"
"github.com/spf13/cobra"
)

var workflowApproveCmd = &cobra.Command{
Use: "approve <execution-id>",
Short: "Complete a human approval gate for a pending workflow execution",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
executionID := strings.TrimSpace(args[0])
if executionID == "" {
return fmt.Errorf("execution-id cannot be empty")
}
taskID, _ := cmd.Flags().GetString("task")
taskID = strings.TrimSpace(taskID)
if taskID == "" {
return fmt.Errorf("task flag is required and cannot be empty")
}

reject, _ := cmd.Flags().GetBool("reject")
comments, _ := cmd.Flags().GetString("comments")
Comment thread
blue4209211 marked this conversation as resolved.
Comment thread
blue4209211 marked this conversation as resolved.
comments = strings.TrimSpace(comments)

status := "approved"
if reject {
status = "rejected"
}

req := client.NewRequest(`
mutation CompleteWorkflowApproval($request: WorkflowCompleteApprovalRequest!) {
workflow_complete_approval(request: $request) {
status
message
}
}
`)
input := map[string]any{
"execution_id": executionID,
"task_id": taskID,
"status": status,
}
if comments != "" {
input["comments"] = comments
}
req.Var("request", input)

var respData struct {
WorkflowCompleteApproval struct {
Status string `json:"status"`
Message string `json:"message"`
} `json:"workflow_complete_approval"`
}

if err := client.Run(cmd.Context(), req, &respData); err != nil {
return err
}

format.GetFormat().Print(respData.WorkflowCompleteApproval)
return nil
},
}

func init() {
workflowCmd.AddCommand(workflowApproveCmd)
workflowApproveCmd.Flags().String("task", "", "Task ID waiting for approval (required)")
workflowApproveCmd.Flags().Bool("reject", false, "Reject the approval step instead of approving")
workflowApproveCmd.Flags().String("comments", "", "Optional comments for the approval decision")
_ = workflowApproveCmd.MarkFlagRequired("task")
}
53 changes: 53 additions & 0 deletions cmd/workflow_replay.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package cmd

import (
"fmt"
"strings"

"github.com/nudgebee/nbctl/pkg/client"
"github.com/nudgebee/nbctl/pkg/format"
"github.com/spf13/cobra"
)

var workflowReplayCmd = &cobra.Command{
Use: "replay <execution-id>",
Short: "Replay a previous or failed workflow execution",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
executionID := strings.TrimSpace(args[0])
if executionID == "" {
return fmt.Errorf("execution-id cannot be empty")
}
req := client.NewRequest(`
mutation ReplayWorkflowExecution($request: WorkflowRetriggerRequest!) {
workflow_replay_execution(request: $request) {
execution_id
status
message
}
}
`)
req.Var("request", map[string]any{
"execution_id": executionID,
})

var respData struct {
WorkflowReplayExecution struct {
ExecutionID string `json:"execution_id"`
Status string `json:"status"`
Message string `json:"message"`
} `json:"workflow_replay_execution"`
}

if err := client.Run(cmd.Context(), req, &respData); err != nil {
return err
}

format.GetFormat().Print(respData.WorkflowReplayExecution)
return nil
},
}

func init() {
workflowCmd.AddCommand(workflowReplayCmd)
}
90 changes: 90 additions & 0 deletions cmd/workflow_task_definitions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package cmd

import (
"strings"

"github.com/nudgebee/nbctl/pkg/client"
"github.com/nudgebee/nbctl/pkg/format"
"github.com/spf13/cobra"
)
Comment thread
blue4209211 marked this conversation as resolved.

var workflowTaskDefinitionsCmd = &cobra.Command{
Use: "task-definitions",
Short: "List supported workflow task definitions and action schemas",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
Comment thread
blue4209211 marked this conversation as resolved.
nameFilter, _ := cmd.Flags().GetString("name")
nameFilter = strings.TrimSpace(nameFilter)
limit, _ := cmd.Flags().GetInt("limit")
Comment thread
blue4209211 marked this conversation as resolved.

req := client.NewRequest(`
query ListWorkflowTaskDefinitions($params: WorkflowTaskDefinitionListRequest!) {
workflow_list_taskdefinitions(params: $params) {
tasks {
name
description
aliases
}
}
}
`)

params := map[string]any{
"limit": limit,
}
if nameFilter != "" {
params["name"] = nameFilter
}
req.Var("params", params)

var respData struct {
WorkflowListTaskdefinitions struct {
Tasks []struct {
Name string `json:"name"`
Description string `json:"description"`
Aliases []string `json:"aliases"`
} `json:"tasks"`
} `json:"workflow_list_taskdefinitions"`
}

if err := client.Run(cmd.Context(), req, &respData); err != nil {
return err
}

type taskRow struct {
Name string `json:"name"`
Description string `json:"description"`
Aliases string `json:"aliases"`
}
rows := make([]taskRow, 0, len(respData.WorkflowListTaskdefinitions.Tasks))
for _, t := range respData.WorkflowListTaskdefinitions.Tasks {
aliasesStr := "-"
if len(t.Aliases) > 0 {
aliasesStr = strings.Join(t.Aliases, ", ")
}
rows = append(rows, taskRow{
Name: t.Name,
Description: t.Description,
Aliases: aliasesStr,
})
}

table := format.TabularData{
Data: rows,
Fields: []format.TableField{
{Header: "Task Name", Field: "Name"},
{Header: "Description", Field: "Description"},
{Header: "Aliases", Field: "Aliases"},
},
}
format.GetFormat().Print(table)

return nil
},
}

func init() {
workflowCmd.AddCommand(workflowTaskDefinitionsCmd)
workflowTaskDefinitionsCmd.Flags().String("name", "", "Filter task definitions by name")
workflowTaskDefinitionsCmd.Flags().Int("limit", 100, "Maximum number of task definitions to return")
}
158 changes: 158 additions & 0 deletions cmd/workflow_templates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package cmd

import (
"fmt"
"strings"

"github.com/nudgebee/nbctl/pkg/client"
"github.com/nudgebee/nbctl/pkg/format"
"github.com/spf13/cobra"
)

type workflowTemplate struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
IsSystem bool `json:"is_system"`
Status string `json:"status"`
}

var workflowTemplatesCmd = &cobra.Command{
Use: "templates",
Short: "Browse and inspect pre-built workflow templates",
}

var workflowTemplatesListCmd = &cobra.Command{
Use: "list",
Short: "List pre-built workflow templates",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
Comment thread
blue4209211 marked this conversation as resolved.
typeFlag, _ := cmd.Flags().GetString("type")
Comment thread
blue4209211 marked this conversation as resolved.
typeFlag = strings.ToLower(typeFlag)
category, _ := cmd.Flags().GetString("category")
category = strings.TrimSpace(category)
limit, _ := cmd.Flags().GetInt("limit")
Comment thread
blue4209211 marked this conversation as resolved.

if typeFlag != "system" && typeFlag != "custom" && typeFlag != "all" {
return fmt.Errorf("invalid template type '%s': must be 'system', 'custom', or 'all'", typeFlag)
}
Comment thread
blue4209211 marked this conversation as resolved.

req := client.NewRequest(`
query ListWorkflowTemplates($request: WorkflowListTemplateRequest!) {
workflow_list_template(request: $request) {
total_count
templates {
id
name
description
category
is_system
status
}
}
}
`)

input := map[string]any{
"type": typeFlag,
"limit": limit,
}
if category != "" {
input["category"] = category
}
req.Var("request", input)
Comment thread
blue4209211 marked this conversation as resolved.

var respData struct {
WorkflowListTemplate struct {
TotalCount int `json:"total_count"`
Templates []workflowTemplate `json:"templates"`
} `json:"workflow_list_template"`
}

if err := client.Run(cmd.Context(), req, &respData); err != nil {
return err
}

table := format.TabularData{
Data: respData.WorkflowListTemplate.Templates,
Fields: []format.TableField{
{Header: "Template ID", Field: "ID"},
{Header: "Template Name", Field: "Name"},
{Header: "Category", Field: "Category"},
{Header: "System", Field: "IsSystem"},
{Header: "Description", Field: "Description"},
},
}
format.GetFormat().Print(table)

return nil
},
}

var workflowTemplatesGetCmd = &cobra.Command{
Use: "get <template-id>",
Short: "Get details for a specific workflow template",
Args: cobra.ExactArgs(1),
Comment thread
blue4209211 marked this conversation as resolved.
RunE: func(cmd *cobra.Command, args []string) error {
templateID := strings.TrimSpace(args[0])
if templateID == "" {
return fmt.Errorf("template-id cannot be empty")
}
typeFlag, _ := cmd.Flags().GetString("type")
Comment thread
blue4209211 marked this conversation as resolved.
typeFlag = strings.ToLower(typeFlag)

if typeFlag != "" && typeFlag != "system" && typeFlag != "custom" && typeFlag != "all" {
return fmt.Errorf("invalid template type '%s': must be 'system', 'custom', or 'all'", typeFlag)
}
Comment thread
blue4209211 marked this conversation as resolved.

typeVal := typeFlag
if typeVal == "" {
typeVal = "all"
}

req := client.NewRequest(`
query GetWorkflowTemplate($request: WorkflowGetTemplateRequest!) {
workflow_get_template(request: $request) {
id
name
description
category
is_system
status
}
}
`)
req.Var("request", map[string]any{
"type": typeVal,
"id": templateID,
})

var respData struct {
WorkflowGetTemplate *workflowTemplate `json:"workflow_get_template"`
}
Comment thread
blue4209211 marked this conversation as resolved.

Comment thread
blue4209211 marked this conversation as resolved.
if err := client.Run(cmd.Context(), req, &respData); err != nil {
return err
}
Comment thread
blue4209211 marked this conversation as resolved.

if respData.WorkflowGetTemplate == nil || respData.WorkflowGetTemplate.ID == "" {
return fmt.Errorf("workflow template '%s' not found", templateID)
}
Comment thread
blue4209211 marked this conversation as resolved.
Comment thread
blue4209211 marked this conversation as resolved.
Comment thread
blue4209211 marked this conversation as resolved.

format.GetFormat().Print(*respData.WorkflowGetTemplate)
Comment thread
blue4209211 marked this conversation as resolved.
return nil
},
}

func init() {
workflowCmd.AddCommand(workflowTemplatesCmd)
workflowTemplatesCmd.AddCommand(workflowTemplatesListCmd)
workflowTemplatesCmd.AddCommand(workflowTemplatesGetCmd)

workflowTemplatesListCmd.Flags().String("type", "system", "Template type (system, custom, or all)")
workflowTemplatesListCmd.Flags().String("category", "", "Filter templates by category")
workflowTemplatesListCmd.Flags().Int("limit", 50, "Maximum number of templates to return")

workflowTemplatesGetCmd.Flags().String("type", "", "Optional template type filter (system, custom, or all)")
}