Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 61 additions & 35 deletions command/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"

Expand Down Expand Up @@ -38,53 +39,41 @@ func defaultDeployOutputOptions() *deployOutputOptions {

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

func monitorDeploy(deployId string) (ForceCheckDeploymentStatusResult, error) {
var result ForceCheckDeploymentStatusResult
var err error
retrying := false
for {
result, err = force.Metadata.CheckDeployStatus(deployId)
if err != nil {
if retrying {
return result, fmt.Errorf("Error getting deploy status: %w", err)
} else {
retrying = true
Log.Info(fmt.Sprintf("Received error checking deploy status: %s. Will retry once before aborting.", err.Error()))
}
} else {
retrying = false
}
if result.Done {
break
}
if !retrying {
Log.Info(result)
}
time.Sleep(5000 * time.Millisecond)
}
return result, err
type deployStatus struct {
mu sync.Mutex
aborted bool
}

func (c *deployStatus) abort() {
c.mu.Lock()
c.aborted = true
c.mu.Unlock()
}

func (c *deployStatus) isAborted() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.aborted
}

func deploy(force *Force, files ForceMetadataFiles, deployOptions *ForceDeployOptions, outputOptions *deployOutputOptions) error {
if outputOptions.quiet {
previousLogger := Log
var l quietLogger
Log = l
defer func() {
Log = previousLogger
}()
}
status := deployStatus{aborted: false}

return deployWith(force, &status, files, deployOptions, outputOptions)
}

func deployWith(force *Force, status *deployStatus, files ForceMetadataFiles, deployOptions *ForceDeployOptions, outputOptions *deployOutputOptions) error {
startTime := time.Now()
deployId, err := force.Metadata.StartDeploy(files, *deployOptions)
if err != nil {
ErrorAndExit(err.Error())
return err
}
stopDeployUponSignal(force, deployId)
if outputOptions.interactive {
watchDeploy(deployId)
return nil
}
result, err := monitorDeploy(deployId)
result, err := monitorDeploy(force, deployId, status)
if err != nil {
return err
}
Expand Down Expand Up @@ -156,6 +145,39 @@ func deploy(force *Force, files ForceMetadataFiles, deployOptions *ForceDeployOp
return nil
}

func monitorDeploy(force *Force, deployId string, status *deployStatus) (ForceCheckDeploymentStatusResult, error) {
var result ForceCheckDeploymentStatusResult
var err error
retrying := false
for {
if status.isAborted() {
fmt.Fprintf(os.Stderr, "Cancelling deploy %s\n", deployId)
force.Metadata.CancelDeploy(deployId)
return result, nil
}
result, err = force.Metadata.CheckDeployStatus(deployId)
if err != nil {
if retrying {
return result, fmt.Errorf("Error getting deploy status: %w", err)
} else {
retrying = true
Log.Info(fmt.Sprintf("Received error checking deploy status: %s. Will retry once before aborting.", err.Error()))
}
} else {
retrying = false
}
result.UserName = force.GetCredentials().UserInfo.UserName
if result.Done {
break
}
if !retrying {
Log.Info(result)
}
time.Sleep(5000 * time.Millisecond)
}
return result, err
}

func stopDeployUponSignal(force *Force, deployId string) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
Expand Down Expand Up @@ -191,6 +213,10 @@ func getDeploymentOutputOptions(cmd *cobra.Command) *deployOutputOptions {

if interactive, err := cmd.Flags().GetBool("interactive"); err == nil {
outputOptions.interactive = interactive

if interactive && len(manager.connections) > 1 {
ErrorAndExit("interactive flag cannot be used with multiple accounts")
}
}

if ignoreCoverageWarnings, err := cmd.Flags().GetBool("ignorecoverage"); err == nil {
Expand Down
37 changes: 31 additions & 6 deletions command/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os/user"
"path/filepath"
"strings"
"sync"

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

func runImport(root string, options ForceDeployOptions, displayOptions *deployOutputOptions) {
if displayOptions.quiet {
previousLogger := Log
var l quietLogger
Log = l
defer func() {
Log = previousLogger
}()
}
files := make(ForceMetadataFiles)
if _, err := os.Stat(filepath.Join(root, "package.xml")); os.IsNotExist(err) {
ErrorAndExit(" \n" + filepath.Join(root, "package.xml") + "\ndoes not exist")
Expand All @@ -113,11 +122,27 @@ func runImport(root string, options ForceDeployOptions, displayOptions *deployOu
ErrorAndExit(err.Error())
}

err = deploy(force, files, &options, displayOptions)
if err == nil && displayOptions.reportFormat == "text" && !displayOptions.quiet {
fmt.Printf("Imported from %s\n", root)
}
if err != nil && (!errors.Is(err, testFailureError) || displayOptions.errorOnTestFailure) {
ErrorAndExit(err.Error())
var deployments sync.WaitGroup
status := deployStatus{aborted: false}

for _, f := range manager.getAllForce() {
if status.isAborted() {
break
}
current := f
deployments.Add(1)
go func() {
defer deployments.Done()
err := deployWith(current, &status, files, &options, displayOptions)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll need to return the results so we can combine them. We'll get invalid output, especially with --reporttype junit, if we let each goroutine write it its own results.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we display all the messages from one goroutine then the next and so on so forth?

if err == nil && displayOptions.reportFormat == "text" && !displayOptions.quiet {
fmt.Printf("Imported from %s\n", root)
}
if err != nil && (!errors.Is(err, testFailureError) || displayOptions.errorOnTestFailure) && !status.isAborted() {
fmt.Fprintf(os.Stderr, "Aborting deploy due to %s\n", err.Error())
status.abort()
}
}()
}

deployments.Wait()
}
2 changes: 1 addition & 1 deletion command/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func runLogout() {
SetActiveLoginDefault()
}
if runtime.GOOS == "windows" {
cmd := exec.Command("title", account)
cmd := exec.Command("title", username)
cmd.Run()
} else {
title := fmt.Sprintf("\033];%s\007", "")
Expand Down
89 changes: 77 additions & 12 deletions command/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import (
)

var (
account string
accounts []string
configName string
_apiVersion string

force *Force
manager forceManager
force *Force
)

func init() {
Expand All @@ -30,7 +31,7 @@ func init() {
}
}
RootCmd.SetArgs(args)
RootCmd.PersistentFlags().StringVarP(&account, "account", "a", "", "account `username` to use")
RootCmd.PersistentFlags().StringArrayVarP(&accounts, "account", "a", []string{}, "account `username` to use")
RootCmd.PersistentFlags().StringVar(&configName, "config", "", "config directory to use (default: .force)")
RootCmd.PersistentFlags().StringVarP(&_apiVersion, "apiversion", "V", "", "API version to use")
}
Expand All @@ -40,6 +41,7 @@ var RootCmd = &cobra.Command{
Short: "force CLI",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
initializeConfig()
checkAccounts(cmd.Name())
switch cmd.Use {
case "force", "login":
default:
Expand All @@ -60,6 +62,14 @@ func initializeConfig() {
}
}

func checkAccounts(command string) {
//currently only import allows many accounts so a catch all error handling is simpler

if len(accounts) > 1 && command != "import" {
ErrorAndExit(fmt.Sprintf("Multiple accounts are not supported for %s yet", command))
}
}

func envSession() *Force {
token := os.Getenv("SF_ACCESS_TOKEN")
instance := os.Getenv("SF_INSTANCE_URL")
Expand All @@ -75,21 +85,16 @@ func envSession() *Force {
}

func initializeSession() {
var err error
if account != "" {
force, err = GetForce(account)
} else if force = envSession(); force == nil {
force, err = ActiveForce()
}
if err != nil {
ErrorAndExit(err.Error())
}
manager = newForceManager(accounts)

if _apiVersion != "" {
err := SetApiVersion(_apiVersion)
if err != nil {
ErrorAndExit(err.Error())
}
}

force = manager.getCurrentForce()
}

func Execute() {
Expand All @@ -103,3 +108,63 @@ type quietLogger struct{}

func (l quietLogger) Info(args ...interface{}) {
}

// provides support for commands that can be run concurrently for many accounts
type forceManager struct {
connections map[string]*Force
currentAccount string
}

func (manager forceManager) getCurrentForce() *Force {
return manager.connections[manager.currentAccount]
}

func (manager forceManager) getAllForce() []*Force {
fs := make([]*Force, 0, len(manager.connections))

for _, v := range manager.connections {
fs = append(fs, v)
}
return fs
}

func newForceManager(accounts []string) forceManager {
var err error
fm := forceManager{connections: make(map[string]*Force, 1)}

if len(accounts) > 1 {
for _, a := range accounts {
if _, exists := fm.connections[a]; exists {
ErrorAndExit("Duplicate account: " + a)
}

var f *Force

f, err = GetForce(a)
if err != nil {
ErrorAndExit(err.Error())
}

fm.connections[a] = f
}

fm.currentAccount = accounts[0]
} else {
var f *Force

if len(accounts) == 1 {
f, err = GetForce(accounts[0])
} else if f = envSession(); f == nil {
f, err = ActiveForce()
}

if err != nil {
ErrorAndExit(err.Error())
}

fm.currentAccount = f.GetCredentials().UserInfo.UserName
fm.connections[fm.currentAccount] = f
}

return fm
}
3 changes: 2 additions & 1 deletion lib/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ type ComponentDetails struct {
}

type ForceCheckDeploymentStatusResult struct {
UserName string
CheckOnly bool `xml:"checkOnly"`
CompletedDate time.Time `xml:"completedDate"`
CreatedDate time.Time `xml:"createdDate"`
Expand Down Expand Up @@ -745,7 +746,7 @@ func (results ForceCheckDeploymentStatusResult) String() string {
complete = fmt.Sprintf(" (%d/%d)", results.NumberTestsCompleted, results.NumberTestsTotal)
}

return fmt.Sprintf("Status: %s%s %s", results.Status, complete, results.StateDetail)
return fmt.Sprintf("Status (%s): %s%s %s", results.UserName, results.Status, complete, results.StateDetail)
}

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