Beispiel #1
0
func checkStoryIdTag(tag string) error {
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return err
	}

	_, err = tracker.StoryTagToReadableStoryId(tag)
	return err
}
Beispiel #2
0
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
}
Beispiel #3
0
func commitsToReviewContexts(commits []*git.Commit) ([]*common.ReviewContext, error) {
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return nil, err
	}

	// Fetch the stories from the issue tracker.
	tags := storyTags(tracker, commits)
	stories, err := tracker.ListStoriesByTag(tags)
	if err != nil {
		return nil, err
	}

	// Build the story map.
	storiesByTag := make(map[string]common.Story, 1)
	for i, story := range stories {
		tag := tags[i]
		if story == nil {
			log.Warn(fmt.Sprintf("Story for tag '%v' was not found in the issue tracker", tag))
			continue
		}
		storiesByTag[tag] = story
	}

	// Build the final list of review contexts.
	ctxs := make([]*common.ReviewContext, 0, len(commits))
	for _, commit := range commits {
		// Story can be set to nil here in case the story is unassigned.
		// In that case there will be, obviously, no story object in the map.
		ctxs = append(ctxs, &common.ReviewContext{
			Commit: commit,
			Story:  storiesByTag[commit.StoryIdTag],
		})
	}

	// Return the commit review contexts.
	return ctxs, nil
}
Beispiel #4
0
func runMain(versionString string) (err error) {
	// Get issue tracker.
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return err
	}

	// Parse the version string.
	v, err := version.Parse(versionString)
	if err != nil {
		return err
	}

	// Get the relevant stories.
	task := fmt.Sprintf("Fetch stories assigned to release %v", v.BaseString())
	stories, err := tracker.ListStoriesByRelease(v)
	if err != nil {
		return errs.NewError(task, err)
	}
	if len(stories) == 0 {
		return errs.NewError(task, errors.New("no stories found"))
	}

	// Generate the release notes.
	nts := notes.GenerateReleaseNotes(v, stories)

	// Dump the release notes.
	encoder, err := notes.NewEncoder(notes.Encoding(flagFormat.Value()), os.Stdout)
	if err != nil {
		return err
	}

	return encoder.Encode(nts, &notes.EncodeOptions{
		Pretty: flagPretty,
	})
}
Beispiel #5
0
func runMain(storyIdPattern string) (err error) {
	// Get the issue tracker instance.
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return err
	}

	// Get the list of all relevant changes.
	task := "Get the list of relevant story commits"
	groups, err := collectChanges(storyIdPattern)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Dump the changes to stdout.
	if !porcelain {
		fmt.Println()
	}
	err = changes.DumpStoryChanges(os.Stdout, groups, tracker, porcelain)
	if !porcelain {
		fmt.Println()
	}
	return err
}
Beispiel #6
0
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
}
Beispiel #7
0
func rewriteCommits(commits []*git.Commit, firstMissingOffset int) ([]*git.Commit, error) {
	// Fetch the stories in progress from the issue tracker.
	storiesTask := "Missing Story-Id detected, fetch stories from the issue tracker"
	log.Run(storiesTask)

	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return nil, errs.NewError(storiesTask, err)
	}

	task := "Fetch the user record from the issue tracker"
	me, err := tracker.CurrentUser()
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	stories, err := tracker.ReviewableStories()
	if err != nil {
		return nil, errs.NewError(storiesTask, err)
	}

	reviewedStories, err := tracker.ReviewedStories()
	if err != nil {
		return nil, errs.NewError(storiesTask, err)
	}

	// Show only the stories owned by the current user.
	// Note: Go sucks here, badly.
	filterStories := func(stories []common.Story, filter func(common.Story) bool) []common.Story {
		ss := make([]common.Story, 0, len(stories))
		for _, story := range stories {
			if filter(story) {
				ss = append(ss, story)
			}
		}
		return ss
	}

	mine := func(story common.Story) bool {
		for _, assignee := range story.Assignees() {
			if assignee.Id() == me.Id() {
				return true
			}
		}
		return false
	}

	stories = filterStories(stories, mine)
	reviewedStories = filterStories(reviewedStories, mine)

	// Tell the user what is happening.
	log.Run("Prepare a temporary branch to rewrite commit messages")

	// Get the current branch name.
	currentBranch, err := gitutil.CurrentBranch()
	if err != nil {
		return nil, err
	}

	// Get the parent of the first commit in the chain.
	task = "Get the parent commit of the commit chain to be posted"
	var parentSHA string
	if firstMissingOffset != 0 {
		// In case there are multiple commits being posted
		// and the first missing offset is not pointing to the first commit,
		// we can easily get the parent SHA by just accessing the commit list.
		parentSHA = commits[firstMissingOffset-1].SHA
	} else {
		// Otherwise we have to ask git for help.
		stdout, err := git.Log("--pretty=%P", "-n", "1", commits[firstMissingOffset].SHA)
		if err != nil {
			return nil, errs.NewError(task, err)
		}
		parentSHA = strings.Fields(stdout.String())[0]
	}

	// Prepare a temporary branch that will be used to amend commit messages.
	task = "Create a temporary branch to rewrite commit messages"
	if err := git.SetBranch(constants.TempBranchName, parentSHA); err != nil {
		return nil, errs.NewError(task, err)
	}
	defer func() {
		// Delete the temporary branch on exit.
		task := "Delete the temporary branch"
		if err := git.Branch("-D", constants.TempBranchName); err != nil {
			errs.LogError(task, err)
		}
	}()

	// Checkout the temporary branch.
	task = "Checkout the temporary branch"
	if err := git.Checkout(constants.TempBranchName); err != nil {
		return nil, errs.NewError(task, err)
	}
	defer func() {
		// Checkout the original branch on exit.
		task := fmt.Sprintf("Checkout branch '%v'", currentBranch)
		if err := git.Checkout(currentBranch); err != nil {
			errs.LogError(task, err)
		}
	}()

	// Loop and rewrite the commit messages.
	var story common.Story
	if flagAskOnce {
		header := `
Some of the commits listed above are not assigned to any story.
Please pick up the story that these commits will be assigned to.
You can also insert 'u' to mark the commits as unassigned:`
		selectedStory, err := promptForStory(header, stories, reviewedStories)
		if err != nil {
			return nil, err
		}
		story = selectedStory
	}

	// The temp branch is pointing to the parent of the first commit missing
	// the Story-Id tag. So we only need to cherry-pick the commits that
	// follow the first commit missing the Story-Id tag.
	commitsToCherryPick := commits[firstMissingOffset:]
	for _, commit := range commitsToCherryPick {
		// Cherry-pick the commit.
		task := fmt.Sprintf("Move commit %v onto the temporary branch", commit.SHA)
		if err := git.CherryPick(commit.SHA); err != nil {
			return nil, errs.NewError(task, err)
		}

		if commit.StoryIdTag == "" {
			if !flagAskOnce {
				commitMessageTitle := prompt.ShortenCommitTitle(commit.MessageTitle)

				// Ask for the story ID for the current commit.
				header := fmt.Sprintf(`
The following commit is not assigned to any story:

  commit hash:  %v
  commit title: %v

Please pick up the story to assign the commit to.
Inserting 'u' will mark the commit as unassigned:`, commit.SHA, commitMessageTitle)
				selectedStory, err := promptForStory(header, stories, reviewedStories)
				if err != nil {
					return nil, err
				}
				story = selectedStory
			}

			// Use the unassigned tag value in case no story is selected.
			storyTag := git.StoryIdUnassignedTagValue
			if story != nil {
				storyTag = story.Tag()
			}

			// Extend the commit message to include Story-Id.
			commitMessage := fmt.Sprintf("%v\nStory-Id: %v\n", commit.Message, storyTag)

			// Amend the cherry-picked commit to include the new commit message.
			task = "Amend the commit message for " + commit.SHA
			stderr := new(bytes.Buffer)
			cmd := exec.Command("git", "commit", "--amend", "-F", "-")
			cmd.Stdin = bytes.NewBufferString(commitMessage)
			cmd.Stderr = stderr
			if err := cmd.Run(); err != nil {
				return nil, errs.NewErrorWithHint(task, err, stderr.String())
			}
		}
	}

	// Reset the current branch to point to the new branch.
	task = "Reset the current branch to point to the temporary branch"
	if err := git.SetBranch(currentBranch, constants.TempBranchName); err != nil {
		return nil, errs.NewError(task, err)
	}

	// Parse the commits again since the commit hashes have changed.
	newCommits, err := git.ShowCommitRange(parentSHA + "..")
	if err != nil {
		return nil, err
	}

	log.NewLine("")
	log.Log("Commit messages amended successfully")

	// And we are done!
	return newCommits, nil
}
Beispiel #8
0
func filterBranches(storyBranches []*git.GitBranch, trunkName string) ([]*gitBranch, error) {
	// Pair the branches with commit ranges specified by trunk..story
	task := "Collected commits associated with the story branches"
	branches := make([]*gitBranch, 0, len(storyBranches))
	for _, branch := range storyBranches {
		var revRange string
		if branch.BranchName != "" {
			// Handle branches that exist locally.
			revRange = fmt.Sprintf("%v..%v", trunkName, branch.BranchName)
		} else {
			// Handle branches that exist only in the remote repository.
			// We can use trunkName here since trunk is up to date.
			revRange = fmt.Sprintf("%v..%v/%v", trunkName, branch.Remote, branch.RemoteBranchName)
		}

		commits, err := git.ShowCommitRange(revRange)
		if err != nil {
			return nil, errs.NewError(task, err)
		}
		branches = append(branches, &gitBranch{
			tip:     branch,
			commits: commits,
		})
		continue
	}

	// Collect story tags.
	task = "Collect affected story tags"
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	tags := make([]string, 0, len(storyBranches))
BranchLoop:
	for _, branch := range branches {
		for _, commit := range branch.commits {
			commitTag := commit.StoryIdTag

			// Make sure the tag is not in the list already.
			for _, tag := range tags {
				if tag == commitTag {
					continue BranchLoop
				}
			}

			// Drop tags not recognized by the current issue tracker.
			_, err := tracker.StoryTagToReadableStoryId(commitTag)
			if err == nil {
				tags = append(tags, commitTag)
			}
		}
	}

	// Fetch the collected stories.
	task = "Fetch associated stories from the issue tracker"
	log.Run(task)
	stories, err := tracker.ListStoriesByTag(tags)
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	// Filter the branches according to the story state.
	storyByTag := make(map[string]common.Story, len(stories))
	for i, story := range stories {
		// tags[i] corresponds to stories[i]
		tag := tags[i]
		if story != nil {
			storyByTag[tag] = story
		} else {
			log.Warn(fmt.Sprintf("Story for tag '%v' was not found in the issue tracker", tag))
		}
	}

	allowedStates := allowedStoryStates()

	// checkCommits returns whether the commits passed in are ok
	// considering the state of the stories found in these commits,
	// whether the branch containing these commits can be deleted.
	checkCommits := func(commits []*git.Commit) (common.StoryState, bool) {
		var storyFound bool
		for _, commit := range commits {
			// Skip commits with empty Story-Id tag.
			if commit.StoryIdTag == "" {
				continue
			}

			// In case the story is not found, the tag is not recognized
			// by the current issue tracker. In that case we just skip the commit.
			story, ok := storyByTag[commit.StoryIdTag]
			if !ok {
				continue
			}

			// When the story state associated with the commit is not ok,
			// we can return false here to reject the branch.
			storyState := story.State()
			if _, ok := allowedStates[storyState]; !ok {
				return storyState, false
			}

			storyFound = true
		}

		// We went through all the commits and they are fine, check passed.
		return common.StoryStateInvalid, storyFound
	}

	// Go through the branches and only return these that
	// comply with the story state requirements.
	bs := make([]*gitBranch, 0, len(branches))
	for _, branch := range branches {
		tip := branch.tip

		logger := log.V(log.Verbose)
		if logger {
			logger.Log(fmt.Sprintf("Processing branch %v", tip.CanonicalName()))
		}

		// The branch can be for sure deleted in case there are no commits
		// contained in the commit range. That means the branch is merged into trunk.
		if len(branch.commits) == 0 {
			if logger {
				logger.Log("  Include the branch (reason: merged into trunk)")
			}
			branch.reason = "merged"
			bs = append(bs, branch)
			continue
		}

		// In case the commit check passed, we append the branch.
		state, ok := checkCommits(branch.commits)
		if ok {
			if logger {
				logger.Log("  Include the branch (reason: branch check passed)")
			}
			branch.reason = "check passed"
			bs = append(bs, branch)
			continue
		}

		// Otherwise we print the skip warning.
		if logger {
			if state == common.StoryStateInvalid {
				logger.Log(
					"  Exclude the branch (reason: no story commits found on the branch)")
			} else {
				logger.Log(fmt.Sprintf(
					"  Exclude the branch (reason: story state is '%v')", state))
			}
		}
	}

	return bs, nil
}
Beispiel #9
0
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
}
Beispiel #10
0
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
}
Beispiel #11
0
func runMain() error {
	// Load git-related config.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return err
	}
	var (
		remoteName    = gitConfig.RemoteName
		trunkBranch   = gitConfig.TrunkBranchName
		releaseBranch = gitConfig.ReleaseBranchName
	)

	// Fetch the remote repository unless explicitly skipped.
	if !flagNoFetch {
		task := "Fetch the remote repository"
		log.Run(task)
		if err := git.UpdateRemotes(remoteName); err != nil {
			return errs.NewError(task, err)
		}
	}

	log.Run("Make sure all important branches are up to date")

	// Check branches.
	checkBranch := func(branchName string) error {
		task := fmt.Sprintf("Make sure that branch '%v' exists and is up to date", branchName)
		if err := git.CheckOrCreateTrackingBranch(branchName, remoteName); err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}

	for _, branch := range [...]string{releaseBranch, trunkBranch} {
		if err := checkBranch(branch); err != nil {
			return err
		}
	}

	// Remember the current branch.
	currentBranch, err := gitutil.CurrentBranch()
	if err != nil {
		return err
	}

	// Checkout the release branch.
	task := "Checkout the release branch"
	if err := git.Checkout(releaseBranch); err != nil {
		return errs.NewError(task, err)
	}
	defer func() {
		// Do not checkout the original branch in case the name is empty.
		// This is later used to disable the checkout of the original branch.
		if currentBranch == "" {
			return
		}
		// Otherwise checkout the original branch.
		task := fmt.Sprintf("Checkout the original branch (%v)", currentBranch)
		if err := git.Checkout(currentBranch); err != nil {
			errs.LogError(task, err)
		}
	}()

	// Get the current release version string.
	// It is enough to just call version.Get since
	// we are already on the release branch.
	task = "Get the release branch version string"
	releaseVersion, err := version.Get()
	if err != nil {
		return errs.NewError(task, err)
	}

	// Get the stories associated with the current release.
	task = "Fetch the stories associated with the current release"
	log.Run(task)
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return errs.NewError(task, err)
	}
	release := tracker.RunningRelease(releaseVersion)
	stories, err := release.Stories()
	if err != nil {
		return errs.NewError(task, err)
	}

	if len(stories) == 0 {
		return errs.NewError(task, errors.New("no relevant stories found"))
	}

	// Get the release changes.
	task = "Collect the release changes"
	log.Run(task)
	groups, err := changes.StoryChanges(stories)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Just return in case there are no relevant commits found.
	if len(groups) == 0 {
		return errs.NewError(task, errors.New("no relevant commits found"))
	}

	// Sort the change groups.
	groups = changes.SortStoryChanges(groups, stories)
	groups, err = releases.StoryChangesToCherryPick(groups)
	if err != nil {
		return errs.NewError(task, err)
	}

	var (
		// Collect the changes not reachable from trunk.
		// In case there are any, we abort the cherry-picking process.
		unreachable = make([]*changes.StoryChangeGroup, 0, len(groups))

		// When we are at iterating, we also collect all release commits
		// so that we know what trunk commits to cherry-pick later.
		releaseCommits = make(map[string]struct{})

		trunkRef = fmt.Sprintf("refs/heads/%v", trunkBranch)
	)
	for _, group := range groups {
		g := &changes.StoryChangeGroup{
			StoryIdTag: group.StoryIdTag,
		}

		for _, ch := range group.Changes {
			var ok bool
			for _, c := range ch.Commits {
				// Add the commit to the map of release commits.
				releaseCommits[c.SHA] = struct{}{}

				// Look for a commit that is on trunk.
				if c.Source == trunkRef {
					ok = true
				}
			}
			if !ok {
				// In case there is none, remember the change.
				g.Changes = append(g.Changes, ch)
			}
		}

		// In case there are some changes not reachable from trunk,
		// add the story change to the list of unreachable story changes.
		if len(g.Changes) != 0 {
			unreachable = append(unreachable, g)
		}
	}

	// In case there are some changes not reachable from the trunk branch,
	// abort the process and tell the user to get the changes into trunk first.
	if len(unreachable) != 0 {
		var details bytes.Buffer
		fmt.Fprint(&details, `
The following story changes are not reachable from the trunk branch:

`)
		changes.DumpStoryChanges(&details, unreachable, tracker, false)
		fmt.Fprint(&details, `
Please cherry-pick these changes onto the trunk branch.
Only then we can proceed and cherry-pick the changes.

`)
		return errs.NewErrorWithHint(
			task, errors.New("commits not reachable from trunk detected"), details.String())
	}

	// Everything seems fine, let's continue with the process
	// by dumping the change details into the console.
	fmt.Println()
	changes.DumpStoryChanges(os.Stdout, groups, tracker, false)

	// Ask the user to confirm before doing any cherry-picking.
	task = "Ask the user to confirm cherry-picking"
	fmt.Println(`
The changes listed above will be cherry-picked into the release branch.`)
	confirmed, err := prompt.Confirm("Are you sure you want to continue?", false)
	if err != nil {
		return errs.NewError(task, err)
	}
	if !confirmed {
		prompt.PanicCancel()
	}
	fmt.Println()

	// Collect the trunk commits that were created since the last release.
	task = "Collect the trunk commits added since the last release"
	trunkCommits, err := releases.ListNewTrunkCommits()
	if err != nil {
		return errs.NewError(task, err)
	}
	// We need the list to start with the oldest commit.
	for i, j := 0, len(trunkCommits)-1; i < j; i, j = i+1, j-1 {
		trunkCommits[i], trunkCommits[j] = trunkCommits[j], trunkCommits[i]
	}

	// Collect the commits to cherry pick. These are the commits
	// that are on trunk and they are associated with the release.
	hashesToCherryPick := make([]string, 0, len(trunkCommits))
	for _, commit := range trunkCommits {
		if _, ok := releaseCommits[commit.SHA]; ok {
			hashesToCherryPick = append(hashesToCherryPick, commit.SHA)
		}
	}

	// Perform the cherry-pick itself.
	task = "Cherry-pick the missing changes into the release branch"
	log.Run(task)
	if err := git.CherryPick(hashesToCherryPick...); err != nil {
		hint := `
It was not possible to cherry-pick the missing changes into the release branch.
The cherry-picking process might be still in progress, though. Please check
the repository status and potentially resolve the cherry-picking manually.

`
		// Do not checkout the original branch.
		currentBranch = ""
		return errs.NewErrorWithHint(task, err, hint)
	}

	log.Log("All missing changes cherry-picked into the release branch")
	fmt.Println(`
  ###################################################################
  # IMPORTANT: The release branch is not being pushed automatically #
  ###################################################################
`)
	return nil
}
Beispiel #12
0
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())
}
Beispiel #13
0
func runMain() (err error) {
	// Load repo config.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return err
	}

	var (
		remoteName    = gitConfig.RemoteName
		releaseBranch = gitConfig.ReleaseBranchName
	)

	// Make sure that the local release branch exists.
	task := "Make sure that the local release branch exists"
	if err := git.CheckOrCreateTrackingBranch(releaseBranch, remoteName); err != nil {
		return errs.NewError(task, err)
	}

	// Get the current release version string.
	task = "Get the release branch version string"
	releaseVersion, err := version.GetByBranch(releaseBranch)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Get the stories associated with the current release.
	task = "Fetch the stories associated with the current release"
	log.Run(task)
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return errs.NewError(task, err)
	}
	release := tracker.RunningRelease(releaseVersion)
	stories, err := release.Stories()
	if err != nil {
		return errs.NewError(task, err)
	}

	if len(stories) == 0 {
		return errs.NewError(task, errors.New("no relevant stories found"))
	}

	// Get the story changes.
	task = "Collect the story changes"
	log.Run(task)
	groups, err := changes.StoryChanges(stories)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Just return in case there are no relevant commits found.
	if len(groups) == 0 {
		return errs.NewError(task, errors.New("no relevant commits found"))
	}

	// Sort the change groups.
	groups = changes.SortStoryChanges(groups, stories)

	if flagToCherryPick {
		groups, err = releases.StoryChangesToCherryPick(groups)
		if err != nil {
			return errs.NewError(task, err)
		}
	}

	// Dump the change details into the console.
	if !flagPorcelain {
		fmt.Println()
	}
	changes.DumpStoryChanges(os.Stdout, groups, tracker, flagPorcelain)
	if !flagPorcelain {
		fmt.Println()
	}

	return nil
}
Beispiel #14
0
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
}
Beispiel #15
0
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 {
		if err := git.UpdateRemotes(remoteName); err != nil {
			return 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)
		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)
		currentBranch, err := git.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)
	}

	stagingVersion, err := version.GetByBranch(stagingBranch)
	if err != nil {
		return errs.NewError(task, err)
	}

	release, err := tracker.RunningRelease(stagingVersion)
	if err != nil {
		return err
	}

	if err := release.EnsureReleasable(); 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 {
		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, skipping ...")
	}

	// 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 ...")
		}
	}

	// 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
}