Skip to content

Commit 1620093

Browse files
committed
Further refactoring and tests.
1 parent cf8a286 commit 1620093

File tree

8 files changed

+165
-30
lines changed

8 files changed

+165
-30
lines changed

cmd/services/main.go

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package main
22

33
import (
4+
"errors"
45
"fmt"
56
"log"
67
"os"
78

89
"github.com/bigkevmcd/promote/pkg/cache"
910
"github.com/bigkevmcd/promote/pkg/promote"
1011
"github.com/mitchellh/go-homedir"
12+
"github.com/tcnksm/go-gitconfig"
1113
"github.com/urfave/cli/v2"
1214
)
1315

@@ -18,13 +20,14 @@ const (
1820
serviceFlag = "service"
1921
mappingFileFlag = "mapping-file"
2022
cacheDirFlag = "cache-dir"
23+
nameFlag = "commit-name"
24+
emailFlag = "commit-email"
2125
)
2226

2327
var (
2428
globalFlags = []cli.Flag{
2529
&cli.StringFlag{
2630
Name: githubTokenFlag,
27-
Value: "",
2831
Usage: "oauth access token to authenticate the request",
2932
EnvVars: []string{"GITHUB_TOKEN"},
3033
Required: true,
@@ -54,13 +57,25 @@ var (
5457
Usage: "mapping from environment to repository",
5558
Required: false,
5659
},
57-
5860
&cli.StringFlag{
5961
Name: cacheDirFlag,
6062
Value: "~/.promotion/cache",
6163
Usage: "where to cache Git checkouts",
6264
Required: false,
6365
},
66+
67+
&cli.StringFlag{
68+
Name: nameFlag,
69+
Usage: "the name to use for commits when creating branches",
70+
Required: false,
71+
EnvVars: []string{"COMMIT_NAME"},
72+
},
73+
&cli.StringFlag{
74+
Name: emailFlag,
75+
Usage: "the email to use for commits when creating branches",
76+
Required: false,
77+
EnvVars: []string{"COMMIT_TOKEN"},
78+
},
6479
}
6580
)
6681

@@ -89,7 +104,6 @@ func promoteAction(c *cli.Context) error {
89104
fromRepo := c.String(fromFlag)
90105
toRepo := c.String(toFlag)
91106
service := c.String(serviceFlag)
92-
mappingFilename := c.String(mappingFileFlag)
93107
token := c.String(githubTokenFlag)
94108
newBranchName := "testing"
95109

@@ -98,15 +112,50 @@ func promoteAction(c *cli.Context) error {
98112
return fmt.Errorf("failed to expand cacheDir path: %w", err)
99113
}
100114

101-
cache, err := cache.NewLocalCache(cacheDir)
115+
user, email, err := credentials(c)
116+
if err != nil {
117+
return fmt.Errorf("unable to establish: %w", err)
118+
}
119+
cache, err := cache.NewLocalCache(cacheDir, user, email)
102120
if err != nil {
103121
return fmt.Errorf("failed to create a local cache in %v: %w", cacheDir, err)
104122
}
105123

124+
mappingFilename, err := homedir.Expand(c.String(mappingFileFlag))
125+
if err != nil {
126+
return fmt.Errorf("failed to expand mapping file path: %w", err)
127+
}
106128
mapping, err := promote.LoadMappingFromFile(mappingFilename)
107129
if err != nil {
108130
return fmt.Errorf("failed to load the mappingfile %s: %w", mappingFilename, err)
109131
}
110132

111133
return promote.PromoteService(cache, token, service, fromRepo, toRepo, newBranchName, mapping)
112134
}
135+
136+
func credentials(c *cli.Context) (string, string, error) {
137+
name := c.String(nameFlag)
138+
email := c.String(emailFlag)
139+
140+
var err error
141+
if name == "" {
142+
name, err = gitconfig.Username()
143+
if err != nil {
144+
return "", "", err
145+
}
146+
}
147+
148+
if email == "" {
149+
email, err = gitconfig.Email()
150+
if err != nil {
151+
return "", "", err
152+
}
153+
}
154+
155+
// TODO: make this a multierror with both errors?
156+
if name == "" || email == "" {
157+
return "", "", errors.New("unable to identify user and email for commits")
158+
}
159+
160+
return name, email, nil
161+
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ go 1.13
44

55
require (
66
github.com/golang/protobuf v1.3.2 // indirect
7+
github.com/google/go-cmp v0.3.0
78
github.com/jenkins-x/go-scm v1.5.71
89
github.com/mitchellh/go-homedir v1.1.0
910
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 // indirect
11+
github.com/tcnksm/go-gitconfig v0.1.2
1012
github.com/urfave/cli/v2 v2.1.1
1113
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
1214
gopkg.in/src-d/go-git.v4 v4.13.1

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3
1919
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
2020
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
2121
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
22+
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
2223
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
2324
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
2425
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
@@ -37,6 +38,7 @@ github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsC
3738
github.com/h2non/gock v1.0.9 h1:17gCehSo8ZOgEsFKpQgqHiR7VLyjxdAG3lkhVvO9QZU=
3839
github.com/h2non/gock v1.0.9/go.mod h1:CZMcB0Lg5IWnr9bF79pPMg9WeV6WumxQiUJ1UvdO1iE=
3940
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
41+
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
4042
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
4143
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
4244
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
@@ -62,7 +64,9 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
6264
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
6365
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
6466
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
67+
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
6568
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
69+
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
6670
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
6771
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
6872
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
@@ -90,6 +94,8 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
9094
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
9195
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
9296
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
97+
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
98+
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
9399
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
94100
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
95101
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
@@ -134,6 +140,7 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
134140
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
135141
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
136142
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
143+
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
137144
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
138145
gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
139146
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
@@ -142,6 +149,7 @@ gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOA
142149
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
143150
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
144151
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
152+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
145153
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
146154
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
147155
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=

pkg/cache/local_cache.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,19 @@ import (
1414
"gopkg.in/src-d/go-git.v4/plumbing/transport/http"
1515
)
1616

17+
type userCredentials struct {
18+
name string
19+
email string
20+
}
21+
1722
type LocalCache struct {
18-
cacheDir string
23+
cacheDir string
24+
credentials userCredentials
1925
}
2026

2127
// NewLocalCache creates a new LocalCache and ensures that the provided cacheDir
2228
// exists.
23-
func NewLocalCache(cacheDir string) (*LocalCache, error) {
29+
func NewLocalCache(cacheDir, name, email string) (*LocalCache, error) {
2430
cacheDirExists, err := dirExists(cacheDir)
2531
if err != nil {
2632
return nil, fmt.Errorf("error getting the cache dir: %w", err)
@@ -32,7 +38,7 @@ func NewLocalCache(cacheDir string) (*LocalCache, error) {
3238
return nil, fmt.Errorf("error getting the cache dir: %w", err)
3339
}
3440
}
35-
return &LocalCache{cacheDir: cacheDir}, nil
41+
return &LocalCache{cacheDir: cacheDir, credentials: userCredentials{name: name, email: email}}, nil
3642
}
3743

3844
func (l *LocalCache) ReadFileFromBranch(ctx context.Context, repoURL, filePath, branch string) ([]byte, error) {
@@ -129,8 +135,8 @@ func (l *LocalCache) CommitAndPushBranch(ctx context.Context, repoURL, branch, m
129135
// TODO: provide credentials.
130136
_, err = w.Commit(message, &git.CommitOptions{
131137
Author: &object.Signature{
132-
Name: "Kevin McDermott",
133-
138+
Name: l.credentials.name,
139+
Email: l.credentials.email,
134140
When: time.Now(),
135141
},
136142
})

pkg/promote/promote.go

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,21 @@ import (
88

99
"github.com/jenkins-x/go-scm/scm"
1010
"github.com/jenkins-x/go-scm/scm/driver/github"
11-
"github.com/mitchellh/go-homedir"
1211
"golang.org/x/oauth2"
1312
"gopkg.in/yaml.v2"
1413

1514
"github.com/bigkevmcd/promote/pkg/cache"
1615
"github.com/bigkevmcd/promote/pkg/util"
1716
)
1817

18+
// PromoteService is the main driver for promoting files between two
19+
// repositories.
20+
//
21+
// It uses a GitCache to checkout the code to, and will copy the environment
22+
// configuration for the `fromEnv` to the `toEnv` in a named branch.
23+
//
24+
// The Git repositories are looked up in the mapping, and a pull request is
25+
// opened in the toEnv's project,
1926
func PromoteService(cache cache.GitCache, token, service, fromEnv, toEnv, newBranchName string, mapping map[string]string) error {
2027
ctx := context.Background()
2128
fromURL, ok := mapping[fromEnv]
@@ -47,20 +54,9 @@ func PromoteService(cache cache.GitCache, token, service, fromEnv, toEnv, newBra
4754
return fmt.Errorf("failed to commit and push branch for environment %v: %s", toEnv, err)
4855
}
4956

50-
prInput, err := makePullRequestInput(fromEnv, fromURL, toEnv, toURL, newBranchName)
57+
pr, err := createPullRequest(ctx, fromEnv, fromURL, toEnv, toURL, token, newBranchName)
5158
if err != nil {
52-
// TODO: improve this
53-
return err
54-
}
55-
user, repo, err := util.ExtractUserAndRepo(toURL)
56-
if err != nil {
57-
// TODO: improve this
58-
return err
59-
}
60-
61-
pr, _, err := createClient(token).PullRequests.Create(ctx, fmt.Sprintf("%s/%s", user, repo), prInput)
62-
if err != nil {
63-
// TODO: improve this
59+
// TODO: improve this error message
6460
return err
6561
}
6662
log.Printf("created PR %d", pr.Number)
@@ -70,12 +66,7 @@ func PromoteService(cache cache.GitCache, token, service, fromEnv, toEnv, newBra
7066
// LoadMappingFromFile takes a filename with a YAML mapping of environment name
7167
// to git repository URLs.
7268
func LoadMappingFromFile(fname string) (map[string]string, error) {
73-
expanded, err := homedir.Expand(fname)
74-
if err != nil {
75-
return nil, fmt.Errorf("failed to expand mapping filename: %w", err)
76-
}
77-
78-
f, err := os.Open(expanded)
69+
f, err := os.Open(fname)
7970
if err != nil {
8071
return nil, err
8172
}
@@ -99,7 +90,10 @@ func createClient(token string) *scm.Client {
9990
return client
10091
}
10192

102-
func makePullRequestInput(fromEnv, fromURL, toEnv, toURL, branchName string) (*scm.PullRequestInput, error) {
93+
// TODO: OptionFuncs for Base and Title?
94+
// TODO: For the Head, should this try and determine whether or not this is a
95+
// fork ("user" of both repoURLs) and if so, simplify the Head?
96+
func makePullRequestInput(fromEnv, fromURL, toEnv, branchName string) (*scm.PullRequestInput, error) {
10397
title := fmt.Sprintf("promotion from %s to %s", fromEnv, toEnv)
10498
fromUser, _, err := util.ExtractUserAndRepo(fromURL)
10599
if err != nil {
@@ -112,3 +106,21 @@ func makePullRequestInput(fromEnv, fromURL, toEnv, toURL, branchName string) (*s
112106
Body: "this is a test body",
113107
}, nil
114108
}
109+
110+
func createPullRequest(ctx context.Context, fromEnv, fromURL, toEnv, toURL, token, newBranchName string) (*scm.PullRequest, error) {
111+
prInput, err := makePullRequestInput(fromEnv, fromURL, toEnv, newBranchName)
112+
if err != nil {
113+
// TODO: improve this error message
114+
return nil, err
115+
}
116+
117+
user, repo, err := util.ExtractUserAndRepo(toURL)
118+
if err != nil {
119+
// TODO: improve this error message
120+
return nil, err
121+
}
122+
// TODO: come up with a better way of generating the repo URL (this
123+
// only works for GitHub)
124+
pr, _, err := createClient(token).PullRequests.Create(ctx, fmt.Sprintf("%s/%s", user, repo), prInput)
125+
return pr, err
126+
}

pkg/promote/promote_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package promote
2+
3+
import (
4+
"testing"
5+
6+
"github.com/jenkins-x/go-scm/scm"
7+
8+
"github.com/google/go-cmp/cmp"
9+
)
10+
11+
func TestLoadMappingFromFile(t *testing.T) {
12+
mapping, err := LoadMappingFromFile("testdata/env-mapping.yaml")
13+
if err != nil {
14+
t.Fatal(err)
15+
}
16+
17+
u, ok := mapping["dev"]
18+
if !ok {
19+
t.Fatal("failed to find env 'dev' in mapping")
20+
}
21+
want := "https://example.com/testing/env-dev.git"
22+
if u != want {
23+
t.Fatalf("env 'dev' url incorrect: got %v, wanted %v", u, want)
24+
}
25+
26+
if u, ok := mapping["unknown"]; ok {
27+
t.Fatalf("found mapping for 'unknown' env: %s", u)
28+
}
29+
}
30+
31+
func TestLoadMappingFromFileWithError(t *testing.T) {
32+
_, err := LoadMappingFromFile("testdata/bad-mapping.yaml")
33+
if err == nil {
34+
t.Fatal(err)
35+
}
36+
}
37+
38+
func TestMakePullRequestInput(t *testing.T) {
39+
pr, err := makePullRequestInput("dev", "https://example.com/project/dev-env.git", "staging", "my-test-branch")
40+
if err != nil {
41+
t.Fatal(err)
42+
}
43+
44+
want := &scm.PullRequestInput{
45+
Title: "promotion from dev to staging",
46+
Head: "project:my-test-branch",
47+
Base: "master",
48+
Body: "this is a test body",
49+
}
50+
51+
if diff := cmp.Diff(want, pr); diff != "" {
52+
t.Fatalf("pull request input is different from expected: %s", diff)
53+
}
54+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
envs:
2+
- test: test
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dev: https://example.com/testing/env-dev.git
2+
staging: https://example.com/testing/env-staging.git

0 commit comments

Comments
 (0)