From 2b0a6284a2f090c0778a988e09974f7063478954 Mon Sep 17 00:00:00 2001 From: SteveRuble Date: Thu, 19 Nov 2020 16:46:53 -0500 Subject: [PATCH] feat: implement better release commit --- cmd/app.go | 18 - cmd/app_publish_image.go | 37 +- cmd/release.go | 29 +- cmd/release_commit.go | 95 +++++ pkg/bosun/app.go | 33 +- pkg/bosun/app_image_helper.go | 26 +- pkg/bosun/platform_commit_release.go | 314 ---------------- pkg/bosun/platform_release_committer.go | 461 ++++++++++++++++++++++++ pkg/git/helpers.go | 15 + 9 files changed, 662 insertions(+), 366 deletions(-) create mode 100644 cmd/release_commit.go delete mode 100644 pkg/bosun/platform_commit_release.go create mode 100644 pkg/bosun/platform_release_committer.go diff --git a/cmd/app.go b/cmd/app.go index 35fbc6f..9085229 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -698,24 +698,6 @@ var appPublishChartCmd = addCommand( -var appBuildImageCmd = addCommand( - appCmd, - &cobra.Command{ - Use: "build-image [app]", - Aliases: []string{"build-images"}, - Args: cobra.MaximumNArgs(1), - Short: "Builds the image(s) for an app.", - Long: `If app is not provided, the current directory is used. The image(s) will be built with the "latest" tag.`, - SilenceUsage: true, - SilenceErrors: true, - RunE: func(cmd *cobra.Command, args []string) error { - b := MustGetBosun(cli.Parameters{NoEnvironment:true}) - app := mustGetApp(b, args) - ctx := b.NewContext().WithApp(app) - err := app.BuildImages(ctx) - return err - }, - }) var appPullCmd = addCommand( appCmd, diff --git a/cmd/app_publish_image.go b/cmd/app_publish_image.go index 2ef36bf..a381722 100644 --- a/cmd/app_publish_image.go +++ b/cmd/app_publish_image.go @@ -4,6 +4,7 @@ import ( "github.com/naveego/bosun/pkg/bosun" "github.com/naveego/bosun/pkg/cli" "github.com/spf13/cobra" + "github.com/spf13/viper" ) var appPublishImageCmd = addCommand( @@ -26,9 +27,41 @@ as "version-release". app := mustGetApp(b, args) helper := bosun.NewAppImageHelper(b) - req := bosun.PublishImagesRequest{App:app} + req := bosun.PublishImagesRequest{App:app, Pattern: viper.GetString(ArgImagePattern)} err := helper.PublishImages(req) return err }, - }) \ No newline at end of file + }, func(cmd *cobra.Command) { + cmd.Flags().String(ArgImagePattern, "", "filter pattern for images to actually process") + }) + + +var appBuildImageCmd = addCommand( + appCmd, + &cobra.Command{ + Use: "build-image [app]", + Aliases: []string{"build-images"}, + Args: cobra.MaximumNArgs(1), + Short: "Builds the image(s) for an app.", + Long: `If app is not provided, the current directory is used. The image(s) will be built with the "latest" tag.`, + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + b := MustGetBosun(cli.Parameters{NoEnvironment:true}) + app := mustGetApp(b, args) + ctx := b.NewContext().WithApp(app) + req := bosun.BuildImageRequest{ + Ctx: ctx, + Pattern: viper.GetString(ArgImagePattern), + } + err := app.BuildImages(req) + return err + }, + }, func(cmd *cobra.Command) { + cmd.Flags().String(ArgImagePattern, "", "filter pattern for images to actually process") + }) + +const ( + ArgImagePattern = "pattern" +) \ No newline at end of file diff --git a/cmd/release.go b/cmd/release.go index 4d42fd2..c9252f5 100644 --- a/cmd/release.go +++ b/cmd/release.go @@ -650,30 +650,7 @@ only those apps will be deployed. Otherwise, all apps in the release will be dep withFilteringFlags, withValueSetFlags) -var releaseCommitCmd = addCommand(releaseCmd, &cobra.Command{ - Use: "commit", - Short: "Merges the release branch back to master for each app in the release, and the platform repository.", - SilenceErrors: true, - SilenceUsage: true, - RunE: func(cmd *cobra.Command, args []string) error { - viper.BindPFlags(cmd.Flags()) - - b := MustGetBosun() - ctx := b.NewContext() - p, err := b.GetCurrentPlatform() - if err != nil { - return err - } - - err = p.CommitCurrentRelease(ctx) - if err != nil { - return err - } - - return nil - }, -}, withFilteringFlags) var releaseUpdateCmd = addCommand(releaseCmd, &cobra.Command{ Use: "update {stable|unstable} [apps...]", @@ -735,7 +712,9 @@ var releaseChangelogCmd = addCommand(releaseCmd, &cobra.Command{ SilenceErrors: true, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - viper.BindPFlags(cmd.Flags()) + + return errors.New("not implemented") +/* viper.BindPFlags(cmd.Flags()) b := MustGetBosun() ctx := b.NewContext() @@ -750,7 +729,7 @@ var releaseChangelogCmd = addCommand(releaseCmd, &cobra.Command{ return err } - return nil + return nil*/ }, }, withFilteringFlags) diff --git a/cmd/release_commit.go b/cmd/release_commit.go new file mode 100644 index 0000000..8191852 --- /dev/null +++ b/cmd/release_commit.go @@ -0,0 +1,95 @@ +package cmd + +import ( + "github.com/naveego/bosun/pkg/bosun" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var releaseCommitCmd = addCommand(releaseCmd, &cobra.Command{ + Use: "commit", + Short: "Commands for merging a release branch back to develop and master.", + SilenceErrors: true, + SilenceUsage: true, +}) + +var releaseCommitPlanCmd = addCommand(releaseCommitCmd, &cobra.Command{ + Use: "plan", + Short: "Plans the commit of the release branch back to master for each app in the release, and the platform repository.", + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + + b := MustGetBosun() + + p, err := b.GetCurrentPlatform() + if err != nil { + return err + } + + committer, err := bosun.NewReleaseCommitter(p, b) + if err != nil { + return err + } + + err = committer.Plan() + + return err + }, +}) + +var releaseCommitShowCmd = addCommand(releaseCommitCmd, &cobra.Command{ + Use: "show", + Short: "Shows the plan the commit of the release branch back to master for each app in the release, and the platform repository.", + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + + b := MustGetBosun() + + p, err := b.GetCurrentPlatform() + if err != nil { + return err + } + + committer, err := bosun.NewReleaseCommitter(p, b) + if err != nil { + return err + } + + plan, err := committer.GetPlan() + + if err != nil { + return err + } + + return printOutput(plan) + }, +}) + +var releaseCommitExecuteCmd = addCommand(releaseCommitCmd, &cobra.Command{ + Use: "execute", + Short: "Merges the release branch back to master for each app in the release, and the platform repository.", + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + viper.BindPFlags(cmd.Flags()) + + b := MustGetBosun() + + p, err := b.GetCurrentPlatform() + if err != nil { + return err + } + + committer, err := bosun.NewReleaseCommitter(p, b) + if err != nil { + return err + } + + + err = committer.Execute() + + return err + }, +}) \ No newline at end of file diff --git a/pkg/bosun/app.go b/pkg/bosun/app.go index c28f1a1..05adc9d 100644 --- a/pkg/bosun/app.go +++ b/pkg/bosun/app.go @@ -279,7 +279,25 @@ func (a *App) GetTaggedImageNames(versionAndRelease ...string) []string { return taggedNames } -func (a *App) BuildImages(ctx BosunContext) error { +type BuildImageRequest struct { + Pattern string + Ctx BosunContext +} + +func (a *App) BuildImages(req BuildImageRequest) error { + + ctx := req.Ctx + + var err error + var re *regexp.Regexp + + if req.Pattern != "" { + re, err = regexp.Compile(req.Pattern) + if err != nil { + return err + } + } + var report []string for _, image := range a.GetImages() { @@ -299,6 +317,13 @@ func (a *App) BuildImages(ctx BosunContext) error { contextPath = ctx.ResolvePath(contextPath) } + fullName := image.GetFullName() + + if re != nil && !re.MatchString(fullName){ + ctx.Log().Infof("Skipping image %s because it did not match pattern %q", fullName, req.Pattern) + continue + } + var buildCommand []string if len(image.BuildCommand) > 0 { buildCommand = image.BuildCommand @@ -310,7 +335,7 @@ func (a *App) BuildImages(ctx BosunContext) error { "--build-arg", fmt.Sprintf("VERSION_NUMBER=%s", a.Version), "--build-arg", fmt.Sprintf("COMMIT=%s", a.GetCommit()), "--build-arg", fmt.Sprintf("BUILD_NUMBER=%s", os.Getenv("BUILD_NUMBER")), - "--tag", image.GetFullName(), + "--tag", fullName, contextPath, } @@ -505,6 +530,8 @@ func (a *App) BumpVersion(ctx BosunContext, bumpOrVersion string) error { log := ctx.WithApp(a).Log() wasDirty := a.Repo.LocalRepo.IsDirty() + originalVersion := a.Version + version, err := NewVersion(bumpOrVersion) if err == nil { a.Version = version @@ -561,7 +588,7 @@ func (a *App) BumpVersion(ctx BosunContext, bumpOrVersion string) error { if wasDirty { log.Warn("Repo was dirty, will not commit bumped files.") } else { - commitMsg := fmt.Sprintf("chore(version): %s bump to %s", bumpOrVersion, a.Version) + commitMsg := fmt.Sprintf("chore(version): bumped version from %s to %s", originalVersion, a.Version) err = a.Repo.LocalRepo.Commit(commitMsg, ".") if err != nil { return errors.Wrap(err, "commit bumped files") diff --git a/pkg/bosun/app_image_helper.go b/pkg/bosun/app_image_helper.go index f402ebc..f30e73c 100644 --- a/pkg/bosun/app_image_helper.go +++ b/pkg/bosun/app_image_helper.go @@ -6,11 +6,12 @@ import ( "github.com/naveego/bosun/pkg" "github.com/naveego/bosun/pkg/slack" "github.com/pkg/errors" + "regexp" "strings" ) func NewAppImageHelper(b *Bosun) AppImageHelper { - return AppImageHelper{Bosun:b} + return AppImageHelper{Bosun: b} } type AppImageHelper struct { @@ -18,7 +19,8 @@ type AppImageHelper struct { } type PublishImagesRequest struct { - App *App + App *App + Pattern string } func (x AppImageHelper) PublishImages(req PublishImagesRequest) error { @@ -27,6 +29,16 @@ func (x AppImageHelper) PublishImages(req PublishImagesRequest) error { ctx := x.Bosun.NewContext() + var err error + var re *regexp.Regexp + + if req.Pattern != "" { + re, err = regexp.Compile(req.Pattern) + if err != nil { + return err + } + } + var report []string tags := []string{"latest", a.Version.String()} @@ -57,9 +69,15 @@ func (x AppImageHelper) PublishImages(req PublishImagesRequest) error { for _, tag := range tags { for _, taggedName := range a.GetTaggedImageNames(tag) { + + if re != nil && !re.MatchString(taggedName) { + ctx.Log().Infof("Skipping %s because it didn't match pattern %q", taggedName, req.Pattern) + continue + } + ctx.Log().Infof("Tagging and pushing %q", taggedName) untaggedName := strings.Split(taggedName, ":")[0] - _, err := pkg.NewShellExe("docker", "tag", untaggedName, taggedName).Sudo(ctx.GetParameters().Sudo).RunOutLog() + _, err = pkg.NewShellExe("docker", "tag", untaggedName, taggedName).Sudo(ctx.GetParameters().Sudo).RunOutLog() if err != nil { return err } @@ -80,7 +98,7 @@ func (x AppImageHelper) PublishImages(req PublishImagesRequest) error { changes, _ := g.ExecLines("log", "--pretty=oneline", "-n", "5", "--no-color") slack.Notification{ - IconEmoji:":frame_with_picture:", + IconEmoji: ":frame_with_picture:", }.WithMessage(`Pushed images for %s from branch %s: %s diff --git a/pkg/bosun/platform_commit_release.go b/pkg/bosun/platform_commit_release.go deleted file mode 100644 index 6be4e0d..0000000 --- a/pkg/bosun/platform_commit_release.go +++ /dev/null @@ -1,314 +0,0 @@ -package bosun - -import ( - "fmt" - "github.com/naveego/bosun/pkg/cli" - "github.com/naveego/bosun/pkg/git" - "github.com/naveego/bosun/pkg/util/multierr" - "github.com/naveego/bosun/pkg/vcs" - yaml2 "github.com/naveego/bosun/pkg/yaml" - "github.com/pkg/errors" - "os" - "path/filepath" -) - - -type mergeTarget struct { - dir string - version string - name string - fromBranch string - toBranch string - postMergeAction func(g git.GitWrapper) error - tags map[string]string -} - - -func (p *Platform) CommitCurrentRelease(ctx BosunContext) error { - - release, err := p.GetCurrentRelease() - if err != nil { - return err - } - - platformDir, err := git.GetRepoPath(p.FromPath) - if err != nil { - return err - } - - releaseBranch := fmt.Sprintf("release/%s", release.Version) - - progress := map[string]bool{} - progressFileName := filepath.Join(os.TempDir(), fmt.Sprintf("bosun-release-commit-%s.yaml", release.Version)) - _ = yaml2.LoadYaml(progressFileName, &progress) - - defer func() { - _ = yaml2.SaveYaml(progressFileName, progress) - }() - - mergeTargets := map[string]*mergeTarget{ - "devops-develop": { - dir: platformDir, - name: "devops", - version: release.Version.String(), - fromBranch: releaseBranch, - toBranch: "develop", - tags: map[string]string{ - "": release.Version.String(), - "release": release.Name, - }, - }, - "devops-master": { - dir: platformDir, - name: "devops", - version: release.Version.String(), - fromBranch: releaseBranch, - toBranch: "master", - tags: map[string]string{ - "": release.Version.String(), - "release": release.Name, - }, - }, - } - - appsNames := map[string]bool{} - for appName := range release.GetAllAppMetadata() { - appsNames[appName] = true - } - - b := ctx.Bosun - - for name := range release.UpgradedApps { - log := ctx.Log().WithField("app", name) - - appDeploy, appErr := release.GetAppManifest(name) - if appErr != nil { - return appErr - } - - app, appErr := b.GetAppFromProvider(name, WorkspaceProviderName) - if appErr != nil { - ctx.Log().WithError(appErr).Errorf("App repo %s (%s) not available.", appDeploy.Name, appDeploy.Repo) - continue - } - - if !app.BranchForRelease { - ctx.Log().Warnf("App repo (%s) for app %s is not branched for release.", app.RepoName, app.Name) - continue - } - - // if appDeploy.PinnedReleaseVersion == nil { - // ctx.Log().Warnf("App repo (%s) does not have a release branch pinned, probably not part of this release.", app.RepoName, release.Name, release.Version) - // continue - // } - // - // if *appDeploy.PinnedReleaseVersion != release.Version { - // ctx.Log().Warnf("App repo (%s) is not changed for this release.", app.RepoName) - // continue - // } - - manifest, appErr := app.GetManifest(ctx) - if appErr != nil { - return errors.Wrapf(appErr, "App manifest %s (%s) not available.", appDeploy.Name, appDeploy.Repo) - } - - if !app.IsRepoCloned() { - return errors.Errorf("App repo (%s) for app %s is not cloned, cannot merge.", app.RepoName, app.Name) - } - - appBranch, appErr := app.Branching.RenderRelease(release.GetBranchParts()) - if appErr != nil { - return appErr - } - - mt, ok := mergeTargets[app.Repo.Name] - if !ok { - masterName := app.Repo.Name - if progress[masterName] { - log.Infof("Release version has already been merged to master.") - } else { - mt = &mergeTarget{ - dir: app.Repo.LocalRepo.Path, - version: manifest.Version.String(), - name: manifest.Name, - fromBranch: appBranch, - toBranch: app.Branching.Master, - tags: map[string]string{}, - } - mt.tags[app.RepoName] = fmt.Sprintf("%s@%s-%s", app.Name, manifest.Version.String(), release.Version.String()) - mergeTargets[masterName] = mt - } - - if app.Branching.Develop != app.Branching.Master { - developName := app.RepoName + "-develop" - if progress[developName] { - log.Info("Release version has already been merged to develop.") - } else { - - mergeTargets[developName] = &mergeTarget{ - dir: app.Repo.LocalRepo.Path, - version: manifest.Version.String(), - name: manifest.Name, - fromBranch: appBranch, - toBranch: app.Branching.Develop, - tags: map[string]string{}, - } - } - } - } - } - - if len(mergeTargets) == 0 { - return errors.New("no apps found") - } - - fmt.Println("About to merge:") - for label, target := range mergeTargets { - fmt.Printf("- %s: %s@%s %s -> %s (tags %+v)\n", label, target.name, target.version, target.fromBranch, target.toBranch, target.tags) - } - - warnings := multierr.New() - - errs := multierr.New() - // validate that merge will work - for _, target := range mergeTargets { - - localRepo := &vcs.LocalRepo{Name: target.name, Path: target.dir} - - if localRepo.IsDirty() { - errs.Collect(errors.Errorf("Repo at %s is dirty, cannot merge.", localRepo.Path)) - } - } - - if err = errs.ToError(); err != nil { - return err - } - - for targetLabel, target := range mergeTargets { - - log := ctx.Log().WithField("repo", target.name) - - localRepo := &vcs.LocalRepo{Name: target.name, Path: target.dir} - - if localRepo.IsDirty() { - return errors.Errorf("Repo at %s is dirty, cannot merge.", localRepo.Path) - } - - repoDir := localRepo.Path - - g, _ := git.NewGitWrapper(repoDir) - - err = g.Fetch() - if err != nil { - return err - } - - log.Info("Checking out release branch...") - - _, err = g.Exec("checkout", target.fromBranch) - if err != nil { - return errors.Errorf("checkout %s: %s", repoDir, target.fromBranch) - } - - log.Info("Pulling release branch...") - err = g.Pull() - if err != nil { - return err - } - - log.Infof("Checking out base branch %s...", target.toBranch) - _, err = g.Exec("checkout", target.toBranch) - if err != nil { - return err - } - - log.Infof("Pulling base branch %s...", target.toBranch) - _, err = g.Exec("pull") - if err != nil { - return errors.Wrapf(err, "Could not pull branch, you'll need to resolve any merge conflicts.") - } - - var tags []string - for _, tag := range target.tags { - tags = []string{tag} - } - - var changes string - changes, err = g.Exec("log", fmt.Sprintf("%s..%s", target.toBranch, target.fromBranch), "--oneline") - if err != nil { - return err - } - if len(changes) == 0 { - log.Infof("Branch %q has already been merged into %q.", target.fromBranch, target.toBranch) - } else { - tagged := false - log.Info("Tagging release branch...") - for _, tag := range tags { - tagArgs := []string{"tag", tag, "-a", "-m", fmt.Sprintf("Release %s", release.Name)} - tagArgs = append(tagArgs, "--force") - _, err = g.Exec(tagArgs...) - if err != nil { - log.WithError(err).Warn("Could not tag repo, skipping merge. Set --force flag to force tag.") - } else { - tagged = true - } - } - - if tagged { - log.Info("Pushing tags...") - - pushArgs := []string{"push", "--tags"} - pushArgs = append(pushArgs, "--force") - - _, err = g.Exec(pushArgs...) - if err != nil { - return errors.Errorf("push tags: %s", err) - } - } - - log.Infof("Merging into branch %s...", target.toBranch) - - _, err = g.Exec("merge", "-m", fmt.Sprintf("Merge %s into %s to commit release %s", target.fromBranch, target.toBranch, release.Version), target.fromBranch) - for err != nil { - - confirmed := cli.RequestConfirmFromUser("Merge for %s from %s to %s in %s failed, you'll need to complete the merge yourself: %s\nEnter 'y' when you have completed the merge in another terminal, 'n' to abort release commit", targetLabel, target.fromBranch, target.toBranch, repoDir, err) - if !confirmed { - _, err = g.Exec("merge", "--abort") - break - } - - _, err = g.Exec("merge", "--continue") - - } - } - - changes, err = g.Exec("log", fmt.Sprintf("origin/%s..%s", target.toBranch, target.fromBranch), "--oneline") - if err != nil { - return err - } - if len(changes) == 0 { - log.Infof("Branch %s has already been pushed", target.toBranch) - progress[targetLabel] = true - continue - } - - log.Infof("Pushing branch %s...", target.toBranch) - - _, err = g.Exec("push") - if err != nil { - warnings.Collect(errors.Errorf("Push for %s of branch %s failed (you'll need to push it yourself): %s", targetLabel, target.toBranch, err)) - continue - } - - log.Infof("Merged back to %s and pushed.", target.toBranch) - - progress[targetLabel] = true - } - - err = warnings.ToError() - if err != nil { - return warnings.ToError() - } - - return nil -} diff --git a/pkg/bosun/platform_release_committer.go b/pkg/bosun/platform_release_committer.go new file mode 100644 index 0000000..1eae637 --- /dev/null +++ b/pkg/bosun/platform_release_committer.go @@ -0,0 +1,461 @@ +package bosun + +import ( + "fmt" + "github.com/fatih/color" + "github.com/naveego/bosun/pkg/cli" + "github.com/naveego/bosun/pkg/git" + "github.com/naveego/bosun/pkg/issues" + "github.com/naveego/bosun/pkg/semver" + "github.com/naveego/bosun/pkg/util" + yaml2 "github.com/naveego/bosun/pkg/yaml" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "os" + "path/filepath" + "time" +) + +type ReleaseCommitterPlan struct { + ReleaseVersion semver.Version `yaml:"releaseVersion"` + Steps []ReleaseCommitterPlanStep `yaml:"steps"` +} + +type ReleaseCommitterPlanStep struct { + CompletedAt time.Time `yaml:"completed,omitempty"` + Error string `yaml:"error,omitempty"` + Repo issues.RepoRef `yaml:"repo"` + RepoPath string `yaml:"repoPath"` + App string `yaml:"app,omitempty"` + Description string `yaml:"description"` + Bump *ReleaseCommitBumpAction `yaml:"bump,omitempty"` + Merge *ReleaseCommitMergeAction `yaml:"merge,omitempty"` + Tag *ReleaseCommitTagAction `yaml:"tag,omitempty"` +} + +type ReleaseCommitBumpAction struct { + Version semver.Version `yaml:"version,omitempty"` + Branch string `yaml:"branch"` +} + +type ReleaseCommitMergeAction struct { + ToBranch string `yaml:"targetBranch"` + FromBranch string `yaml:"fromBranch"` +} + +type ReleaseCommitTagAction struct { + Branch string `yaml:"branch"` + Tags []string `yaml:"tags"` +} + +func (r ReleaseCommitterPlanStep) String() string { + return r.Description +} + +type ReleaseCommitter struct { + release *ReleaseManifest + releaseBranch string + planPath string + plan *ReleaseCommitterPlan + log *logrus.Entry + platformRepo issues.RepoRef + platform *Platform + platformRepoPath string + bosun *Bosun +} + +func NewReleaseCommitter(platform *Platform, b *Bosun) (*ReleaseCommitter, error) { + + log := b.NewContext().Log() + + release, err := platform.GetCurrentRelease() + if err != nil { + return nil, err + } + platformRepoPath, err := git.GetRepoPath(platform.FromPath) + if err != nil { + return nil, err + } + + releaseBranch := fmt.Sprintf("release/%s", release.Version) + + progressFileName := filepath.Join(os.TempDir(), fmt.Sprintf("bosun-release-commit-plan-%s.yaml", release.Version)) + + log.Infof("Storing plan at %s", progressFileName) + + org, repo := git.GetOrgAndRepoFromPath(platform.FromPath) + + platformRepo := issues.RepoRef{Org: org, Repo: repo} + + var plan ReleaseCommitterPlan + + _ = yaml2.LoadYaml(progressFileName, &plan) + + r := &ReleaseCommitter{ + bosun: b, + release: release, + platform: platform, + releaseBranch: releaseBranch, + planPath: progressFileName, + platformRepo: platformRepo, + platformRepoPath: platformRepoPath, + plan: &plan, + log: log, + } + + return r, nil +} + +func (r *ReleaseCommitter) updatePlan(mutator func(plan *ReleaseCommitterPlan)) error { + mutator(r.plan) + return yaml2.SaveYaml(r.planPath, r.plan) +} + +func (r *ReleaseCommitter) updatePlanStep(index int, mutator func(plan *ReleaseCommitterPlanStep)) error { + return r.updatePlan(func(plan *ReleaseCommitterPlan) { + + step := plan.Steps[index] + mutator(&step) + plan.Steps[index] = step + + }) +} + +func (r *ReleaseCommitter) addPlanSteps(steps ...ReleaseCommitterPlanStep) error { + return r.updatePlan(func(plan *ReleaseCommitterPlan) { + plan.Steps = append(plan.Steps, steps...) + }) +} + +func (r *ReleaseCommitter) Plan() error { + + r.log.Info("Planning release commit") + + if err := r.updatePlan(func(plan *ReleaseCommitterPlan) { + plan.ReleaseVersion = r.release.Version + plan.Steps = []ReleaseCommitterPlanStep{} + }); err != nil { + return err + } + + for _, appName := range util.SortedKeys(r.release.UpgradedApps) { + + log := r.log.WithField("app", appName) + + upgraded := r.release.UpgradedApps[appName] + + if !upgraded { + log.Info("Skipping app because it wasn't marked as upgraded in the manifest.") + continue + } + + log.Info("Planning steps to commit app.") + + app, ok := r.release.appManifests[appName] + if !ok { + log.Warn("App was marked as upgraded but it was not found in the manifest.") + continue + } + + if app.RepoRef() == r.platformRepo { + log.Infof("Skipping planning for app because it is in the platform repo and will commit with the platform.") + continue + } + + localApp, err := r.bosun.GetApp(appName, WorkspaceProviderName) + if err != nil { + log.WithError(err).Warnf("Skipping planning for app because it could not be found in local workspace.") + continue + } + + localAppRepoPath, err := git.GetRepoPath(localApp.FromPath) + if err != nil { + log.WithError(err).Warnf("Skipping planning for app because it did not have a local repo path.") + continue + } + + steps := []ReleaseCommitterPlanStep{ + { + Repo: app.RepoRef(), + RepoPath: localAppRepoPath, + Description: "Tag release commits", + Tag: &ReleaseCommitTagAction{ + Branch: r.releaseBranch, + Tags: []string{ + fmt.Sprintf("v%s", app.Version), + fmt.Sprintf("release-%s", r.release.Version), + }, + }, + }, { + Repo: app.RepoRef(), + Description: "Merge to develop", + App: appName, + RepoPath: localAppRepoPath, + Merge: &ReleaseCommitMergeAction{ + FromBranch: r.releaseBranch, + ToBranch: app.AppConfig.Branching.Develop, + }, + }, { + Repo: app.RepoRef(), + Description: "Bump develop", + App: appName, + RepoPath: localAppRepoPath, + Bump: &ReleaseCommitBumpAction{ + Version: app.Version.BumpPatch(), + Branch: app.AppConfig.Branching.Develop, + }, + }, { + Repo: app.RepoRef(), + RepoPath: localAppRepoPath, + Description: "Merge to master", + App: appName, + Merge: &ReleaseCommitMergeAction{ + FromBranch: r.releaseBranch, + ToBranch: app.AppConfig.Branching.Master, + }, + }, + } + + err = r.addPlanSteps(steps...) + if err != nil { + return err + } + } + + err := r.addPlanSteps( + ReleaseCommitterPlanStep{ + + Repo: r.platformRepo, + RepoPath: r.platformRepoPath, + Description: "Tag release commits", + Tag: &ReleaseCommitTagAction{ + Branch: r.releaseBranch, + Tags: []string{ + fmt.Sprintf("release-%s", r.release.Version), + }, + }, + }, + + ReleaseCommitterPlanStep{ + Repo: r.platformRepo, + RepoPath: r.platformRepoPath, + Description: "Merge to develop", + Merge: &ReleaseCommitMergeAction{ + FromBranch: r.releaseBranch, + ToBranch: r.platform.Branching.Develop, + }, + }, ReleaseCommitterPlanStep{ + Repo: r.platformRepo, + RepoPath: r.platformRepoPath, + Description: "Merge to master", + Merge: &ReleaseCommitMergeAction{ + FromBranch: r.releaseBranch, + ToBranch: r.platform.Branching.Master, + }, + }) + + if err != nil { + r.log.WithError(err).Error("Error adding master plan step.") + return err + } + + return nil +} + +func (r *ReleaseCommitter) Execute() error { + + if len(r.plan.Steps) == 0 { + return errors.New("no steps planned") + } + + r.log.Infof("Executing %d steps", len(r.plan.Steps)) + + for i, step := range r.plan.Steps { + if !step.CompletedAt.IsZero() { + r.log.Debugf("Skipping step %d (%s) because it is completed.", i, step) + continue + } + + for { + err := r.ExecuteStep(i, step) + if err != nil { + + color.Red("Step %d failed\n", i) + fmt.Print("Step: ") + color.Blue(step.String() + "\n") + fmt.Println("Error: ") + color.Red(err.Error()) + fmt.Println() + fmt.Println("You can try to fix the issue in another terminal or you can abort.") + confirmed := cli.RequestConfirmFromUser(" Enter 'y' when you have completed or 'n' to abort.", ) + if !confirmed { + updateErr := r.updatePlanStep(i, func(step *ReleaseCommitterPlanStep) { + step.Error = err.Error() + }) + if updateErr != nil { + return errors.Wrapf(updateErr, "error recording error on step %d %s; original error: %s", i, step, err) + } + return errors.Wrapf(err, "error on step %d %s", i, step) + } + } else { + + updateErr := r.updatePlanStep(i, func(step *ReleaseCommitterPlanStep) { + step.CompletedAt = time.Now() + }) + if updateErr != nil { + return errors.Wrapf(updateErr, "error recording completion on step %d %s; original error: %s", i, step, err) + } + break + } + } + } + + r.log.Infof("Completed %d steps", len(r.plan.Steps)) + + return nil +} + +func (r *ReleaseCommitter) ExecuteStep(i int, step ReleaseCommitterPlanStep) error { + + log := r.log.WithField("step", step.String()).WithField("index", i).WithField("repo", step.Repo) + log.Info("Executing step.") + + g, err := getGitWrapper(step, log) + if err != nil { + return err + } + + if g.IsDirty() { + return errors.Errorf("repo %s is dirty, commit all changes before proceeding", step.RepoPath) + } + + if step.Tag != nil { + + err = ensureBranch(g, step.Tag.Branch, log) + if err != nil { + return err + } + + log.Infof("Applying tags %v", step.Tag.Tags) + + for _, tag := range step.Tag.Tags { + _, err = g.Exec("tag", tag, "--force") + if err != nil { + return err + } + } + + _, err = g.Exec("push", "--force", "--tags") + + return err + } + + if step.Bump != nil { + + log.Infof("Bumping app to version %s", step.Bump.Version) + + err = ensureBranch(g, step.Bump.Branch, log) + if err != nil { + return err + } + + var app *App + app, err = r.bosun.GetApp(step.App, WorkspaceProviderName) + if err != nil { + return err + } + + if app.Version == step.Bump.Version { + log.Infof("App is already on version %s", step.Bump.Version) + return nil + } + + err = app.BumpVersion(r.bosun.NewContext(), step.Bump.Version.String()) + + if err != nil { + return err + } + + _, err = g.Exec("push", "--force") + + return err + } + + if step.Merge != nil { + + log.Infof("Merging %s into %s", step.Merge.FromBranch, step.Merge.ToBranch) + + err = ensureBranch(g, step.Merge.FromBranch, log) + if err != nil { + return err + } + + err = ensureBranch(g, step.Merge.ToBranch, log) + if err != nil { + return err + } + + _, err = g.ExecVerbose("merge", "-m", fmt.Sprintf("Merge %s into %s to commit release %s", step.Merge.FromBranch, step.Merge.ToBranch, r.release.Version), step.Merge.FromBranch) + for err != nil { + + confirmed := cli.RequestConfirmFromUser("Merge for %s from %s to %s in %s failed, you'll need to complete the merge yourself: %s\nEnter 'y' when you have completed the merge in another terminal, 'n' to abort release commit", r.release.Version, step.Merge.FromBranch, step.Merge.ToBranch, r.release.Version, step.RepoPath, err) + if !confirmed { + _, err = g.Exec("merge", "--abort") + break + } + + _, err = g.Exec("merge", "--continue") + } + + err = g.Push() + if err != nil { + return err + } + + return nil + } + + return errors.Errorf("unknown action type") +} + +func ensureBranch(g git.GitWrapper, branch string, log *logrus.Entry) error { + log.Infof("Ensuring branch %q is present and up-to-date", branch) + + err := g.CheckOutOrCreateBranch(branch) + if err != nil { + return err + } + + err = g.Pull() + if err != nil { + return err + } + return nil +} + +func getGitWrapper(step ReleaseCommitterPlanStep, log *logrus.Entry) (git.GitWrapper, error) { + + if step.RepoPath == "" { + return git.GitWrapper{}, errors.New("repo path was not set") + } + + log.Infof("Switching git to use path %s", step.RepoPath) + g, err := git.NewGitWrapper(step.RepoPath) + + if err != nil { + return git.GitWrapper{}, err + } + err = g.Fetch() + if err != nil { + return git.GitWrapper{}, err + } + return g, err +} + +func (r *ReleaseCommitter) GetPlan() (ReleaseCommitterPlan, error) { + if len(r.plan.Steps) == 0 { + return ReleaseCommitterPlan{}, errors.New("no steps planned") + } + + return *r.plan, nil +} diff --git a/pkg/git/helpers.go b/pkg/git/helpers.go index 10dccd0..e0bbde3 100644 --- a/pkg/git/helpers.go +++ b/pkg/git/helpers.go @@ -52,6 +52,21 @@ func GetRepoPath(path string) (string, error) { return repoPath, nil } +func GetOrgAndRepoFromPath(path string) (string, string) { + + g, _ := NewGitWrapper(path) + out, _ := g.Exec("config", "--get", "remote.origin.url") + repoURL := strings.Split(out, ":") + if len(repoURL) > 1 { + path = strings.TrimSuffix(repoURL[1], ".git") + } + + repo := filepath.Base(path) + org := filepath.Base(filepath.Dir(path)) + return org, repo +} + + func GetRepoRefFromPath(path string) issues.RepoRef { g, _ := NewGitWrapper(path)