Add repo sync
Change-Id: I6b61873c97e9ff46a699151fc52aa937248bcf81
diff --git a/server/app/app.go b/server/app/app.go
index a016c4b..ace8904 100644
--- a/server/app/app.go
+++ b/server/app/app.go
@@ -7,14 +7,17 @@
"github.com/iomodo/staff/agent"
"github.com/iomodo/staff/config"
"github.com/iomodo/staff/git"
+ "github.com/iomodo/staff/tm"
"github.com/iomodo/staff/tm/git_tm"
)
// App type defines application global state
type App struct {
- logger *slog.Logger
- config *config.Config
- manager *agent.Manager
+ logger *slog.Logger
+ config *config.Config
+ manager *agent.Manager
+ git git.GitInterface
+ taskManager tm.TaskManager
}
// NewApp creates new App
@@ -29,9 +32,11 @@
}
return &App{
- logger: logger,
- config: config,
- manager: manager,
+ logger: logger,
+ config: config,
+ manager: manager,
+ git: gitInterface,
+ taskManager: taskManager,
}, nil
}
diff --git a/server/app/proposal.go b/server/app/proposal.go
index 165f336..8feaded 100644
--- a/server/app/proposal.go
+++ b/server/app/proposal.go
@@ -1,26 +1,105 @@
package app
import (
+ "context"
"fmt"
+ "log/slog"
+ "time"
"github.com/iomodo/staff/git"
)
func (a *App) ProposalApproval(body []byte, signature string) error {
- // Validate the webhook signature
- if err := git.ValidateSignature(body, signature, a.config.GitHub.WebhookSecret); err != nil {
- return fmt.Errorf("invalid webhook signature: %w", err)
+ // 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
+ // 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)
}
- // Log the successful approval
a.logger.Info("Proposal approved via webhook",
- "task_id", taskID,
+ 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),
+ )
+ }
+}
diff --git a/server/cmd/commands/config_check.go b/server/cmd/commands/config_check.go
index 5256c03..67aac9b 100644
--- a/server/cmd/commands/config_check.go
+++ b/server/cmd/commands/config_check.go
@@ -35,7 +35,7 @@
// Check Git provider configuration
fmt.Printf("\nGit Provider: %s\n", cfg.GetPrimaryGitProvider())
-
+
// Check GitHub configuration
if cfg.HasGitHubConfig() {
fmt.Printf("✅ GitHub configured (ends with: ...%s)\n", cfg.GitHub.Token[len(cfg.GitHub.Token)-4:])
@@ -43,15 +43,15 @@
} else if cfg.GitHub.Token != "" || cfg.GitHub.Owner != "" || cfg.GitHub.Repo != "" {
fmt.Println("⚠️ GitHub partially configured (incomplete)")
}
-
- // Check Gerrit configuration
+
+ // Check Gerrit configuration
if cfg.HasGerritConfig() {
fmt.Printf("✅ Gerrit configured (user: %s)\n", cfg.Gerrit.Username)
fmt.Printf(" Base URL: %s, Project: %s\n", cfg.Gerrit.BaseURL, cfg.Gerrit.Project)
} else if cfg.Gerrit.Username != "" || cfg.Gerrit.BaseURL != "" || cfg.Gerrit.Project != "" {
fmt.Println("⚠️ Gerrit partially configured (incomplete)")
}
-
+
if !cfg.HasGitHubConfig() && !cfg.HasGerritConfig() {
fmt.Println("❌ No Git provider configured (need GitHub or Gerrit)")
}
diff --git a/server/cmd/commands/webhook.go b/server/cmd/commands/webhook.go
index e62d1ce..da75284 100644
--- a/server/cmd/commands/webhook.go
+++ b/server/cmd/commands/webhook.go
@@ -52,14 +52,14 @@
webhookConfigureCmd.Flags().StringVar(&webhookURL, "webhook-url", "", "Webhook URL (if not provided, will prompt)")
webhookConfigureCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show what would be done without executing")
webhookConfigureCmd.Flags().BoolVar(&force, "force", false, "Force re-registration even if webhook exists")
-
+
webhookCmd.AddCommand(webhookConfigureCmd)
rootCmd.AddCommand(webhookCmd)
}
func runWebhookConfigure(cmd *cobra.Command, args []string) error {
ctx := context.Background()
-
+
// Validate GitHub configuration
if !cfg.HasGitHubConfig() {
return fmt.Errorf("GitHub configuration is required. Please configure github.token, github.owner, and github.repo in config.yaml")
@@ -125,13 +125,13 @@
if !force {
return fmt.Errorf("webhook already exists with URL %s (ID: %d). Use --force to update it", webhookURL, existingWebhook.ID)
}
-
+
fmt.Printf("Updating existing webhook (ID: %d)...\n", existingWebhook.ID)
updatedWebhook, err := gitInterface.UpdateWebhook(ctx, existingWebhook.ID, webhookURL, secret)
if err != nil {
return fmt.Errorf("failed to update webhook: %w", err)
}
-
+
fmt.Printf("Successfully updated webhook!\n")
fmt.Printf("- Webhook ID: %d\n", updatedWebhook.ID)
fmt.Printf("- URL: %s\n", updatedWebhook.Config.URL)
@@ -143,7 +143,7 @@
if err != nil {
return fmt.Errorf("failed to create webhook: %w", err)
}
-
+
fmt.Printf("Successfully created webhook!\n")
fmt.Printf("- Webhook ID: %d\n", newWebhook.ID)
fmt.Printf("- URL: %s\n", newWebhook.Config.URL)
@@ -164,26 +164,26 @@
fmt.Printf("\nWebhook Secret: %s\n", secret)
fmt.Printf("Please save this secret for your webhook endpoint configuration.\n")
fmt.Printf("\nYour Staff system will now automatically detect merged PRs and complete tasks.\n")
-
+
return nil
}
func promptWebhookURL() (string, error) {
reader := bufio.NewReader(os.Stdin)
-
+
// Show server listen address if configured
if cfg.Server.ListenAddress != "" {
fmt.Printf("Detected server listen address: %s\n", cfg.Server.ListenAddress)
fmt.Printf("Suggested webhook URL: http://%s/webhooks/github\n", cfg.Server.ListenAddress)
fmt.Printf("(Note: Use https:// and a public domain for production)\n")
}
-
+
fmt.Printf("Enter webhook URL (e.g., https://your-domain.com/webhooks/github): ")
url, err := reader.ReadString('\n')
if err != nil {
return "", err
}
-
+
return strings.TrimSpace(url), nil
}
@@ -192,15 +192,15 @@
if err != nil {
return fmt.Errorf("invalid URL format: %w", err)
}
-
+
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return fmt.Errorf("URL must use http or https scheme")
}
-
+
if parsedURL.Host == "" {
return fmt.Errorf("URL must include a host")
}
-
+
return nil
}
@@ -210,7 +210,7 @@
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
-
+
// Encode as hex string
return hex.EncodeToString(bytes), nil
}
@@ -221,13 +221,13 @@
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
-
+
// Parse YAML
var configMap map[string]interface{}
if err := yaml.Unmarshal(configData, &configMap); err != nil {
return fmt.Errorf("failed to parse config YAML: %w", err)
}
-
+
// Update webhook secret
github, ok := configMap["github"].(map[string]interface{})
if !ok {
@@ -235,17 +235,17 @@
configMap["github"] = github
}
github["webhook_secret"] = secret
-
+
// Marshal back to YAML
updatedData, err := yaml.Marshal(configMap)
if err != nil {
return fmt.Errorf("failed to marshal config YAML: %w", err)
}
-
+
// Write back to file
if err := os.WriteFile("config.yaml", updatedData, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
-
+
return nil
-}
\ No newline at end of file
+}
diff --git a/server/git/gerrit.go b/server/git/gerrit.go
index d1d029f..d9f1f38 100644
--- a/server/git/gerrit.go
+++ b/server/git/gerrit.go
@@ -152,7 +152,7 @@
}
url := fmt.Sprintf("%s/a/changes/", g.config.BaseURL)
-
+
// Log change creation with structured data
g.logger.Info("Creating Gerrit change",
slog.String("url", url),
diff --git a/server/git/git.go b/server/git/git.go
index 29cee16..9ee3074 100644
--- a/server/git/git.go
+++ b/server/git/git.go
@@ -32,6 +32,7 @@
Checkout(ctx context.Context, ref string) error
DeleteBranch(ctx context.Context, name string, force bool) error
GetCurrentBranch(ctx context.Context) (string, error)
+ GetUpstreamBranch(ctx context.Context, branch string) (string, error)
// Commit operations
Add(ctx context.Context, paths []string) error
@@ -394,6 +395,22 @@
return strings.TrimSpace(output), nil
}
+// GetUpstreamBranch returns the upstream branch for a given branch
+func (g *Git) GetUpstreamBranch(ctx context.Context, branch string) (string, error) {
+ cmd := exec.CommandContext(ctx, "git", "rev-parse", "--abbrev-ref", branch+"@{upstream}")
+ cmd.Dir = g.repoPath
+ output, err := g.runCommandWithOutput(cmd, "rev-parse")
+ if err != nil {
+ return "", err
+ }
+
+ // The output will be in format "origin/branch-name", we want just "branch-name"
+ upstream := strings.TrimSpace(output)
+ upstream = strings.TrimPrefix(upstream, "origin/")
+
+ return upstream, nil
+}
+
// Add stages files for commit
func (g *Git) Add(ctx context.Context, paths []string) error {
args := append([]string{"add"}, paths...)
diff --git a/server/git/github.go b/server/git/github.go
index a73c4c6..87ec363 100644
--- a/server/git/github.go
+++ b/server/git/github.go
@@ -114,10 +114,10 @@
// GitHub webhook API types
type githubWebhookRequest struct {
- Name string `json:"name"`
- Active bool `json:"active"`
- Events []string `json:"events"`
- Config githubWebhookConfig `json:"config"`
+ Name string `json:"name"`
+ Active bool `json:"active"`
+ Events []string `json:"events"`
+ Config githubWebhookConfig `json:"config"`
}
type githubWebhookConfig struct {