// Package ghapi provides GitHub issue management functionality including // label operations, milestone management, and project interactions. package ghapi import ( "encoding/json" "fmt" ) // ParseJSONtoIssues converts JSON data to a slice of Issue structs. func ParseJSONtoIssues(jsonData []byte) ([]Issue, error) { var issues []Issue err := json.Unmarshal(jsonData, &issues) if err != nil { return nil, err } return issues, nil } // GetIssues fetches issues from GitHub using optional search criteria. func GetIssues(search string) ([]Issue, error) { var issues []Issue command := "gh issue list --json number,title,author,createdAt,updatedAt,state,labels,body" if search != "" { command = fmt.Sprintf("%s -S '%s'", command, search) } results, err := RunCommandAndReturnOutput(command) if err != nil { return nil, err } issues, err = ParseJSONtoIssues(results) if err != nil { return nil, err } return issues, nil } // GetIssuesByMilestoneLimited returns up to 'limit' issues for the given milestone title. // It fetches limit+1 to detect whether there are more results than the limit; the returned // slice is trimmed to 'limit', and the boolean indicates if more were available. func GetIssuesByMilestoneLimited(title string, limit int) ([]Issue, bool, error) { if limit <= 0 { limit = 300 } // Request one extra to detect overflow reqLimit := limit + 1 cmd := fmt.Sprintf("gh issue list --milestone %q --json number,title,author,createdAt,updatedAt,state,labels,body --limit %d", title, reqLimit) out, err := RunCommandAndReturnOutput(cmd) if err != nil { return nil, false, err } issues, err := ParseJSONtoIssues(out) if err != nil { return nil, false, err } exceeded := false if len(issues) > limit { exceeded = true issues = issues[:limit] } return issues, exceeded, nil } // AddLabelToIssue adds a label to an issue. func AddLabelToIssue(issueNumber int, label string) error { command := fmt.Sprintf("gh issue edit %d --add-label %s", issueNumber, label) _, err := RunCommandAndReturnOutput(command) if err != nil { return err } return nil } // RemoveLabelFromIssue removes a label from an issue. func RemoveLabelFromIssue(issueNumber int, label string) error { command := fmt.Sprintf("gh issue edit %d --remove-label %s", issueNumber, label) _, err := RunCommandAndReturnOutput(command) if err != nil { return err } return nil } // CloseIssue closes a GitHub issue. func CloseIssue(issueNumber int) error { command := fmt.Sprintf("gh issue close %d", issueNumber) _, err := RunCommandAndReturnOutput(command) if err != nil { return err } return nil } // SetMilestoneToIssue sets a milestone for an issue. func SetMilestoneToIssue(issueNumber int, milestone string) error { command := fmt.Sprintf("gh issue edit %d --milestone %s", issueNumber, milestone) _, err := RunCommandAndReturnOutput(command) if err != nil { return err } return nil } // AddIssueToProject adds an issue to a project. func AddIssueToProject(issueNumber int, projectID int) error { command := fmt.Sprintf("gh project item-add %d --owner fleetdm --url https://github.com/fleetdm/fleet/issues/%d", projectID, issueNumber) _, err := RunCommandAndReturnOutput(command) if err != nil { return err } return nil } // RemoveIssueFromProject removes an issue from a project. func RemoveIssueFromProject(issueNumber int, projectID int) error { // Get the project item ID for this issue using the same method as other functions itemID, err := GetProjectItemID(issueNumber, projectID) if err != nil { // If the issue is not found in the project, that's not an error if err.Error() == fmt.Sprintf("issue #%d not found in project %d", issueNumber, projectID) { return nil } return fmt.Errorf("failed to get project item ID: %v", err) } // Remove the item using the project ID and item ID command := fmt.Sprintf("gh project item-delete %d --owner fleetdm --id %s", projectID, itemID) _, err = RunCommandAndReturnOutput(command) if err != nil { return fmt.Errorf("failed to remove issue %d from project %d: %v", issueNumber, projectID, err) } return nil } // SyncEstimateField synchronizes the estimate field value from one project to another. func SyncEstimateField(issueNumber int, sourceProjectID, targetProjectID int) error { // Get the source project item to find the current estimate sourceItemID, err := GetProjectItemID(issueNumber, sourceProjectID) if err != nil { return fmt.Errorf("failed to get source project item ID: %v", err) } // Get the target project item targetItemID, err := GetProjectItemID(issueNumber, targetProjectID) if err != nil { return fmt.Errorf("failed to get target project item ID: %v", err) } // Get the estimate value from the source project using GraphQL sourceEstimate, err := GetProjectItemFieldValue(sourceItemID, sourceProjectID, "Estimate") if err != nil { return fmt.Errorf("failed to get source estimate: %v", err) } if sourceEstimate == "" || sourceEstimate == "0" { return nil // No estimate to sync } // Set the estimate in the target project err = SetProjectItemFieldValue(targetItemID, targetProjectID, "Estimate", sourceEstimate) if err != nil { return fmt.Errorf("failed to set target estimate: %v", err) } return nil } // SetCurrentSprint sets the current sprint for an issue in a project. func SetCurrentSprint(issueNumber int, projectID int) error { // Get the project item ID itemID, err := GetProjectItemID(issueNumber, projectID) if err != nil { return fmt.Errorf("failed to get project item ID: %v", err) } // Look up the sprint field ID sprintField, err := LookupProjectFieldName(projectID, "sprint") if err != nil { return fmt.Errorf("failed to lookup sprint field: %v", err) } // Use the general field setting function to set the sprint field to @current err = SetProjectItemFieldValue(itemID, projectID, sprintField.Name, "@current") if err != nil { return fmt.Errorf("failed to set current sprint: %v", err) } return nil } // SetIssueStatus sets the status of an issue in a project using the Status field. func SetIssueStatus(issueNumber int, projectID int, status string) error { // Get the project item ID itemID, err := GetProjectItemID(issueNumber, projectID) if err != nil { return fmt.Errorf("failed to get project item ID: %v", err) } // Use the general field setting function to set the Status field err = SetProjectItemFieldValue(itemID, projectID, "Status", status) if err != nil { return fmt.Errorf("failed to set status: %v", err) } return nil }