Skip to content

Commit 0b20067

Browse files
authored
Add options for from-branch and to-branch (#106)
* Add toBranch and fromBranch The new `fromBranch` and `toBranch` determine which branches to use for the new service configuration, and where to base the Pull Request. Previously, both of these were hard-coded to "master". I think there is some confusion in naming here... the Promote function already takes an (undocumented) `newBranchName` parameter, which is later just called `branchName` in the makePullRequestInput function. That's just the name of the (temporary) branch created on the destination repo, where the changes are pushed so they can be merged into the "Base" branch (previously hard-coded as "master", now the new `toBranch` parameter). Fixes: #100 * Add EnvLocation and refactor EnvLocation holds information about the environment's location so that it can be passed around more compactly in functions. Currently it only holds the Path (URL or local) and the Branch, but it could hold other information, for example the environment folder in Repositories with multiple environments.
1 parent a01e4ec commit 0b20067

File tree

8 files changed

+249
-198
lines changed

8 files changed

+249
-198
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,21 @@ Usage:
7070
services promote [flags]
7171

7272
Flags:
73-
--branch-name string the name of the branch on the destination repository for the pull request
73+
--branch-name string the name of the branch on the destination repository for the pull request (auto-generated if empty)
7474
--cache-dir string where to cache Git checkouts (default "~/.promotion/cache")
7575
--commit-email string the email to use for commits when creating branches
7676
--commit-message string the msg to use on the resultant commit and pull request
7777
--commit-name string the name to use for commits when creating branches
7878
--debug additional debug logging output
7979
--from string source Git repository
80+
--from-branch string branch on the source Git repository (default "master")
8081
-h, --help help for promote
8182
--insecure-skip-verify Insecure skip verify TLS certificate
8283
--keep-cache whether to retain the locally cloned repositories in the cache directory
8384
--repository-type string the type of repository: github, gitlab or ghe (default "github")
8485
--service string service name to promote
8586
--to string destination Git repository
87+
--to-branch string branch on the destination Git repository (default "master")
8688

8789
Global Flags:
8890
--github-token string oauth access token to authenticate the request
@@ -97,12 +99,14 @@ This will _copy_ all files under `/services/service-a/base/config/*` in `first-e
9799
- `--commit-name` : The other half of `commit-email`. Both must be set.
98100
- `--debug` : prints extra debug output if true.
99101
- `--from` : an https URL to a GitOps repository for 'remote' cases, or a path to a Git clone of a microservice for 'local' cases.
102+
- `--from-branch` : use this to specify a branch on the source repository, instead of using the "master" branch.
100103
- `--help`: prints the above text if true.
101104
- `--insecure-skip-verify` : skip TLS cerificate verification if true. Do not set this to true unless you know what you are doing.
102105
- `--keep-cache` : `cache-dir` is deleted unless this is set to true. Keeping the cache will often cause further promotion attempts to fail. This flag is mostly used along with `--debug` when investigating failure cases.
103106
- `--repository-type` : the type of repository: github, gitlab or ghe (default "github"). If `--from` is a Git URL, it must be of the same type as that specified via `--to`.
104107
- `--service` : the destination path for promotion is `/environments/<env-name>/services/<service-name>/base/config/`. This argument defines `service-name` in that path.
105108
- `--to`: an https URL to the destination GitOps repository.
109+
- `--to-branch` : use this to specify a branch on the destination repository, instead of using the "master" branch.
106110
107111
### Troubleshooting
108112

pkg/cmd/promote.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ func makePromoteCmd() *cobra.Command {
8888
)
8989
logIfError(viper.BindPFlag(debugFlag, cmd.Flags().Lookup(debugFlag)))
9090

91+
cmd.Flags().String(
92+
fromBranchFlag,
93+
"master",
94+
"branch on the source Git repository",
95+
)
96+
logIfError(viper.BindPFlag(fromBranchFlag, cmd.Flags().Lookup(fromBranchFlag)))
97+
9198
cmd.Flags().Bool(
9299
insecureSkipVerifyFlag,
93100
false,
@@ -108,6 +115,13 @@ func makePromoteCmd() *cobra.Command {
108115
"the type of repository: github, gitlab or ghe",
109116
)
110117
logIfError(viper.BindPFlag(repoTypeFlag, cmd.Flags().Lookup(repoTypeFlag)))
118+
119+
cmd.Flags().String(
120+
toBranchFlag,
121+
"master",
122+
"branch on the destination Git repository",
123+
)
124+
logIfError(viper.BindPFlag(toBranchFlag, cmd.Flags().Lookup(toBranchFlag)))
111125
return cmd
112126
}
113127

@@ -127,9 +141,11 @@ func promoteAction(c *cobra.Command, args []string) error {
127141
newBranchName := viper.GetString(branchNameFlag)
128142
msg := viper.GetString(msgFlag)
129143
debug := viper.GetBool(debugFlag)
144+
fromBranch := viper.GetString(fromBranchFlag)
130145
insecureSkipVerify := viper.GetBool(insecureSkipVerifyFlag)
131146
keepCache := viper.GetBool(keepCacheFlag)
132147
repoType := viper.GetString(repoTypeFlag)
148+
toBranch := viper.GetString(toBranchFlag)
133149

134150
cacheDir, err := homedir.Expand(viper.GetString(cacheDirFlag))
135151
if err != nil {
@@ -140,7 +156,18 @@ func promoteAction(c *cobra.Command, args []string) error {
140156
if err != nil {
141157
return fmt.Errorf("unable to establish credentials: %w", err)
142158
}
143-
return promotion.New(cacheDir, author, promotion.WithDebug(debug), promotion.WithInsecureSkipVerify(insecureSkipVerify), promotion.WithRepoType(repoType)).Promote(service, fromRepo, toRepo, newBranchName, msg, keepCache)
159+
160+
from := promotion.EnvLocation{
161+
RepoPath: fromRepo,
162+
Branch: fromBranch,
163+
}
164+
to := promotion.EnvLocation{
165+
RepoPath: toRepo,
166+
Branch: toBranch,
167+
}
168+
169+
sm := promotion.New(cacheDir, author, promotion.WithDebug(debug), promotion.WithInsecureSkipVerify(insecureSkipVerify), promotion.WithRepoType(repoType))
170+
return sm.Promote(service, from, to, newBranchName, msg, keepCache)
144171
}
145172

146173
func newAuthor() (*git.Author, error) {

pkg/cmd/root.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,21 @@ import (
1010
)
1111

1212
const (
13-
githubTokenFlag = "github-token"
1413
branchNameFlag = "branch-name"
15-
fromFlag = "from"
16-
toFlag = "to"
17-
serviceFlag = "service"
18-
repoTypeFlag = "repository-type"
1914
cacheDirFlag = "cache-dir"
15+
emailFlag = "commit-email"
2016
msgFlag = "commit-message"
2117
nameFlag = "commit-name"
22-
emailFlag = "commit-email"
23-
insecureSkipVerifyFlag = "insecure-skip-verify"
2418
debugFlag = "debug"
19+
fromFlag = "from"
20+
fromBranchFlag = "from-branch"
21+
insecureSkipVerifyFlag = "insecure-skip-verify"
2522
keepCacheFlag = "keep-cache"
23+
repoTypeFlag = "repository-type"
24+
serviceFlag = "service"
25+
toFlag = "to"
26+
toBranchFlag = "to-branch"
27+
githubTokenFlag = "github-token"
2628
)
2729

2830
var rootCmd = &cobra.Command{

pkg/promotion/promote.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package promotion
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"net/url"
8+
"strings"
9+
10+
"github.com/rhd-gitops-example/services/pkg/git"
11+
"github.com/rhd-gitops-example/services/pkg/local"
12+
13+
"github.com/google/uuid"
14+
"github.com/jenkins-x/go-scm/scm"
15+
)
16+
17+
type EnvLocation struct {
18+
RepoPath string // URL or local path
19+
Branch string
20+
}
21+
22+
func (env EnvLocation) IsLocal() bool {
23+
parsed, err := url.Parse(env.RepoPath)
24+
if err != nil {
25+
log.Printf("Error while parsing URL for environment path: '%v', assuming it is local.\n", err)
26+
return true
27+
}
28+
return parsed.Scheme == ""
29+
}
30+
31+
// Promote is the main driver for promoting files between two
32+
// repositories.
33+
//
34+
// It uses a Git cache to checkout the code to, and will copy the environment
35+
// configuration for the `fromURL` to the `toURL` in a named branch.
36+
func (s *ServiceManager) Promote(serviceName string, from, to EnvLocation, newBranchName, message string, keepCache bool) error {
37+
var reposToDelete []git.Repo
38+
if !keepCache {
39+
defer clearCache(&reposToDelete)
40+
}
41+
42+
var source git.Source
43+
var err error
44+
if from.IsLocal() {
45+
source = s.localFactory(from.RepoPath, s.debug)
46+
} else {
47+
source, err = s.checkoutSourceRepo(from.RepoPath, from.Branch)
48+
if err != nil {
49+
return git.GitError("error checking out source repository from Git", from.RepoPath)
50+
}
51+
reposToDelete = append(reposToDelete, source.(git.Repo))
52+
}
53+
if newBranchName == "" {
54+
newBranchName = generateBranchName(source)
55+
}
56+
57+
destination, err := s.checkoutDestinationRepo(to.RepoPath, newBranchName)
58+
if err != nil {
59+
return err
60+
}
61+
reposToDelete = append(reposToDelete, destination)
62+
destinationEnvironment, err := destination.GetUniqueEnvironmentFolder()
63+
if err != nil {
64+
return fmt.Errorf("could not determine unique environment name for destination repository - check that only one directory exists under it and you can write to your cache folder")
65+
}
66+
67+
var copied []string
68+
if from.IsLocal() {
69+
copied, err = local.CopyConfig(serviceName, source, destination, destinationEnvironment)
70+
if err != nil {
71+
return fmt.Errorf("failed to set up local repository: %w", err)
72+
}
73+
} else {
74+
repo, ok := source.(git.Repo)
75+
if !ok {
76+
// should not happen, but just in case
77+
return fmt.Errorf("failed to convert source '%v' to Git Repo", source)
78+
}
79+
sourceEnvironment, err := repo.GetUniqueEnvironmentFolder()
80+
if err != nil {
81+
return fmt.Errorf("could not determine unique environment name for source repository - check that only one directory exists under it and you can write to your cache folder")
82+
}
83+
84+
copied, err = git.CopyService(serviceName, source, destination, sourceEnvironment, destinationEnvironment)
85+
if err != nil {
86+
return fmt.Errorf("failed to copy service: %w", err)
87+
}
88+
}
89+
90+
if message == "" {
91+
message = generateDefaultCommitMsg(source, serviceName, from)
92+
}
93+
if err := destination.StageFiles(copied...); err != nil {
94+
return fmt.Errorf("failed to stage files %s: %w", copied, err)
95+
}
96+
if err := destination.Commit(message, s.author); err != nil {
97+
return fmt.Errorf("failed to commit: %w", err)
98+
}
99+
if err := destination.Push(newBranchName); err != nil {
100+
return fmt.Errorf("failed to push to Git repository - check the access token is correct with sufficient permissions: %w", err)
101+
}
102+
103+
ctx := context.Background()
104+
pr, err := createPullRequest(ctx, from, to, newBranchName, message, s.clientFactory(s.author.Token, to.RepoPath, s.repoType, s.tlsVerify))
105+
if err != nil {
106+
message := fmt.Sprintf("failed to create a pull-request for branch %s, error: %s", newBranchName, err)
107+
return git.GitError(message, to.RepoPath)
108+
}
109+
log.Printf("created PR %d", pr.Number)
110+
return nil
111+
}
112+
113+
func clearCache(repos *[]git.Repo) {
114+
for _, repo := range *repos {
115+
err := repo.DeleteCache()
116+
if err != nil {
117+
log.Printf("failed deleting files from cache: %s", err)
118+
}
119+
}
120+
}
121+
122+
// generateBranchName constructs a branch name based on the source (a commit or local) and
123+
// a random UUID
124+
func generateBranchName(source git.Source) string {
125+
var commitID string
126+
repo, ok := source.(git.Repo)
127+
if ok {
128+
commitID = repo.GetCommitID()
129+
} else {
130+
commitID = "local-dir"
131+
}
132+
133+
uniqueString := uuid.New()
134+
runes := []rune(uniqueString.String())
135+
branchName := source.GetName() + "-" + commitID + "-" + string(runes[0:5])
136+
return strings.Replace(branchName, "\n", "", -1)
137+
}
138+
139+
// generateDefaultCommitMsg constructs a default commit message based on the source information.
140+
func generateDefaultCommitMsg(source git.Source, serviceName string, from EnvLocation) string {
141+
repo, ok := source.(git.Repo)
142+
if ok {
143+
commit := repo.GetCommitID()
144+
return fmt.Sprintf("Promoting service %s at commit %s from branch %s in %s.", serviceName, commit, from.Branch, from.RepoPath)
145+
} else {
146+
return fmt.Sprintf("Promoting service %s from local filesystem directory %s.", serviceName, from.RepoPath)
147+
}
148+
}
149+
150+
func createPullRequest(ctx context.Context, from, to EnvLocation, newBranchName, commitMsg string, client *scm.Client) (*scm.PullRequest, error) {
151+
prInput, err := makePullRequestInput(from, to, newBranchName, commitMsg)
152+
if err != nil {
153+
return nil, err
154+
}
155+
156+
u, _ := url.Parse(to.RepoPath)
157+
pathToUse := strings.TrimPrefix(strings.TrimSuffix(u.Path, ".git"), "/")
158+
pr, _, err := client.PullRequests.Create(ctx, pathToUse, prInput)
159+
return pr, err
160+
}

pkg/promotion/pull_request.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,21 @@ import (
77
"github.com/rhd-gitops-example/services/pkg/util"
88
)
99

10-
// TODO: OptionFuncs for Base and Title?
10+
// TODO: OptionFunc for Title?
1111
// TODO: For the Head, should this try and determine whether or not this is a
1212
// fork ("user" of both repoURLs) and if so, simplify the Head?
13-
func makePullRequestInput(fromLocal bool, fromURL, toURL, branchName, prBody string) (*scm.PullRequestInput, error) {
13+
func makePullRequestInput(from, to EnvLocation, branchName, prBody string) (*scm.PullRequestInput, error) {
1414
var title string
1515

16-
_, toRepo, err := util.ExtractUserAndRepo(toURL)
16+
_, toRepo, err := util.ExtractUserAndRepo(to.RepoPath)
1717
if err != nil {
1818
return nil, err
1919
}
2020

21-
if fromLocal {
21+
if from.IsLocal() {
2222
title = fmt.Sprintf("promotion from local filesystem directory to %s", toRepo)
2323
} else {
24-
_, fromRepo, err := util.ExtractUserAndRepo(fromURL)
24+
_, fromRepo, err := util.ExtractUserAndRepo(from.RepoPath)
2525
if err != nil {
2626
return nil, err
2727
}
@@ -31,7 +31,7 @@ func makePullRequestInput(fromLocal bool, fromURL, toURL, branchName, prBody str
3131
return &scm.PullRequestInput{
3232
Title: title,
3333
Head: branchName,
34-
Base: "master",
34+
Base: to.Branch,
3535
Body: prBody,
3636
}, nil
3737
}

pkg/promotion/pull_request_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ import (
99
)
1010

1111
func TestMakePullRequestInput(t *testing.T) {
12-
pr, err := makePullRequestInput(false, "https://example.com/project/dev-env.git", "https://example.com/project/prod-env.git", "my-test-branch", "foo bar wibble")
12+
from := EnvLocation{
13+
RepoPath: "https://example.com/project/dev-env.git",
14+
Branch: "example",
15+
}
16+
to := EnvLocation{
17+
RepoPath: "https://example.com/project/prod-env.git",
18+
Branch: "master",
19+
}
20+
pr, err := makePullRequestInput(from, to, "my-test-branch", "foo bar wibble")
1321
if err != nil {
1422
t.Fatal(err)
1523
}

0 commit comments

Comments
 (0)