From eead1829feec221c29c37ddc12decc60939f23cb Mon Sep 17 00:00:00 2001 From: Mahsum Aktas Date: Fri, 13 Feb 2026 07:59:55 +0300 Subject: [PATCH 1/2] feat(cli): fetch issue by number directly from GitHub Signed-off-by: Mahsum Aktas --- cmd/simili/commands/process.go | 138 ++++++++++++++++++++-------- cmd/simili/commands/process_test.go | 50 ++++++++++ 2 files changed, 150 insertions(+), 38 deletions(-) diff --git a/cmd/simili/commands/process.go b/cmd/simili/commands/process.go index 44a5ee0..15c3359 100644 --- a/cmd/simili/commands/process.go +++ b/cmd/simili/commands/process.go @@ -10,8 +10,10 @@ import ( "encoding/json" "fmt" "os" + "strings" "time" + githubapi "github.com/google/go-github/v60/github" "github.com/spf13/cobra" "github.com/similigh/simili-bot/internal/core/config" @@ -35,7 +37,9 @@ var processCmd = &cobra.Command{ Use: "process", Short: "Process a single issue through the pipeline", Long: `Process a single issue through the Simili-Bot pipeline. -You can provide the issue data via a JSON file or specify the issue number (if fetching from GitHub).`, +You can provide issue data via --issue , or fetch directly from GitHub with --number. +For direct fetch, use --repo owner/name, or --org owner --repo name. +If --repo/--org are omitted, GITHUB_REPOSITORY (owner/name) is used as fallback.`, Run: func(cmd *cobra.Command, args []string) { runProcess() }, @@ -131,46 +135,44 @@ func runProcess() { enrichIssueFromGitHubEvent(&issue, raw) } } - } else { - // TODO: Fetch from GitHub if not provided (Phase 9/10) - fmt.Println("Please provide --issue ") - os.Exit(1) - } - // Override if flags provided - if orgName != "" { - issue.Org = orgName - } - if repoName != "" { - issue.Repo = repoName - } - // Fallback to Env Vars if valid and still empty - if issue.Org == "" || issue.Repo == "" { - if ghRepo := os.Getenv("GITHUB_REPOSITORY"); ghRepo != "" { - // owner/repo - // We need to import strings to split safely - // Since I can't guarantee imports easily without seeing file imports, - // I'll assume simple looping or add imports in a separate step if needed. - // Actually process.go doesn't import strings yet. - // Let's rely on standard split logic or just add the import. - // I'll add "strings" to imports in a separate step to be safe. - // For now, let's just do a manual scan - for i := 0; i < len(ghRepo); i++ { - if ghRepo[i] == '/' { - if issue.Org == "" { - issue.Org = ghRepo[:i] - } - if issue.Repo == "" { - issue.Repo = ghRepo[i+1:] - } - break - } - } + // Apply optional overrides when --issue is used + if orgName != "" { + issue.Org = orgName + } + if repoName != "" && !strings.Contains(repoName, "/") { + issue.Repo = repoName + } + if issueNum != 0 { + issue.Number = issueNum + } + } else if issueNum != 0 { + org, repo := resolveIssueRepo(orgName, repoName) + if org == "" || repo == "" { + fmt.Println("Error: when using --number, provide --repo owner/name or --org owner --repo name, or set GITHUB_REPOSITORY") + os.Exit(1) + } + + token := os.Getenv("TRANSFER_TOKEN") + if token == "" { + token = os.Getenv("GITHUB_TOKEN") + } + if token == "" { + fmt.Println("Error: GITHUB_TOKEN (or TRANSFER_TOKEN) is required to fetch issue from GitHub") + os.Exit(1) } - } - if issueNum != 0 { - issue.Number = issueNum + ghClient := github.NewClient(context.Background(), token) + ghIssue, err := ghClient.GetIssue(context.Background(), org, repo, issueNum) + if err != nil { + fmt.Printf("Error fetching issue from GitHub: %v\n", err) + os.Exit(1) + } + + issue = githubIssueToPipelineIssue(ghIssue, org, repo) + } else { + fmt.Println("Please provide --issue or --number ") + os.Exit(1) } if verbose { @@ -265,6 +267,66 @@ func runProcess() { fmt.Println("[Simili-Bot] Pipeline completed") } +func resolveIssueRepo(flagOrg, flagRepo string) (string, string) { + if strings.Contains(flagRepo, "/") { + parts := strings.SplitN(flagRepo, "/", 2) + if len(parts) == 2 { + return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) + } + } + + if strings.TrimSpace(flagOrg) != "" && strings.TrimSpace(flagRepo) != "" { + return strings.TrimSpace(flagOrg), strings.TrimSpace(flagRepo) + } + + if ghRepo := strings.TrimSpace(os.Getenv("GITHUB_REPOSITORY")); ghRepo != "" { + parts := strings.SplitN(ghRepo, "/", 2) + if len(parts) == 2 { + return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) + } + } + + return "", "" +} + +func githubIssueToPipelineIssue(ghIssue *githubapi.Issue, org, repo string) pipeline.Issue { + if ghIssue == nil { + return pipeline.Issue{Org: org, Repo: repo, EventType: "issues", EventAction: "opened"} + } + + labels := make([]string, 0, len(ghIssue.Labels)) + for _, label := range ghIssue.Labels { + if label != nil && label.Name != nil && *label.Name != "" { + labels = append(labels, *label.Name) + } + } + + createdAt := time.Time{} + if ghIssue.CreatedAt != nil { + createdAt = ghIssue.CreatedAt.Time + } + + author := "" + if ghIssue.User != nil { + author = ghIssue.User.GetLogin() + } + + return pipeline.Issue{ + Org: org, + Repo: repo, + Number: ghIssue.GetNumber(), + Title: ghIssue.GetTitle(), + Body: ghIssue.GetBody(), + State: ghIssue.GetState(), + Labels: labels, + Author: author, + URL: ghIssue.GetHTMLURL(), + CreatedAt: createdAt, + EventType: "issues", + EventAction: "opened", + } +} + func enrichIssueFromGitHubEvent(issue *pipeline.Issue, raw map[string]interface{}) { if action, ok := raw["action"].(string); ok { issue.EventAction = action diff --git a/cmd/simili/commands/process_test.go b/cmd/simili/commands/process_test.go index a6be048..e6aa459 100644 --- a/cmd/simili/commands/process_test.go +++ b/cmd/simili/commands/process_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + githubapi "github.com/google/go-github/v60/github" "github.com/similigh/simili-bot/internal/core/pipeline" ) @@ -115,3 +116,52 @@ func TestEnrichIssueFromGitHubEvent_PRComment(t *testing.T) { t.Fatalf("expected issue number 42, got %d", issue.Number) } } + +func TestGithubIssueToPipelineIssue(t *testing.T) { + createdAt := githubapi.Timestamp{Time: time.Date(2026, 2, 13, 10, 0, 0, 0, time.UTC)} + ghIssue := &githubapi.Issue{ + Number: githubapi.Int(17), + Title: githubapi.String("feat: fetch issue directly"), + Body: githubapi.String("Implement CLI support"), + State: githubapi.String("open"), + HTMLURL: githubapi.String("https://github.com/similigh/simili-bot/issues/17"), + CreatedAt: &createdAt, + User: &githubapi.User{Login: githubapi.String("maintainer")}, + Labels: []*githubapi.Label{ + {Name: githubapi.String("enhancement")}, + {Name: githubapi.String("cli")}, + }, + } + + issue := githubIssueToPipelineIssue(ghIssue, "similigh", "simili-bot") + + if issue.Org != "similigh" || issue.Repo != "simili-bot" || issue.Number != 17 { + t.Fatalf("unexpected issue identity: %+v", issue) + } + if issue.Title != "feat: fetch issue directly" || issue.Body != "Implement CLI support" { + t.Fatalf("unexpected title/body: %+v", issue) + } + if issue.State != "open" || issue.Author != "maintainer" || issue.URL == "" { + t.Fatalf("expected state/author/url parsed, got %+v", issue) + } + if len(issue.Labels) != 2 || issue.Labels[0] != "enhancement" || issue.Labels[1] != "cli" { + t.Fatalf("unexpected labels: %+v", issue.Labels) + } + if !issue.CreatedAt.Equal(createdAt.Time) { + t.Fatalf("expected created_at to be parsed, got %v", issue.CreatedAt) + } + if issue.EventType != "issues" || issue.EventAction != "opened" { + t.Fatalf("expected issues/opened event, got %s/%s", issue.EventType, issue.EventAction) + } +} + +func TestGithubIssueToPipelineIssue_NilIssue(t *testing.T) { + issue := githubIssueToPipelineIssue(nil, "similigh", "simili-bot") + + if issue.Org != "similigh" || issue.Repo != "simili-bot" { + t.Fatalf("unexpected org/repo for nil issue: %+v", issue) + } + if issue.EventType != "issues" || issue.EventAction != "opened" { + t.Fatalf("expected default issues/opened for nil issue, got %s/%s", issue.EventType, issue.EventAction) + } +} From 7d9f865f9d36e6eccabb2f126cc81557df4329e4 Mon Sep 17 00:00:00 2001 From: Mahsum Aktas Date: Tue, 17 Feb 2026 23:58:52 +0300 Subject: [PATCH 2/2] fix(cli): parse owner/repo override with --issue --- cmd/simili/commands/process.go | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/cmd/simili/commands/process.go b/cmd/simili/commands/process.go index a7835fb..0dff7b2 100644 --- a/cmd/simili/commands/process.go +++ b/cmd/simili/commands/process.go @@ -137,12 +137,27 @@ func runProcess() { } // Apply optional overrides when --issue is used + if repoName != "" && strings.Contains(repoName, "/") { + parts := strings.SplitN(repoName, "/", 2) + if len(parts) == 2 { + if strings.TrimSpace(parts[0]) != "" { + issue.Org = strings.TrimSpace(parts[0]) + } + if strings.TrimSpace(parts[1]) != "" { + issue.Repo = strings.TrimSpace(parts[1]) + } + } + } else { + if orgName != "" { + issue.Org = orgName + } + if repoName != "" { + issue.Repo = repoName + } + } if orgName != "" { issue.Org = orgName } - if repoName != "" && !strings.Contains(repoName, "/") { - issue.Repo = repoName - } if issueNum != 0 { issue.Number = issueNum }