Commit d0ac4bb7971d
Changed files (3)
tools
daily-plan
cmd
daily-plan
internal
display
github
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",