fleet/tools/github-manage/pkg/ghapi/workflows.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

489 lines
18 KiB
Go

package ghapi
import (
"strings"
"fleetdm/gm/pkg/logger"
)
// ActionType represents the type of action to be performed on an issue.
type ActionType string
// newworkflow new actions added must have a new actiontype
const (
ATAddLabel ActionType = "add_label"
ATRemoveLabel ActionType = "remove_label"
ATAddIssueToProject ActionType = "add_issue_to_project"
ATRemoveIssueFromProject ActionType = "remove_issue_from_project"
ATSetStatus ActionType = "set_status"
ATSyncEstimate ActionType = "sync_estimate"
ATSetSprint ActionType = "set_sprint"
)
// Action represents a single action to be performed on an issue.
type Action struct {
Type ActionType `json:"type"`
Issue Issue `json:"issue"`
Project int `json:"project,omitempty"` // Project ID for project-related actions
Label string `json:"label,omitempty"` // Label for label-related actions
Status string `json:"status,omitempty"` // Status for status-related actions
Sprint string `json:"sprint,omitempty"` // Sprint for sprint-related actions
SourceProject int `json:"source_project,omitempty"` // Source project for moving issues
}
// Status represents the status of an item in a project.
type Status struct {
Index int `json:"index"`
State string `json:"state"`
}
// newworkflow new workflow 'action builder' functions are here
// CreateBulkAddLableAction creates actions to add a label to multiple issues.
func CreateBulkAddLableAction(issues []Issue, label string) []Action {
var actions []Action
for _, issue := range issues {
actions = append(actions, Action{
Type: ATAddLabel,
Issue: issue,
Label: label,
})
}
return actions
}
// CreateBulkRemoveLabelAction creates actions to remove a label from multiple issues.
func CreateBulkRemoveLabelAction(issues []Issue, label string) []Action {
var actions []Action
for _, issue := range issues {
actions = append(actions, Action{
Type: ATRemoveLabel,
Issue: issue,
Label: label,
})
}
return actions
}
// CreateBulkAddIssueToProjectAction creates actions to add multiple issues to a project.
func CreateBulkAddIssueToProjectAction(issues []Issue, projectID int) []Action {
var actions []Action
for _, issue := range issues {
actions = append(actions, Action{
Type: ATAddIssueToProject,
Issue: issue,
Project: projectID,
})
}
return actions
}
// CreateBulkRemoveIssueFromProjectAction creates actions to remove multiple issues from a project.
func CreateBulkRemoveIssueFromProjectAction(issues []Issue, projectID int) []Action {
var actions []Action
for _, issue := range issues {
actions = append(actions, Action{
Type: ATRemoveIssueFromProject,
Issue: issue,
Project: projectID,
})
}
return actions
}
// CreateBulkSetStatusAction creates actions to set the status for multiple issues in a project.
func CreateBulkSetStatusAction(issues []Issue, projectID int, status string) []Action {
var actions []Action
for _, issue := range issues {
actions = append(actions, Action{
Type: ATSetStatus,
Issue: issue,
Project: projectID,
Status: status,
})
}
return actions
}
// CreateBulkSyncEstimateAction creates actions to sync estimates from source to target projects for multiple issues.
func CreateBulkSyncEstimateAction(issues []Issue, sourceProjectID, targetProjectID int) []Action {
var actions []Action
for _, issue := range issues {
actions = append(actions, Action{
Type: ATSyncEstimate,
Issue: issue,
SourceProject: sourceProjectID,
Project: targetProjectID,
})
}
return actions
}
// CreateBulkSetSprintAction creates actions to set the sprint for multiple issues in a project.
func CreateBulkSetSprintAction(issues []Issue, projectID int) []Action {
var actions []Action
for _, issue := range issues {
actions = append(actions, Action{
Type: ATSetSprint,
Issue: issue,
Project: projectID,
})
}
return actions
}
// CreateBulkMoveToCurrentSprintIfNotReadyQA creates actions to set current sprint for issues whose
// status does NOT contain "ready" or "qa" (case-insensitive) in the given project.
func CreateBulkMoveToCurrentSprintIfNotReadyQA(issues []Issue, projectID int) []Action {
var filtered []Issue
for _, is := range issues {
s := strings.ToLower(strings.TrimSpace(is.Status))
if s == "" {
// No status -> include (move to current sprint)
filtered = append(filtered, is)
continue
}
if strings.Contains(s, "ready") || strings.Contains(s, "qa") {
// Skip statuses with ready or qa
continue
}
filtered = append(filtered, is)
}
return CreateBulkSetSprintAction(filtered, projectID)
}
func CreateBulkSprintKickoffActions(issues []Issue, sourceProjectID, projectID int) []Action {
logger.Infof("Creating sprint kickoff actions for %d issues (source project: %d, target project: %d)", len(issues), sourceProjectID, projectID)
actions := CreateBulkAddIssueToProjectAction(issues, projectID)
actions = append(actions, CreateBulkAddLableAction(issues, ":release")...)
actions = append(actions, CreateBulkSyncEstimateAction(issues, sourceProjectID, projectID)...)
actions = append(actions, CreateBulkSetSprintAction(issues, projectID)...)
actions = append(actions, CreateBulkRemoveLabelAction(issues, ":product")...)
actions = append(actions, CreateBulkRemoveIssueFromProjectAction(issues, Aliases["draft"])...)
logger.Infof("Created %d sprint kickoff actions", len(actions))
return actions
}
func CreateBulkMilestoneCloseActions(issues []Issue) []Action {
logger.Infof("Creating milestone close actions for %d issues", len(issues))
actions := CreateBulkAddIssueToProjectAction(issues, Aliases["draft"])
actions = append(actions, CreateBulkAddLableAction(issues, ":product")...)
actions = append(actions, CreateBulkSetStatusAction(issues, Aliases["draft"], "confirm and")...)
actions = append(actions, CreateBulkRemoveLabelAction(issues, ":release")...)
logger.Infof("Created %d milestone close actions", len(actions))
return actions
}
func CreateBulkKickOutOfSprintActions(issues []Issue, sourceProjectID int) []Action {
logger.Infof("Creating kick out of sprint actions for %d issues (source project: %d)", len(issues), sourceProjectID)
actions := CreateBulkAddIssueToProjectAction(issues, Aliases["draft"])
actions = append(actions, CreateBulkSetStatusAction(issues, Aliases["draft"], "estimated")...)
actions = append(actions, CreateBulkSyncEstimateAction(issues, sourceProjectID, Aliases["draft"])...)
actions = append(actions, CreateBulkAddLableAction(issues, ":product")...)
actions = append(actions, CreateBulkRemoveLabelAction(issues, ":release")...)
actions = append(actions, CreateBulkRemoveIssueFromProjectAction(issues, sourceProjectID)...)
logger.Infof("Created %d kick out of sprint actions", len(actions))
return actions
}
// AsyncManager takes a list of actions and a channel to process them assynchronously.
// This will allow to send status back on the channel for live updates. the channel must return index of the action
// and the status of the action.
func AsyncManager(actions []Action, statusChan chan<- Status) {
defer close(statusChan)
logger.Infof("Starting AsyncManager with %d actions", len(actions))
for i, action := range actions {
logger.Infof("Processing action %d/%d: %s for issue #%d", i+1, len(actions), action.Type, action.Issue.Number)
switch action.Type {
// newworkflow new actions must be supported in this switch
case ATAddLabel:
err := AddLabelToIssue(action.Issue.Number, action.Label)
if err != nil {
logger.Errorf("Failed to add label '%s' to issue #%d: %v", action.Label, action.Issue.Number, err)
statusChan <- Status{Index: i, State: "error"}
continue
}
logger.Infof("Successfully added label '%s' to issue #%d", action.Label, action.Issue.Number)
statusChan <- Status{Index: i, State: "success"}
case ATRemoveLabel:
err := RemoveLabelFromIssue(action.Issue.Number, action.Label)
if err != nil {
logger.Errorf("Failed to remove label '%s' from issue #%d: %v", action.Label, action.Issue.Number, err)
statusChan <- Status{Index: i, State: "error"}
continue
}
logger.Infof("Successfully removed label '%s' from issue #%d", action.Label, action.Issue.Number)
statusChan <- Status{Index: i, State: "success"}
case ATAddIssueToProject:
err := AddIssueToProject(action.Issue.Number, action.Project)
if err != nil {
logger.Errorf("Failed to add issue #%d to project %d: %v", action.Issue.Number, action.Project, err)
statusChan <- Status{Index: i, State: "error"}
continue
}
logger.Infof("Successfully added issue #%d to project %d", action.Issue.Number, action.Project)
statusChan <- Status{Index: i, State: "success"}
case ATRemoveIssueFromProject:
err := RemoveIssueFromProject(action.Issue.Number, action.Project)
if err != nil {
logger.Errorf("Failed to remove issue #%d from project %d: %v", action.Issue.Number, action.Project, err)
statusChan <- Status{Index: i, State: "error"}
continue
}
logger.Infof("Successfully removed issue #%d from project %d", action.Issue.Number, action.Project)
statusChan <- Status{Index: i, State: "success"}
case ATSetStatus:
err := SetIssueStatus(action.Issue.Number, action.Project, action.Status)
if err != nil {
logger.Errorf("Failed to set status '%s' for issue #%d in project %d: %v", action.Status, action.Issue.Number, action.Project, err)
statusChan <- Status{Index: i, State: "error"}
continue
}
logger.Infof("Successfully set status '%s' for issue #%d in project %d", action.Status, action.Issue.Number, action.Project)
statusChan <- Status{Index: i, State: "success"}
case ATSyncEstimate:
err := SyncEstimateField(action.Issue.Number, action.SourceProject, action.Project)
if err != nil {
logger.Errorf("Failed to sync estimate for issue #%d from project %d to project %d: %v", action.Issue.Number, action.SourceProject, action.Project, err)
statusChan <- Status{Index: i, State: "error"}
continue
}
logger.Infof("Successfully synced estimate for issue #%d from project %d to project %d", action.Issue.Number, action.SourceProject, action.Project)
statusChan <- Status{Index: i, State: "success"}
case ATSetSprint:
err := SetCurrentSprint(action.Issue.Number, action.Project)
if err != nil {
logger.Errorf("Failed to set current sprint for issue #%d in project %d: %v", action.Issue.Number, action.Project, err)
statusChan <- Status{Index: i, State: "error"}
continue
}
logger.Infof("Successfully set current sprint for issue #%d in project %d", action.Issue.Number, action.Project)
statusChan <- Status{Index: i, State: "success"}
default:
logger.Errorf("Unknown action type: %s for issue #%d", action.Type, action.Issue.Number)
statusChan <- Status{Index: i, State: "error"}
}
}
logger.Info("AsyncManager completed all actions")
}
//
// IMPORTANT: TUI workflows should use the Async methods above instead of the synchronous methods below
//
// These methods are more for testing individual steps w/o tui.
// BulkAddLabel adds a label to multiple issues.
func BulkAddLabel(issues []Issue, label string) error {
logger.Infof("Adding label '%s' to %d issues", label, len(issues))
for _, issue := range issues {
err := AddLabelToIssue(issue.Number, label)
if err != nil {
logger.Errorf("Failed to add label '%s' to issue #%d: %v", label, issue.Number, err)
return err
}
logger.Debugf("Added label '%s' to issue #%d", label, issue.Number)
}
logger.Infof("Successfully added label '%s' to all %d issues", label, len(issues))
return nil
}
// BulkRemoveLabel removes a label from multiple issues.
func BulkRemoveLabel(issues []Issue, label string) error {
logger.Infof("Removing label '%s' from %d issues", label, len(issues))
for _, issue := range issues {
err := RemoveLabelFromIssue(issue.Number, label)
if err != nil {
logger.Errorf("Failed to remove label '%s' from issue #%d: %v", label, issue.Number, err)
return err
}
logger.Debugf("Removed label '%s' from issue #%d", label, issue.Number)
}
logger.Infof("Successfully removed label '%s' from all %d issues", label, len(issues))
return nil
}
// BulkSprintKickoff performs the sprint kickoff workflow for multiple issues.
// This includes adding issues to the target project, adding release labels, syncing estimates, and setting sprints.
func BulkSprintKickoff(issues []Issue, sourceProjectID, projectID int) error {
logger.Infof("Starting sprint kickoff workflow for %d issues (source: %d, target: %d)", len(issues), sourceProjectID, projectID)
// Add ticket to the target product group project
logger.Info("Step 1/6: Adding issues to target project")
for _, issue := range issues {
err := AddIssueToProject(issue.Number, projectID)
if err != nil {
logger.Errorf("Failed to add issue #%d to project %d: %v", issue.Number, projectID, err)
return err
}
logger.Debugf("Added issue #%d to project %d", issue.Number, projectID)
}
// Add the `:release` label to each issue
logger.Info("Step 2/6: Adding release labels")
err := BulkAddLabel(issues, ":release")
if err != nil {
return err
}
// Sync the Estimate field from drafting project to the target product group project
logger.Info("Step 3/6: Syncing estimates")
for _, issue := range issues {
err := SyncEstimateField(issue.Number, sourceProjectID, projectID)
if err != nil {
logger.Errorf("Failed to sync estimate for issue #%d: %v", issue.Number, err)
return err
}
logger.Debugf("Synced estimate for issue #%d", issue.Number)
}
// Set the sprint to the current sprint
logger.Info("Step 4/6: Setting current sprint")
for _, issue := range issues {
err := SetCurrentSprint(issue.Number, projectID)
if err != nil {
logger.Errorf("Failed to set current sprint for issue #%d: %v", issue.Number, err)
return err
}
logger.Debugf("Set current sprint for issue #%d", issue.Number)
}
// Remove the `:product` label from each issue
logger.Info("Step 5/6: Removing product labels")
err = BulkRemoveLabel(issues, ":product")
if err != nil {
return err
}
// Remove from the drafting project
logger.Info("Step 6/6: Removing from drafting project")
draftingProjectID := Aliases["draft"]
for _, issue := range issues {
err := RemoveIssueFromProject(issue.Number, draftingProjectID)
if err != nil {
logger.Errorf("Failed to remove issue #%d from drafting project: %v", issue.Number, err)
return err
}
logger.Debugf("Removed issue #%d from drafting project", issue.Number)
}
logger.Info("Sprint kickoff workflow completed successfully")
return nil
}
// BulkMilestoneClose performs the milestone close workflow for multiple issues.
// This includes moving issues back to the drafting project and removing them from product group projects.
func BulkMilestoneClose(issues []Issue) error {
logger.Infof("Starting milestone close workflow for %d issues", len(issues))
// Add ticket to the drafting project
logger.Info("Step 1/4: Adding issues to drafting project")
draftingProjectID := Aliases["draft"]
for _, issue := range issues {
err := AddIssueToProject(issue.Number, draftingProjectID)
if err != nil {
logger.Errorf("Failed to add issue #%d to drafting project: %v", issue.Number, err)
return err
}
logger.Debugf("Added issue #%d to drafting project", issue.Number)
}
// Add the `:product` label to each issue
logger.Info("Step 2/4: Adding product labels")
err := BulkAddLabel(issues, ":product")
if err != nil {
return err
}
// Set the status to "confirm and celebrate"
logger.Info("Step 3/4: Setting status to 'confirm and celebrate'")
for _, issue := range issues {
err := SetIssueStatus(issue.Number, draftingProjectID, "confirm and celebrate")
if err != nil {
logger.Errorf("Failed to set status for issue #%d: %v", issue.Number, err)
return err
}
logger.Debugf("Set status for issue #%d", issue.Number)
}
// Remove the `:release` label from each issue
logger.Info("Step 4/4: Removing release labels")
err = BulkRemoveLabel(issues, ":release")
if err != nil {
return err
}
logger.Info("Milestone close workflow completed successfully")
return nil
}
// BulkKickOutOfSprint performs the kick out of sprint workflow for multiple issues.
// This includes moving issues back to the drafting project, setting status to estimated,
// syncing estimates from source project, and updating labels.
func BulkKickOutOfSprint(issues []Issue, sourceProjectID int) error {
logger.Infof("Starting kick out of sprint workflow for %d issues (source: %d)", len(issues), sourceProjectID)
// Add issues to the drafting project
logger.Info("Step 1/5: Adding issues to drafting project")
draftingProjectID := Aliases["draft"]
for _, issue := range issues {
err := AddIssueToProject(issue.Number, draftingProjectID)
if err != nil {
logger.Errorf("Failed to add issue #%d to drafting project: %v", issue.Number, err)
return err
}
logger.Debugf("Added issue #%d to drafting project", issue.Number)
}
// Set the status to "estimated"
logger.Info("Step 2/5: Setting status to 'estimated'")
for _, issue := range issues {
err := SetIssueStatus(issue.Number, draftingProjectID, "estimated")
if err != nil {
logger.Errorf("Failed to set status for issue #%d: %v", issue.Number, err)
return err
}
logger.Debugf("Set status for issue #%d", issue.Number)
}
// Sync the Estimate field from source project to the drafting project
logger.Info("Step 3/5: Syncing estimates from source project")
for _, issue := range issues {
err := SyncEstimateField(issue.Number, sourceProjectID, draftingProjectID)
if err != nil {
logger.Errorf("Failed to sync estimate for issue #%d: %v", issue.Number, err)
return err
}
logger.Debugf("Synced estimate for issue #%d", issue.Number)
}
// Add the `:product` label to each issue
logger.Info("Step 4/5: Adding product labels")
err := BulkAddLabel(issues, ":product")
if err != nil {
return err
}
// Remove the `:release` label from each issue
logger.Info("Step 5/5: Removing release labels")
err = BulkRemoveLabel(issues, ":release")
if err != nil {
return err
}
logger.Info("Kick out of sprint workflow completed successfully")
return nil
}