Commit d0ac4bb7971d

Vincent Demeester <vincent@sbr.pm>
2026-03-27 10:23:08
feat(daily-plan): add discussions, comments, issues filed
Ported GitHub GraphQL queries from review-tool for weekly reports: discussions authored and commented on, issue/PR comments, and issues created by user. Added --no-discussions and --no-comments flags to toggle sections off. First step toward converging review-tool into daily-plan.
1 parent f08e058
Changed files (3)
tools
daily-plan
cmd
daily-plan
internal
tools/daily-plan/cmd/daily-plan/main.go
@@ -23,7 +23,11 @@ import (
 	"github.com/vdemeester/home/tools/daily-plan/internal/org"
 )
 
-var outputJSON bool
+var (
+	outputJSON    bool
+	noDiscussions bool
+	noComments    bool
+)
 
 func main() {
 	if err := run(os.Args[1:]); err != nil {
@@ -37,12 +41,17 @@ func run(args []string) error {
 	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
 	defer cancel()
 
-	// Strip --json flag from anywhere in args
+	// Strip flags from anywhere in args
 	var filtered []string
 	for _, a := range args {
-		if a == "--json" {
+		switch a {
+		case "--json":
 			outputJSON = true
-		} else {
+		case "--no-discussions":
+			noDiscussions = true
+		case "--no-comments":
+			noComments = true
+		default:
 			filtered = append(filtered, a)
 		}
 	}
@@ -175,6 +184,35 @@ func ghToJSON(items []github.Item) []jsonGH {
 	return out
 }
 
+func discussionsToJSON(items []github.DiscussionItem) []jsonDiscussion {
+	result := make([]jsonDiscussion, 0, len(items))
+	for _, d := range items {
+		result = append(result, jsonDiscussion{
+			Repo:     d.Repo,
+			Title:    d.Title,
+			URL:      d.URL,
+			Category: d.Category,
+			Type:     d.Type,
+			Date:     d.CreatedAt.Format("2006-01-02"),
+		})
+	}
+	return result
+}
+
+func commentsToJSON(items []github.CommentItem) []jsonComment {
+	result := make([]jsonComment, 0, len(items))
+	for _, c := range items {
+		result = append(result, jsonComment{
+			Repo:        c.Repo,
+			IssueTitle:  c.IssueTitle,
+			IssueNumber: c.IssueNumber,
+			URL:         c.URL,
+			Date:        c.CreatedAt.Format("2006-01-02"),
+		})
+	}
+	return result
+}
+
 func emitJSON(v any) error {
 	enc := json.NewEncoder(os.Stdout)
 	enc.SetIndent("", "  ")
@@ -428,15 +466,35 @@ func cmdPick(_ context.Context, cfg *config.Config) error {
 	return nil
 }
 
+type jsonDiscussion struct {
+	Repo     string `json:"repo"`
+	Title    string `json:"title"`
+	URL      string `json:"url"`
+	Category string `json:"category,omitempty"`
+	Type     string `json:"type"`
+	Date     string `json:"date"`
+}
+
+type jsonComment struct {
+	Repo        string `json:"repo"`
+	IssueTitle  string `json:"issue_title"`
+	IssueNumber int    `json:"issue_number"`
+	URL         string `json:"url"`
+	Date        string `json:"date"`
+}
+
 type jsonWeekly struct {
-	Week           string     `json:"week"`
-	NextMonday     string     `json:"next_monday"`
-	JiraCompleted  []jsonJira `json:"jira_completed"`
-	GHMerged       []jsonGH   `json:"github_merged"`
-	GHReviewed     []jsonGH   `json:"github_reviewed"`
-	JiraInProgress []jsonJira `json:"jira_in_progress"`
-	JiraBacklog    []jsonJira `json:"jira_backlog"`
-	GHIssues       []jsonGH   `json:"github_issues"`
+	Week            string           `json:"week"`
+	NextMonday      string           `json:"next_monday"`
+	JiraCompleted   []jsonJira       `json:"jira_completed"`
+	GHMerged        []jsonGH         `json:"github_merged"`
+	GHReviewed      []jsonGH         `json:"github_reviewed"`
+	GHIssuesCreated []jsonGH         `json:"github_issues_created"`
+	GHDiscussions   []jsonDiscussion `json:"github_discussions,omitempty"`
+	GHComments      []jsonComment    `json:"github_comments,omitempty"`
+	JiraInProgress  []jsonJira       `json:"jira_in_progress"`
+	JiraBacklog     []jsonJira       `json:"jira_backlog"`
+	GHIssues        []jsonGH         `json:"github_issues"`
 }
 
 func cmdWeekly(ctx context.Context, cfg *config.Config) error {
@@ -449,21 +507,39 @@ func cmdWeekly(ctx context.Context, cfg *config.Config) error {
 	completed, _ := jira.FetchCompletedSince(ctx, cfg.Jira.User, monday)
 	merged, _ := github.FetchMergedPRsSince(ctx, cfg.GitHub.Username, monday, cfg.GitHub.Owners)
 	reviewed, _ := github.FetchReviewsGivenSince(ctx, cfg.GitHub.Username, monday, cfg.GitHub.Owners)
+	issuesCreated, _ := github.FetchIssuesCreatedSince(ctx, cfg.GitHub.Username, monday, cfg.GitHub.Owners)
 	inprog, _ := jira.FetchByStatus(ctx, cfg.Jira.User, []string{"In Progress", "Code Review", "On QA"})
 	todo, _ := jira.FetchByStatus(ctx, cfg.Jira.User, []string{"To Do", "New"})
 	ghIssues, _ := github.FetchAssignedIssues(ctx, cfg.GitHub.Username, cfg.GitHub.Owners)
 
+	var discussions []github.DiscussionItem
+	var comments []github.CommentItem
+	if !noDiscussions {
+		discussions, _ = github.FetchDiscussionsSince(ctx, monday)
+	}
+	if !noComments {
+		comments, _ = github.FetchCommentsSince(ctx, monday)
+	}
+
 	if outputJSON {
-		return emitJSON(jsonWeekly{
-			Week:           monday.Format("2006-01-02"),
-			NextMonday:     nextMonday.Format("2006-01-02"),
-			JiraCompleted:  jiraToJSON(completed, cfg.Jira.BaseURL),
-			GHMerged:       ghToJSON(merged),
-			GHReviewed:     ghToJSON(reviewed),
-			JiraInProgress: jiraToJSON(inprog, cfg.Jira.BaseURL),
-			JiraBacklog:    jiraToJSON(todo, cfg.Jira.BaseURL),
-			GHIssues:       ghToJSON(ghIssues),
-		})
+		w := jsonWeekly{
+			Week:            monday.Format("2006-01-02"),
+			NextMonday:      nextMonday.Format("2006-01-02"),
+			JiraCompleted:   jiraToJSON(completed, cfg.Jira.BaseURL),
+			GHMerged:        ghToJSON(merged),
+			GHReviewed:      ghToJSON(reviewed),
+			GHIssuesCreated: ghToJSON(issuesCreated),
+			JiraInProgress:  jiraToJSON(inprog, cfg.Jira.BaseURL),
+			JiraBacklog:     jiraToJSON(todo, cfg.Jira.BaseURL),
+			GHIssues:        ghToJSON(ghIssues),
+		}
+		if !noDiscussions {
+			w.GHDiscussions = discussionsToJSON(discussions)
+		}
+		if !noComments {
+			w.GHComments = commentsToJSON(comments)
+		}
+		return emitJSON(w)
 	}
 
 	display.Header(fmt.Sprintf("Weekly Review (week of %s)", monday.Format("2006-01-02")))
@@ -477,6 +553,21 @@ func cmdWeekly(ctx context.Context, cfg *config.Config) error {
 	display.SubHeader("Reviews Given This Week (GitHub)")
 	display.GitHubItems(reviewed, "review")
 
+	if len(issuesCreated) > 0 {
+		display.SubHeader("Issues Filed This Week (GitHub)")
+		display.GitHubItems(issuesCreated, "issue")
+	}
+
+	if !noDiscussions && len(discussions) > 0 {
+		display.SubHeader("Discussions (GitHub)")
+		display.DiscussionItems(discussions)
+	}
+
+	if !noComments && len(comments) > 0 {
+		display.SubHeader("Comments on Issues/PRs (GitHub)")
+		display.CommentItems(comments)
+	}
+
 	display.SubHeader("Still In Progress (Jira)")
 	display.JiraIssues(inprog, "active")
 
@@ -574,6 +665,8 @@ Commands:
 
 Flags:
   --json                  Output as JSON (for Emacs/scripting integration)
+  --no-discussions        Skip GitHub discussions in weekly
+  --no-comments           Skip GitHub comments in weekly
 
 Date formats:
   YYYY-MM-DD              Explicit date
tools/daily-plan/internal/display/display.go
@@ -197,6 +197,47 @@ func DependabotAlerts(alerts []github.DependabotAlert) {
 	}
 }
 
+// DiscussionItems prints GitHub discussions.
+func DiscussionItems(items []github.DiscussionItem) {
+	if len(items) == 0 {
+		fmt.Printf("  %s(none)%s\n", dim, reset)
+		return
+	}
+	for _, item := range items {
+		color := cyan
+		if item.Type == "discussion_comment" {
+			color = blue
+		}
+		title := item.Title
+		if len(title) > 60 {
+			title = title[:57] + "..."
+		}
+		cat := ""
+		if item.Category != "" {
+			cat = fmt.Sprintf("[%s] ", item.Category)
+		}
+		fmt.Printf("  %s%-30s%s %s%s %s(%s)%s\n",
+			color, item.Repo, reset, cat, title, dim, item.CreatedAt.Format("2006-01-02"), reset)
+	}
+}
+
+// CommentItems prints GitHub issue/PR comments.
+func CommentItems(items []github.CommentItem) {
+	if len(items) == 0 {
+		fmt.Printf("  %s(none)%s\n", dim, reset)
+		return
+	}
+	for _, item := range items {
+		title := item.IssueTitle
+		if len(title) > 55 {
+			title = title[:52] + "..."
+		}
+		ref := fmt.Sprintf("%s#%d", item.Repo, item.IssueNumber)
+		fmt.Printf("  %s%-40s%s %s %s(%s)%s\n",
+			blue, ref, reset, title, dim, item.CreatedAt.Format("2006-01-02"), reset)
+	}
+}
+
 // Hint prints a dim hint line.
 func Hint(msg string) {
 	fmt.Printf("\n%s%s%s\n", dim, msg, reset)
tools/daily-plan/internal/github/github.go
@@ -151,6 +151,223 @@ func FetchReviewsGivenSince(ctx context.Context, username string, since time.Tim
 	return reviews, nil
 }
 
+// FetchIssuesCreatedSince returns issues created by user since the given date.
+func FetchIssuesCreatedSince(ctx context.Context, username string, since time.Time, owners []string) ([]Item, error) {
+	args := []string{
+		"search", "issues",
+		"--author", username,
+		"--created", fmt.Sprintf(">=%s", since.Format("2006-01-02")),
+		"--limit", "30",
+		"--json", "repository,number,title,createdAt,author",
+	}
+	args = append(args, ownerFlags(owners)...)
+	return runGHSearch(ctx, args, "issue")
+}
+
+// DiscussionItem represents a GitHub discussion or discussion comment.
+type DiscussionItem struct {
+	Repo      string
+	Title     string
+	URL       string
+	Category  string // discussion category (e.g. "General", "RFC")
+	Type      string // "discussion" or "discussion_comment"
+	CreatedAt time.Time
+}
+
+// CommentItem represents a comment on an issue or PR.
+type CommentItem struct {
+	Repo        string
+	IssueTitle  string
+	IssueNumber int
+	URL         string
+	CreatedAt   time.Time
+}
+
+// graphQL response types
+
+type gqlDiscussionResponse struct {
+	Data struct {
+		Viewer struct {
+			RepositoryDiscussions struct {
+				Nodes []struct {
+					Title     string `json:"title"`
+					URL       string `json:"url"`
+					CreatedAt string `json:"createdAt"`
+					Category  struct {
+						Name string `json:"name"`
+					} `json:"category"`
+					Repository struct {
+						NameWithOwner string `json:"nameWithOwner"`
+					} `json:"repository"`
+				} `json:"nodes"`
+			} `json:"repositoryDiscussions"`
+			RepositoryDiscussionComments struct {
+				Nodes []struct {
+					Discussion struct {
+						Title      string `json:"title"`
+						URL        string `json:"url"`
+						Repository struct {
+							NameWithOwner string `json:"nameWithOwner"`
+						} `json:"repository"`
+					} `json:"discussion"`
+					CreatedAt string `json:"createdAt"`
+					URL       string `json:"url"`
+				} `json:"nodes"`
+			} `json:"repositoryDiscussionComments"`
+		} `json:"viewer"`
+	} `json:"data"`
+}
+
+type gqlCommentsResponse struct {
+	Data struct {
+		Viewer struct {
+			IssueComments struct {
+				Nodes []struct {
+					URL       string `json:"url"`
+					CreatedAt string `json:"createdAt"`
+					Issue     struct {
+						Title      string `json:"title"`
+						Number     int    `json:"number"`
+						Repository struct {
+							NameWithOwner string `json:"nameWithOwner"`
+						} `json:"repository"`
+					} `json:"issue"`
+				} `json:"nodes"`
+			} `json:"issueComments"`
+		} `json:"viewer"`
+	} `json:"data"`
+}
+
+// FetchDiscussionsSince fetches discussions authored by or commented on by the user.
+func FetchDiscussionsSince(ctx context.Context, since time.Time) ([]DiscussionItem, error) {
+	query := `{
+  viewer {
+    repositoryDiscussions(first: 50, orderBy: {field: CREATED_AT, direction: DESC}) {
+      nodes {
+        title
+        url
+        createdAt
+        category { name }
+        repository { nameWithOwner }
+      }
+    }
+    repositoryDiscussionComments(first: 50) {
+      nodes {
+        discussion {
+          title
+          url
+          repository { nameWithOwner }
+        }
+        createdAt
+        url
+      }
+    }
+  }
+}`
+	cmd := exec.CommandContext(ctx, "gh", "api", "graphql", "-f", fmt.Sprintf("query=%s", query))
+	var stdout, stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+
+	if err := cmd.Run(); err != nil {
+		return nil, fmt.Errorf("gh api graphql (discussions): %s", stderr.String())
+	}
+
+	var resp gqlDiscussionResponse
+	if err := json.Unmarshal(stdout.Bytes(), &resp); err != nil {
+		return nil, fmt.Errorf("json parse error: %w", err)
+	}
+
+	var items []DiscussionItem
+	seen := make(map[string]bool)
+
+	for _, d := range resp.Data.Viewer.RepositoryDiscussions.Nodes {
+		ts, _ := time.Parse(time.RFC3339, d.CreatedAt)
+		if ts.Before(since) || seen[d.URL] {
+			continue
+		}
+		seen[d.URL] = true
+		items = append(items, DiscussionItem{
+			Repo:      d.Repository.NameWithOwner,
+			Title:     d.Title,
+			URL:       d.URL,
+			Category:  d.Category.Name,
+			Type:      "discussion",
+			CreatedAt: ts,
+		})
+	}
+
+	for _, c := range resp.Data.Viewer.RepositoryDiscussionComments.Nodes {
+		ts, _ := time.Parse(time.RFC3339, c.CreatedAt)
+		if ts.Before(since) || seen[c.URL] {
+			continue
+		}
+		seen[c.URL] = true
+		items = append(items, DiscussionItem{
+			Repo:      c.Discussion.Repository.NameWithOwner,
+			Title:     fmt.Sprintf("Comment on: %s", c.Discussion.Title),
+			URL:       c.URL,
+			Category:  "",
+			Type:      "discussion_comment",
+			CreatedAt: ts,
+		})
+	}
+
+	return items, nil
+}
+
+// FetchCommentsSince fetches issue/PR comments by the user since a given date.
+func FetchCommentsSince(ctx context.Context, since time.Time) ([]CommentItem, error) {
+	query := `{
+  viewer {
+    issueComments(first: 50, orderBy: {field: UPDATED_AT, direction: DESC}) {
+      nodes {
+        url
+        createdAt
+        issue {
+          title
+          number
+          repository { nameWithOwner }
+        }
+      }
+    }
+  }
+}`
+	cmd := exec.CommandContext(ctx, "gh", "api", "graphql", "-f", fmt.Sprintf("query=%s", query))
+	var stdout, stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+
+	if err := cmd.Run(); err != nil {
+		return nil, fmt.Errorf("gh api graphql (comments): %s", stderr.String())
+	}
+
+	var resp gqlCommentsResponse
+	if err := json.Unmarshal(stdout.Bytes(), &resp); err != nil {
+		return nil, fmt.Errorf("json parse error: %w", err)
+	}
+
+	var items []CommentItem
+	seen := make(map[string]bool)
+
+	for _, c := range resp.Data.Viewer.IssueComments.Nodes {
+		ts, _ := time.Parse(time.RFC3339, c.CreatedAt)
+		if ts.Before(since) || seen[c.URL] {
+			continue
+		}
+		seen[c.URL] = true
+		items = append(items, CommentItem{
+			Repo:        c.Issue.Repository.NameWithOwner,
+			IssueTitle:  c.Issue.Title,
+			IssueNumber: c.Issue.Number,
+			URL:         c.URL,
+			CreatedAt:   ts,
+		})
+	}
+
+	return items, nil
+}
+
 // FetchIssueSummary fetches title for a single GitHub issue.
 func FetchIssueSummary(ctx context.Context, repo string, number int) (string, error) {
 	cmd := exec.CommandContext(ctx, "gh", "issue", "view",