fix(update): accept custom types from types.custom config (GH#hq-8hif1z)
The `bd update --type` command was rejecting custom types (like role, agent, rig) even when configured via types.custom. The issue was that type validation used IsValid() which only checks the 5 core types, ignoring custom types. Changes: - CLI (update.go): Use IsValidWithCustom() with types from config - Storage (validators.go): Add validateIssueTypeWithCustom() function - Storage (queries.go, transaction.go): Fetch and pass custom types The error message now dynamically shows all valid types including custom ones. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,9 +4,11 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/steveyegge/beads/internal/config"
|
||||||
"github.com/steveyegge/beads/internal/hooks"
|
"github.com/steveyegge/beads/internal/hooks"
|
||||||
"github.com/steveyegge/beads/internal/rpc"
|
"github.com/steveyegge/beads/internal/rpc"
|
||||||
"github.com/steveyegge/beads/internal/timeparsing"
|
"github.com/steveyegge/beads/internal/timeparsing"
|
||||||
@@ -113,8 +115,12 @@ create, update, show, or close operation).`,
|
|||||||
issueType, _ := cmd.Flags().GetString("type")
|
issueType, _ := cmd.Flags().GetString("type")
|
||||||
// Normalize aliases (e.g., "enhancement" -> "feature") before validating
|
// Normalize aliases (e.g., "enhancement" -> "feature") before validating
|
||||||
issueType = util.NormalizeIssueType(issueType)
|
issueType = util.NormalizeIssueType(issueType)
|
||||||
if !types.IssueType(issueType).IsValid() {
|
// Get custom types from config to validate non-core types (GH#hq-8hif1z)
|
||||||
FatalErrorRespectJSON("invalid issue type %q. Valid types: bug, feature, task, epic, chore, merge-request, molecule, gate, agent, role, rig, convoy, event, slot", issueType)
|
customTypes := getCustomTypesForValidation()
|
||||||
|
if !types.IssueType(issueType).IsValidWithCustom(customTypes) {
|
||||||
|
validTypes := []string{"bug", "feature", "task", "epic", "chore"}
|
||||||
|
validTypes = append(validTypes, customTypes...)
|
||||||
|
FatalErrorRespectJSON("invalid issue type %q. Valid types: %s", issueType, strings.Join(validTypes, ", "))
|
||||||
}
|
}
|
||||||
updates["issue_type"] = issueType
|
updates["issue_type"] = issueType
|
||||||
}
|
}
|
||||||
@@ -643,6 +649,46 @@ create, update, show, or close operation).`,
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getCustomTypesForValidation retrieves custom types for validation.
|
||||||
|
// Works in both daemon and direct modes, with fallback to config.yaml.
|
||||||
|
func getCustomTypesForValidation() []string {
|
||||||
|
ctx := rootCtx
|
||||||
|
|
||||||
|
// Try daemon mode first
|
||||||
|
if daemonClient != nil {
|
||||||
|
resp, err := daemonClient.GetConfig(&rpc.GetConfigArgs{Key: "types.custom"})
|
||||||
|
if err == nil && resp.Value != "" {
|
||||||
|
return parseCustomTypesList(resp.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct mode - use store
|
||||||
|
if store != nil {
|
||||||
|
if ct, err := store.GetCustomTypes(ctx); err == nil && len(ct) > 0 {
|
||||||
|
return ct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to config.yaml
|
||||||
|
return config.GetCustomTypesFromYAML()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCustomTypesList splits a comma-separated string into custom types.
|
||||||
|
func parseCustomTypesList(value string) []string {
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parts := strings.Split(value, ",")
|
||||||
|
result := make([]string, 0, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
trimmed := strings.TrimSpace(p)
|
||||||
|
if trimmed != "" {
|
||||||
|
result = append(result, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
updateCmd.Flags().StringP("status", "s", "", "New status")
|
updateCmd.Flags().StringP("status", "s", "", "New status")
|
||||||
registerPriorityFlag(updateCmd, "")
|
registerPriorityFlag(updateCmd, "")
|
||||||
|
|||||||
@@ -887,12 +887,11 @@ func (s *SQLiteStorage) UpdateIssue(ctx context.Context, id string, updates map[
|
|||||||
return fmt.Errorf("issue %s not found", id)
|
return fmt.Errorf("issue %s not found", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch custom statuses and types for validation
|
// Fetch custom statuses and types for validation (GH#hq-8hif1z)
|
||||||
customStatuses, err := s.GetCustomStatuses(ctx)
|
customStatuses, err := s.GetCustomStatuses(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wrapDBError("get custom statuses", err)
|
return wrapDBError("get custom statuses", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
customTypes, err := s.GetCustomTypes(ctx)
|
customTypes, err := s.GetCustomTypes(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wrapDBError("get custom types", err)
|
return wrapDBError("get custom types", err)
|
||||||
|
|||||||
@@ -400,12 +400,11 @@ func (t *sqliteTxStorage) UpdateIssue(ctx context.Context, id string, updates ma
|
|||||||
return fmt.Errorf("issue %s not found", id)
|
return fmt.Errorf("issue %s not found", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch custom statuses and types for validation
|
// Fetch custom statuses and types for validation (GH#hq-8hif1z)
|
||||||
customStatuses, err := t.GetCustomStatuses(ctx)
|
customStatuses, err := t.GetCustomStatuses(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get custom statuses: %w", err)
|
return fmt.Errorf("failed to get custom statuses: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
customTypes, err := t.GetCustomTypes(ctx)
|
customTypes, err := t.GetCustomTypes(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get custom types: %w", err)
|
return fmt.Errorf("failed to get custom types: %w", err)
|
||||||
|
|||||||
@@ -37,11 +37,17 @@ func validateStatusWithCustom(value interface{}, customStatuses []string) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateIssueType validates an issue type value
|
// validateIssueType validates an issue type value (built-in types only)
|
||||||
func validateIssueType(value interface{}) error {
|
func validateIssueType(value interface{}) error {
|
||||||
|
return validateIssueTypeWithCustom(value, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateIssueTypeWithCustom validates an issue type value, allowing custom types.
|
||||||
|
// Custom types are configured via bd config set types.custom "type1,type2,..."
|
||||||
|
func validateIssueTypeWithCustom(value interface{}, customTypes []string) error {
|
||||||
if issueType, ok := value.(string); ok {
|
if issueType, ok := value.(string); ok {
|
||||||
// Normalize first to support aliases like "enhancement" -> "feature"
|
// Normalize first to support aliases like "enhancement" -> "feature"
|
||||||
if !types.IssueType(issueType).Normalize().IsValid() {
|
if !types.IssueType(issueType).Normalize().IsValidWithCustom(customTypes) {
|
||||||
return fmt.Errorf("invalid issue type: %s", issueType)
|
return fmt.Errorf("invalid issue type: %s", issueType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,19 +83,26 @@ var fieldValidators = map[string]func(interface{}) error{
|
|||||||
"estimated_minutes": validateEstimatedMinutes,
|
"estimated_minutes": validateEstimatedMinutes,
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateFieldUpdate validates a field update value (built-in statuses only)
|
// validateFieldUpdate validates a field update value (built-in statuses and types only)
|
||||||
func validateFieldUpdate(key string, value interface{}) error {
|
func validateFieldUpdate(key string, value interface{}) error {
|
||||||
return validateFieldUpdateWithCustomStatuses(key, value, nil)
|
return validateFieldUpdateWithCustom(key, value, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateFieldUpdateWithCustomStatuses validates a field update value,
|
||||||
|
// allowing custom statuses for status field validation.
|
||||||
|
// DEPRECATED: Use validateFieldUpdateWithCustom instead which also handles custom types.
|
||||||
|
func validateFieldUpdateWithCustomStatuses(key string, value interface{}, customStatuses []string) error {
|
||||||
|
return validateFieldUpdateWithCustom(key, value, customStatuses, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateFieldUpdateWithCustom validates a field update value,
|
// validateFieldUpdateWithCustom validates a field update value,
|
||||||
// allowing custom statuses and custom types for their respective field validations.
|
// allowing custom statuses and custom types for validation (GH#hq-8hif1z).
|
||||||
func validateFieldUpdateWithCustom(key string, value interface{}, customStatuses, customTypes []string) error {
|
func validateFieldUpdateWithCustom(key string, value interface{}, customStatuses, customTypes []string) error {
|
||||||
// Special handling for status field to support custom statuses
|
// Special handling for status field to support custom statuses
|
||||||
if key == "status" {
|
if key == "status" {
|
||||||
return validateStatusWithCustom(value, customStatuses)
|
return validateStatusWithCustom(value, customStatuses)
|
||||||
}
|
}
|
||||||
// Special handling for issue_type field to support custom types (federation trust model)
|
// Special handling for issue_type field to support custom types
|
||||||
if key == "issue_type" {
|
if key == "issue_type" {
|
||||||
return validateIssueTypeWithCustom(value, customTypes)
|
return validateIssueTypeWithCustom(value, customTypes)
|
||||||
}
|
}
|
||||||
@@ -98,21 +111,3 @@ func validateFieldUpdateWithCustom(key string, value interface{}, customStatuses
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateFieldUpdateWithCustomStatuses validates a field update value,
|
|
||||||
// allowing custom statuses for status field validation.
|
|
||||||
func validateFieldUpdateWithCustomStatuses(key string, value interface{}, customStatuses []string) error {
|
|
||||||
return validateFieldUpdateWithCustom(key, value, customStatuses, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateIssueTypeWithCustom validates an issue type value, allowing custom types.
|
|
||||||
func validateIssueTypeWithCustom(value interface{}, customTypes []string) error {
|
|
||||||
if issueType, ok := value.(string); ok {
|
|
||||||
// Normalize first to support aliases like "enhancement" -> "feature"
|
|
||||||
normalized := types.IssueType(issueType).Normalize()
|
|
||||||
if !normalized.IsValidWithCustom(customTypes) {
|
|
||||||
return fmt.Errorf("invalid issue type: %s", issueType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user