// postAssignedReviewRequest can be used to post // the commits associated with the given story for review. func postAssignedReviewRequest( config *moduleConfig, owner string, repo string, story common.Story, commits []*git.Commit, opts map[string]interface{}, ) (*github.Issue, []*git.Commit, error) { // Search for an existing review issue for the given story. task := fmt.Sprintf("Search for an existing review issue for story %v", story.ReadableId()) log.Run(task) client := ghutil.NewClient(config.Token) issue, err := ghissues.FindReviewIssueForStory(client, owner, repo, story.ReadableId()) if err != nil { return nil, nil, errs.NewError(task, err) } // Decide what to do next based on the search results. if issue == nil { // No review issue found for the given story, create a new issue. issue, err := createAssignedReviewRequest(config, owner, repo, story, commits, opts) if err != nil { return nil, nil, err } return issue, commits, nil } // An existing review issue found, extend it. return extendReviewRequest(config, owner, repo, issue, commits, opts) }
func addReviewComment( config Config, owner string, repo string, issueNum int, commits []*git.Commit, ) error { // Generate the comment body. buffer := bytes.NewBufferString("The following commits were added to this issue:") for _, commit := range commits { fmt.Fprintf(buffer, "\n* %v: %v", commit.SHA, commit.MessageTitle) } // Call GitHub API. task := fmt.Sprintf("Add review comment for issue #%v", issueNum) client := ghutil.NewClient(config.Token()) _, _, err := client.Issues.CreateComment(owner, repo, issueNum, &github.IssueComment{ Body: github.String(buffer.String()), }) if err != nil { return errs.NewError(task, err) } return nil }
func createMilestone( config Config, owner string, repo string, v *version.Version, ) (*github.Milestone, action.Action, error) { // Create the review milestone. var ( title = milestoneTitle(v) milestoneTask = fmt.Sprintf("Create GitHub review milestone '%v'", title) client = ghutil.NewClient(config.Token()) ) log.Run(milestoneTask) milestone, _, err := client.Issues.CreateMilestone(owner, repo, &github.Milestone{ Title: github.String(title), }) if err != nil { return nil, nil, errs.NewError(milestoneTask, err) } // Return a rollback function. return milestone, action.ActionFunc(func() error { log.Rollback(milestoneTask) task := fmt.Sprintf("Delete GitHub review milestone '%v'", title) _, err := client.Issues.DeleteMilestone(owner, repo, *milestone.Number) if err != nil { return errs.NewError(task, err) } return nil }), nil }
func createIssue( task string, config Config, owner string, repo string, issueTitle string, issueBody string, milestone *github.Milestone, ) (issue *github.Issue, err error) { log.Run(task) client := ghutil.NewClient(config.Token()) labels := []string{config.ReviewLabel()} issue, _, err = client.Issues.Create(owner, repo, &github.IssueRequest{ Title: github.String(issueTitle), Body: github.String(issueBody), Labels: &labels, Milestone: milestone.Number, }) if err != nil { return nil, errs.NewError(task, err) } log.Log(fmt.Sprintf("GitHub issue #%v created", *issue.Number)) return issue, nil }
func linkCommitsToReviewIssue( config *moduleConfig, owner string, repo string, issueNum int, commits []*git.Commit, ) { // Instantiate an API client. client := ghutil.NewClient(config.Token) // Loop over the commits and post a commit comment for each of them. for _, commit := range commits { task := fmt.Sprintf("Link commit %v to the associated review issue", commit.SHA) log.Run(task) body := fmt.Sprintf( "This commit is being reviewed as a part of review issue #%v.", issueNum) comment := &github.RepositoryComment{ Body: &body, } _, _, err := client.Repositories.CreateComment(owner, repo, commit.SHA, comment) if err != nil { // Just print the error to the console. errs.LogError(task, err) } } }
func milestoneForVersion( config Config, owner string, repo string, v *version.Version, ) (*github.Milestone, error) { // Fetch milestones for the given repository. var ( task = fmt.Sprintf("Fetch GitHub milestones for %v/%v", owner, repo) client = ghutil.NewClient(config.Token()) title = milestoneTitle(v) ) milestones, _, err := client.Issues.ListMilestones(owner, repo, nil) if err != nil { return nil, errs.NewError(task, err) } // Find the right one. task = fmt.Sprintf("Find review milestone for release %v", v) for _, milestone := range milestones { if *milestone.Title == title { return &milestone, nil } } // Milestone not found. return nil, nil }
// postUnassignedReviewRequest can be used to post the given commit for review. // This function is to be used to post commits that are not associated with any story. func postUnassignedReviewRequest( config *moduleConfig, owner string, repo string, commit *git.Commit, opts map[string]interface{}, ) (*github.Issue, []*git.Commit, error) { // Search for an existing issue. task := fmt.Sprintf("Search for an existing review issue for commit %v", commit.SHA) log.Run(task) client := ghutil.NewClient(config.Token) issue, err := ghissues.FindReviewIssueForCommit(client, owner, repo, commit.SHA) if err != nil { return nil, nil, errs.NewError(task, err) } // Return an error in case the issue for the given commit already exists. if issue != nil { issueNum := *issue.Number err = fmt.Errorf("existing review issue found for commit %v: %v", commit.SHA, issueNum) return nil, nil, errs.NewError("Make sure the review issue can be created", err) } // Create a new unassigned review request. issue, err = createUnassignedReviewRequest(config, owner, repo, commit, opts) if err != nil { return nil, nil, err } return issue, []*git.Commit{commit}, nil }
func (tool *codeReviewTool) FinaliseRelease(v *version.Version) (action.Action, error) { // Get a GitHub client. config, err := LoadConfig() if err != nil { return nil, err } client := ghutil.NewClient(config.Token()) owner, repo, err := git.ParseUpstreamURL() if err != nil { return nil, err } // Get the relevant review milestone. releaseString := v.BaseString() task := fmt.Sprintf("Get GitHub review milestone for release %v", releaseString) log.Run(task) milestone, err := milestoneForVersion(config, owner, repo, v) if err != nil { return nil, errs.NewError(task, err) } if milestone == nil { log.Warn(fmt.Sprintf( "Weird, GitHub review milestone for release %v not found", releaseString)) return nil, nil } // Close the milestone unless there are some issues open. task = fmt.Sprintf( "Make sure the review milestone for release %v can be closed", releaseString) if num := *milestone.OpenIssues; num != 0 { return nil, errs.NewError( task, fmt.Errorf( "review milestone for release %v cannot be closed: %v issue(s) open", releaseString, num)) } milestoneTask := fmt.Sprintf("Close GitHub review milestone for release %v", releaseString) log.Run(milestoneTask) milestone, _, err = client.Issues.EditMilestone(owner, repo, *milestone.Number, &github.Milestone{ State: github.String("closed"), }) if err != nil { return nil, errs.NewError(milestoneTask, err) } // Return a rollback function. return action.ActionFunc(func() error { log.Rollback(milestoneTask) task := fmt.Sprintf("Reopen GitHub review milestone for release %v", releaseString) _, _, err := client.Issues.EditMilestone(owner, repo, *milestone.Number, &github.Milestone{ State: github.String("open"), }) if err != nil { return errs.NewError(task, err) } return nil }), nil }
func newGitHubClient() (*github.Client, error) { config, err := ghutil.LoadConfig() if err != nil { return nil, err } return ghutil.NewClient(config.ApiToken()), nil }
// extendReviewRequest is a general function that can be used to extend // the given review issue with the given list of commits. func extendReviewRequest( config Config, owner string, repo string, issue *github.Issue, commits []*git.Commit, opts map[string]interface{}, ) error { var ( issueNum = *issue.Number issueBody = *issue.Body bodyBuffer = bytes.NewBufferString(issueBody) addedCommits = make([]*git.Commit, 0, len(commits)) ) for _, commit := range commits { // Make sure the commit is not added yet. commitString := fmt.Sprintf("] %v: %v", commit.SHA, commit.MessageTitle) if strings.Contains(issueBody, commitString) { log.Log(fmt.Sprintf("Commit %v already listed in issue #%v", commit.SHA, issueNum)) continue } // Extend the issue body. addedCommits = append(addedCommits, commit) fmt.Fprintf(bodyBuffer, "\n- [ ] %v: %v", commit.SHA, commit.MessageTitle) } if len(addedCommits) == 0 { log.Log(fmt.Sprintf("All commits already listed in issue #%v", issueNum)) return nil } // Edit the issue. task := fmt.Sprintf("Update GitHub issue #%v", issueNum) log.Run(task) client := ghutil.NewClient(config.Token()) newIssue, _, err := client.Issues.Edit(owner, repo, issueNum, &github.IssueRequest{ Body: github.String(bodyBuffer.String()), State: github.String("open"), }) if err != nil { return errs.NewError(task, err) } // Add the review comment. if err := addReviewComment(config, owner, repo, issueNum, addedCommits); err != nil { return err } // Open the issue if requested. if _, open := opts["open"]; open { return openIssue(newIssue) } return nil }
func newGitHubClient() (*gh.Client, error) { task := "Instantiate a GitHub API client" // Get the access token. spec := newConfigSpec() if err := loader.LoadConfig(spec); err != nil { return nil, errs.NewError(task, err) } // Return a new API client. return github.NewClient(spec.global.GitHubToken), nil }
func createMilestone( config *moduleConfig, owner string, repo string, v *version.Version, ) (*github.Milestone, action.Action, error) { // Create the review milestone for the given version. var ( client = ghutil.NewClient(config.Token) title = milestoneTitle(v) ) return ghissues.CreateMilestone(client, owner, repo, title) }
func milestoneForVersion( config *moduleConfig, owner string, repo string, v *version.Version, ) (*github.Milestone, error) { // Find the milestone matching the version. var ( client = ghutil.NewClient(config.Token) title = milestoneTitle(v) ) return ghissues.FindMilestoneByTitle(client, owner, repo, title) }
func (r *release) prepareForApiCalls() (client *github.Client, owner, repo string, err error) { if r.client == nil { r.client = ghutil.NewClient(r.tool.config.Token) } if r.owner == "" || r.repo == "" { var err error r.owner, r.repo, err = ghutil.ParseUpstreamURL() if err != nil { return nil, "", "", err } } return r.client, r.owner, r.repo, nil }
// postUnassignedReviewRequest can be used to post the given commit for review. // This function is to be used to post commits that are not associated with any story. func postUnassignedReviewRequest( config Config, owner string, repo string, commit *git.Commit, opts map[string]interface{}, ) error { // Extend the specified review issue in case -fixes is specified. flagFixes, ok := opts["fixes"] if ok { if fixes, ok := flagFixes.(uint); ok && fixes != 0 { return extendUnassignedReviewRequest(config, owner, repo, int(fixes), commit, opts) } } // Search for an existing issue. task := fmt.Sprintf("Search for an existing review issue for commit %v", commit.SHA) log.Run(task) query := fmt.Sprintf( "\"Review commit %v\" repo:%v/%v label:%v type:issue in:title", commit.SHA, owner, repo, config.ReviewLabel()) client := ghutil.NewClient(config.Token()) result, _, err := client.Search.Issues(query, &github.SearchOptions{}) if err != nil { return errs.NewError(task, err) } // Decide what to do next based on the search results. switch len(result.Issues) { case 0: // Create a new unassigned review request. return createUnassignedReviewRequest(config, owner, repo, commit, opts) case 1: // The issues already exists, return an error. issueNum := *result.Issues[0].Number err := fmt.Errorf("existing review issue found for commit %v: %v", commit.SHA, issueNum) return errs.NewError("Make sure the review issue can be created", err) default: // Inconsistency detected: multiple review issues found. err := fmt.Errorf( "inconsistency detected: multiple review issue found for commit %v", commit.SHA) return errs.NewError("Make sure the review issue can be created", err) } }
func createIssue( task string, config *moduleConfig, owner string, repo string, issueTitle string, issueBody string, assignee string, milestone *github.Milestone, implemented bool, ) (issue *github.Issue, err error) { log.Run(task) client := ghutil.NewClient(config.Token) var labels []string if implemented { labels = []string{config.ReviewLabel, config.StoryImplementedLabel} } else { labels = []string{config.ReviewLabel} } var assigneePtr *string if assignee != "" { assigneePtr = &assignee } issue, _, err = client.Issues.Create(owner, repo, &github.IssueRequest{ Title: github.String(issueTitle), Body: github.String(issueBody), Labels: &labels, Assignee: assigneePtr, Milestone: milestone.Number, }) if err != nil { return nil, errs.NewError(task, err) } log.Log(fmt.Sprintf("GitHub issue #%v created", *issue.Number)) return issue, nil }
func getOrCreateMilestoneForCommit( config *moduleConfig, owner string, repo string, sha string, ) (*github.Milestone, error) { // Get the version associated with the given commit. v, err := version.GetByBranch(sha) if err != nil { return nil, err } // Get or create the milestone for the given title. var ( client = ghutil.NewClient(config.Token) title = milestoneTitle(v) ) milestone, _, err := ghissues.GetOrCreateMilestoneForTitle(client, owner, repo, title) return milestone, err }
// extendUnassignedReviewRequest can be used to upload fixes for // the specified unassigned review issue. func extendUnassignedReviewRequest( config Config, owner string, repo string, issueNum int, commit *git.Commit, opts map[string]interface{}, ) error { // Fetch the issue. task := fmt.Sprintf("Fetch GitHub issue #%v", issueNum) log.Run(task) client := ghutil.NewClient(config.Token()) issue, _, err := client.Issues.Get(owner, repo, issueNum) if err != nil { return errs.NewError(task, err) } // Extend the given review issue. return extendReviewRequest(config, owner, repo, issue, []*git.Commit{commit}, opts) }
// postAssignedReviewRequest can be used to post // the commits associated with the given story for review. func postAssignedReviewRequest( config Config, owner string, repo string, story common.Story, commits []*git.Commit, opts map[string]interface{}, ) error { // Search for an existing review issue for the given story. task := fmt.Sprintf("Search for an existing review issue for story %v", story.ReadableId()) log.Run(task) query := fmt.Sprintf( "\"Review story %v\" repo:%v/%v label:%v type:issue in:title", story.ReadableId(), owner, repo, config.ReviewLabel()) client := ghutil.NewClient(config.Token()) result, _, err := client.Search.Issues(query, &github.SearchOptions{}) if err != nil { return errs.NewError(task, err) } // Decide what to do next based on the search results. switch len(result.Issues) { case 0: // No review issue found for the given story, create a new issue. return createAssignedReviewRequest(config, owner, repo, story, commits, opts) case 1: // An existing review issue found, extend it. return extendReviewRequest(config, owner, repo, &result.Issues[0], commits, opts) default: // Multiple review issue found for the given story, that is clearly wrong // since there is always just a single review issue for every story. err := errors.New("inconsistency detected: multiple story review issues found") return errs.NewError("Make sure the review issue can be created", err) } }
// extendReviewRequest is a general function that can be used to extend // the given review issue with the given list of commits. func extendReviewRequest( config *moduleConfig, owner string, repo string, issue *github.Issue, commits []*git.Commit, opts map[string]interface{}, ) (*github.Issue, []*git.Commit, error) { issueNum := *issue.Number // Parse the issue. task := fmt.Sprintf("Parse review issue #%v", issueNum) reviewIssue, err := ghissues.ParseReviewIssue(issue) if err != nil { return nil, nil, errs.NewError(task, err) } // Add the commits. newCommits := make([]*git.Commit, 0, len(commits)) for _, commit := range commits { if reviewIssue.AddCommit(false, commit.SHA, commit.MessageTitle) { newCommits = append(newCommits, commit) } } if len(newCommits) == 0 { log.Log(fmt.Sprintf("All commits already listed in issue #%v", issueNum)) return issue, nil, nil } // Add the implemented label if necessary. var ( implemented bool implementedLabel = config.StoryImplementedLabel labelsPtr *[]string ) implementedOpt, ok := opts["implemented"] if ok { implemented = implementedOpt.(bool) } if implemented { labels := make([]string, 0, len(issue.Labels)+1) labelsPtr = &labels for _, label := range issue.Labels { if *label.Name == implementedLabel { // The label is already there, for some reason. // Set the pointer to nil so that we don't update labels. labelsPtr = nil break } labels = append(labels, *label.Name) } if labelsPtr != nil { labels = append(labels, implementedLabel) } } // Edit the issue. task = fmt.Sprintf("Update GitHub issue #%v", issueNum) log.Run(task) client := ghutil.NewClient(config.Token) updatedIssue, _, err := client.Issues.Edit(owner, repo, issueNum, &github.IssueRequest{ Body: github.String(reviewIssue.FormatBody()), State: github.String("open"), Labels: labelsPtr, }) if err != nil { return nil, nil, errs.NewError(task, err) } // Add the review comment. if err := addReviewComment(config, owner, repo, issueNum, newCommits); err != nil { return nil, nil, err } return updatedIssue, newCommits, nil }
func (tracker *issueTracker) newClient() *github.Client { return ghutil.NewClient(tracker.config.UserToken) }
func (manager *releaseNotesManager) PostReleaseNotes( releaseNotes *common.ReleaseNotes, ) (action.Action, error) { // Get the GitHub owner and repository from the upstream URL. owner, repo, err := github.ParseUpstreamURL() if err != nil { return nil, err } // Instantiate the API client. client := github.NewClient(manager.config.Token) // Format the release notes. task := "Format the release notes" body := bytes.NewBufferString(` ## Summary ## **PLEASE FILL IN THE RELEASE SUMMARY** `) encoder, err := notes.NewEncoder(notes.EncodingMarkdown, body) if err != nil { return nil, errs.NewError(task, err) } if err := encoder.Encode(releaseNotes, nil); err != nil { return nil, errs.NewError(task, err) } bodyString := body.String() // Create GitHub release for the given version. tag := releaseNotes.Version.ReleaseTagString() releaseTask := fmt.Sprintf("Create GitHub release for tag '%v'", tag) log.Run(releaseTask) release, _, err := client.Repositories.CreateRelease(owner, repo, &gh.RepositoryRelease{ TagName: gh.String(tag), Name: gh.String("Release " + releaseNotes.Version.BaseString()), Body: &bodyString, Draft: gh.Bool(true), }) if err != nil { return nil, err } // Delete the GitHub release on rollback. rollback := func() error { log.Rollback(releaseTask) task := fmt.Sprintf("Delete GitHub release for tag '%v'", tag) _, err := client.Repositories.DeleteRelease(owner, repo, *release.ID) if err != nil { return errs.NewError(task, err) } return nil } // Open the release in the browser so that the user can fill in the details. task = "Open the release notes in the browser" if err := webbrowser.Open(*release.HTMLURL); err != nil { if ex := rollback(); ex != nil { errs.Log(ex) } return nil, errs.NewError(task, err) } // Return the rollback function. return action.ActionFunc(rollback), nil }