fleet/tools/github-manage/cmd/gm/roadmap.go
George Karr 0f2b83fcc0
gkarr gm new workflows (#35712)
- **Adding new command gm milestone report to veiw all statuses from all
projects for all issues tied to a milestone**
- **Add descriptions to workflow select and new workflows**
- **Adding new commands**
2025-11-14 10:13:22 -06:00

160 lines
5.3 KiB
Go

package main
import (
"fmt"
"sort"
"strings"
"fleetdm/gm/pkg/ghapi"
"github.com/spf13/cobra"
)
var roadmapCmd = &cobra.Command{
Use: "roadmap",
Short: "Roadmap utilities",
}
var roadmapSyncEstimatesCmd = &cobra.Command{
Use: "sync-estimates",
Short: "Sync estimates for issues on the Roadmap project from drafting/sprint projects or sub-issues",
Run: func(cmd *cobra.Command, args []string) {
roadmapProjectID := 87 // fleet Roadmap project
issueNum, _ := cmd.Flags().GetInt("issue")
overwrite, _ := cmd.Flags().GetBool("overwrite")
sprintTitle, _ := cmd.Flags().GetString("sprint")
// Define candidate source projects for estimates
sources := ghapi.DefaultEstimateSourceProjects()
var targets []int
if issueNum > 0 {
fmt.Printf("Checking Roadmap membership for #%d...\n", issueNum)
targets = []int{issueNum}
// Ensure the specified issue is on Roadmap
if !ghapi.IsIssueInProject(issueNum, roadmapProjectID) {
fmt.Printf("Error: issue #%d is not currently on the Roadmap project (%d)\n", issueNum, roadmapProjectID)
return
}
fmt.Printf("Gathering estimates for #%d...\n", issueNum)
// Optional sprint filter for single issue
if sprintTitle != "" {
fmt.Printf("Filtering by sprint '%s'...\n", sprintTitle)
checkProjects := append(append([]int{}, sources...), roadmapProjectID)
sprintSet, _ := ghapi.GetIssueNumbersForSprintAcrossProjects(checkProjects, sprintTitle, 2000)
if _, ok := sprintSet[issueNum]; !ok {
fmt.Printf("Issue #%d is not in sprint '%s' across known projects; skipping.\n", issueNum, sprintTitle)
return
}
fmt.Printf("Issue #%d matched sprint '%s'.\n", issueNum, sprintTitle)
}
} else {
fmt.Println("Finding Roadmap issues...")
// Fetch all roadmap issues
items, _, err := ghapi.GetProjectItemsWithTotal(roadmapProjectID, 1000)
if err != nil {
fmt.Printf("Failed to fetch roadmap items: %v\n", err)
return
}
for _, it := range items {
if it.Content.Number > 0 {
targets = append(targets, it.Content.Number)
}
}
fmt.Printf("Found %d Roadmap issues.\n", len(targets))
// Apply sprint filter if provided
if sprintTitle != "" {
fmt.Printf("Filtering by sprint '%s'...\n", sprintTitle)
// Build sprint set from source projects only to avoid re-querying Roadmap (we already have its items)
sprintSet, _ := ghapi.GetIssueNumbersForSprintAcrossProjects(sources, sprintTitle, 2000)
filtered := make([]int, 0, len(targets))
for _, n := range targets {
// include if in sources' sprint set or roadmap item shows matching sprint
_, inSet := sprintSet[n]
if inSet {
filtered = append(filtered, n)
continue
}
// check roadmap item sprint title from prefetched items
for _, it := range items {
if it.Content.Number == n && it.Sprint != nil && strings.EqualFold(strings.TrimSpace(it.Sprint.Title), strings.TrimSpace(sprintTitle)) {
filtered = append(filtered, n)
break
}
}
}
targets = filtered
fmt.Printf("%d issues match sprint '%s'.\n", len(targets), sprintTitle)
}
}
// Make iteration stable for output
sort.Ints(targets)
var (
updated int
skipped int
errors int
)
for _, n := range targets {
fmt.Printf("\nGathering estimate for #%d...\n", n)
// Skip if roadmap already has estimate and --all-issues not provided
if !overwrite {
if val, ok, _ := ghapi.GetEstimateFromProject(n, roadmapProjectID); ok && val > 0 {
skipped++
fmt.Printf("Skipping #%d: roadmap estimate already set to %d\n", n, val)
continue
}
}
// Primary: direct estimate from known projects
est, src, _ := ghapi.GetEstimateForIssueAcrossProjects(n, sources)
if est > 0 && src > 0 {
fmt.Printf("Found estimate %d in project %d.\n", est, src)
}
// Secondary: sum of sub-issue estimates
if est == 0 {
related, _ := ghapi.GetRelatedIssueNumbers(n)
if len(related) > 0 {
fmt.Printf("Found %d issues tied to #%d...\n", len(related), n)
} else {
fmt.Printf("No direct estimate found; checking sub-issues for #%d...\n", n)
}
sum, _ := ghapi.SumEstimatesFromSubIssues(n, sources)
est = sum
src = 0 // aggregated
if est > 0 {
fmt.Printf("Using aggregated sub-issue estimate: %d.\n", est)
}
}
if est == 0 {
fmt.Printf("No estimate found for #%d; leaving roadmap unchanged\n", n)
continue
}
if err := ghapi.SetEstimateInProject(n, roadmapProjectID, est); err != nil {
errors++
fmt.Printf("Failed to set roadmap estimate for #%d: %v\n", n, err)
continue
}
updated++
if src == 0 {
fmt.Printf("Updated #%d: roadmap estimate set to %d (sum of sub-issues)\n", n, est)
} else {
fmt.Printf("Updated #%d: roadmap estimate set to %d (from project %d)\n", n, est, src)
}
}
fmt.Printf("\nSummary: %d updated, %d skipped, %d errors\n", updated, skipped, errors)
},
}
func init() {
roadmapCmd.AddCommand(roadmapSyncEstimatesCmd)
roadmapSyncEstimatesCmd.Flags().IntP("issue", "i", 0, "Only sync for the given issue number; must be on the Roadmap project")
roadmapSyncEstimatesCmd.Flags().BoolP("overwrite", "o", false, "Overwrite existing Roadmap estimates (by default, issues with estimates are skipped)")
roadmapSyncEstimatesCmd.Flags().StringP("sprint", "s", "", "Only process issues in this sprint (iteration title), e.g., 4.77.0")
}