From b5c0deb6bcc6ba0755a0d87591acfed2ba7dcb9b Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 10 Sep 2025 12:57:11 +1000 Subject: [PATCH] Fix critical issues in WHOOSH Gitea issue monitoring and task creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit resolves multiple blocking issues that were preventing WHOOSH from properly detecting and converting bzzz-task labeled issues from Gitea: ## Issues Fixed: 1. **JSON Parsing Error**: Gitea API returns repository owner as string in issue responses, but code expected User object. Added IssueRepository struct to handle this API response format difference. 2. **Database Error Handling**: Code was using database/sql.ErrNoRows but system uses pgx driver. Updated imports and error constants to use pgx.ErrNoRows consistently. 3. **NULL Value Scanning**: Database fields (repository, project_id, estimated_hours, complexity_score) can be NULL but Go structs used non-pointer types. Added proper NULL handling with pointer scanning and safe conversion. ## Results: - ✅ WHOOSH now successfully detects bzzz-task labeled issues - ✅ Task creation pipeline working end-to-end - ✅ Tasks API functioning properly - ✅ First bzzz-task converted: "Logic around registered agents faulty" The core issue monitoring workflow is now fully operational and ready for CHORUS integration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- internal/gitea/client.go | 42 ++++++++++++++++++++++--------------- internal/monitor/monitor.go | 25 ++++++++++++---------- internal/tasks/service.go | 21 +++++++++++++++++-- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/internal/gitea/client.go b/internal/gitea/client.go index b476a94..00ea89e 100644 --- a/internal/gitea/client.go +++ b/internal/gitea/client.go @@ -22,19 +22,19 @@ type Client struct { // Issue represents a Gitea issue type Issue struct { - ID int64 `json:"id"` - Number int64 `json:"number"` - Title string `json:"title"` - Body string `json:"body"` - State string `json:"state"` - Labels []Label `json:"labels"` - Assignees []User `json:"assignees"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - ClosedAt *time.Time `json:"closed_at"` - HTMLURL string `json:"html_url"` - User User `json:"user"` - Repository Repository `json:"repository,omitempty"` + ID int64 `json:"id"` + Number int64 `json:"number"` + Title string `json:"title"` + Body string `json:"body"` + State string `json:"state"` + Labels []Label `json:"labels"` + Assignees []User `json:"assignees"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ClosedAt *time.Time `json:"closed_at"` + HTMLURL string `json:"html_url"` + User User `json:"user"` + Repository IssueRepository `json:"repository,omitempty"` } // Label represents a Gitea issue label @@ -68,6 +68,14 @@ type Repository struct { Language string `json:"language"` } +// IssueRepository represents the simplified repository info in issue responses +type IssueRepository struct { + ID int64 `json:"id"` + Name string `json:"name"` + FullName string `json:"full_name"` + Owner string `json:"owner"` // Note: This is a string, not a User object +} + // NewClient creates a new Gitea API client func NewClient(cfg config.GITEAConfig) *Client { token := cfg.Token @@ -167,10 +175,10 @@ func (c *Client) GetIssues(ctx context.Context, owner, repo string, opts IssueLi // Set repository information on each issue for context for i := range issues { - issues[i].Repository = Repository{ + issues[i].Repository = IssueRepository{ Name: repo, FullName: fmt.Sprintf("%s/%s", owner, repo), - Owner: User{Login: owner}, + Owner: owner, // Now a string instead of User object } } @@ -193,10 +201,10 @@ func (c *Client) GetIssue(ctx context.Context, owner, repo string, issueNumber i } // Set repository information - issue.Repository = Repository{ + issue.Repository = IssueRepository{ Name: repo, FullName: fmt.Sprintf("%s/%s", owner, repo), - Owner: User{Login: owner}, + Owner: owner, // Now a string instead of User object } return &issue, nil diff --git a/internal/monitor/monitor.go b/internal/monitor/monitor.go index d9d4cb8..8d0c74b 100644 --- a/internal/monitor/monitor.go +++ b/internal/monitor/monitor.go @@ -2,7 +2,6 @@ package monitor import ( "context" - "database/sql" "encoding/json" "fmt" "strconv" @@ -11,6 +10,7 @@ import ( "github.com/chorus-services/whoosh/internal/config" "github.com/chorus-services/whoosh/internal/gitea" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/rs/zerolog/log" ) @@ -202,11 +202,14 @@ func (m *Monitor) syncRepository(ctx context.Context, repo RepositoryConfig) { // createOrUpdateTask creates a new task or updates an existing one from a Gitea issue func (m *Monitor) createOrUpdateTask(ctx context.Context, repo RepositoryConfig, issue gitea.Issue) (string, bool, error) { // Check if task already exists - var existingTaskID sql.NullString + var existingTaskID string query := `SELECT id FROM tasks WHERE external_id = $1 AND source_type = $2` err := m.db.QueryRow(ctx, query, strconv.FormatInt(issue.Number, 10), repo.SourceType).Scan(&existingTaskID) - if err != nil && err != sql.ErrNoRows { + taskExists := err == nil + + // Handle errors other than "no rows found" + if err != nil && err != pgx.ErrNoRows { return "", false, fmt.Errorf("failed to check existing task: %w", err) } @@ -230,8 +233,8 @@ func (m *Monitor) createOrUpdateTask(ctx context.Context, repo RepositoryConfig, techStack := m.extractTechStackFromIssue(issue) techStackJSON, _ := json.Marshal(techStack) - if existingTaskID.Valid { - // Update existing task + if taskExists { + // Task exists - update it updateQuery := ` UPDATE tasks SET title = $1, @@ -253,14 +256,14 @@ func (m *Monitor) createOrUpdateTask(ctx context.Context, repo RepositoryConfig, labelsJSON, techStackJSON, issue.UpdatedAt, - existingTaskID.String, + existingTaskID, ) if err != nil { return "", false, fmt.Errorf("failed to update task: %w", err) } - return existingTaskID.String, false, nil + return existingTaskID, false, nil } else { // Create new task var taskID string @@ -440,10 +443,10 @@ func (m *Monitor) getMonitoredRepositories(ctx context.Context) ([]RepositoryCon // updateRepositoryStatus updates the sync status of a repository func (m *Monitor) updateRepositoryStatus(ctx context.Context, repoID, status string, err error) error { - var errorMsg sql.NullString + var errorMsg *string if err != nil { - errorMsg.String = err.Error() - errorMsg.Valid = true + errStr := err.Error() + errorMsg = &errStr } query := ` @@ -555,7 +558,7 @@ func (m *Monitor) getRepositoryByID(ctx context.Context, repoID string) (*Reposi &chorusLabelsJSON, &repo.LastSync, &repo.LastIssueSync, &repo.SyncStatus, ) if err != nil { - if err == sql.ErrNoRows { + if err == pgx.ErrNoRows { return nil, fmt.Errorf("repository not found: %s", repoID) } return nil, fmt.Errorf("failed to query repository: %w", err) diff --git a/internal/tasks/service.go b/internal/tasks/service.go index cd213b1..c44723b 100644 --- a/internal/tasks/service.go +++ b/internal/tasks/service.go @@ -354,13 +354,16 @@ func (s *Service) UpdateTaskStatus(ctx context.Context, update *TaskStatusUpdate func (s *Service) scanTask(scanner interface{ Scan(...interface{}) error }) (*Task, error) { task := &Task{} var sourceConfigJSON, labelsJSON, techStackJSON, requirementsJSON []byte + var repository, projectID *string + var estimatedHours *int + var complexityScore *float64 err := scanner.Scan( &task.ID, &task.ExternalID, &task.ExternalURL, &task.SourceType, &sourceConfigJSON, &task.Title, &task.Description, &task.Status, &task.Priority, &task.AssignedTeamID, &task.AssignedAgentID, - &task.Repository, &task.ProjectID, &labelsJSON, &techStackJSON, &requirementsJSON, - &task.EstimatedHours, &task.ComplexityScore, + &repository, &projectID, &labelsJSON, &techStackJSON, &requirementsJSON, + &estimatedHours, &complexityScore, &task.ClaimedAt, &task.StartedAt, &task.CompletedAt, &task.CreatedAt, &task.UpdatedAt, &task.ExternalCreatedAt, &task.ExternalUpdatedAt, ) @@ -369,6 +372,20 @@ func (s *Service) scanTask(scanner interface{ Scan(...interface{}) error }) (*Ta return nil, err } + // Handle nullable fields + if repository != nil { + task.Repository = *repository + } + if projectID != nil { + task.ProjectID = *projectID + } + if estimatedHours != nil { + task.EstimatedHours = *estimatedHours + } + if complexityScore != nil { + task.ComplexityScore = *complexityScore + } + // Parse JSON fields if len(sourceConfigJSON) > 0 { json.Unmarshal(sourceConfigJSON, &task.SourceConfig)