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 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 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 (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 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 (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 }
// 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 (chain *ActionChain) Rollback() error { var ex error for i := range chain.actions { act := chain.actions[len(chain.actions)-1-i] // Inform the user what is happening. if task := act.task; task != "" { log.Rollback(task) } // Run the rollback function registered. if err := act.action.Rollback(); err != nil { errs.Log(err) ex = ErrRollbackFailed } } return ex }
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 (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 }
// 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 }
func runMain() (err error) { var ( task string stderr *bytes.Buffer ) defer func() { // Print error details. if err != nil { log.FailWithDetails(task, stderr) } }() // Fetch the remote repository unless we are restricted to the local branches only. if !flagLocalOnly { task = "Fetch the remote repository" log.Run(task) stderr, err = git.UpdateRemotes(config.OriginName) if err != nil { return } } // Get the list of story references. task = "Collect all story branches" log.Run(task) localRefs, remoteRefs, stderr, err := git.ListStoryRefs() if err != nil { return } var refs []string switch { case flagLocalOnly: refs = localRefs case flagRemoteOnly: refs = remoteRefs default: refs = append(localRefs, remoteRefs...) } if len(refs) == 0 { task = "" log.Println("\nNo story branches found, exiting...") return } // Collect all the story IDs. idMap := make(map[string]struct{}) for _, ref := range refs { // This cannot fail here since we got the refs using ListStoryRefs. id, _ := git.RefToStoryId(ref) idMap[id] = struct{}{} } var ids []string for id := range idMap { ids = append(ids, id) } // Get the list of active story IDs. activeIds, err := modules.GetIssueTracker().SelectActiveStoryIds(ids) if err != nil { return } ids = activeIds // Select only the refs that can be safely deleted. refs = selectInactiveRefs(refs, ids) if len(refs) == 0 { task = "" log.Println("\nThere are no branches to be deleted, exiting...") return } // Sort the refs. sort.Sort(sort.StringSlice(refs)) // Prompt the user to confirm the delete operation. var ( toDeleteLocally []string toDeleteRemotely []string ok bool ) // Go through the local branches. if strings.HasPrefix(refs[0], "refs/heads/") { fmt.Println("\n---> Local branches\n") } for len(refs) > 0 { ref := refs[0] if !strings.HasPrefix(ref, "refs/heads/") { break } branch := ref[len("refs/heads/"):] question := fmt.Sprintf("Delete local branch '%v'", branch) ok, err = prompt.Confirm(question) if err != nil { return } if ok { toDeleteLocally = append(toDeleteLocally, branch) } refs = refs[1:] } // All that is left are remote branches. if len(refs) != 0 { fmt.Println("\n---> Remote branches\n") } for _, ref := range refs { branch := ref[len("refs/remotes/origin/"):] question := fmt.Sprintf("Delete remote branch '%v'", branch) ok, err = prompt.Confirm(question) if err != nil { return } if ok { toDeleteRemotely = append(toDeleteRemotely, branch) } } fmt.Println() if len(toDeleteLocally) == 0 && len(toDeleteRemotely) == 0 { task = "" fmt.Println("No branches selected, exiting...") return } // Delete the local branches. if len(toDeleteLocally) != 0 { task = "Delete the chosen local branches" log.Run(task) // Remember the position of the branches to be deleted. // This is used in case we need to perform a rollback. var ( currentPositions []string hexsha string ) for _, branchName := range toDeleteLocally { hexsha, stderr, err = git.Hexsha("refs/heads/" + branchName) if err != nil { return } currentPositions = append(currentPositions, hexsha) } // Delete the selected local branches. args := append([]string{"-d"}, toDeleteLocally...) stderr, err = git.Branch(args...) if err != nil { return } defer func(taskMsg string) { // On error, try to restore the local branches that were deleted. if err != nil { log.Rollback(taskMsg) for i, branchName := range toDeleteLocally { out, ex := git.ResetKeep(branchName, currentPositions[i]) if ex != nil { log.FailWithDetails(task, out) } } } }(task) } // Delete the remote branches. if len(toDeleteRemotely) != 0 { task = "Delete the chosen remote branches" log.Run(task) var refs []string for _, branchName := range toDeleteRemotely { refs = append(refs, ":"+branchName) } stderr, err = git.Push(config.OriginName, refs...) } return }
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 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() }
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 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 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 }