Commit d8468848504b
tools/daily-plan/cmd/daily-plan/main.go
@@ -126,13 +126,15 @@ type jsonInbox struct {
}
type jsonAdvisory struct {
- GHSAID string `json:"ghsa_id"`
- CVEID string `json:"cve_id,omitempty"`
- Summary string `json:"summary"`
- Severity string `json:"severity"`
- State string `json:"state"`
- Repo string `json:"repo"`
- URL string `json:"url"`
+ GHSAID string `json:"ghsa_id"`
+ CVEID string `json:"cve_id,omitempty"`
+ Summary string `json:"summary"`
+ Severity string `json:"severity"`
+ State string `json:"state"`
+ Repo string `json:"repo"`
+ URL string `json:"url"`
+ Credits []string `json:"credits,omitempty"`
+ Role string `json:"role"` // "author", "credited", or "collaborator"
}
type jsonDependabot struct {
@@ -363,6 +365,7 @@ func cmdInbox(ctx context.Context, cfg *config.Config, sinceStr string) error {
ja = append(ja, jsonAdvisory{
GHSAID: a.GHSAID, CVEID: a.CVEID, Summary: a.Summary,
Severity: a.Severity, State: a.State, Repo: a.Repo, URL: a.HTMLURL,
+ Credits: a.Credits, Role: a.Role,
})
}
jd := make([]jsonDependabot, 0, len(dependabot))
@@ -569,19 +572,20 @@ type jsonAI struct {
}
type jsonWeekly struct {
- Week string `json:"week"`
- NextMonday string `json:"next_monday"`
- OrgDone []jsonOrgDone `json:"org_done,omitempty"`
- AISessions []jsonAI `json:"ai_sessions,omitempty"`
- 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"`
+ Week string `json:"week"`
+ NextMonday string `json:"next_monday"`
+ OrgDone []jsonOrgDone `json:"org_done,omitempty"`
+ AISessions []jsonAI `json:"ai_sessions,omitempty"`
+ 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"`
+ SecurityAdvisories []jsonAdvisory `json:"github_security_advisories,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 {
@@ -822,18 +826,38 @@ func cmdReview(ctx context.Context, cfg *config.Config, period string) error {
})
}
+ // Security advisories
+ advisories, _ := cache.GetOrFetch(apiCache, "gh:advisories:"+startStr, func() ([]github.SecurityAdvisory, error) {
+ return github.FetchSecurityAdvisoriesSince(ctx, cfg.GitHub.SecurityRepos, start, cfg.GitHub.Username)
+ })
+
if outputJSON {
+ var ja []jsonAdvisory
+ for _, a := range advisories {
+ ja = append(ja, jsonAdvisory{
+ GHSAID: a.GHSAID,
+ CVEID: a.CVEID,
+ Summary: a.Summary,
+ Severity: a.Severity,
+ State: a.State,
+ Repo: a.Repo,
+ URL: a.HTMLURL,
+ Credits: a.Credits,
+ Role: a.Role,
+ })
+ }
return emitJSON(jsonWeekly{
- Week: startStr,
- NextMonday: endStr,
- OrgDone: orgDoneToJSON(orgDone),
- AISessions: aiToJSON(aiItems),
- JiraCompleted: jiraToJSON(completed, cfg.Jira.BaseURL),
- GHMerged: ghToJSON(merged),
- GHReviewed: ghToJSON(reviewed),
- GHIssuesCreated: ghToJSON(issuesCreated),
- GHDiscussions: discussionsToJSON(discussions),
- GHComments: commentsToJSON(comments),
+ Week: startStr,
+ NextMonday: endStr,
+ OrgDone: orgDoneToJSON(orgDone),
+ AISessions: aiToJSON(aiItems),
+ JiraCompleted: jiraToJSON(completed, cfg.Jira.BaseURL),
+ GHMerged: ghToJSON(merged),
+ GHReviewed: ghToJSON(reviewed),
+ GHIssuesCreated: ghToJSON(issuesCreated),
+ GHDiscussions: discussionsToJSON(discussions),
+ GHComments: commentsToJSON(comments),
+ SecurityAdvisories: ja,
})
}
@@ -916,6 +940,18 @@ func cmdReview(ctx context.Context, cfg *config.Config, period string) error {
fmt.Println()
}
+ if len(advisories) > 0 {
+ fmt.Printf("## Security Advisories (%d)\n\n", len(advisories))
+ for _, a := range advisories {
+ cve := ""
+ if a.CVEID != "" {
+ cve = fmt.Sprintf(" (%s)", a.CVEID)
+ }
+ fmt.Printf("- [%s] **%s** %s%s — %s\n", a.State, a.GHSAID, a.Summary, cve, a.Repo)
+ }
+ fmt.Println()
+ }
+
if len(aiItems) > 0 {
// Filter auto-recovered
var filtered []ai.Item
tools/daily-plan/internal/github/github.go
@@ -115,29 +115,37 @@ func FetchNewPRsSince(ctx context.Context, since time.Time, owners []string) ([]
}
// FetchMergedPRsSince returns PRs by user merged since the given date.
+// Splits the query by month to avoid GitHub Search API result limits.
func FetchMergedPRsSince(ctx context.Context, username string, since time.Time, owners []string) ([]Item, error) {
- args := []string{
- "search", "prs",
- "--author", username,
- "--merged", fmt.Sprintf(">=%s", since.Format("2006-01-02")),
- "--limit", "30",
- "--json", "repository,number,title,closedAt",
- }
- args = append(args, ownerFlags(owners)...)
- return runGHSearch(ctx, args, "pr")
+ return fetchByMonth(ctx, since, func(start, end string) ([]Item, error) {
+ args := []string{
+ "search", "prs",
+ "--author", username,
+ "--merged",
+ "--merged-at", start + ".." + end,
+ "--limit", "1000",
+ "--json", "repository,number,title,closedAt",
+ }
+ args = append(args, ownerFlags(owners)...)
+ return runGHSearch(ctx, args, "pr")
+ })
}
// FetchReviewsGivenSince fetches PRs reviewed by the user since a given date.
+// Splits the query by month to avoid GitHub Search API result limits.
func FetchReviewsGivenSince(ctx context.Context, username string, since time.Time, owners []string) ([]Item, error) {
- args := []string{
- "search", "prs",
- "--reviewed-by", username,
- "--updated", fmt.Sprintf(">=%s", since.Format("2006-01-02")),
- "--limit", "30",
- "--json", "repository,number,title,author,createdAt,closedAt",
- }
- args = append(args, ownerFlags(owners)...)
- items, err := runGHSearch(ctx, args, "pr")
+ items, err := fetchByMonth(ctx, since, func(start, end string) ([]Item, error) {
+ args := []string{
+ "search", "prs",
+ "--reviewed-by", username,
+ "--merged",
+ "--merged-at", start + ".." + end,
+ "--limit", "1000",
+ "--json", "repository,number,title,author,createdAt,closedAt",
+ }
+ args = append(args, ownerFlags(owners)...)
+ return runGHSearch(ctx, args, "pr")
+ })
if err != nil {
return nil, err
}
@@ -152,16 +160,19 @@ func FetchReviewsGivenSince(ctx context.Context, username string, since time.Tim
}
// FetchIssuesCreatedSince returns issues created by user since the given date.
+// Splits the query by month to avoid GitHub Search API result limits.
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")
+ return fetchByMonth(ctx, since, func(start, end string) ([]Item, error) {
+ args := []string{
+ "search", "issues",
+ "--author", username,
+ "--created", start + ".." + end,
+ "--limit", "1000",
+ "--json", "repository,number,title,createdAt,author",
+ }
+ args = append(args, ownerFlags(owners)...)
+ return runGHSearch(ctx, args, "issue")
+ })
}
// DiscussionItem represents a GitHub discussion or discussion comment.
@@ -412,6 +423,8 @@ type SecurityAdvisory struct {
HTMLURL string `json:"html_url"`
Repo string `json:"-"` // filled in by caller
CreatedAt time.Time `json:"created_at"`
+ Credits []string `json:"-"` // credit logins, filled from raw response
+ Role string `json:"-"` // "credited", "collaborator", "author"
}
// FetchSecurityAdvisories fetches open/triage security advisories for specific repos.
@@ -433,6 +446,27 @@ func FetchSecurityAdvisories(ctx context.Context, repos []string, states []strin
return all, nil
}
+// advisoryRaw is the raw JSON shape from the GitHub Security Advisories API,
+// including credits and collaborating_users for involvement filtering.
+type advisoryRaw struct {
+ GHSAID string `json:"ghsa_id"`
+ CVEID string `json:"cve_id"`
+ Summary string `json:"summary"`
+ Severity string `json:"severity"`
+ State string `json:"state"`
+ HTMLURL string `json:"html_url"`
+ CreatedAt time.Time `json:"created_at"`
+ Author struct {
+ Login string `json:"login"`
+ } `json:"author"`
+ Credits []struct {
+ Login string `json:"login"`
+ } `json:"credits"`
+ CollaboratingUsers []struct {
+ Login string `json:"login"`
+ } `json:"collaborating_users"`
+}
+
func fetchRepoAdvisories(ctx context.Context, repo, state string) ([]SecurityAdvisory, error) {
endpoint := fmt.Sprintf("repos/%s/security-advisories?state=%s&per_page=30", repo, state)
cmd := exec.CommandContext(ctx, "gh", "api", endpoint)
@@ -443,13 +477,49 @@ func fetchRepoAdvisories(ctx context.Context, repo, state string) ([]SecurityAdv
return nil, fmt.Errorf("gh api error: %s", stderr.String())
}
- var advisories []SecurityAdvisory
- if err := json.Unmarshal(stdout.Bytes(), &advisories); err != nil {
+ var raw []advisoryRaw
+ if err := json.Unmarshal(stdout.Bytes(), &raw); err != nil {
return nil, err
}
+
+ advisories := make([]SecurityAdvisory, 0, len(raw))
+ for _, r := range raw {
+ var credits []string
+ for _, c := range r.Credits {
+ credits = append(credits, c.Login)
+ }
+ a := SecurityAdvisory{
+ GHSAID: r.GHSAID,
+ CVEID: r.CVEID,
+ Summary: r.Summary,
+ Severity: r.Severity,
+ State: r.State,
+ HTMLURL: r.HTMLURL,
+ CreatedAt: r.CreatedAt,
+ Credits: credits,
+ }
+ advisories = append(advisories, a)
+ }
return advisories, nil
}
+// userInvolved checks if a username appears in credits or as author.
+// We intentionally skip collaborating_users because that only indicates
+// access (e.g. via team membership like tekton-vmt), not active involvement.
+// Comments on advisories live in a private fork and are not accessible
+// via the REST API, so we cannot detect comment-based involvement.
+func userInvolved(raw advisoryRaw, username string) string {
+ if raw.Author.Login == username {
+ return "author"
+ }
+ for _, c := range raw.Credits {
+ if c.Login == username {
+ return "credited"
+ }
+ }
+ return ""
+}
+
// DependabotAlert represents a Dependabot security alert.
type DependabotAlert struct {
Number int `json:"number"`
@@ -520,6 +590,97 @@ func FetchDependabotAlerts(ctx context.Context, owners []string, severity string
return all, nil
}
+// fetchByMonth splits a date range into monthly chunks and runs the given
+// query function for each chunk, deduplicating results by repo+number.
+// This avoids the GitHub Search API's 1000-result-per-query limit.
+func fetchByMonth(ctx context.Context, since time.Time, queryFn func(start, end string) ([]Item, error)) ([]Item, error) {
+ now := time.Now()
+ var all []Item
+ seen := make(map[string]bool)
+
+ current := since
+ for current.Before(now) {
+ // End of this chunk: last day of the month or now, whichever is earlier
+ nextMonth := time.Date(current.Year(), current.Month()+1, 1, 0, 0, 0, 0, current.Location())
+ end := nextMonth.AddDate(0, 0, -1) // last day of current month
+ if end.After(now) {
+ end = now
+ }
+
+ startStr := current.Format("2006-01-02")
+ endStr := end.Format("2006-01-02")
+
+ items, err := queryFn(startStr, endStr)
+ if err != nil {
+ // Continue on error — partial data is better than none
+ current = nextMonth
+ continue
+ }
+
+ for _, item := range items {
+ key := fmt.Sprintf("%s#%d", item.Repo, item.Number)
+ if !seen[key] {
+ seen[key] = true
+ all = append(all, item)
+ }
+ }
+
+ current = nextMonth
+ }
+ return all, nil
+}
+
+// FetchSecurityAdvisoriesSince fetches security advisories created since the
+// given date where the specified user is involved (credited, collaborating, or author).
+func FetchSecurityAdvisoriesSince(ctx context.Context, repos []string, since time.Time, username string) ([]SecurityAdvisory, error) {
+ var all []SecurityAdvisory
+ for _, repo := range repos {
+ for _, state := range []string{"published", "closed", "draft", "triage"} {
+ // Fetch raw to check involvement
+ endpoint := fmt.Sprintf("repos/%s/security-advisories?state=%s&per_page=30", repo, state)
+ cmd := exec.CommandContext(ctx, "gh", "api", endpoint)
+ var stdout, stderr bytes.Buffer
+ cmd.Stdout = &stdout
+ cmd.Stderr = &stderr
+ if err := cmd.Run(); err != nil {
+ continue
+ }
+
+ var raw []advisoryRaw
+ if err := json.Unmarshal(stdout.Bytes(), &raw); err != nil {
+ continue
+ }
+
+ for _, r := range raw {
+ if r.CreatedAt.Before(since) {
+ continue
+ }
+ role := userInvolved(r, username)
+ if role == "" {
+ continue
+ }
+ var credits []string
+ for _, c := range r.Credits {
+ credits = append(credits, c.Login)
+ }
+ all = append(all, SecurityAdvisory{
+ GHSAID: r.GHSAID,
+ CVEID: r.CVEID,
+ Summary: r.Summary,
+ Severity: r.Severity,
+ State: r.State,
+ HTMLURL: r.HTMLURL,
+ CreatedAt: r.CreatedAt,
+ Repo: repo,
+ Credits: credits,
+ Role: role,
+ })
+ }
+ }
+ }
+ return all, nil
+}
+
func ownerFlags(owners []string) []string {
var flags []string
for _, owner := range owners {
tools/daily-plan/internal/jira/jira.go
@@ -57,23 +57,19 @@ func FetchSecuritySince(ctx context.Context, since time.Time) ([]Issue, error) {
}
// FetchCompletedSince fetches issues resolved by user since the given date.
+// Uses JQL with resolutiondate to get only issues actually resolved in the
+// period, not merely updated while in a Done/Closed state.
func FetchCompletedSince(ctx context.Context, user string, since time.Time) ([]Issue, error) {
- var allIssues []Issue
- for _, status := range []string{"Done", "Closed"} {
- sinceStr := since.Format("2006-01-02")
- args := []string{
- "issue", "list", "--plain", "--no-truncate",
- "-a", jiraMe(),
- "-s", status,
- "--updated-after", sinceStr,
- }
- issues, err := runJiraList(ctx, args)
- if err != nil {
- continue
- }
- allIssues = append(allIssues, issues...)
+ sinceStr := since.Format("2006-01-02")
+ jql := fmt.Sprintf(
+ `assignee = currentUser() AND statusCategory = Done AND resolution not in ("Obsolete", "Won't Do", "Won't Fix", "Duplicate") AND resolutiondate >= "%s"`,
+ sinceStr,
+ )
+ args := []string{
+ "issue", "list", "--plain", "--no-truncate",
+ "-q", jql,
}
- return allIssues, nil
+ return runJiraList(ctx, args)
}
// FetchSummary fetches the summary for a single issue key.