Skip to content

Commit 75cc0f4

Browse files
committed
Multiple accounts for deployment
Add support for several --account flags. The accounts passed must already have been logged in. `force import` uses a mutex to synchonize concurrent deployments.
1 parent 1d63a14 commit 75cc0f4

File tree

5 files changed

+155
-56
lines changed

5 files changed

+155
-56
lines changed

command/deploy.go

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import (
66
"os"
77
"os/signal"
88
"strings"
9+
"sync"
910
"syscall"
1011
"time"
1112

12-
. "github.com/ForceCLI/force/error"
1313
. "github.com/ForceCLI/force/lib"
1414
"github.com/spf13/cobra"
1515
)
@@ -38,53 +38,41 @@ func defaultDeployOutputOptions() *deployOutputOptions {
3838

3939
var testFailureError = errors.New("Apex tests failed")
4040

41-
func monitorDeploy(deployId string) (ForceCheckDeploymentStatusResult, error) {
42-
var result ForceCheckDeploymentStatusResult
43-
var err error
44-
retrying := false
45-
for {
46-
result, err = force.Metadata.CheckDeployStatus(deployId)
47-
if err != nil {
48-
if retrying {
49-
return result, fmt.Errorf("Error getting deploy status: %w", err)
50-
} else {
51-
retrying = true
52-
Log.Info(fmt.Sprintf("Received error checking deploy status: %s. Will retry once before aborting.", err.Error()))
53-
}
54-
} else {
55-
retrying = false
56-
}
57-
if result.Done {
58-
break
59-
}
60-
if !retrying {
61-
Log.Info(result)
62-
}
63-
time.Sleep(5000 * time.Millisecond)
64-
}
65-
return result, err
41+
type deployStatus struct {
42+
mu sync.Mutex
43+
aborted bool
44+
}
45+
46+
func (c *deployStatus) abort() {
47+
c.mu.Lock()
48+
c.aborted = true
49+
c.mu.Unlock()
50+
}
51+
52+
func (c *deployStatus) isAborted() bool {
53+
c.mu.Lock()
54+
defer c.mu.Unlock()
55+
return c.aborted
6656
}
6757

6858
func deploy(force *Force, files ForceMetadataFiles, deployOptions *ForceDeployOptions, outputOptions *deployOutputOptions) error {
69-
if outputOptions.quiet {
70-
previousLogger := Log
71-
var l quietLogger
72-
Log = l
73-
defer func() {
74-
Log = previousLogger
75-
}()
76-
}
59+
status := deployStatus{aborted: false}
60+
61+
return deployWith(force, &status, files, deployOptions, outputOptions)
62+
}
63+
64+
func deployWith(force *Force, status *deployStatus, files ForceMetadataFiles, deployOptions *ForceDeployOptions, outputOptions *deployOutputOptions) error {
7765
startTime := time.Now()
7866
deployId, err := force.Metadata.StartDeploy(files, *deployOptions)
7967
if err != nil {
80-
ErrorAndExit(err.Error())
68+
return err
8169
}
8270
stopDeployUponSignal(force, deployId)
8371
if outputOptions.interactive {
8472
watchDeploy(deployId)
8573
return nil
8674
}
87-
result, err := monitorDeploy(deployId)
75+
result, err := monitorDeploy(force, deployId, status)
8876
if err != nil {
8977
return err
9078
}
@@ -156,6 +144,39 @@ func deploy(force *Force, files ForceMetadataFiles, deployOptions *ForceDeployOp
156144
return nil
157145
}
158146

147+
func monitorDeploy(force *Force, deployId string, status *deployStatus) (ForceCheckDeploymentStatusResult, error) {
148+
var result ForceCheckDeploymentStatusResult
149+
var err error
150+
retrying := false
151+
for {
152+
if status.isAborted() {
153+
fmt.Fprintf(os.Stderr, "Cancelling deploy %s\n", deployId)
154+
force.Metadata.CancelDeploy(deployId)
155+
return result, nil
156+
}
157+
result, err = force.Metadata.CheckDeployStatus(deployId)
158+
if err != nil {
159+
if retrying {
160+
return result, fmt.Errorf("Error getting deploy status: %w", err)
161+
} else {
162+
retrying = true
163+
Log.Info(fmt.Sprintf("Received error checking deploy status: %s. Will retry once before aborting.", err.Error()))
164+
}
165+
} else {
166+
retrying = false
167+
}
168+
result.UserName = force.GetCredentials().UserInfo.UserName
169+
if result.Done {
170+
break
171+
}
172+
if !retrying {
173+
Log.Info(result)
174+
}
175+
time.Sleep(5000 * time.Millisecond)
176+
}
177+
return result, err
178+
}
179+
159180
func stopDeployUponSignal(force *Force, deployId string) {
160181
sigs := make(chan os.Signal, 1)
161182
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

command/import.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os/user"
99
"path/filepath"
1010
"strings"
11+
"sync"
1112

1213
. "github.com/ForceCLI/force/error"
1314
. "github.com/ForceCLI/force/lib"
@@ -92,6 +93,14 @@ func sourceDir(cmd *cobra.Command) string {
9293
}
9394

9495
func runImport(root string, options ForceDeployOptions, displayOptions *deployOutputOptions) {
96+
if displayOptions.quiet {
97+
previousLogger := Log
98+
var l quietLogger
99+
Log = l
100+
defer func() {
101+
Log = previousLogger
102+
}()
103+
}
95104
files := make(ForceMetadataFiles)
96105
if _, err := os.Stat(filepath.Join(root, "package.xml")); os.IsNotExist(err) {
97106
ErrorAndExit(" \n" + filepath.Join(root, "package.xml") + "\ndoes not exist")
@@ -113,11 +122,27 @@ func runImport(root string, options ForceDeployOptions, displayOptions *deployOu
113122
ErrorAndExit(err.Error())
114123
}
115124

116-
err = deploy(force, files, &options, displayOptions)
117-
if err == nil && displayOptions.reportFormat == "text" && !displayOptions.quiet {
118-
fmt.Printf("Imported from %s\n", root)
119-
}
120-
if err != nil && (!errors.Is(err, testFailureError) || displayOptions.errorOnTestFailure) {
121-
ErrorAndExit(err.Error())
125+
var deployments sync.WaitGroup
126+
status := deployStatus{aborted: false}
127+
128+
for _, f := range manager.getAllForce() {
129+
if status.isAborted() {
130+
break
131+
}
132+
current := f
133+
deployments.Add(1)
134+
go func() {
135+
defer deployments.Done()
136+
err := deployWith(current, &status, files, &options, displayOptions)
137+
if err == nil && displayOptions.reportFormat == "text" && !displayOptions.quiet {
138+
fmt.Printf("Imported from %s\n", root)
139+
}
140+
if err != nil && (!errors.Is(err, testFailureError) || displayOptions.errorOnTestFailure) && !status.isAborted() {
141+
fmt.Fprintf(os.Stderr, "Aborting deploy due to %s\n", err.Error())
142+
status.abort()
143+
}
144+
}()
122145
}
146+
147+
deployments.Wait()
123148
}

command/logout.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func runLogout() {
3737
SetActiveLoginDefault()
3838
}
3939
if runtime.GOOS == "windows" {
40-
cmd := exec.Command("title", account)
40+
cmd := exec.Command("title", username)
4141
cmd.Run()
4242
} else {
4343
title := fmt.Sprintf("\033];%s\007", "")

command/root.go

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import (
1414
)
1515

1616
var (
17-
account string
17+
accounts []string
1818
configName string
1919
_apiVersion string
2020

21-
force *Force
21+
manager forceManager
22+
force *Force
2223
)
2324

2425
func init() {
@@ -30,7 +31,7 @@ func init() {
3031
}
3132
}
3233
RootCmd.SetArgs(args)
33-
RootCmd.PersistentFlags().StringVarP(&account, "account", "a", "", "account `username` to use")
34+
RootCmd.PersistentFlags().StringArrayVarP(&accounts, "account", "a", []string{}, "account `username` to use")
3435
RootCmd.PersistentFlags().StringVar(&configName, "config", "", "config directory to use (default: .force)")
3536
RootCmd.PersistentFlags().StringVarP(&_apiVersion, "apiversion", "V", "", "API version to use")
3637
}
@@ -61,21 +62,16 @@ func initializeConfig() {
6162
}
6263

6364
func initializeSession() {
64-
var err error
65-
if account != "" {
66-
force, err = GetForce(account)
67-
} else {
68-
force, err = ActiveForce()
69-
}
70-
if err != nil {
71-
ErrorAndExit(err.Error())
72-
}
65+
manager = newForceManager(accounts)
66+
7367
if _apiVersion != "" {
7468
err := SetApiVersion(_apiVersion)
7569
if err != nil {
7670
ErrorAndExit(err.Error())
7771
}
7872
}
73+
74+
force = manager.getCurrentForce()
7975
}
8076

8177
func Execute() {
@@ -89,3 +85,59 @@ type quietLogger struct{}
8985

9086
func (l quietLogger) Info(args ...interface{}) {
9187
}
88+
89+
// provides support for commands that can be run concurrently for many accounts
90+
type forceManager struct {
91+
connections map[string]*Force
92+
currentAccount string
93+
}
94+
95+
func (manager forceManager) getCurrentForce() *Force {
96+
return manager.connections[manager.currentAccount]
97+
}
98+
99+
func (manager forceManager) getAllForce() []*Force {
100+
fs := make([]*Force, 0, len(manager.connections))
101+
102+
for _, v := range manager.connections {
103+
fs = append(fs, v)
104+
}
105+
return fs
106+
}
107+
108+
func newForceManager(accounts []string) forceManager {
109+
var err error
110+
fm := forceManager{connections: make(map[string]*Force, 1)}
111+
112+
if len(accounts) > 1 {
113+
for _, a := range accounts {
114+
var f *Force
115+
116+
f, err = GetForce(a)
117+
if err != nil {
118+
ErrorAndExit(err.Error())
119+
}
120+
121+
fm.connections[a] = f
122+
}
123+
124+
fm.currentAccount = accounts[0]
125+
} else {
126+
var f *Force
127+
128+
if len(accounts) == 0 {
129+
f, err = ActiveForce()
130+
} else {
131+
f, err = GetForce(accounts[0])
132+
}
133+
134+
if err != nil {
135+
ErrorAndExit(err.Error())
136+
}
137+
138+
fm.currentAccount = f.GetCredentials().UserInfo.UserName
139+
fm.connections[fm.currentAccount] = f
140+
}
141+
142+
return fm
143+
}

lib/metadata.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ type ComponentDetails struct {
172172
}
173173

174174
type ForceCheckDeploymentStatusResult struct {
175+
UserName string
175176
CheckOnly bool `xml:"checkOnly"`
176177
CompletedDate time.Time `xml:"completedDate"`
177178
CreatedDate time.Time `xml:"createdDate"`
@@ -743,7 +744,7 @@ func (results ForceCheckDeploymentStatusResult) String() string {
743744
complete = fmt.Sprintf(" (%d/%d)", results.NumberTestsCompleted, results.NumberTestsTotal)
744745
}
745746

746-
return fmt.Sprintf("Status: %s%s %s", results.Status, complete, results.StateDetail)
747+
return fmt.Sprintf("Status (%s): %s%s %s", results.UserName, results.Status, complete, results.StateDetail)
747748
}
748749

749750
func (fm *ForceMetadata) CancelDeploy(id string) (ForceCancelDeployResult, error) {

0 commit comments

Comments
 (0)