| package app |
| |
| import ( |
| "context" |
| "fmt" |
| "log/slog" |
| "time" |
| |
| "github.com/iomodo/staff/git" |
| ) |
| |
| func (a *App) ProposalApproval(body []byte, signature string) error { |
| // Set reasonable timeout for webhook processing |
| ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
| defer cancel() |
| |
| // Validate required dependencies |
| if a.git == nil { |
| return fmt.Errorf("git interface not initialized") |
| } |
| if a.taskManager == nil { |
| return fmt.Errorf("task manager not initialized") |
| } |
| |
| // Process the webhook payload (includes signature validation) |
| taskID, err := git.ProcessMergeWebhook(body, signature, a.config.GitHub.WebhookSecret) |
| if err != nil { |
| return fmt.Errorf("failed to process webhook: %w", err) |
| } |
| |
| // Complete the task for the agent that was performing it |
| if err := a.manager.CompleteTaskForAgent(taskID); err != nil { |
| a.logger.Warn("Failed to complete task for agent", |
| slog.String("task_id", taskID), |
| slog.String("error", err.Error())) |
| // Don't fail the webhook if agent completion fails |
| } |
| |
| a.logger.Info("Proposal approved via webhook", |
| slog.String("task_id", taskID), |
| slog.String("repository", fmt.Sprintf("%s/%s", a.config.GitHub.Owner, a.config.GitHub.Repo)), |
| ) |
| |
| // Synchronize repository to get any new tasks that were added |
| a.syncRepository(ctx) |
| a.logger.Info("Webhook processing completed successfully", |
| slog.String("task_id", taskID), |
| ) |
| |
| return nil |
| } |
| |
| // syncRepository pulls the remote repository to fetch new tasks |
| // This is non-critical for webhook success, so failures are logged but don't fail the webhook |
| func (a *App) syncRepository(ctx context.Context) { |
| a.logger.Info("Synchronizing repository to fetch new tasks") |
| |
| // Get current branch to determine the upstream branch to pull from |
| currentBranch, err := a.git.GetCurrentBranch(ctx) |
| if err != nil { |
| a.logger.Warn("Failed to get current branch, using default pull", |
| slog.String("error", err.Error()), |
| ) |
| // Fallback to simple pull without specifying branch |
| if err := a.git.Pull(ctx, "origin", ""); err != nil { |
| a.logger.Warn("Failed to synchronize repository", |
| slog.String("remote", "origin"), |
| slog.String("error", err.Error()), |
| ) |
| } else { |
| a.logger.Info("Successfully synchronized repository", |
| slog.String("remote", "origin"), |
| ) |
| } |
| return |
| } |
| |
| // Get the upstream branch for the current branch |
| upstreamBranch, err := a.git.GetUpstreamBranch(ctx, currentBranch) |
| if err != nil { |
| a.logger.Warn("Failed to get upstream branch, trying simple pull", |
| slog.String("current_branch", currentBranch), |
| slog.String("error", err.Error()), |
| ) |
| // Fallback to simple pull without specifying branch |
| if err := a.git.Pull(ctx, "origin", ""); err != nil { |
| a.logger.Warn("Failed to synchronize repository", |
| slog.String("remote", "origin"), |
| slog.String("error", err.Error()), |
| ) |
| } else { |
| a.logger.Info("Successfully synchronized repository", |
| slog.String("remote", "origin"), |
| ) |
| } |
| return |
| } |
| |
| // Use the upstream branch for pulling |
| if err := a.git.Pull(ctx, "origin", upstreamBranch); err != nil { |
| // Don't fail the entire webhook if git pull fails - agents can still work with existing tasks |
| a.logger.Warn("Failed to synchronize repository", |
| slog.String("remote", "origin"), |
| slog.String("branch", upstreamBranch), |
| slog.String("error", err.Error()), |
| ) |
| } else { |
| a.logger.Info("Successfully synchronized repository", |
| slog.String("remote", "origin"), |
| slog.String("branch", upstreamBranch), |
| ) |
| } |
| } |