diff --git a/bitrise.yml b/bitrise.yml index c801c82..ab6eb5e 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -11,6 +11,7 @@ app: # Define these in your .bitrise.secrets.yml - GMCLOUD_SAAS_EMAIL: $GMCLOUD_SAAS_EMAIL - GMCLOUD_SAAS_PASSWORD: $GMCLOUD_SAAS_PASSWORD + - GMCLOUD_SAAS_API_TOKEN: $GMCLOUD_SAAS_API_TOKEN workflows: ci: @@ -20,9 +21,74 @@ workflows: - errcheck: - go-test: - test: - before_run: - - audit-this-step + + test-credentials: + steps: + - change-workdir: + title: Switch working dir to test / _tmp dir + description: |- + To prevent step testing issues, like referencing relative + files with just './some-file' in the step's code, which would + work for testing the step from this directory directly + but would break if the step is included in another `bitrise.yml`. + run_if: true + inputs: + - path: ./_tmp + - is_create_path: true + - path::./: + title: Genymotion Cloud SaaS Start + run_if: "true" + inputs: + - email: $GMCLOUD_SAAS_EMAIL + - password: $GMCLOUD_SAAS_PASSWORD + - recipe_uuid: + - script: + inputs: + - content: | + #!/bin/bash + echo "The value of 'GMCLOUD_SAAS_INSTANCE_UUID' is: $GMCLOUD_SAAS_INSTANCE_UUID + echo "The value of 'GMCLOUD_SAAS_INSTANCE_ADB_SERIAL_PORT' is: $GMCLOUD_SAAS_INSTANCE_ADB_SERIAL_PORT + - git::https://github.com/Genymobile/bitrise-step-genymotion-cloud-saas-stop.git: + title: "Genymotion Cloud SaaS Stop" + description: |- + Stop Genymotion Cloud SaaS Android Devices. + inputs: + - instance_uuid: $GMCLOUD_SAAS_INSTANCE_UUID + + test-api-token: + steps: + - change-workdir: + title: Switch working dir to test / _tmp dir + description: |- + To prevent step testing issues, like referencing relative + files with just './some-file' in the step's code, which would + work for testing the step from this directory directly + but would break if the step is included in another `bitrise.yml`. + run_if: true + inputs: + - path: ./_tmp + - is_create_path: true + - path::./: + title: Genymotion Cloud SaaS Start + run_if: "true" + inputs: + - api_token: $GMCLOUD_SAAS_API_TOKEN + - recipe_uuid: + - adb_serial_port: + - script: + inputs: + - content: | + #!/bin/bash + echo "The value of 'GMCLOUD_SAAS_INSTANCE_UUID' is: $GMCLOUD_SAAS_INSTANCE_UUID + echo "The value of 'GMCLOUD_SAAS_INSTANCE_ADB_SERIAL_PORT' is: $GMCLOUD_SAAS_INSTANCE_ADB_SERIAL_PORT + - git::https://github.com/Genymobile/bitrise-step-genymotion-cloud-saas-stop.git: + title: "Genymotion Cloud SaaS Stop" + description: |- + Stop Genymotion Cloud SaaS Android Devices. + inputs: + - instance_uuid: $GMCLOUD_SAAS_INSTANCE_UUID + + test-adb-serial-port: steps: - change-workdir: title: Switch working dir to test / _tmp dir diff --git a/main.go b/main.go index 9a6e021..a1323ee 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,7 @@ package main import ( - "bufio" + "encoding/json" "fmt" "os" "os/exec" @@ -25,12 +25,24 @@ var isError bool = false // Config ... type Config struct { - GMCloudSaaSEmail string `env:"email,required"` - GMCloudSaaSPassword stepconf.Secret `env:"password,required"` + GMCloudSaaSEmail string `env:"email"` + GMCloudSaaSPassword stepconf.Secret `env:"password"` + GMCloudSaaSAPIToken stepconf.Secret `env:"api_token"` GMCloudSaaSRecipeUUID string `env:"recipe_uuid,required"` GMCloudSaaSAdbSerialPort string `env:"adb_serial_port"` - GMCloudSaaSGmsaasVersion string `env:"gmsaas_version"` + GMCloudSaaSGmsaasVersion string `env:"gmsaas_version"` +} + +type Instance struct { + UUID string `json:"uuid"` + ADB_SERIAL string `json:"adb_serial"` + NAME string `json:"name"` +} + +type Output struct { + Instance Instance `json:"instance"` + Instances []Instance `json:"instances"` } // install gmsaas if not installed. @@ -41,15 +53,18 @@ func ensureGMSAASisInstalled(version string) error { var installCmd *exec.Cmd if version != "" { - installCmd = exec.Command("pip3", "install", "gmsaas=="+version) + installCmd = exec.Command("pip3", "install", "gmsaas=="+version, "--break-system-packages") } else { - installCmd = exec.Command("pip3", "install", "gmsaas") + installCmd = exec.Command("pip3", "install", "gmsaas", "--break-system-packages") } if out, err := installCmd.CombinedOutput(); err != nil { return fmt.Errorf("%s failed, error: %s | output: %s", installCmd.Args, err, out) } + // Execute asdf reshim to update PATH + exec.Command("asdf", "reshim", "python").CombinedOutput() + if version != "" { log.Infof("gmsaas %s has been installed.", version) } else { @@ -82,42 +97,32 @@ func setOperationFailed(format string, args ...interface{}) { isError = true } -func getInstanceDetails(name string) (string, string) { - for index, line := range getInstancesList() { - if index >= 2 { - s := strings.Fields(line) - if strings.Compare(s[1], name) == 0 { - uuid := s[0] - serial := s[2] - return uuid, serial - } - } +func getADBSerialFromJSON(jsonData string) string { + var output Output + if err := json.Unmarshal([]byte(jsonData), &output); err != nil { + setOperationFailed("Issue with JSON parsing : %w", err) } - return "", "" + return output.Instance.ADB_SERIAL } -func getInstancesList() []string { - result := []string{} - - adminList := exec.Command("gmsaas", "instances", "list") - out, err := adminList.StdoutPipe() +func getInstanceDetails(name string) (string, string) { + cmd := command.New("gmsaas", "--format", "json", "instances", "list") + out, err := cmd.RunAndReturnTrimmedCombinedOutput() if err != nil { - setOperationFailed("Issue with gmsaas command line: %s", err) - return result - } - if err := adminList.Start(); err != nil { - setOperationFailed("Issue with gmsaas command line: %s", err) - return result - } - // Create new Scanner. - scanner := bufio.NewScanner(out) - // Use Scan. - for scanner.Scan() { - line := scanner.Text() - // Append line to result. - result = append(result, line) - } - return result + setOperationFailed("Failed to get instances list, error: error: %s | output: %s", cmd.PrintableCommandArgs(), err, out) + return "", "" + } + var output Output + if err := json.Unmarshal([]byte(out), &output); err != nil { + setOperationFailed("Issue with JSON parsing : %w", err) + } + + for _, instance := range output.Instances { + if instance.NAME == name { + return instance.UUID, instance.ADB_SERIAL + } + } + return "", "" } func configureAndroidSDKPath() { @@ -138,44 +143,66 @@ func configureAndroidSDKPath() { } } -func login(username, password string) { +func login(api_token, username, password string) { log.Infof("Login Genymotion Account") - cmd := command.New("gmsaas", "auth", "login", username, password) - out, err := cmd.RunAndReturnTrimmedCombinedOutput() - if err != nil { - abortf("Failed to log with gmsaas, error: error: %s | output: %s", cmd.PrintableCommandArgs(), err, out) + + var cmd *exec.Cmd + if api_token != "" { + cmd = exec.Command("gmsaas", "auth", "token", api_token) + } else if username != "" && password != "" { + cmd = exec.Command("gmsaas", "auth", "login", username, password) + } else { + abortf("Invalid arguments. Must provide either a token or both email and password.") + return } + + if out, err := cmd.CombinedOutput(); err != nil { + abortf("Failed to login with gmsaas, error: error: %s | output: %s", cmd.Args, err, out) + return + } + log.Infof("Logged to Genymotion Cloud SaaS platform") } func startInstanceAndConnect(wg *sync.WaitGroup, recipeUUID, instanceName, adbSerialPort string) { + var output Output defer wg.Done() - - cmd := command.New("gmsaas", "instances", "start", recipeUUID, instanceName) - instanceUUID, err := cmd.RunAndReturnTrimmedCombinedOutput() + cmd := command.New("gmsaas", "--format", "json", "instances", "start", recipeUUID, instanceName) + jsonData, err := cmd.RunAndReturnTrimmedCombinedOutput() if err != nil { - setOperationFailed("Failed to start a device, error: error: %s | output: %s", cmd.PrintableCommandArgs(), err, instanceUUID) + setOperationFailed("Failed to start a device, error: %s | output: %s\n", err, jsonData) return } - log.Infof("Device started %s", instanceUUID) + if err := json.Unmarshal([]byte(jsonData), &output); err != nil { + setOperationFailed("Issue with JSON parsing : %s", err) + } // Connect to adb with adb-serial-port if adbSerialPort != "" { - cmd := command.New("gmsaas", "instances", "adbconnect", instanceUUID, "--adb-serial-port", adbSerialPort) - out, err := cmd.RunAndReturnTrimmedCombinedOutput() + cmd := command.New("gmsaas", "--format", "json", "instances", "adbconnect", output.Instance.UUID, "--adb-serial-port", adbSerialPort) + ADBjsonData, err := cmd.RunAndReturnTrimmedCombinedOutput() if err != nil { - setOperationFailed("Failed to connect a device, error: error: %s | output: %s", cmd.PrintableCommandArgs(), err, out) + setOperationFailed("Failed to connect a device, error: error: %s | output: %s", cmd.PrintableCommandArgs(), err, ADBjsonData) return } + if err := json.Unmarshal([]byte(ADBjsonData), &output); err != nil { + setOperationFailed("Issue with JSON parsing : %s", err) + } } else { - cmd := command.New("gmsaas", "instances", "adbconnect", instanceUUID) - out, err := cmd.RunAndReturnTrimmedCombinedOutput() + cmd := command.New("gmsaas", "--format", "json", "instances", "adbconnect", output.Instance.UUID) + ADBjsonData, err := cmd.RunAndReturnTrimmedCombinedOutput() if err != nil { - setOperationFailed("Failed to connect a device, error: error: %s | output: %s", cmd.PrintableCommandArgs(), err, out) + setOperationFailed("Failed to connect a device, error: error: %s | output: %s", cmd.PrintableCommandArgs(), err, ADBjsonData) return } + if err := json.Unmarshal([]byte(ADBjsonData), &output); err != nil { + setOperationFailed("Issue with JSON parsing : %s", err) + } } + + log.Infof("Genymotion instance UUID : %s has been started and connected with ADB Serial Port : %s", output.Instance.UUID, output.Instance.ADB_SERIAL) + } func main() { @@ -195,7 +222,11 @@ func main() { printError("Failed to export %s, error: %v", "GMSAAS_USER_AGENT_EXTRA_DATA", err) } - login(c.GMCloudSaaSEmail, string(c.GMCloudSaaSPassword)) + if c.GMCloudSaaSAPIToken != "" { + login(string(c.GMCloudSaaSAPIToken), "", "") + } else { + login("", c.GMCloudSaaSEmail, string(c.GMCloudSaaSPassword)) + } instancesList := []string{} adbSerialList := []string{} diff --git a/step.yml b/step.yml index a360ca3..b2b7111 100644 --- a/step.yml +++ b/step.yml @@ -73,21 +73,26 @@ toolkit: inputs: + - api_token: "" + opts: + title: Genymotion Cloud SaaS API Token + summary: "" + description: |- + API Token to authenticate to your Genymotion Cloud SaaS account. If you don't have an account please register on [https://cloud.geny.io](https://cloud.geny.io/?&utm_source=web-referral&utm_medium=docs&utm_campaign=bitrise&utm_content=signup) and create an [API Token](https://cloud.geny.io/api) + - email: "" opts: title: Genymotion Cloud SaaS email summary: "" description: |- - Email of your Genymotion Cloud SaaS account, if you don't have an account please create it first on [https://cloud.geny.io](https://cloud.geny.io/?&utm_source=web-referral&utm_medium=docs&utm_campaign=bitrise&utm_content=signup) - is_required: true + DEPRECATED : Email of your Genymotion Cloud SaaS account, if you don't have an account please create it first on [https://cloud.geny.io](https://cloud.geny.io/?&utm_source=web-referral&utm_medium=docs&utm_campaign=bitrise&utm_content=signup) - password: "" opts: title: Genymotion Cloud SaaS password summary: "" description: |- - Password of your Genymotion Cloud SaaS account. - is_required: true + DEPRECATED: Password of your Genymotion Cloud SaaS account. is_sensitive: true - recipe_uuid: "" @@ -125,12 +130,12 @@ inputs: For example: `4321,4322,4323` - - gmsaas_version: "1.9.0" + - gmsaas_version: "1.11.0" opts: title: gmsaas version summary: "" description: |- - Install a specific version of gmsaas, per default it will install the latest gmsaas compatible : 1.9.0 + Install a specific version of gmsaas, per default it will install the latest compatible gmsaas version : 1.11.0