Skip to content

Commit 138702f

Browse files
Justin Kulikauskasmnuttall
authored andcommitted
Add sub-commands for specific promotion paths
These new sub-commands simplify the interface when promoting between branches in a repository, between environments in a repository, or between two "simple" repositories. The previous behavior is preserved if no sub-command is given: the user can specify any combination of repository, branch and environment folder (even a local repo). For #92
1 parent 5ff623d commit 138702f

File tree

6 files changed

+342
-151
lines changed

6 files changed

+342
-151
lines changed

README.md

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -68,26 +68,34 @@ promote from one environment to another
6868

6969
Usage:
7070
services promote [flags]
71+
services promote [command]
72+
73+
Available Commands:
74+
branch promote between branches within one repository
75+
env promote between environment folders within one repository
76+
repo promote between repositories
7177

7278
Flags:
73-
--branch-name string the name of the branch on the destination repository for the pull request (auto-generated if empty)
79+
--branch-name string the branch on the destination repository for the pull request (auto-generated if empty)
7480
--cache-dir string where to cache Git checkouts (default "~/.promotion/cache")
81+
--from string the source Git repository (URL or local)
82+
--from-branch string the branch on the source Git repository (default "master")
83+
--from-env-folder string env folder on the source Git repository (if not provided, the repository should only have one folder under environments/)
84+
-h, --help help for promote
85+
--keep-cache whether to retain the locally cloned repositories in the cache directory
86+
--service string the name of the service to promote
87+
--to string the destination Git repository
88+
--to-branch string the branch on the destination Git repository (default "master")
89+
--to-env-folder string env folder on the destination Git repository (if not provided, the repository should only have one folder under environments/)
90+
91+
Global Flags:
7592
--commit-email string the email to use for commits when creating branches
76-
--commit-message string the msg to use on the resultant commit and pull request
93+
--commit-message string the message to use on the resultant commit and pull request
7794
--commit-name string the name to use for commits when creating branches
7895
--debug additional debug logging output
79-
--from string source Git repository
80-
--from-branch string branch on the source Git repository (default "master")
81-
-h, --help help for promote
96+
--github-token string oauth access token to authenticate the request
8297
--insecure-skip-verify Insecure skip verify TLS certificate
83-
--keep-cache whether to retain the locally cloned repositories in the cache directory
8498
--repository-type string the type of repository: github, gitlab or ghe (default "github")
85-
--service string service name to promote
86-
--to string destination Git repository
87-
--to-branch string branch on the destination Git repository (default "master")
88-
89-
Global Flags:
90-
--github-token string oauth access token to authenticate the request
9199
```
92100
93101
This will _copy_ all files under `/services/service-a/base/config/*` in `first-environment` to `second-environment`, commit and push, and open a PR for the change. Any of these arguments may be provided as environment variables, using all upper case and replacing `-` with `_`. Hence you can set CACHE_DIR, COMMIT_EMAIL, etc.
@@ -99,15 +107,27 @@ This will _copy_ all files under `/services/service-a/base/config/*` in `first-e
99107
- `--commit-name` : The other half of `commit-email`. Both must be set.
100108
- `--debug` : prints extra debug output if true.
101109
- `--from` : an https URL to a GitOps repository for 'remote' cases, or a path to a Git clone of a microservice for 'local' cases.
110+
- `--from-env` : use this to specify an environment folder in the source repository, for when you have more than one environment per repository. If this is not provided when the repository has more than one folder under `environments/`, then the operation will fail.
102111
- `--from-branch` : use this to specify a branch on the source repository, instead of using the "master" branch.
103112
- `--help`: prints the above text if true.
104113
- `--insecure-skip-verify` : skip TLS cerificate verification if true. Do not set this to true unless you know what you are doing.
105114
- `--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.
106115
- `--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`.
107116
- `--service` : the destination path for promotion is `/environments/<env-name>/services/<service-name>/base/config/`. This argument defines `service-name` in that path.
108117
- `--to`: an https URL to the destination GitOps repository.
118+
- `--to-env` : use this to specify an environment folder in the destination repository, for when you have more than one environment per repository. If this is not provided when the repository has more than one folder under `environments/`, then the operation will fail.
109119
- `--to-branch` : use this to specify a branch on the destination repository, instead of using the "master" branch.
110120
121+
### Promote Sub-commands
122+
The main promote commands provides a lot of flexibility with all of its options, but the subcommands provide a simpler interface for the usual promotion paths. For example, when promoting between environment folders in the same repository and branch, you could use either of these commands:
123+
``` bash
124+
services promote --from "https://github.com/example/my-gitops.git" --from-env-folder "dev" --to "https://github.com/example/my-gitops.git" --to-env-folder "prod" --service "example"
125+
126+
# or
127+
128+
services promote env --from "dev" --to "prod" --repo "https://github.com/example/my-gitops.git" --service "example"
129+
```
130+
111131
### Troubleshooting
112132
113133
- Authentication and authorisation failures: ensure that GITHUB_TOKEN is set and has the necessary permissions.

pkg/cmd/promote.go

Lines changed: 75 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package cmd
33
import (
44
"errors"
55
"fmt"
6-
"log"
76

87
"github.com/mitchellh/go-homedir"
98
"github.com/rhd-gitops-example/services/pkg/git"
@@ -13,125 +12,61 @@ import (
1312
"github.com/tcnksm/go-gitconfig"
1413
)
1514

16-
func makePromoteCmd() *cobra.Command {
17-
cmd := &cobra.Command{
18-
Use: "promote",
19-
Short: "promote from one environment to another",
20-
RunE: promoteAction,
21-
}
15+
var promoteCmd = &cobra.Command{
16+
Use: "promote",
17+
Short: "promote from one environment to another",
18+
RunE: promoteAction,
19+
}
2220

23-
// Required flags
24-
cmd.Flags().String(
25-
fromFlag,
26-
"",
27-
"source Git repository",
28-
)
29-
logIfError(cmd.MarkFlagRequired(fromFlag))
30-
logIfError(viper.BindPFlag(fromFlag, cmd.Flags().Lookup(fromFlag)))
21+
const (
22+
branchNameFlag = "branch-name"
23+
fromFlag = "from"
24+
fromBranchFlag = "from-branch"
25+
fromEnvFolderFlag = "from-env-folder"
26+
serviceFlag = "service"
27+
toFlag = "to"
28+
toBranchFlag = "to-branch"
29+
toEnvFolderFlag = "to-env-folder"
30+
31+
repoFlag = "repo" // used by subcommands
32+
)
3133

32-
cmd.Flags().String(
33-
toFlag,
34-
"",
35-
"destination Git repository",
36-
)
37-
logIfError(cmd.MarkFlagRequired(toFlag))
38-
logIfError(viper.BindPFlag(toFlag, cmd.Flags().Lookup(toFlag)))
34+
func init() {
35+
rootCmd.AddCommand(promoteCmd)
3936

40-
cmd.Flags().String(
41-
serviceFlag,
42-
"",
43-
"service name to promote",
44-
)
45-
logIfError(cmd.MarkFlagRequired(serviceFlag))
46-
logIfError(viper.BindPFlag(serviceFlag, cmd.Flags().Lookup(serviceFlag)))
37+
promoteCmd.PersistentFlags().String(branchNameFlag, "", "the branch on the destination repository for the pull request (auto-generated if empty)")
38+
promoteCmd.PersistentFlags().String(cacheDirFlag, "~/.promotion/cache", "where to cache Git checkouts")
39+
promoteCmd.PersistentFlags().Bool(keepCacheFlag, false, "whether to retain the locally cloned repositories in the cache directory")
4740

48-
// Optional flags
49-
cmd.Flags().String(
50-
branchNameFlag,
51-
"",
52-
"the name of the branch on the destination repository for the pull request (auto-generated if empty)",
53-
)
54-
logIfError(viper.BindPFlag(branchNameFlag, cmd.Flags().Lookup(branchNameFlag)))
41+
promoteCmd.Flags().String(fromFlag, "", "the source Git repository (URL or local)")
42+
promoteCmd.Flags().String(toFlag, "", "the destination Git repository")
43+
promoteCmd.Flags().String(serviceFlag, "", "the name of the service to promote")
44+
promoteCmd.Flags().String(fromBranchFlag, "master", "the branch on the source Git repository")
45+
promoteCmd.Flags().String(fromEnvFolderFlag, "", "env folder on the source Git repository (if not provided, the repository should only have one folder under environments/)")
46+
promoteCmd.Flags().String(toBranchFlag, "master", "the branch on the destination Git repository")
47+
promoteCmd.Flags().String(toEnvFolderFlag, "", "env folder on the destination Git repository (if not provided, the repository should only have one folder under environments/)")
48+
49+
logIfError(promoteCmd.MarkFlagRequired(fromFlag))
50+
logIfError(promoteCmd.MarkFlagRequired(toFlag))
51+
logIfError(promoteCmd.MarkFlagRequired(serviceFlag))
52+
}
5553

56-
cmd.Flags().String(
54+
func promoteAction(c *cobra.Command, args []string) error {
55+
bindFlags(c.PersistentFlags(), []string{
56+
branchNameFlag,
5757
cacheDirFlag,
58-
"~/.promotion/cache",
59-
"where to cache Git checkouts",
60-
)
61-
logIfError(viper.BindPFlag(cacheDirFlag, cmd.Flags().Lookup(cacheDirFlag)))
62-
63-
cmd.Flags().String(
64-
emailFlag,
65-
"",
66-
"the email to use for commits when creating branches",
67-
)
68-
logIfError(viper.BindPFlag(emailFlag, cmd.Flags().Lookup(emailFlag)))
69-
70-
cmd.Flags().String(
71-
msgFlag,
72-
"",
73-
"the msg to use on the resultant commit and pull request",
74-
)
75-
logIfError(viper.BindPFlag(msgFlag, cmd.Flags().Lookup(msgFlag)))
76-
77-
cmd.Flags().String(
78-
nameFlag,
79-
"",
80-
"the name to use for commits when creating branches",
81-
)
82-
logIfError(viper.BindPFlag(nameFlag, cmd.Flags().Lookup(nameFlag)))
83-
84-
cmd.Flags().Bool(
85-
debugFlag,
86-
false,
87-
"additional debug logging output",
88-
)
89-
logIfError(viper.BindPFlag(debugFlag, cmd.Flags().Lookup(debugFlag)))
90-
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-
98-
cmd.Flags().Bool(
99-
insecureSkipVerifyFlag,
100-
false,
101-
"Insecure skip verify TLS certificate",
102-
)
103-
logIfError(viper.BindPFlag(insecureSkipVerifyFlag, cmd.Flags().Lookup(insecureSkipVerifyFlag)))
104-
105-
cmd.Flags().Bool(
10658
keepCacheFlag,
107-
false,
108-
"whether to retain the locally cloned repositories in the cache directory",
109-
)
110-
logIfError(viper.BindPFlag(keepCacheFlag, cmd.Flags().Lookup(keepCacheFlag)))
111-
112-
cmd.Flags().String(
113-
repoTypeFlag,
114-
"github",
115-
"the type of repository: github, gitlab or ghe",
116-
)
117-
logIfError(viper.BindPFlag(repoTypeFlag, cmd.Flags().Lookup(repoTypeFlag)))
118-
119-
cmd.Flags().String(
59+
})
60+
bindFlags(c.Flags(), []string{
61+
fromFlag,
62+
toFlag,
63+
serviceFlag,
64+
fromBranchFlag,
65+
fromEnvFolderFlag,
12066
toBranchFlag,
121-
"master",
122-
"branch on the destination Git repository",
123-
)
124-
logIfError(viper.BindPFlag(toBranchFlag, cmd.Flags().Lookup(toBranchFlag)))
125-
return cmd
126-
}
127-
128-
func logIfError(e error) {
129-
if e != nil {
130-
log.Fatal(e)
131-
}
132-
}
67+
toEnvFolderFlag,
68+
})
13369

134-
func promoteAction(c *cobra.Command, args []string) error {
13570
// Required flags
13671
fromRepo := viper.GetString(fromFlag)
13772
toRepo := viper.GetString(toFlag)
@@ -140,36 +75,51 @@ func promoteAction(c *cobra.Command, args []string) error {
14075
// Optional flags
14176
newBranchName := viper.GetString(branchNameFlag)
14277
msg := viper.GetString(msgFlag)
143-
debug := viper.GetBool(debugFlag)
14478
fromBranch := viper.GetString(fromBranchFlag)
145-
insecureSkipVerify := viper.GetBool(insecureSkipVerifyFlag)
79+
fromEnvFolder := viper.GetString(fromEnvFolderFlag)
14680
keepCache := viper.GetBool(keepCacheFlag)
147-
repoType := viper.GetString(repoTypeFlag)
14881
toBranch := viper.GetString(toBranchFlag)
149-
150-
cacheDir, err := homedir.Expand(viper.GetString(cacheDirFlag))
151-
if err != nil {
152-
return fmt.Errorf("failed to expand cacheDir path: %w", err)
153-
}
154-
155-
author, err := newAuthor()
156-
if err != nil {
157-
return fmt.Errorf("unable to establish credentials: %w", err)
158-
}
82+
toEnvFolder := viper.GetString(toEnvFolderFlag)
15983

16084
from := promotion.EnvLocation{
16185
RepoPath: fromRepo,
16286
Branch: fromBranch,
87+
Folder: fromEnvFolder,
16388
}
16489
to := promotion.EnvLocation{
16590
RepoPath: toRepo,
16691
Branch: toBranch,
92+
Folder: toEnvFolder,
93+
}
94+
95+
sm, err := newServiceManager()
96+
if err != nil {
97+
return err
16798
}
16899

169-
sm := promotion.New(cacheDir, author, promotion.WithDebug(debug), promotion.WithInsecureSkipVerify(insecureSkipVerify), promotion.WithRepoType(repoType))
170100
return sm.Promote(service, from, to, newBranchName, msg, keepCache)
171101
}
172102

103+
func newServiceManager() (*promotion.ServiceManager, error) {
104+
cacheDir, err := homedir.Expand(viper.GetString(cacheDirFlag))
105+
if err != nil {
106+
return nil, fmt.Errorf("failed to expand cacheDir path: %w", err)
107+
}
108+
109+
author, err := newAuthor()
110+
if err != nil {
111+
return nil, fmt.Errorf("unable to establish credentials: %w", err)
112+
}
113+
114+
return promotion.New(
115+
cacheDir,
116+
author,
117+
promotion.WithDebug(viper.GetBool(debugFlag)),
118+
promotion.WithInsecureSkipVerify(viper.GetBool(insecureSkipVerifyFlag)),
119+
promotion.WithRepoType(viper.GetString(repoTypeFlag)),
120+
), nil
121+
}
122+
173123
func newAuthor() (*git.Author, error) {
174124
name := viper.GetString(nameFlag)
175125
email := viper.GetString(emailFlag)

pkg/cmd/promote_branch.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/rhd-gitops-example/services/pkg/promotion"
7+
"github.com/spf13/cobra"
8+
"github.com/spf13/viper"
9+
)
10+
11+
var promoteBranchCmd = &cobra.Command{
12+
Use: "branch",
13+
Short: "promote between branches within one repository",
14+
RunE: promoteBranchAction,
15+
}
16+
17+
func init() {
18+
promoteCmd.AddCommand(promoteBranchCmd)
19+
20+
promoteBranchCmd.Flags().String(fromFlag, "", "the source branch")
21+
promoteBranchCmd.Flags().String(toFlag, "", "the destination branch")
22+
promoteBranchCmd.Flags().String(serviceFlag, "", "the name of the service to promote")
23+
promoteBranchCmd.Flags().String(repoFlag, "", "the URL of the Git repository")
24+
25+
logIfError(promoteBranchCmd.MarkFlagRequired(fromFlag))
26+
logIfError(promoteBranchCmd.MarkFlagRequired(toFlag))
27+
logIfError(promoteBranchCmd.MarkFlagRequired(serviceFlag))
28+
logIfError(promoteBranchCmd.MarkFlagRequired(repoFlag))
29+
}
30+
31+
func promoteBranchAction(c *cobra.Command, args []string) error {
32+
bindFlags(c.Flags(), []string{
33+
fromFlag,
34+
toFlag,
35+
serviceFlag,
36+
repoFlag,
37+
})
38+
39+
fromBranch := viper.GetString(fromFlag)
40+
toBranch := viper.GetString(toFlag)
41+
service := viper.GetString(serviceFlag)
42+
repo := viper.GetString(repoFlag)
43+
44+
newBranchName := viper.GetString(branchNameFlag)
45+
msg := viper.GetString(msgFlag)
46+
keepCache := viper.GetBool(keepCacheFlag)
47+
48+
from := promotion.EnvLocation{
49+
RepoPath: repo,
50+
Branch: fromBranch,
51+
Folder: "",
52+
}
53+
to := promotion.EnvLocation{
54+
RepoPath: repo,
55+
Branch: toBranch,
56+
Folder: "",
57+
}
58+
59+
sm, err := newServiceManager()
60+
if err != nil {
61+
return err
62+
}
63+
64+
if msg == "" {
65+
msg = fmt.Sprintf("Promote branch %s to %s", fromBranch, toBranch)
66+
}
67+
68+
return sm.Promote(service, from, to, newBranchName, msg, keepCache)
69+
}

0 commit comments

Comments
 (0)