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 (release *nextRelease) PromptUserToConfirmStart() (bool, error) { // Collect the issues to be added to the current release. task := "Collect the issues that modified trunk since the last release" log.Run(task) ids, err := releases.ListStoryIdsToBeAssigned(release.tracker) if err != nil { return false, errs.NewError(task, err) } // Fetch the additional issues from JIRA. task = "Fetch the collected issues from JIRA" log.Run(task) issues, err := listStoriesById(newClient(release.tracker.config), ids) if len(issues) == 0 && err != nil { return false, errs.NewError(task, err) } if len(issues) != len(ids) { log.Warn("Some issues were dropped since they were not found in JIRA") } // Drop the issues that were already assigned to the right version. releaseLabel := release.trunkVersion.ReleaseTagString() filteredIssues := make([]*jira.Issue, 0, len(issues)) IssueLoop: for _, issue := range issues { // Add only the parent tasks, i.e. skip sub-tasks. if issue.Fields.Parent != nil { continue } // Add only the issues that have not been assigned to the release yet. for _, label := range issue.Fields.Labels { if label == releaseLabel { continue IssueLoop } } filteredIssues = append(filteredIssues, issue) } issues = filteredIssues // Present the issues to the user. if len(issues) != 0 { fmt.Println("\nThe following issues are going to be added to the release:\n") err := prompt.ListStories(toCommonStories(issues, release.tracker), os.Stdout) if err != nil { return false, err } } // Ask the user to confirm. ok, err := prompt.Confirm( fmt.Sprintf( "\nAre you sure you want to start release %v?", release.trunkVersion.BaseString())) if err == nil { release.additionalIssues = issues } return ok, err }
func fetchOrUpdateSkeleton(skeleton string) error { // Parse the skeleton string. parts := strings.SplitN(skeleton, "/", 2) if len(parts) != 2 { return fmt.Errorf("not a valid repository path string: %v", skeleton) } owner, repo := parts[0], parts[1] // Create the cache directory if necessary. task := "Make sure the local cache directory exists" cacheDir, err := cacheDirectoryAbsolutePath() if err != nil { return errs.NewError(task, err) } if err := os.MkdirAll(cacheDir, 0755); err != nil { return errs.NewError(task, err) } // Pull or close the given skeleton. task = "Pull or clone the given skeleton" skeletonDir := filepath.Join(cacheDir, "github.com", owner) if err := os.MkdirAll(skeletonDir, 0755); err != nil { return errs.NewError(task, err) } skeletonPath := filepath.Join(skeletonDir, repo) if _, err := os.Stat(skeletonPath); err != nil { if !os.IsNotExist(err) { return errs.NewError(task, err) } // The directory does not exist, hence we clone. task := fmt.Sprintf("Clone skeleton '%v'", skeleton) log.Run(task) args := []string{ "clone", "--single-branch", fmt.Sprintf("https://github.com/%v/%v", owner, repo), skeletonPath, } if _, err := git.Run(args...); err != nil { return errs.NewError(task, err) } return nil } // The skeleton directory exists, hence we pull. task = fmt.Sprintf("Pull skeleton '%v'", skeleton) log.Run(task) cmd, _, stderr := shell.Command("git", "pull") cmd.Dir = skeletonPath if err := cmd.Run(); err != nil { return errs.NewErrorWithHint(task, err, stderr.String()) } return 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) } } }
// 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 }
// 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 (release *runningRelease) Release() error { task := "Ping Sprintly to register the deployment" log.Run(task) // Create the Sprintly deployment. return release.deploy(release.config.ProductionEnvironment()) }
func (release *runningRelease) EnsureStageable() error { task := "Make sure the stories can be staged" log.Run(task) // Load the assigned stories. stories, err := release.loadStories() if err != nil { return errs.NewError(task, err) } var details bytes.Buffer tw := tabwriter.NewWriter(&details, 0, 8, 4, '\t', 0) io.WriteString(tw, "\n") io.WriteString(tw, "Story URL\tError\n") io.WriteString(tw, "=========\t=====\n") // For a story to be stageable, it must be in the Finished stage. // That by definition means that it has been reviewed and verified. for _, story := range stories { if !stateAtLeast(story, pivotal.StoryStateFinished) { fmt.Fprintf(tw, "%v\t%v\n", story.URL, "story not finished yet") err = common.ErrNotStageable } } if err != nil { io.WriteString(tw, "\n") tw.Flush() return errs.NewErrorWithHint(task, err, details.String()) } return nil }
func getAndPourSkeleton(skeleton string) error { // Get or update given skeleton. task := fmt.Sprintf("Get or update skeleton '%v'", skeleton) log.Run(task) if err := getOrUpdateSkeleton(flagSkeleton); err != nil { return errs.NewError(task, err) } // Move the skeleton files into place. task = "Copy the skeleton into the configuration directory" log.Go(task) localConfigDir, err := config.LocalConfigDirectoryAbsolutePath() if err != nil { return errs.NewError(task, err) } log.NewLine("") if err := pourSkeleton(flagSkeleton, localConfigDir); err != nil { return errs.NewError(task, err) } log.NewLine("") log.Ok(task) return nil }
func FindMilestoneByTitle( client *github.Client, owner string, repo string, title string, ) (*github.Milestone, error) { // Fetch milestones for the given repository. task := fmt.Sprintf("Search for GitHub milestone '%v'", title) log.Run(task) milestones, _, err := client.Issues.ListMilestones(owner, repo, nil) if err != nil { return nil, errs.NewError(task, err) } // Find the right one. for _, milestone := range milestones { if *milestone.Title == title { return &milestone, nil } } // Milestone not found. return nil, nil }
func CreateMilestone( client *github.Client, owner string, repo string, title string, ) (*github.Milestone, action.Action, error) { // Create the milestone. milestoneTask := fmt.Sprintf("Create GitHub milestone '%v'", title) 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 milestone '%v'", title) _, err := client.Issues.DeleteMilestone(owner, repo, *milestone.Number) if err != nil { return errs.NewError(task, err) } return nil }), nil }
func (release *nextRelease) Start() (action.Action, error) { var ( client = release.client productId = release.tracker.config.ProductId() itemReleaseTag = getItemReleaseTag(release.trunkVersion) ) // Add the release tag to the relevant Sprintly items. task := "Tag relevant items with the release tag" log.Run(task) items, err := addTag(client, productId, release.additionalItems, itemReleaseTag) if err != nil { return nil, errs.NewError(task, err) } release.additionalItems = nil // Return the rollback action, which removes the release tags that were added. return action.ActionFunc(func() error { log.Rollback(task) _, err := removeTag(client, productId, items, itemReleaseTag) if err != nil { return errs.NewError("Remove the release tag from relevant items", err, nil) } return nil }), nil }
func (r *release) EnsureClosable() error { // Prepare for API calls. _, owner, repo, err := r.prepareForApiCalls() if err != nil { return err } // Get the relevant review milestone. releaseString := r.v.BaseString() task := fmt.Sprintf("Get GitHub review milestone for release %v", releaseString) log.Run(task) milestone, err := milestoneForVersion(r.tool.config, owner, repo, r.v) if err != nil { return errs.NewError(task, err) } if milestone == nil { return errs.NewErrorWithHint(task, errors.New("milestone not found"), fmt.Sprintf("\nMake sure the review milestone for release %v exists\n\n", r.v)) } // 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 { hint := fmt.Sprintf( "\nreview milestone for release %v cannot be closed: %v issue(s) open\n\n", releaseString, num) return errs.NewErrorWithHint(task, common.ErrNotClosable, hint) } r.closingMilestone = milestone return 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 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 (tool *codeReviewTool) InitialiseRelease(v *version.Version) (action.Action, error) { // Get necessary config. config, err := LoadConfig() if err != nil { return nil, err } owner, repo, err := git.ParseUpstreamURL() if err != nil { return nil, err } // Check whether the review milestone exists or not. // People can create milestones manually, so this makes the thing more robust. task := fmt.Sprintf("Check whether GitHub review milestone exists for release %v", v.BaseString()) log.Run(task) milestone, err := milestoneForVersion(config, owner, repo, v) if err != nil { return nil, errs.NewError(task, err) } if milestone != nil { // Milestone already exists, we are done. log.Log(fmt.Sprintf("GitHub review milestone '%v' already exists", milestoneTitle(v))) return nil, nil } // Create the review milestone. _, act, err := createMilestone(config, owner, repo, v) return act, err }
func (release *runningRelease) EnsureStageable() error { versionString := release.releaseVersion.BaseString() var task = fmt.Sprintf( "Make sure that release %v can be staged", versionString) log.Run(task) var details bytes.Buffer tw := tabwriter.NewWriter(&details, 0, 8, 4, '\t', 0) io.WriteString(tw, "\n") io.WriteString(tw, "Issue Key\tError\n") io.WriteString(tw, "=========\t=====\n") var err error for _, issue := range release.issues { if ex := ensureStageableIssue(issue); ex != nil { fmt.Fprintf(tw, "%v\t%v\n", issue.Key, ex) err = common.ErrNotStageable } } if err != nil { io.WriteString(tw, "\n") tw.Flush() return errs.NewErrorWithHint(task, err, details.String()) } return nil }
func (release *runningRelease) Stage() (action.Action, error) { var ( api = newClient(release.tracker.config) versionString = release.releaseVersion.BaseString() stageTask = fmt.Sprintf("Stage JIRA issues associated with release %v", versionString) ) log.Run(stageTask) // Make sure we only try to stage the issues that are in Tested. var issuesToStage []*jira.Issue for _, issue := range release.issues { if issue.Fields.Status.Id == stateIdTested { issuesToStage = append(issuesToStage, issue) } } // Perform the transition. err := performBulkTransition(api, issuesToStage, transitionIdStage, transitionIdUnstage) if err != nil { return nil, errs.NewError(stageTask, err) } return action.ActionFunc(func() error { log.Rollback(stageTask) unstageTask := fmt.Sprintf("Unstage JIRA issues associated with release %v", versionString) if err := performBulkTransition(api, issuesToStage, transitionIdUnstage, ""); err != nil { return errs.NewError(unstageTask, err) } return nil }), nil }
func (release *nextRelease) Start() (action.Action, error) { // In case there are no additional stories, we are done. if len(release.additionalStories) == 0 { return action.Noop, nil } // Add release labels to the relevant stories. var ( config = release.tracker.config client = pivotal.NewClient(config.UserToken) projectId = config.ProjectId ) task := "Label the stories with the release label" log.Run(task) releaseLabel := getReleaseLabel(release.trunkVersion) stories, err := addLabel(client, projectId, release.additionalStories, releaseLabel) if err != nil { return nil, errs.NewError(task, err) } release.additionalStories = nil // Return the rollback action, which removes the release labels that were appended. return action.ActionFunc(func() error { log.Rollback(task) _, err := removeLabel(client, projectId, stories, releaseLabel) if err != nil { return errs.NewError("Remove the release label from the stories", err) } return nil }), nil }
func rebase(currentBranch, parentBranch string) ([]*git.Commit, error) { // Tell the user what is happening. task := fmt.Sprintf("Rebase branch '%v' onto '%v'", currentBranch, parentBranch) log.Run(task) // Do the rebase. if err := git.Rebase(parentBranch); err != nil { ex := errs.Log(errs.NewError(task, err)) asciiart.PrintGrimReaper("GIT REBASE FAILED") fmt.Printf(`Git failed to rebase your branch onto '%v'. The repository might have been left in the middle of the rebase process. In case you do not know how to handle this, just execute $ git rebase --abort to make your repository clean again. In any case, you have to rebase your current branch onto '%v' if you want to continue and post a review request. In the edge cases you can as well use -no_rebase to skip this step, but try not to do it. `, parentBranch, parentBranch) return nil, ex } // Reload the commits. task = "Get the commits to be posted for code review, again" commits, err := git.ShowCommitRange(parentBranch + "..") if err != nil { return nil, errs.NewError(task, err) } // Return new commits. return commits, nil }
func EnsureDirectoryExists(path string) (action.Action, error) { // Check whether the directory exists already. task := fmt.Sprintf("Check whether '%v' exists and is a directory", path) info, err := os.Stat(path) if err != nil { if !os.IsNotExist(err) { return nil, errs.NewError(task, err) } } else { // In case the path exists, make sure it is a directory. if !info.IsDir() { return nil, errs.NewError(task, errors.New("not a directory: "+path)) } // We are done. return action.Noop, nil } // Now we know that path does not exist, so we need to create it. createTask := fmt.Sprintf("Create directory '%v'", path) log.Run(createTask) if err := os.MkdirAll(path, 0755); err != nil { return nil, errs.NewError(createTask, err) } return action.ActionFunc(func() error { log.Rollback(createTask) task := fmt.Sprintf("Remove directory '%v'", path) if err := os.RemoveAll(path); err != nil { return errs.NewError(task, err) } return nil }), nil }
// Start is a part of common.NextRelease interface. func (release *nextRelease) Start() (act action.Action, err error) { // In case there are no additional issues, we are done. if len(release.additionalIssues) == 0 { return action.Noop, nil } // Set milestone for the additional issues. task := fmt.Sprintf( "Set milestone to '%v' for the issues added automatically", release.trunkVersion.BaseString()) log.Run(task) // Get the milestone corresponding to the release branch version string. chain := action.NewActionChain() defer chain.RollbackOnError(&err) milestone, act, err := release.tracker.getOrCreateMilestone(release.trunkVersion) if err != nil { return nil, errs.NewError(task, err) } chain.Push(act) // Update the issues. issues, act, err := release.tracker.updateIssues( release.additionalIssues, setMilestone(milestone), unsetMilestone()) if err != nil { return nil, errs.NewError(task, err) } chain.Push(act) release.additionalIssues = issues return chain, 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 ensureRbtVersion() error { hint := ` You need to install RBTools version 0.7. Please run $ pip install rbtools==0.7 --allow-external rbtools --allow-unverified rbtools to install the correct version. ` // Load configuration and check the RBTools version only if Review Board is being used. config, err := common.LoadConfig() if err != nil { return err } if config.CodeReviewToolId() != Id { return nil } // Check the RBTools version being used. task := "Check the RBTools version being used" log.Run(task) // rbt 0.5.x prints the version string to stdout, // rbt 0.6.x prints the version string to stderr. stdout, stderr, err := shell.Run("rbt", "--version") if err != nil { // Return the hint instead of stderr. // Failing to run rbt --version probably means that it's not installed. return errs.NewErrorWithHint(task, err, hint) } var outputBuffer *bytes.Buffer if stdout.Len() != 0 { outputBuffer = stdout } else { outputBuffer = stderr } output := outputBuffer.String() pattern := regexp.MustCompile("^RBTools (([0-9]+)[.]([0-9]+).*)") parts := pattern.FindStringSubmatch(output) if len(parts) != 4 { err := fmt.Errorf("failed to parse 'rbt --version' output: %v", output) return errs.NewError(task, err) } rbtVersion := parts[1] // No need to check errors, we know the format is correct. major, _ := strconv.Atoi(parts[2]) minor, _ := strconv.Atoi(parts[3]) if !(major == 0 && minor == 7) { return errs.NewErrorWithHint( task, errors.New("unsupported rbt version detected: "+rbtVersion), hint) } return nil }
func listItemsByTag( client *sprintly.Client, productId int, tags []string, ) ([]sprintly.Item, error) { task := "Fetch Sprintly items by tag" log.Run(task) // Since Sprintly API is not exactly powerful, we need to get all the items. // Then we need to locally pair stories with their sub-items. itms, err := listItems(client, productId, &sprintly.ItemListArgs{ Status: []sprintly.ItemStatus{ sprintly.ItemStatusSomeday, sprintly.ItemStatusBacklog, sprintly.ItemStatusInProgress, sprintly.ItemStatusCompleted, }, }) var ( items []sprintly.Item itemIndex = make(map[int]struct{}, 0) ) for _, item := range itms { // In case the tag matches, add the item to the list. // Also add the item to the index of potential parent items. for _, tag := range tags { if tagged(&item, tag) { items = append(items, item) itemIndex[item.Number] = struct{}{} } } } for _, item := range itms { // In case the parent is not empty and it matches an item // that is already in the list, add the current item to the list as well, // but only if the item is not there yet. number, err := item.ParentNumber() if err != nil { return nil, errs.NewError(task, err) } if number != 0 { if _, ok := itemIndex[number]; ok { if _, ok := itemIndex[item.Number]; !ok { items = append(items, item) } } } } if err != nil { return nil, errs.NewError(task, err) } return items, nil }
func checkCommits( tracker common.IssueTracker, release common.RunningRelease, releaseBranch string, ) error { var task = "Make sure no changes are being left behind" log.Run(task) stories, err := release.Stories() if err != nil { return errs.NewError(task, err) } if len(stories) == 0 { return nil } groups, err := changes.StoryChanges(stories) if err != nil { return errs.NewError(task, err) } toCherryPick, err := releases.StoryChangesToCherryPick(groups) if err != nil { return errs.NewError(task, err) } // In case there are some changes being left behind, // ask the user to confirm whether to proceed or not. if len(toCherryPick) == 0 { return nil } fmt.Println(` Some changes are being left behind! In other words, some changes that are assigned to the current release have not been cherry-picked onto the release branch yet. `) if err := changes.DumpStoryChanges(os.Stdout, toCherryPick, tracker, false); err != nil { panic(err) } fmt.Println() confirmed, err := prompt.Confirm("Are you sure you really want to stage the release?", false) if err != nil { return errs.NewError(task, err) } if !confirmed { prompt.PanicCancel() } fmt.Println() return nil }
func runMain(storyId string) error { tracker, err := modules.GetIssueTracker() if err != nil { return err } task := fmt.Sprintf("Open story %s", storyId) log.Run(task) if err := tracker.OpenStory(storyId); err != nil { return errs.NewError(task, err) } return nil }
func (release *runningRelease) Stage() (action.Action, error) { stageTask := fmt.Sprintf( "Mark the stories as %v in Pivotal Tracker", pivotal.StoryStateDelivered) log.Run(stageTask) // Load the assigned stories. stories, err := release.loadStories() if err != nil { return nil, errs.NewError(stageTask, err) } // Pick only the stories that are in the right state. ss := make([]*pivotal.Story, 0, len(stories)) for _, s := range stories { if release.tracker.canStoryBeStaged(s) { ss = append(ss, s) } } stories = ss // Mark the selected stories as delivered. Leave the labels as they are. updateRequest := &pivotal.StoryRequest{State: pivotal.StoryStateDelivered} updateFunc := func(story *pivotal.Story) *pivotal.StoryRequest { return updateRequest } // On rollback, set the story state to finished again. rollbackFunc := func(story *pivotal.Story) *pivotal.StoryRequest { return &pivotal.StoryRequest{State: pivotal.StoryStateFinished} } // Update the stories. updatedStories, err := release.tracker.updateStories(stories, updateFunc, rollbackFunc) if err != nil { return nil, errs.NewError(stageTask, err) } release.stories = updatedStories // Return the rollback function. return action.ActionFunc(func() error { // On error, set the states back to the original ones. log.Rollback(stageTask) task := fmt.Sprintf("Reset the story states back to %v", pivotal.StoryStateFinished) updatedStories, err := release.tracker.updateStories(release.stories, rollbackFunc, nil) if err != nil { return errs.NewError(task, err) } release.stories = updatedStories return nil }), nil }
func (release *runningRelease) loadStories() ([]*pivotal.Story, error) { // Fetch the stories unless cached. if release.stories == nil { task := "Fetch data from Pivotal Tracker" log.Run(task) stories, err := release.tracker.storiesByRelease(release.version) if err != nil { return nil, errs.NewError(task, err) } release.stories = stories } // Return the cached stories. return release.stories, nil }
func newRunningRelease( tracker *issueTracker, releaseVersion *version.Version, ) (*runningRelease, error) { // Fetch relevant issues from JIRA. task := fmt.Sprintf("Fetch issues for release '%v'", releaseVersion) log.Run(task) issues, err := tracker.issuesByRelease(releaseVersion) if err != nil { return nil, errs.NewError(task, err) } // Return a new release instance. return &runningRelease{tracker, releaseVersion, issues}, nil }