diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..ef99618 Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md index b5897c7..fc9d2d4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,122 @@ -# kobiton-execute-test-buildkite-plugin -A Buildkite plugin to (synchronously) execute an automated test script on Kobiton service +# Kobiton Execute Test Buildkite Plugin + +A Buildkite Plugin to (synchronously) execute an automated test script on Kobiton service. + +## Example + +Add the following to your `pipeline.yml`: + +```yml +steps: + - label: "Kobiton Execute Test" + plugins: + - kobiton/kobiton-execute-test#v1.0.0: + kobi-username: 'your kobiton username' + kobi-api-key: "your kobiton api key" + executor-url: 'https://executor-demo.kobiton.com' + executor-username: 'your kobiton executor server username' + executor-password: "your kobiton executor server password" + git-repo-url: 'https://github.com/Nhattd97/azure-devops-sample-java-prod.git' + git-repo-branch: 'master' + git-repo-ssh-key: '' + app-id: 'kobiton-store:91041' + root-directory: "/" + command: 'mvn test' + device-name: 'Galaxy S10' + device-platform-version: '10' + use-custom-device: 'false' + device-platform: 'android' + wait-for-execution: 'true' + log-type: 'combined' +``` + +## Configuration + +### `kobiton-username` (Required, string) + +Kobiton Username to upload to Kobiton, for example `"kobitonadmin"`. + +### `kobi-api-key` (Required, string) + +API key to access Kobiton API, for example `"2c8n41e4-b30d-4f19-ba63-6596016c9e58"`. + +### `executor-url` (Required, string) + +Kobiton Automation Test Executor URL, please contact our Support Team to get this. + +### `executor-username` (Required, string) + +The Username for Kobiton Automation Test Executor, please contact our Support Team to get this. + +### `executor-password` (Required, string) + +The Password Kobiton Automation Test Executor, please contact our Support Team to get this. + +### `git-repo-url` (Required, string) + +Link to your Git repository. + +### `git-repo-branch` (Required, string) + +The branch of your Git repository you want to execute automation test with. + +### `git-repo-ssh-key` (Optional, string) + +This is required if your Git Repository is private. + +### `kobiton-app-id` (Optional, string) + +The App ID or App URL to use in your test script, for example `"kobiton-store:91041"`. + +### `root-directory` (Required, string) + +Input the root directory of your Git repository, for example `"\"`. + +### `command` (Required, string) + +Command lines to install dependencies and execute your automation test script. These commands will run from the root directory of your Git repository. For example `"mvn test"`. + +### `use-custom-device` (Optional, boolean) + +Check if you want to execute one or some test cases with a specific Kobiton Cloud Device. If you already set your device information in your test script, leave this field `false`. + +### `device-name` (Optional, string) + +This value will be consumed by the `KOBITON_DEVICE_NAME` environment variable in your test script. + +### `device-platform` (Optional, string) + +This value will be consumed by the `KOBITON_DEVICE_PLATFORM_NAME` environment variable in your test script. + +### `device-platform-version` (Optional, string) + +This value will be consumed by the `KOBITON_SESSION_PLATFORM_VERSION` environment variable in your test script. + +### `wait-for-execution` (Optional, boolean) + +Check if your want the release pipeline to wait until your automation testing is completed or failed, then print out the console log and test result. + +### `log-type` (Optional, string) + +Your desired log type to be showed. Choose `"combined"` to show logs in chronological order, or Separated for single type of log (`"ouput"` or `"error"`). + +## Developing + +To run the tests: + +```shell +docker-compose run --rm tests +``` + +To validate the `plugin.yml`: +```shell +docker-compose run --rm lint +``` + +## Contributing + +1. Fork the repo +2. Make the changes +3. Run the tests +4. Commit and push your changes +5. Send a pull request diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..c740594 --- /dev/null +++ b/app/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "encoding/json" + "log" + "os" + "strings" + "time" + + "github.com/Nhattd97/kobiton-execute-test-buildkite-plugin/app/model" + "github.com/Nhattd97/kobiton-execute-test-buildkite-plugin/app/utils" +) + +const MAX_MS_WAIT_FOR_EXECUTION = 1 * 3600 * 1000 // 1 hour in miliseconds + +var jobId = "" +var reportUrl = "" + +func main() { + + stepConfig := new(model.StepConfig) + stepConfig.Init() + + var executorBasicAuth = strings.Join([]string{stepConfig.GetExecutorUsername(), stepConfig.GetExecutorPassword()}, ":") + var executorBasicAuthEncoded = utils.Base64Encode(executorBasicAuth) + + var headers = map[string]string{} + headers["x-kobiton-credential-username"] = stepConfig.GetKobiUsername() + headers["x-kobiton-credential-api-key"] = stepConfig.GetKobiPassword() + headers["authorization"] = "Basic " + executorBasicAuthEncoded + headers["content-type"] = "application/json" + headers["accept"] = "application/json" + + executorPayload := new(model.ExecutorRequestPayload) + model.BuildExecutorRequestPayload(executorPayload, stepConfig) + executorJsonPayload, _ := json.MarshalIndent(executorPayload, "", " ") + client := utils.HttpClient() + + var executorUrl = stepConfig.GetExecutorUrl() + "/submit" + + var response = utils.SendRequest(client, "POST", executorUrl, headers, executorJsonPayload) + + jobId = string(response) + + if stepConfig.IsWaitForExecution() { + + log.Printf("Requesting to get logs for job %s", jobId) + + var getJobInfoUrl = stepConfig.GetExecutorUrl() + "/jobs/" + jobId + var getJobLogUrl = getJobInfoUrl + "/logs?type=" + stepConfig.GetLogType() + var getReportUrl = getJobInfoUrl + "/report" + var isTimeout = false + + ticker := time.NewTicker(30 * time.Second) + var authHeader = map[string]string{"authorization": "Basic " + executorBasicAuthEncoded} + var jobResponse model.JobResponse + var waitingBeginAt = time.Now().UnixMilli() + + for range ticker.C { + var response = utils.SendRequest(client, "GET", getJobInfoUrl, authHeader, nil) + json.Unmarshal(response, &jobResponse) + log.Println("Job Status: ", jobResponse.Status) + + if jobResponse.Status == "COMPLETED" || jobResponse.Status == "FAILED" { + log.Printf("Job ID %s is finish with status: %s", jobId, jobResponse.Status) + break + } else { + var currentTime = time.Now().UnixMilli() + + if currentTime-waitingBeginAt >= MAX_MS_WAIT_FOR_EXECUTION { + isTimeout = true + break + } + } + } + defer ticker.Stop() + + if isTimeout { + log.Println("==============================================================================") + log.Println("Execution has reached maximum waiting time") + } else { + var logResponse = utils.SendRequest(client, "GET", getJobLogUrl, authHeader, nil) + + log.Println("==============================================================================") + log.Println(string(logResponse)) + + var reportResponse = utils.SendRequest(client, "GET", getReportUrl, authHeader, nil) + reportUrl = string(reportResponse) + } + } + + log.Println("==============================================================================") + + if jobId != "" { + log.Println("Job ID: ", jobId) + } + + if reportUrl != "" { + log.Println("Report URL: ", reportUrl) + } + + // + // --- Exit codes: + // The exit code of your Step is very important. If you return + // with a 0 exit code `bitrise` will register your Step as "successful". + // Any non zero exit code will be registered as "failed" by `bitrise`. + os.Exit(0) +} diff --git a/app/model/executorrequestpayload.go b/app/model/executorrequestpayload.go new file mode 100644 index 0000000..44ecba3 --- /dev/null +++ b/app/model/executorrequestpayload.go @@ -0,0 +1,53 @@ +package model + +import "strings" + +type DesiredCaps struct { + DeviceName string `json:"deviceName,omitempty"` + PlatformVersion string `json:"platformVersion,omitempty"` + PlatformName string `json:"platformName,omitempty"` + AppId string `json:"appId,omitempty"` +} + +type TestConfig struct { + Git string `json:"git"` + Ssh string `json:"ssh"` + Branch string `json:"branch"` + RootDirectory string `json:"rootDirectory,omitempty` + Commands []string `json:"commands"` +} + +type BitriseConfig struct { + ReleaseId string `json:"releaseId"` +} + +type ExecutorRequestPayload struct { + DesiredCaps DesiredCaps `json:"desiredCaps,omitempty"` + TestConfig TestConfig `json:"testConfig"` + AzureConfig BitriseConfig `json:"azureConfig"` +} + +type JobResponse struct { + ID string `json:"id"` + Status string `json:"status"` +} + +func BuildExecutorRequestPayload(e *ExecutorRequestPayload, s *StepConfig) { + // TestConfig + e.TestConfig.Git = s.gitRepoUrl + e.TestConfig.Branch = s.gitRepoBranch + e.TestConfig.Ssh = s.gitSSHKey + e.TestConfig.Commands = strings.Split(s.commands, "\n") + e.TestConfig.RootDirectory = s.rootDirectory + + // DesiredCaps + if s.useCustomDevice { + e.DesiredCaps.DeviceName = s.deviceName + e.DesiredCaps.PlatformName = s.devicePlatformName + e.DesiredCaps.PlatformVersion = s.devicePlatformVersion + e.DesiredCaps.AppId = s.kobiAppId + } + + // BitriseConfig + e.AzureConfig.ReleaseId = "123" +} diff --git a/app/model/stepconfig.go b/app/model/stepconfig.go new file mode 100644 index 0000000..7271919 --- /dev/null +++ b/app/model/stepconfig.go @@ -0,0 +1,123 @@ +package model + +import ( + "os" + "strconv" +) + +type StepConfig struct { + kobiUsername string + kobiApiKey string + executorUrl string + executorUsername string + executorPassword string + gitRepoUrl string + gitRepoBranch string + gitSSHKey string + kobiAppId string + useCustomDevice bool + deviceName string + devicePlatformVersion string + devicePlatformName string + rootDirectory string + commands string + waitForExecution bool + logType string +} + +func (stepConfig *StepConfig) Init() { + + stepConfig.kobiUsername = os.Getenv("KOBI_USERNAME") + stepConfig.kobiApiKey = os.Getenv("KOBI_API_KEY") + stepConfig.executorUrl = os.Getenv("EXECUTOR_URL") + stepConfig.executorUsername = os.Getenv("EXECUTOR_USERNAME") + stepConfig.executorPassword = os.Getenv("EXECUTOR_PASSWORD") + stepConfig.gitRepoUrl = os.Getenv("GIT_REPO_URL") + stepConfig.gitRepoBranch = os.Getenv("GIT_REPO_BRANCH") + stepConfig.gitSSHKey = os.Getenv("GIT_REPO_SSH_KEY") + stepConfig.kobiAppId = os.Getenv("APP_ID") + stepConfig.useCustomDevice, _ = strconv.ParseBool(os.Getenv("USE_CUSTOM_DEVICE")) + stepConfig.deviceName = os.Getenv("DEVICE_NAME") + stepConfig.devicePlatformVersion = os.Getenv("DEVICE_PLATFORM_VERSION") + stepConfig.devicePlatformName = os.Getenv("DEVICE_PLATFORM") + stepConfig.rootDirectory = os.Getenv("ROOT_DIRECTORY") + stepConfig.commands = os.Getenv("COMMAND") + stepConfig.waitForExecution, _ = strconv.ParseBool(os.Getenv("WAIT_FOR_EXECUTION")) + + switch os.Getenv("LOG_TYPE") { + case "output": + stepConfig.logType = "out" + case "error": + stepConfig.logType = "error" + default: + stepConfig.logType = "all" + } +} + +func (stepConfig *StepConfig) GetKobiUsername() string { + return stepConfig.kobiUsername +} + +func (stepConfig *StepConfig) GetKobiPassword() string { + return stepConfig.kobiApiKey +} + +func (stepConfig *StepConfig) GetExecutorUrl() string { + return stepConfig.executorUrl +} + +func (stepConfig *StepConfig) GetExecutorUsername() string { + return stepConfig.executorUsername +} + +func (stepConfig *StepConfig) GetExecutorPassword() string { + return stepConfig.executorPassword +} + +func (stepConfig *StepConfig) GetGitRepoUrl() string { + return stepConfig.gitRepoUrl +} + +func (stepConfig *StepConfig) GetGitRepoBranch() string { + return stepConfig.gitRepoBranch +} + +func (stepConfig *StepConfig) GetGitSSHKey() string { + return stepConfig.gitSSHKey +} + +func (stepConfig *StepConfig) GetKobiAppId() string { + return stepConfig.kobiAppId +} + +func (stepConfig *StepConfig) IsUseCustomDevices() bool { + return stepConfig.useCustomDevice +} + +func (stepConfig *StepConfig) GetDeviceName() string { + return stepConfig.deviceName +} + +func (stepConfig *StepConfig) GetDevicePlatformVersion() string { + return stepConfig.devicePlatformVersion +} + +func (stepConfig *StepConfig) GetDevicePlatformname() string { + return stepConfig.devicePlatformName +} + +func (stepConfig *StepConfig) GetRootDirectory() string { + return stepConfig.rootDirectory +} + +func (stepConfig *StepConfig) GetCommands() string { + return stepConfig.commands +} + +func (stepConfig *StepConfig) IsWaitForExecution() bool { + return stepConfig.waitForExecution +} + +func (stepConfig *StepConfig) GetLogType() string { + return stepConfig.logType +} diff --git a/app/utils/utils.go b/app/utils/utils.go new file mode 100644 index 0000000..dd3072d --- /dev/null +++ b/app/utils/utils.go @@ -0,0 +1,73 @@ +package utils + +import ( + "bytes" + "encoding/base64" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "time" +) + +func HttpClient() *http.Client { + client := &http.Client{Timeout: 10 * time.Second} + return client +} + +func SendRequest(client *http.Client, method string, url string, headers map[string]string, payload []byte) []byte { + log.Printf("[%s] Sending request to %s", method, url) + + req, err := http.NewRequest(method, url, bytes.NewBuffer(payload)) + + if err != nil { + log.Fatalf("Error Occurred. %+v", err) + } + + for k, v := range headers { + req.Header.Set(k, v) + } + + response, err := client.Do(req) + if err != nil { + log.Fatalf("Error sending request to API endpoint. %+v", err) + } + + // Close the connection to reuse it + defer response.Body.Close() + + body, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Fatalf("Couldn't parse response body. %+v", err) + } + + if response.StatusCode >= 300 { + log.Fatalf("Server returns status code %d and message: \n %s", response.StatusCode, string(body)) + } + + return body +} + +func ExposeEnv(key string, value string) { + cmdLog, err := exec.Command("bitrise", "envman", "add", "--key", key, "--value", value).CombinedOutput() + if err != nil { + log.Printf("Failed to expose output with envman, error: %#v | output: %s", err, cmdLog) + os.Exit(1) + } +} + +func Base64Encode(data string) string { + encodedStr := base64.StdEncoding.EncodeToString([]byte(data)) + + return string(encodedStr) +} + +func Base64Decode(encodedData string) string { + decodedData, err := base64.StdEncoding.DecodeString(encodedData) + if err != nil { + log.Printf("Error decoding Base64 encoded data %v", err) + } + + return string(decodedData) +} diff --git a/build/app_darwin b/build/app_darwin new file mode 100755 index 0000000..e13ecd5 Binary files /dev/null and b/build/app_darwin differ diff --git a/build/app_linux b/build/app_linux new file mode 100755 index 0000000..151884a Binary files /dev/null and b/build/app_linux differ diff --git a/build/app_windows b/build/app_windows new file mode 100755 index 0000000..ca7ef6c Binary files /dev/null and b/build/app_windows differ diff --git a/docker-conpose.yml b/docker-conpose.yml new file mode 100644 index 0000000..568362d --- /dev/null +++ b/docker-conpose.yml @@ -0,0 +1,10 @@ +services: + lint: + image: buildkite/plugin-linter + command: ['--id', 'kobiton/kobiton-execute-test'] + volumes: + - ".:/plugin:ro" + tests: + image: buildkite/plugin-tester + volumes: + - ".:/plugin:ro" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bc91936 --- /dev/null +++ b/go.mod @@ -0,0 +1,2 @@ +module github.com/Nhattd97/kobiton-execute-test-buildkite-plugin +go 1.17 diff --git a/hooks/command b/hooks/command new file mode 100644 index 0000000..892ca5d --- /dev/null +++ b/hooks/command @@ -0,0 +1,61 @@ +#!/bin/bash +set -euo pipefail + +printf "\n=====================================================================================\n" +printf "KOBITON EXECUTE TEST PLUGIN" +printf "\n=====================================================================================\n\n" + +export KOBI_USERNAME="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_KOBI_USERNAME:-""}" +export KOBI_API_KEY="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_KOBI_API_KEY:-""}" +export EXECUTOR_URL="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_EXECUTOR_URL:-""}" +export EXECUTOR_USERNAME="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_EXECUTOR_USERNAME:-""}" +export EXECUTOR_PASSWORD="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_EXECUTOR_PASSWORD:-""}" +export GIT_REPO_URL="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_GIT_REPO_URL:-""}" +export GIT_REPO_BRANCH="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_GIT_REPO_BRANCH:-""}" +export GIT_REPO_SSH_KEY="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_GIT_REPO_SSH_KEY:-""}" +export APP_ID="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_APP_ID:-""}" +export USE_CUSTOM_DEVICE="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_USE_CUSTOM_DEVICE:-"false"}" +export DEVICE_NAME="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_DEVICE_NAME:-""}" +export DEVICE_PLATFORM_VERSION="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_DEVICE_PLATFORM_VERSION:-""}" +export DEVICE_PLATFORM="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_DEVICE_PLATFORM:-""}" +export ROOT_DIRECTORY="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_ROOT_DIRECTORY:-"/"}" +export COMMAND="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_COMMAND:-""}" +export WAIT_FOR_EXECUTION="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_WAIT_FOR_EXECUTION:-"true"}" +export LOG_TYPE="${BUILDKITE_PLUGIN_KOBITON_EXECUTE_TEST_LOG_TYPE:-"combined"}" + + +case "$OSTYPE" in + linux*) + export AGENT_OS_NAME=linux + ;; + darwin*) + export AGENT_OS_NAME=darwin + ;; + msys*) + export AGENT_OS_NAME=windows + ;; + cygwin*) + export AGENT_OS_NAME=windows + ;; + *) + export AGENT_OS_NAME=notset + ;; +esac + +echo "$AGENT_OS_NAME" + +plugin=${BUILDKITE_PLUGINS:-""} +version=$(echo $plugin | sed -e 's/.*kobiton-execute-test-buildkite-plugin//' -e 's/\".*//') +repo="https://github.com/kobiton/kobiton-execute-test-buildkite-plugin" +executable="app_${AGENT_OS_NAME}" + +if [ -z ${version} ]; then + url=${repo}/releases/latest/download/${executable} +else + url=${repo}/releases/download/${version:1}/${executable} +fi + +echo ${url} +curl -Lf -o ${executable} $url && chmod +x ${executable} + +./${executable} diff --git a/plugin.yml b/plugin.yml new file mode 100644 index 0000000..4296d6e --- /dev/null +++ b/plugin.yml @@ -0,0 +1,41 @@ +name: Kobiton Execute Test +description: A Buildkite Plugin to (synchronously) execute an automated test script on Kobiton service +author: https://github.com/kobiton +requirements: [] +configuration: + properties: + kobi-username: + type: string + kobi-api-key: + type: string + executor-url: + type: string + executor-username: + type: string + executor-password: + type: string + git-repo-url: + type: string + git-repo-branch: + type: string + git-repo-ssh-key: + type: string + app-id: + type: string + root-directory: + type: string + command: + type: string + device-name: + type: string + device-platform-version: + type: string + use-custom-device: + type: string + device-platform: + type: string + wait-for-execution: + type: string + log-type: + type: string + additionalProperties: false \ No newline at end of file diff --git a/scripts/build-atifacts.sh b/scripts/build-atifacts.sh new file mode 100644 index 0000000..e3a5345 --- /dev/null +++ b/scripts/build-atifacts.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +echo "Building application for multi OS" +cd ../app + +# build for linux and ubuntu +GOOS=linux GOARCH=amd64 go build -o ../build/app_linux + +# build for windows +GOOS=windows GOARCH=amd64 go build -o ../build/app_windows + +#build for MacOs +GOOS=darwin GOARCH=amd64 go build -o ../build/app_darwin + +echo "Build Atifacts at /build" +echo "Done." \ No newline at end of file