mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
- **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**
489 lines
18 KiB
Go
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
|
|
}
|