commit c2155367455955e1e142b212fa4d41acef664b7e Author: Olivier Tremblay Date: Sat Sep 13 08:40:40 2025 -0400 initial commit diff --git a/cmd/acb/main.go b/cmd/acb/main.go new file mode 100644 index 0000000..d1d9d70 --- /dev/null +++ b/cmd/acb/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "time" + + "o5r.ca/autocrossbow/contributions" + "o5r.ca/autocrossbow/issues" +) + +func main() { + proj := os.Args[1] + ghusername := os.Args[2] + start := os.Args[3] + end := os.Args[4] + DoPrs(proj, ghusername, start, end) + DoJira(start, end) +} + +func DoPrs(proj, ghusername, start, end string) { + + prs, err := contributions.GetPRs(proj, ghusername, start, end) + if err != nil { + fmt.Println(fmt.Errorf("error getting PRs: %w", err)) + os.Exit(1) + } + + ghf, err := os.Create(fmt.Sprintf("gh-%s-%s-%s-%s-%d.json", proj, ghusername, start, end, time.Now().Unix())) + if err != nil { + fmt.Println(fmt.Errorf("error creating PR file: %w", err)) + os.Exit(1) + } + enc := json.NewEncoder(ghf) + err = enc.Encode(prs) + if err != nil { + fmt.Println(fmt.Errorf("error writing out PRs: %w", err)) + os.Exit(1) + } +} +func DoJira(start, end string) { + host := os.Getenv("JIRA_HOST") + user := os.Getenv("JIRA_TARGET_USER") + prs, err := issues.GetIssues(host, user, start, end) + if err != nil { + fmt.Println(fmt.Errorf("error getting PRs: %w", err)) + os.Exit(1) + } + + ghf, err := os.Create(fmt.Sprintf("jira-%s-%s-%s-%s-%d.json", host, user, start, end, time.Now().Unix())) + if err != nil { + fmt.Println(fmt.Errorf("error creating PR file: %w", err)) + os.Exit(1) + } + enc := json.NewEncoder(ghf) + err = enc.Encode(prs) + if err != nil { + fmt.Println(fmt.Errorf("error writing out PRs: %w", err)) + os.Exit(1) + } +} diff --git a/cmd/acb/summarize.go b/cmd/acb/summarize.go new file mode 100644 index 0000000..b9fb3cb --- /dev/null +++ b/cmd/acb/summarize.go @@ -0,0 +1,66 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" +) + +// SummarizeData takes GitHub PRs and Jira issues data and sends it to an OpenAI-compatible endpoint for summarization. +func SummarizeData(prs map[string][]PullRequest, issues []Issue) (string, error) { + // Build a prompt string + prompt := "Summarize the following GitHub PRs and Jira issues:\n\n" + for repo, prList := range prs { + prompt += fmt.Sprintf("Repository: %s\n", repo) + for _, pr := range prList { + prompt += fmt.Sprintf("- Title: %s\n", pr.Title) + prompt += fmt.Sprintf(" Body: %s\n", pr.Body) + } + } + for _, issue := range issues { + prompt += fmt.Sprintf("\nJira Issue: %s\n", issue.Key) + prompt += fmt.Sprintf("Summary: %s\n", issue.Summary) + prompt += fmt.Sprintf("Description: %s\n", issue.Description) + } + + // Get OpenAI endpoint and token from environment variables + openaiEndpoint := os.Getenv("OPENAI_ENDPOINT") + openaiToken := os.Getenv("OPENAI_TOKEN") + + if openaiEndpoint == "" || openaiToken == "" { + return "", fmt.Errorf("OPENAI_ENDPOINT and OPENAI_TOKEN must be set in environment variables") + } + + // Create a JSON payload for the OpenAI API + payload := map[string]string{"prompt": prompt} + jsonPayload, err := json.Marshal(payload) + if err != nil { + return "", err + } + + // Create a POST request to the OpenAI endpoint with JSON body + req, err := http.NewRequest("POST", openaiEndpoint, bytes.NewBuffer(jsonPayload)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", openaiToken)) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return string(body), nil +} diff --git a/contributions/gh.go b/contributions/gh.go new file mode 100644 index 0000000..80e3bd3 --- /dev/null +++ b/contributions/gh.go @@ -0,0 +1,80 @@ +package contributions + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" +) + +var ghc = http.Client{} +var gh_token = os.Getenv("GH_TOKEN") + +type SearchResponse struct { + Items []PRResp +} + +type PRResp struct { + Body string + Title string + HtmlUrl string `json:"html_url"` + ClosedAt string `json:"closed_at"` + RepositoryUrl string `json:"repository_url"` +} + +type PullRequest struct { + Body string + Title string + ClosedAt string + URL string +} + +func GetPRs(org, username string, from string, to string) (map[string][]PullRequest, error) { + req, err := http.NewRequest( + http.MethodGet, + fmt.Sprintf( + "https://api.github.com/search/issues?q=type:pr+org:%s+author:%s+is:closed+merged:%s..%s", + org, + username, + from, + to, + ), + nil) + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", gh_token)) + resp, err := ghc.Do(req) + if err != nil { + return nil, fmt.Errorf("error talking to github: %w", err) + } + + if resp.StatusCode > 400 { + b := bytes.NewBuffer(nil) + io.Copy(b, resp.Body) + return nil, fmt.Errorf("error talking to github: %s", b.String()) + } + r := &SearchResponse{} + dec := json.NewDecoder(resp.Body) + err = dec.Decode(&r) + if err != nil { + return nil, fmt.Errorf("error decoding response: %w", err) + } + + allthings := map[string][]PullRequest{} + + for _, pr := range r.Items { + reponameparts := strings.Split(pr.RepositoryUrl, "/") + reponame := reponameparts[len(reponameparts)-1] + if prs, ok := allthings[reponame]; !ok { + allthings[reponame] = []PullRequest{{pr.Body, pr.Title, pr.ClosedAt, pr.HtmlUrl}} + } else { + allthings[reponame] = append(prs, PullRequest{pr.Body, pr.Title, pr.ClosedAt, pr.HtmlUrl}) + } + } + return allthings, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fdb17da --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module o5r.ca/autocrossbow + +go 1.25.0 diff --git a/issues/jira.go b/issues/jira.go new file mode 100644 index 0000000..d68c24c --- /dev/null +++ b/issues/jira.go @@ -0,0 +1,87 @@ +package issues + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" +) + +//curl -u olivier@circleci.com:"$(cat ~/jiratoken)" 'https://circleci.atlassian.net/rest/api/2/search?jql=sprint+in+openSprints()+and+project=CIAM' + +var jcl = http.Client{} + +type Issue struct { + Summary string + Description string + Comments []Comment +} + +type Comment struct { + Author Author + Body string +} + +type Author struct { + DisplayName string + EmailAddress string +} + +type JiraSearchResp struct { + Issues []RespIssue +} + +type RespIssue struct { + Key string + Fields map[string]any +} + +func GetIssues(instance, user, from, to string) ([]Issue, error) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/rest/api/2/search", instance), nil) + if err != nil { + return nil, fmt.Errorf("error building jira search request: %w", err) + } + q := req.URL.Query() + q.Add("jql", fmt.Sprintf("assignee was '%s' and resolved >= %s and resolved <= %s", user, from, to)) + req.URL.RawQuery = q.Encode() + req.SetBasicAuth(os.Getenv("JIRA_USER"), os.Getenv("JIRA_TOKEN")) + resp, err := jcl.Do(req) + if err != nil { + return nil, fmt.Errorf("error executing jira search request: %w", err) + } + if resp.StatusCode >= 400 { + b := bytes.NewBuffer(nil) + io.Copy(b, resp.Body) + return nil, fmt.Errorf("error talking to jira: %s", b.String()) + } + + dec := json.NewDecoder(resp.Body) + var jsr *JiraSearchResp + dec.Decode(&jsr) + + out := []Issue{} + + for _, i := range jsr.Issues { + iss := Issue{} + if s, ok := i.Fields["summary"]; ok { + iss.Summary, _ = s.(string) + } + if d, ok := i.Fields["description"]; ok { + iss.Description, _ = d.(string) + } + if comms, ok := i.Fields["comment"].(map[string]any); ok { + if c := comms["comments"].([]map[string]any); len(c) > 0 { + iss.Comments = make([]Comment, 0, len(c)) + for _, c2 := range c { + iss.Comments = append(iss.Comments, Comment{Author{DisplayName: c2["displayName"].(string), EmailAddress: c2["emailAddress"].(string)}, c2["body"].(string)}) + } + } + } + out = append(out, iss) + + } + + return out, nil +}