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 }
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 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) { // 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 (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 (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 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 createBranch(branch, target string) (action.Action, error) { // Create the branch. if err := Branch(branch, target); err != nil { return nil, err } return action.ActionFunc(func() error { // On rollback, delete the branch. return Branch("-D", branch) }), 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 resetBranch(branch, target string) (action.Action, error) { // Remember the current position. current, err := Hexsha("refs/heads/" + branch) if err != nil { return nil, err } // Reset the branch. if err := SetBranch(branch, target); err != nil { return nil, err } return action.ActionFunc(func() error { // On rollback, reset the branch to the original position. return SetBranch(branch, current) }), nil }
// Stage can be used to stage the items associated with this release. // // The rollback function is a NOOP in this case since there is no way // how to delete a deployment once created in Sprintly. func (release *runningRelease) Stage() (action.Action, error) { task := "Ping Sprintly to register the deployment" log.Run(task) // Create the Sprintly deployment. if err := release.deploy(release.config.StagingEnvironment()); err != nil { return nil, errs.NewError(task, err) } // Return the rollback function, which is empty in this case. return action.ActionFunc(func() error { log.Rollback(task) log.Warn("It is not possible to delete a Sprintly deployment, skipping ...") return nil }), nil }
func (r *release) Close() (action.Action, error) { // Make sure EnsureClosable has been called. if r.closingMilestone == nil { if err := r.EnsureClosable(); err != nil { return nil, err } } // Prepare for API calls. client, owner, repo, err := r.prepareForApiCalls() if err != nil { return nil, err } // Close the milestone. releaseString := r.v.BaseString() milestoneTask := fmt.Sprintf("Close GitHub review milestone for release %v", releaseString) log.Run(milestoneTask) milestone, _, err := client.Issues.EditMilestone( owner, repo, *r.closingMilestone.Number, &github.Milestone{ State: github.String("closed"), }) if err != nil { return nil, errs.NewError(milestoneTask, err) } r.closingMilestone = milestone // Return a rollback function. return action.ActionFunc(func() error { log.Rollback(milestoneTask) task := fmt.Sprintf("Reopen GitHub review milestone for release %v", releaseString) milestone, _, err := client.Issues.EditMilestone( owner, repo, *r.closingMilestone.Number, &github.Milestone{ State: github.String("open"), }) if err != nil { return errs.NewError(task, err) } r.closingMilestone = milestone return nil }), nil }
func (release *nextRelease) Start() (action.Action, error) { // In case there are no additional stories, we are done. if len(release.additionalIssues) == 0 { return action.Noop, nil } // Add the release label to the stories that were assigned automatically. releaseLabel := release.trunkVersion.ReleaseTagString() task := fmt.Sprintf("Label the newly added issues with the release label (%v)", releaseLabel) log.Run(task) api := newClient(release.tracker.config) if err := addLabel(api, release.additionalIssues, releaseLabel); err != nil { return nil, errs.NewError(task, err) } return action.ActionFunc(func() error { return removeLabel(api, release.additionalIssues, releaseLabel) }), nil }
func (story *story) MarkAsImplemented() (action.Action, error) { // Make sure the story is started. switch story.Story.State { case pivotal.StoryStateStarted: // Continue further to set the state to finished. case pivotal.StoryStateFinished: // Nothing to do here. return nil, nil default: // Foobar, an unexpected story state encountered. return nil, fmt.Errorf("unexpected story state: %v", story.State) } // Set the story state to finished. var ( config = story.tracker.config client = pivotal.NewClient(config.UserToken) projectId = config.ProjectId ) updateTask := fmt.Sprintf("Update Pivotal Tracker story (id = %v)", story.Story.Id) updateRequest := &pivotal.StoryRequest{State: pivotal.StoryStateFinished} updatedStory, _, err := client.Stories.Update(projectId, story.Story.Id, updateRequest) if err != nil { return nil, errs.NewError(updateTask, err) } originalStory := story.Story story.Story = updatedStory return action.ActionFunc(func() error { log.Rollback(updateTask) updateRequest := &pivotal.StoryRequest{State: originalStory.State} updatedStory, _, err := client.Stories.Update(projectId, story.Story.Id, updateRequest) if err != nil { return err } story.Story = updatedStory return nil }), nil }
func ensureLocalConfigDirectoryExists() (action.Action, error) { task := "Make sure the local configuration directory exists" // Get the directory absolute path. localConfigDir, err := config.LocalConfigDirectoryAbsolutePath() if err != nil { return nil, errs.NewError(task, err) } // In case the path exists, make sure it is a directory. info, err := os.Stat(localConfigDir) if err != nil { if !os.IsNotExist(err) { return nil, errs.NewError(task, err) } } else { if !info.IsDir() { return nil, errs.NewError(task, fmt.Errorf("not a directory: %v", localConfigDir)) } return action.Noop, nil } // Otherwise create the directory. if err := os.MkdirAll(localConfigDir, 0755); err != nil { return nil, errs.NewError(task, err) } // Return the rollback function. act := action.ActionFunc(func() error { // Delete the directory. log.Rollback(task) task := "Delete the local configuration directory" if err := os.RemoveAll(localConfigDir); err != nil { return errs.NewError(task, err) } return nil }) return act, nil }
func CloseMilestone( client *github.Client, owner string, repo string, milestone *github.Milestone, ) (*github.Milestone, action.Action, error) { // Copy the milestone to have it stored locally for the rollback closure. mstone := *milestone // A helper closure. setState := func(milestone *github.Milestone, state string) (*github.Milestone, error) { task := fmt.Sprintf("Mark GitHub milestone '%v' as %v", *milestone.Title, state) log.Run(task) m, _, err := client.Issues.EditMilestone(owner, repo, *milestone.Number, &github.Milestone{ State: &state, }) if err != nil { return nil, errs.NewError(task, err) } return m, nil } // Close the chosen milestone. m, err := setState(&mstone, "closed") if err != nil { return nil, nil, err } // Return the rollback function. act := action.ActionFunc(func() error { _, err := setState(&mstone, "open") return err }) return m, act, nil }
func Stage(options *StageOptions) (act action.Action, err error) { // Rollback machinery. chain := action.NewActionChain() defer chain.RollbackOnError(&err) // Make sure opts are not nil. if options == nil { options = DefaultStageOptions } // Load git config. gitConfig, err := git.LoadConfig() if err != nil { return nil, err } var ( remoteName = gitConfig.RemoteName releaseBranch = gitConfig.ReleaseBranchName stagingBranch = gitConfig.StagingBranchName ) // Instantiate the issue tracker. tracker, err := modules.GetIssueTracker() if err != nil { return nil, err } // Get the current branch. task := "Get the current branch" currentBranch, err := gitutil.CurrentBranch() if err != nil { return nil, err } // Cannot be on the release branch, it will be deleted. task = fmt.Sprintf("Make sure that branch '%v' is not checked out", releaseBranch) if currentBranch == releaseBranch { return nil, errs.NewError( task, fmt.Errorf("cannot stage the release while on branch '%v'", releaseBranch)) } // Fetch the remote repository. if !options.SkipFetch { task = "Fetch the remote repository" log.Run(task) if err := git.UpdateRemotes(remoteName); err != nil { return nil, errs.NewError(task, err) } } // Make sure that the local release branch exists and is up to date. task = fmt.Sprintf("Make sure that branch '%v' is up to date", releaseBranch) log.Run(task) if err := git.CheckOrCreateTrackingBranch(releaseBranch, remoteName); err != nil { return nil, errs.NewError(task, err) } // Read the current release version. task = "Read the current release version" releaseVersion, err := version.GetByBranch(releaseBranch) if err != nil { return nil, errs.NewError(task, err) } // Make sure the release is stageable. release := tracker.RunningRelease(releaseVersion) if err := release.EnsureStageable(); err != nil { return nil, err } // Make sure there are no commits being left behind, // e.g. make sure no commits are forgotten on the trunk branch, // i.e. make sure that everything necessary was cherry-picked. if err := checkCommits(tracker, release, releaseBranch); err != nil { return nil, err } // Reset the staging branch to point to the newly created tag. task = fmt.Sprintf("Reset branch '%v' to point to branch '%v'", stagingBranch, releaseBranch) log.Run(task) act, err = git.CreateOrResetBranch(stagingBranch, releaseBranch) if err != nil { return nil, errs.NewError(task, err) } chain.PushTask(task, act) // Delete the local release branch. task = fmt.Sprintf("Delete branch '%v'", releaseBranch) log.Run(task) if err := git.Branch("-D", releaseBranch); err != nil { return nil, errs.NewError(task, err) } chain.PushTask(task, action.ActionFunc(func() error { task := fmt.Sprintf("Recreate branch '%v'", releaseBranch) // In case the release branch exists locally, do nothing. // This might look like an extra and useless check, but it looks like // the final git push at the end of the command function actually creates // the release branch locally when it is aborted from the pre-push hook. // Not sure why and how that is happening. exists, err := git.LocalBranchExists(releaseBranch) if err != nil { return errs.NewError(task, err) } if exists { return nil } // In case the branch indeed does not exist, create it. if err := git.Branch(releaseBranch, remoteName+"/"+releaseBranch); err != nil { return errs.NewError(task, err) } return nil })) // Update the version string on the staging branch. stagingVersion, err := releaseVersion.ToStageVersion() if err != nil { return nil, err } task = fmt.Sprintf("Bump version (branch '%v' -> %v)", stagingBranch, stagingVersion) log.Run(task) act, err = version.SetForBranch(stagingVersion, stagingBranch) if err != nil { return nil, errs.NewError(task, err) } chain.PushTask(task, act) // Stage the release in the issue tracker. act, err = release.Stage() if err != nil { return nil, err } chain.Push(act) // Push to create the tag, reset client and delete release in the remote repository. task = "Push changes to the remote repository" log.Run(task) err = git.Push(remoteName, "-f", // Use the Force, Luke. ":"+releaseBranch, // Delete the release branch. stagingBranch+":"+stagingBranch) // Push the staging branch. if err != nil { return nil, err } return chain, nil }
func (s *story) setStateLabel(label string) (action.Action, error) { // A helper function for setting issue labels. setLabels := func(labels []string) error { var ( client = s.tracker.newClient() owner = s.tracker.config.GitHubOwner repo = s.tracker.config.GitHubRepository issueNum = *s.issue.Number updatedLabels []github.Label err error ) withRequestAllocated(func() { updatedLabels, _, err = client.Issues.ReplaceLabelsForIssue( owner, repo, issueNum, labels) }) if err != nil { return err } s.issue.Labels = updatedLabels return nil } // A helper function for appending label names. appendLabelNames := func(names []string, labels []github.Label) []string { for _, label := range labels { names = append(names, *label.Name) } return names } // Set the state labels. task := fmt.Sprintf("Set state label to '%v' for issue %v", label, s.ReadableId()) // Get the right label list. otherLabels, prunedLabels := pruneStateLabels(s.tracker.config, s.issue.Labels) newLabels := make([]string, 0, len(otherLabels)+1) newLabels = appendLabelNames(newLabels, otherLabels) newLabels = append(newLabels, label) // Update the issue. if err := setLabels(newLabels); err != nil { return nil, errs.NewError(task, err) } // Return a rollback function. return action.ActionFunc(func() error { // Append the pruned labels. newLabels := make([]string, 0, len(otherLabels)+len(prunedLabels)) newLabels = appendLabelNames(newLabels, prunedLabels) // Generate the task string. task := fmt.Sprintf("Set the state labels to [%v] for issue %v", strings.Join(newLabels, ", "), s.ReadableId()) // Append the other labels as well, thus getting the original label list. newLabels = appendLabelNames(newLabels, otherLabels) // Update the issue. if err := setLabels(newLabels); err != nil { return errs.NewError(task, err) } return nil }), nil }
func createBranch() (action.Action, error) { // Get the current branch name. originalBranch, err := gitutil.CurrentBranch() if err != nil { return nil, err } // Fetch the remote repository. task := "Fetch the remote repository" log.Run(task) gitConfig, err := git.LoadConfig() if err != nil { return nil, errs.NewError(task, err) } var ( remoteName = gitConfig.RemoteName baseBranch = gitConfig.TrunkBranchName ) if flagBase != "" { baseBranch = flagBase } // Fetch the remote repository. if err := git.UpdateRemotes(remoteName); err != nil { return nil, errs.NewError(task, err) } // Make sure the trunk branch is up to date. task = fmt.Sprintf("Make sure branch '%v' is up to date", baseBranch) log.Run(task) if err := git.CheckOrCreateTrackingBranch(baseBranch, remoteName); err != nil { return nil, errs.NewError(task, err) } // Prompt the user for the branch name. task = "Prompt the user for the branch name" line, err := prompt.Prompt(` Please insert the branch slug now. Insert an empty string to skip the branch creation step: `) if err != nil && err != prompt.ErrCanceled { return nil, errs.NewError(task, err) } sluggedLine := slug.Slug(line) if sluggedLine == "" { fmt.Println() log.Log("Not creating any feature branch") return nil, nil } branchName := "story/" + sluggedLine ok, err := prompt.Confirm( fmt.Sprintf( "\nThe branch that is going to be created will be called '%s'.\nIs that alright?", branchName), true) if err != nil { return nil, errs.NewError(task, err) } if !ok { panic(prompt.ErrCanceled) } fmt.Println() createTask := fmt.Sprintf( "Create branch '%v' on top of branch '%v'", branchName, baseBranch) log.Run(createTask) if err := git.Branch(branchName, baseBranch); err != nil { return nil, errs.NewError(createTask, err) } deleteTask := fmt.Sprintf("Delete branch '%v'", branchName) deleteBranch := func() error { // Roll back and delete the newly created branch. log.Rollback(createTask) if err := git.Branch("-D", branchName); err != nil { return errs.NewError(deleteTask, err) } return nil } // Checkout the newly created branch. checkoutTask := fmt.Sprintf("Checkout branch '%v'", branchName) log.Run(checkoutTask) if err := git.Checkout(branchName); err != nil { if err := deleteBranch(); err != nil { errs.Log(err) } return nil, errs.NewError(checkoutTask, err) } // Push the newly created branch unless -no_push. pushTask := fmt.Sprintf("Push branch '%v' to remote '%v'", branchName, remoteName) if flagPush { log.Run(pushTask) if err := git.Push(remoteName, branchName); err != nil { if err := deleteBranch(); err != nil { errs.Log(err) } return nil, errs.NewError(pushTask, err) } } return action.ActionFunc(func() error { // Checkout the original branch. log.Rollback(checkoutTask) if err := git.Checkout(originalBranch); err != nil { return errs.NewError( fmt.Sprintf("Checkout the original branch '%v'", originalBranch), err) } // Delete the newly created branch. deleteErr := deleteBranch() // In case we haven't pushed anything, we are done. if !flagPush { return deleteErr } // Delete the branch from the remote repository. log.Rollback(pushTask) if _, err := git.Run("push", "--delete", remoteName, branchName); err != nil { // In case deleteBranch failed, tell the user now // since we are not going to return that error. if deleteErr != nil { errs.Log(deleteErr) } return errs.NewError( fmt.Sprintf("Delete branch '%v' from remote '%v'", branchName, remoteName), err) } // Return deleteErr to make sure it propagates up. return deleteErr }), nil }
func runMain() (err error) { tracker, err := modules.GetIssueTracker() if err != nil { return err } // Fetch stories from the issue tracker. task := "Fetch stories from the issue tracker" log.Run(task) stories, err := tracker.StartableStories() if err != nil { return errs.NewError(task, err) } if len(stories) == 0 { return errs.NewError(task, errors.New("no startable stories found")) } // Filter out the stories that are not relevant, // i.e. not owned by the current user or assigned to someone else. task = "Fetch the current user record from the issue tracker" user, err := tracker.CurrentUser() if err != nil { return errs.NewError(task, err) } var filteredStories []common.Story StoryLoop: for _, story := range stories { assignees := story.Assignees() // Include the story in case there is no assignee set yet. if len(assignees) == 0 { filteredStories = append(filteredStories, story) continue StoryLoop } // Include the story in case the current user is assigned. for _, assignee := range assignees { if assignee.Id() == user.Id() { filteredStories = append(filteredStories, story) continue StoryLoop } } } stories = filteredStories // Prompt the user to select a story. story, err := dialog( "\nYou can start working on one of the following stories:", stories) if err != nil { switch err { case prompt.ErrNoStories: return errors.New("no startable stories found") case prompt.ErrCanceled: prompt.PanicCancel() default: return err } } fmt.Println() // Create the story branch, optionally. if flagNoBranch { log.Log("Not creating any feature branch") } else { var act action.Action act, err = createBranch() if err != nil { return err } // Roll back on error. defer action.RollbackTaskOnError(&err, task, act) } // Add the current user to the list of story assignees. task = "Amend the list of story assignees" log.Run(task) originalAssignees := story.Assignees() if err := story.AddAssignee(user); err != nil { return errs.NewError(task, err) } defer action.RollbackTaskOnError(&err, task, action.ActionFunc(func() error { task := "Reset the list of story assignees" if err := story.SetAssignees(originalAssignees); err != nil { return errs.NewError(task, err) } return nil })) // Start the selected story. No need to roll back. task = "Start the selected story" log.Run(task) return errs.Wrap(task, story.Start()) }
func merge(mergeTask, current, parent string) (act action.Action, err error) { // Remember the current branch hash. currentSHA, err := git.BranchHexsha(current) if err != nil { return nil, err } // Checkout the parent branch so that we can perform the merge. if err := git.Checkout(parent); err != nil { return nil, err } // Checkout the current branch on return to be consistent. defer func() { if ex := git.Checkout(current); ex != nil { if err == nil { err = ex } else { errs.Log(ex) } } }() // Perform the merge. // Use --no-ff in case -merge_no_ff is set. if flagMergeNoFF { err = git.Merge(current, "--no-ff") } else { err = git.Merge(current) } if err != nil { return nil, err } // Return a rollback action. return action.ActionFunc(func() (err error) { log.Rollback(mergeTask) task := fmt.Sprintf("Reset branch '%v' to the original position", current) // Get the branch is the current branch now. currentNow, err := gitutil.CurrentBranch() if err != nil { return errs.NewError(task, err) } // Checkout current in case it is not the same as the current branch now. if currentNow != current { if err := git.Checkout(current); err != nil { return errs.NewError(task, err) } defer func() { if ex := git.Checkout(currentNow); ex != nil { if err == nil { err = ex } else { errs.Log(ex) } } }() } // Reset the branch to the original position. if err := git.Reset("--keep", currentSHA); err != nil { return errs.NewError(task, err) } return nil }), nil }
func postBranch(parentBranch string) (err error) { // Load the git-related config. gitConfig, err := git.LoadConfig() if err != nil { return err } var ( remoteName = gitConfig.RemoteName ) // Get the current branch name. currentBranch, err := gitutil.CurrentBranch() if err != nil { return err } if !flagNoFetch { // Fetch the remote repository. task := "Fetch the remote repository" log.Run(task) if err := git.UpdateRemotes(remoteName); err != nil { return errs.NewError(task, err) } } // Make sure the parent branch is up to date. task := fmt.Sprintf("Make sure reference '%v' is up to date", parentBranch) log.Run(task) if err := git.EnsureBranchSynchronized(parentBranch, remoteName); err != nil { return errs.NewError(task, err) } // Make sure the current branch is up to date. task = fmt.Sprintf("Make sure branch '%v' is up to date", currentBranch) log.Run(task) if err = git.EnsureBranchSynchronized(currentBranch, remoteName); err != nil { return errs.NewError(task, err) } // Get the commits to be posted task = "Get the commits to be posted for code review" commits, err := git.ShowCommitRange(parentBranch + "..") if err != nil { return errs.NewError(task, err) } // Make sure there are no merge commits. if err := ensureNoMergeCommits(commits); err != nil { return err } // Prompt the user to confirm. if err := promptUserToConfirmCommits(commits); err != nil { return err } // Rebase the current branch on top the parent branch. if !flagNoRebase { commits, err = rebase(currentBranch, parentBranch) if err != nil { return err } } // Ensure the Story-Id tag is there. commits, _, err = ensureStoryId(commits) if err != nil { return err } // Get data on the current branch. task = fmt.Sprintf("Get data on branch '%v'", currentBranch) remoteCurrentExists, err := git.RemoteBranchExists(currentBranch, remoteName) if err != nil { return errs.NewError(task, err) } currentUpToDate, err := git.IsBranchSynchronized(currentBranch, remoteName) if err != nil { return errs.NewError(task, err) } // Merge the current branch into the parent branch unless -no_merge. pushTask := "Push the current branch" if flagNoMerge { // In case the user doesn't want to merge, // we need to push the current branch. if !remoteCurrentExists || !currentUpToDate { if err := push(remoteName, currentBranch); err != nil { return errs.NewError(pushTask, err) } } } else { // Still push the current branch if necessary. if remoteCurrentExists && !currentUpToDate { if err := push(remoteName, currentBranch); err != nil { return errs.NewError(pushTask, err) } } // Merge the branch into the parent branch mergeTask := fmt.Sprintf("Merge branch '%v' into branch '%v'", currentBranch, parentBranch) log.Run(mergeTask) act, err := merge(mergeTask, currentBranch, parentBranch) if err != nil { return err } // Push the parent branch. if err := push(remoteName, parentBranch); err != nil { // In case the push fails, we revert the merge as well. if err := act.Rollback(); err != nil { errs.Log(err) } return errs.NewError(mergeTask, err) } // Register a rollback function that just says that // a pushed merge cannot be reverted. defer action.RollbackOnError(&err, action.ActionFunc(func() error { log.Rollback(mergeTask) hint := "\nCannot revert merge that has already been pushed.\n" return errs.NewErrorWithHint( "Revert the merge", errors.New("merge commit already pushed"), hint) })) } // Post the review requests. if err := postCommitsForReview(commits); err != nil { return err } // In case there is no error, tell the user they can do next. return printFollowup() }
// pourSkeleton counts on the fact that skeleton is a valid skeleton // that is available in the local cache directory. func pourSkeleton(skeletonName string, localConfigDir string) (err error) { // Get the skeleton src path. cacheDir, err := cacheDirectoryAbsolutePath() if err != nil { return err } skeletonDir := filepath.Join(cacheDir, "github.com", skeletonName) // Make sure src is a directory, just to be sure. skeletonInfo, err := os.Stat(skeletonDir) if err != nil { return err } if !skeletonInfo.IsDir() { return fmt.Errorf("skeleton source path not a directory: %v", skeletonDir) } // Get the list of script files. srcScriptsDir := filepath.Join(skeletonDir, "scripts") scripts, err := filepath.Glob(srcScriptsDir + "/*") if err != nil { return err } if len(scripts) == 0 { log.Warn("No script files found in the skeleton repository") return nil } // Create the destination directory. dstScriptsDir := filepath.Join(localConfigDir, "scripts") if err := os.MkdirAll(dstScriptsDir, 0755); err != nil { return err } // Delete the directory on error. defer action.RollbackOnError(&err, action.ActionFunc(func() error { log.Rollback("Create the local scripts directory") if err := os.RemoveAll(dstScriptsDir); err != nil { return errs.NewError("Remove the local scripts directory", err) } return nil })) for _, script := range scripts { err := func(script string) error { // Skip directories. scriptInfo, err := os.Stat(script) if err != nil { return err } if scriptInfo.IsDir() { return nil } // Copy the file. filename := script[len(srcScriptsDir)+1:] fmt.Println("---> Copy", filepath.Join("scripts", filename)) srcFd, err := os.Open(script) if err != nil { return err } defer srcFd.Close() dstPath := filepath.Join(dstScriptsDir, filename) dstFd, err := os.OpenFile( dstPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, scriptInfo.Mode()) if err != nil { return err } defer dstFd.Close() _, err = io.Copy(dstFd, srcFd) return err }(script) if err != nil { return err } } return nil }
// updateIssues can be used to update multiple issues at once concurrently. // It basically calls the given update function on all given issues and // collects the results. In case there is any error, updateIssues tries // to revert partial changes. The error returned contains the complete list // of API call errors as the error hint. func updateIssues( client *github.Client, owner string, repo string, issues []*github.Issue, updateFunc issueUpdateFunc, rollbackFunc issueUpdateFunc, ) ([]*github.Issue, action.Action, error) { // Prepare a function that can be used to apply the given updateFunc. // It is later used to both update issues and revert changes. update := func( issues []*github.Issue, updateFunc issueUpdateFunc, ) (newIssues []*github.Issue, errHint string, err error) { // Send the requests concurrently. retCh := make(chan *issueUpdateResult, len(issues)) for _, issue := range issues { go func(issue *github.Issue) { var ( updatedIssue *github.Issue err error ) withRequestAllocated(func() { updatedIssue, err = updateFunc(client, owner, repo, issue) }) if err == nil { // On success, return the updated story. retCh <- &issueUpdateResult{updatedIssue, nil} } else { // On error, keep the original story, add the error. retCh <- &issueUpdateResult{nil, err} } }(issue) } // Wait for the requests to complete. var ( updatedIssues = make([]*github.Issue, 0, len(issues)) errFailed = errors.New("failed to update GitHub issues") stderr bytes.Buffer ) for range issues { if ret := <-retCh; ret.err != nil { fmt.Fprintln(&stderr, ret.err) err = errFailed } else { updatedIssues = append(updatedIssues, ret.issue) } } return updatedIssues, stderr.String(), err } // Apply the update function. updatedIssues, errHint, err := update(issues, updateFunc) if err != nil { // In case there is an error, generate the error hint. var errHintAcc bytes.Buffer errHintAcc.WriteString("\nUpdate Errors\n-------------\n") errHintAcc.WriteString(errHint) errHintAcc.WriteString("\n") // Revert the changes. _, errHint, ex := update(updatedIssues, rollbackFunc) if ex != nil { // In case there is an error during rollback, extend the error hint. errHintAcc.WriteString("Rollback Errors\n---------------\n") errHintAcc.WriteString(errHint) errHintAcc.WriteString("\n") } return nil, nil, errs.NewErrorWithHint("Update GitHub issues", err, errHintAcc.String()) } // On success, return the updated issues and a rollback function. act := action.ActionFunc(func() error { _, errHint, err := update(updatedIssues, rollbackFunc) if err != nil { var errHintAcc bytes.Buffer errHintAcc.WriteString("\nRollback Errors\n---------------\n") errHintAcc.WriteString(errHint) errHintAcc.WriteString("\n") return errs.NewErrorWithHint("Revert GitHub issue updates", err, errHintAcc.String()) } return nil }) return updatedIssues, act, nil }
func SetForBranch(ver *Version, branch string) (act action.Action, err error) { var mainTask = fmt.Sprintf("Bump version to %v for branch '%v'", ver, branch) // Make sure the repository is clean (don't check untracked files). task := "Make sure the repository is clean" if err := git.EnsureCleanWorkingTree(false); err != nil { return nil, errs.NewError(task, err) } // Remember the current branch. currentBranch, err := git.CurrentBranch() if err != nil { return nil, err } // Remember the current position of the target branch. task = fmt.Sprintf("Remember the position of branch '%v'", branch) originalPosition, err := git.Hexsha("refs/heads/" + branch) if err != nil { return nil, errs.NewError(task, err) } // Checkout the target branch. task = fmt.Sprintf("Checkout branch '%v'", branch) if err := git.Checkout(branch); err != nil { return nil, errs.NewError(task, err) } defer func() { // Checkout the original branch on return. task := fmt.Sprintf("Checkout branch '%v'", currentBranch) if ex := git.Checkout(currentBranch); ex != nil { if err == nil { err = ex } else { errs.LogError(task, ex) } } }() // Set the project version to the desired value. if err := Set(ver); err != nil { if ex, ok := err.(*scripts.ErrNotFound); ok { return nil, fmt.Errorf( "custom SalsaFlow script '%v' not found on branch '%v'", ex.ScriptName(), branch) } return nil, err } // Commit changes. _, err = git.RunCommand("commit", "-a", "-m", fmt.Sprintf("Bump version to %v", ver), "-m", fmt.Sprintf("Story-Id: %v", git.StoryIdUnassignedTagValue)) if err != nil { task := "Reset the working tree to the original state" if err := git.Reset("--keep"); err != nil { errs.LogError(task, err) } return nil, err } return action.ActionFunc(func() (err error) { // On rollback, reset the target branch to the original position. log.Rollback(mainTask) return git.ResetKeep(branch, originalPosition) }), nil }
func (tool *codeReviewTool) FinaliseRelease(v *version.Version) (action.Action, error) { return action.ActionFunc(func() error { return nil }), nil }
func (release *runningRelease) Stage() (action.Action, error) { stageTask := "Mark the stories as delivered in Pivotal Tracker" 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 finished. // All other stories are delivered or further. // That is checked in EnsureStageable(). ss := make([]*pivotal.Story, 0, len(stories)) for _, s := range stories { if s.State == pivotal.StoryStateFinished { ss = append(ss, s) } } stories = ss // Save the original states into a map. originalStates := make(map[int]string, len(stories)) for _, story := range stories { originalStates[story.Id] = story.State } // Set all the states to Delivered. updateRequest := &pivotal.StoryRequest{State: pivotal.StoryStateDelivered} updateFunc := func(story *pivotal.Story) *pivotal.StoryRequest { return updateRequest } // On rollback, get the original state from the map. rollbackFunc := func(story *pivotal.Story) *pivotal.StoryRequest { return &pivotal.StoryRequest{State: originalStates[story.Id]} } // Update the stories. var ( config = release.tracker.config client = pivotal.NewClient(config.UserToken()) projectId = config.ProjectId() ) updatedStories, err := updateStories(client, projectId, 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 := "Reset the story states back to the original ones" updatedStories, err := updateStories(client, projectId, release.stories, rollbackFunc, nil) if err != nil { return errs.NewError(task, err) } release.stories = updatedStories return nil }), nil }
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 }
func runMain() (err error) { // Load repo config. gitConfig, err := git.LoadConfig() if err != nil { return err } var ( remote = gitConfig.RemoteName trunkBranch = gitConfig.TrunkBranchName releaseBranch = gitConfig.ReleaseBranchName stagingBranch = gitConfig.StagingBranchName ) // Fetch the remote repository. if !flagNoFetch { task := "Fetch the remote repository" log.Run(task) if err := git.UpdateRemotes(remote); err != nil { return errs.NewError(task, err) } } // Make sure trunk is up to date. task := fmt.Sprintf("Make sure that branch '%v' is up to date", trunkBranch) if err := git.CheckOrCreateTrackingBranch(trunkBranch, remote); err != nil { return errs.NewError(task, err) } // Make sure the staging branch is up to date, in case it exists. // // We check stage here as well since it is otherwise checked later // in releases.ListNewTrunkCommits(), which is usually called in // release.PromptUserToConfirmStart(). task = fmt.Sprintf("Make sure that branch '%v' is up to date", stagingBranch) if err := git.CheckOrCreateTrackingBranch(stagingBranch, remote); err != nil { // The staging branch actually doesn't need to exist. if _, ok := err.(*git.ErrRefNotFound); !ok { return errs.NewError(task, err) } } // Make sure that the release branch does not exist. task = fmt.Sprintf("Make sure that branch '%v' does not exist", releaseBranch) if err := git.EnsureBranchNotExist(releaseBranch, remote); err != nil { return errs.NewError(task, err) } // Get the current trunk version string. task = "Get the current trunk version string" trunkVersion, err := version.GetByBranch(trunkBranch) if err != nil { return errs.NewError(task, err) } // Get the next trunk version (the future release version). var nextTrunkVersion *version.Version if !flagNextTrunk.Zero() { // Make sure it's only major, minor and patch that are set. // Make sure the new version is actually incrementing the current one. var ( current = trunkVersion next = flagNextTrunk ) var part string switch { case len(next.Pre) != 0: part = "Pre" case len(next.Build) != 0: part = "Build" } if part != "" { return fmt.Errorf("invalid future version string: %v version part cannot be set", part) } if current.GE(next.Version) { return fmt.Errorf("future version string not an increment: %v <= %v", next, current) } nextTrunkVersion = &flagNextTrunk } else { nextTrunkVersion = trunkVersion.IncrementMinor() } // Make sure the next trunk version has the right format. nextTrunkVersion, err = nextTrunkVersion.ToTrunkVersion() if err != nil { return err } // Fetch the stories from the issue tracker. tracker, err := modules.GetIssueTracker() if err != nil { return errs.NewError(task, err) } release := tracker.NextRelease(trunkVersion, nextTrunkVersion) // Prompt the user to confirm the release. fmt.Printf(` You are about to start a new release branch. The relevant version strings are: current release (current trunk version): %v future release (next trunk version): %v `, trunkVersion, nextTrunkVersion) ok, err := release.PromptUserToConfirmStart() if err != nil { return err } if !ok { fmt.Println("\nYour wish is my command, exiting now!") return nil } fmt.Println() // Create the release branch on top of the trunk branch. task = fmt.Sprintf("Create branch '%v' on top of branch '%v'", releaseBranch, trunkBranch) log.Run(task) if err := git.Branch(releaseBranch, trunkBranch); err != nil { return errs.NewError(task, err) } defer action.RollbackTaskOnError(&err, task, action.ActionFunc(func() error { task := fmt.Sprintf("Delete branch '%v'", releaseBranch) if err := git.Branch("-D", releaseBranch); err != nil { errs.NewError(task, err) } return nil })) // Bump the release branch version. testingVersion, err := trunkVersion.ToTestingVersion() if err != nil { return err } task = fmt.Sprintf("Bump version (branch '%v' -> %v)", releaseBranch, testingVersion) log.Run(task) _, err = version.SetForBranch(testingVersion, releaseBranch) if err != nil { return errs.NewError(task, err) } // No need for a rollback function here, git branch -d specified as a rollback // for the previous step will take care of deleting this change as well. // Bump the trunk branch version. task = fmt.Sprintf("Bump version (branch '%v' -> %v)", trunkBranch, nextTrunkVersion) log.Run(task) act, err := version.SetForBranch(nextTrunkVersion, trunkBranch) if err != nil { return errs.NewError(task, err) } defer action.RollbackTaskOnError(&err, task, act) // Initialise the next release in the code review tool. codeReviewTool, err := modules.GetCodeReviewTool() if err != nil { return err } act, err = codeReviewTool.NewRelease(nextTrunkVersion).Initialise() if err != nil { return err } defer action.RollbackTaskOnError(&err, task, act) // Start the release in the issue tracker. act, err = release.Start() if err != nil { return err } defer action.RollbackTaskOnError(&err, task, act) // Push the modified branches. task = "Push changes to the remote repository" log.Run(task) err = git.Push(remote, trunkBranch+":"+trunkBranch, releaseBranch+":"+releaseBranch) if err != nil { return errs.NewError(task, err) } return nil }
func runMain() (err error) { // Load repo config. gitConfig, err := git.LoadConfig() if err != nil { return err } var ( remoteName = gitConfig.RemoteName stagingBranch = gitConfig.StagingBranchName stableBranch = gitConfig.StableBranchName ) // Fetch the repository. if !flagNoFetch { task := "Fetch the remote repository" log.Run(task) if err := git.UpdateRemotes(remoteName); err != nil { return errs.NewError(task, err) } } // Check branches. checkBranch := func(branchName string) error { // Make sure the branch exists. task := fmt.Sprintf("Make sure that branch '%v' exists and is up to date", branchName) log.Run(task) if err := git.CheckOrCreateTrackingBranch(branchName, remoteName); err != nil { return errs.NewError(task, err) } // Make sure we are not on the branch. task = fmt.Sprintf("Make sure that branch '%v' is not checked out", branchName) log.Run(task) currentBranch, err := gitutil.CurrentBranch() if err != nil { return errs.NewError(task, err) } if currentBranch == branchName { err := fmt.Errorf("cannot deploy while on branch '%v'", branchName) return errs.NewError(task, err) } return nil } for _, branch := range []string{stableBranch, stagingBranch} { if err := checkBranch(branch); err != nil { return err } } // Make sure the current staging branch can be released. task := fmt.Sprintf("Make sure that branch '%v' can be released", stagingBranch) log.Run(task) tracker, err := modules.GetIssueTracker() if err != nil { return errs.NewError(task, err) } codeReviewTool, err := modules.GetCodeReviewTool() if err != nil { return errs.NewError(task, err) } stagingVersion, err := version.GetByBranch(stagingBranch) if err != nil { return errs.NewError(task, err) } // Make sure the release can be closed in the issue tracker. issueTrackerRelease := tracker.RunningRelease(stagingVersion) if err := issueTrackerRelease.EnsureClosable(); err != nil { return err } // Make sure the release can be closed in the code review tool. codeReviewRelease := codeReviewTool.NewRelease(stagingVersion) if err := codeReviewRelease.EnsureClosable(); err != nil { return err } // Reset the stable branch to point to stage. task = fmt.Sprintf("Reset branch '%v' to point to branch '%v'", stableBranch, stagingBranch) log.Run(task) act, err := git.CreateOrResetBranch(stableBranch, stagingBranch) if err != nil { return errs.NewError(task, err) } defer action.RollbackTaskOnError(&err, task, act) // Bump version for the stable branch. stableVersion, err := stagingVersion.ToStableVersion() if err != nil { return err } task = fmt.Sprintf("Bump version (branch '%v' -> %v)", stableBranch, stableVersion) log.Run(task) act, err = version.SetForBranch(stableVersion, stableBranch) if err != nil { return errs.NewError(task, err) } defer action.RollbackTaskOnError(&err, task, act) // Tag the stable branch. tag := stableVersion.ReleaseTagString() task = fmt.Sprintf("Tag branch '%v' with tag '%v'", stableBranch, tag) log.Run(task) if err := git.Tag(tag, stableBranch); err != nil { return errs.NewError(task, err) } defer action.RollbackTaskOnError(&err, task, action.ActionFunc(func() error { task := fmt.Sprintf("Delete tag '%v'", tag) if err := git.Tag("-d", tag); err != nil { return errs.NewError(task, err) } return nil })) // Generate the release notes. // We try to do as much as possible before pushing. task = fmt.Sprintf("Generate release notes for version '%v'", stableVersion) log.Run(task) rnm, err := modules.GetReleaseNotesManager() if err != nil { if _, ok := err.(*modules.ErrModuleNotSet); !ok { return errs.NewError(task, err) } } var nts *common.ReleaseNotes // rnm will be nil in case the module is disabled. if rnm != nil { // Get the relevant stories. stories, err := tracker.ListStoriesByRelease(stableVersion) if err != nil { return errs.NewError(task, err) } // Generate the release notes. nts = notes.GenerateReleaseNotes(stableVersion, stories) } else { log.Log("Release notes module disabled, not doing anything") } // Close the release in the issue tracker. act, err = issueTrackerRelease.Close() if err != nil { return err } defer action.RollbackOnError(&err, act) // Close the release in the code review tool. act, err = codeReviewRelease.Close() if err != nil { return err } defer action.RollbackOnError(&err, act) // Push the changes to the remote repository. task = "Push changes to the remote repository" log.Run(task) toPush := []string{ "--tags", fmt.Sprintf("%v:%v", stableBranch, stableBranch), } if err := git.PushForce(remoteName, toPush...); err != nil { return errs.NewError(task, err) } // Post the release notes. task = fmt.Sprintf("Post the release notes for version '%v'", stableVersion) if rnm != nil { log.Run(task) if _, err := rnm.PostReleaseNotes(nts); err != nil { errs.LogError(task, err) log.Warn("Failed to post the release notes, continuing anyway ...") } } // Tell the user we succeeded. color.Green("\n-----> Release %v deployed successfully!\n\n", stableVersion) color.Cyan("Let's check whether the next release branch can be staged already.\n") color.Cyan("In other words, we will try to run `release stage` and see what happens.\n\n") // Now we proceed to the staging step. We do not roll back // the previous changes on error since this is a separate step. tryToStageRunningRelease() return nil }