Skip to content

Commit d079636

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 0a29dfb commit d079636

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
}
@@ -75,21 +76,16 @@ func envSession() *Force {
7576
}
7677

7778
func initializeSession() {
78-
var err error
79-
if account != "" {
80-
force, err = GetForce(account)
81-
} else if force = envSession(); force == nil {
82-
force, err = ActiveForce()
83-
}
84-
if err != nil {
85-
ErrorAndExit(err.Error())
86-
}
79+
manager = newForceManager(accounts)
80+
8781
if _apiVersion != "" {
8882
err := SetApiVersion(_apiVersion)
8983
if err != nil {
9084
ErrorAndExit(err.Error())
9185
}
9286
}
87+
88+
force = manager.getCurrentForce()
9389
}
9490

9591
func Execute() {
@@ -103,3 +99,59 @@ type quietLogger struct{}
10399

104100
func (l quietLogger) Info(args ...interface{}) {
105101
}
102+
103+
// provides support for commands that can be run concurrently for many accounts
104+
type forceManager struct {
105+
connections map[string]*Force
106+
currentAccount string
107+
}
108+
109+
func (manager forceManager) getCurrentForce() *Force {
110+
return manager.connections[manager.currentAccount]
111+
}
112+
113+
func (manager forceManager) getAllForce() []*Force {
114+
fs := make([]*Force, 0, len(manager.connections))
115+
116+
for _, v := range manager.connections {
117+
fs = append(fs, v)
118+
}
119+
return fs
120+
}
121+
122+
func newForceManager(accounts []string) forceManager {
123+
var err error
124+
fm := forceManager{connections: make(map[string]*Force, 1)}
125+
126+
if len(accounts) > 1 {
127+
for _, a := range accounts {
128+
var f *Force
129+
130+
f, err = GetForce(a)
131+
if err != nil {
132+
ErrorAndExit(err.Error())
133+
}
134+
135+
fm.connections[a] = f
136+
}
137+
138+
fm.currentAccount = accounts[0]
139+
} else {
140+
var f *Force
141+
142+
if len(accounts) == 1 {
143+
f, err = GetForce(accounts[0])
144+
} else if f = envSession(); f == nil {
145+
f, err = ActiveForce()
146+
}
147+
148+
if err != nil {
149+
ErrorAndExit(err.Error())
150+
}
151+
152+
fm.currentAccount = f.GetCredentials().UserInfo.UserName
153+
fm.connections[fm.currentAccount] = f
154+
}
155+
156+
return fm
157+
}

lib/metadata.go

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

176176
type ForceCheckDeploymentStatusResult struct {
177+
UserName string
177178
CheckOnly bool `xml:"checkOnly"`
178179
CompletedDate time.Time `xml:"completedDate"`
179180
CreatedDate time.Time `xml:"createdDate"`
@@ -745,7 +746,7 @@ func (results ForceCheckDeploymentStatusResult) String() string {
745746
complete = fmt.Sprintf(" (%d/%d)", results.NumberTestsCompleted, results.NumberTestsTotal)
746747
}
747748

748-
return fmt.Sprintf("Status: %s%s %s", results.Status, complete, results.StateDetail)
749+
return fmt.Sprintf("Status (%s): %s%s %s", results.UserName, results.Status, complete, results.StateDetail)
749750
}
750751

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

0 commit comments

Comments
 (0)