Skip to content

Commit

Permalink
Add support to analyze and package studio projects
Browse files Browse the repository at this point in the history
Integrated uipathcli with UiPath Studio to build, package and analyze
studio projects.

Added two new plugin commands:

- `uipath studio package analyze`
- `uipath studio package pack`

Implementation:

- Created infrastructure to download external plugins like the uipcli.
  The studio commands download the uipcli to the user cache dir
  and use it for packaging any studio project. Depending on the
  targetFramework the uipathcli either downloads the tool chain for
  building and packaging cross-platform or windows Studio projects.

- Added `ExecCmd` abstraction which is used to start processes and
  can easily be faked in unit tests in order to validate the behavior with
  different exit codes

- Refactored the existing browser launcher to use the `ExecCmd`
  abstraction

- Extended the progress bar rendering to allow displaying a simple bar
  without any percentage or bytes indicator so that the build process
  can be visualized without knowing the total time in advance.

- Increment the uipathcli version to 2.0.
  There are no backwards-incompatible changes. The major version increase
  only indicates that an important new feature has been added.

Examples:

`uipath studio package analyze --source plugin/studio/projects/crossplatform`

```
analyzing...        |██████████          |
```

```
{
  "error": null,
  "status": "Succeeded",
  "violations": [
    ...
  ]
}
```

`uipath studio package pack --source plugin/studio/projects/crossplatform --destination . --debug`

```
uipcli Information: 0 : Packing project(s) at path plugin\studio\projects\crossplatform\project.json...
uipcli Information: 0 : Orchestrator information is not provided, hence, orchestrator feeds will not be used.
uipcli Information: 0 : Proceeding with the local feeds...
uipcli Information: 0 : Detected schema version 4.0
...
uipcli Information: 0 : Packaged project MyProcess v1.0.2 saved to MyProcess.1.0.2.nupkg.
```

```
{
  "description": "Blank Process",
  "error": null,
  "name": "MyProcess",
  "output": "MyProcess.1.0.2.nupkg",
  "projectId": "9011ee47-8dd4-4726-8850-299bd6ef057c",
  "status": "Succeeded",
  "version": "1.0.2"
}
```

The following snippet allows users to package a local UiPath studio
project, upload the package, create a release and run a job:

```
uipath studio package pack --source plugin/studio/projects/crossplatform --destination . --package-version 1.0.0
uipath orchestrator processes upload-package --file MyProcess.1.0.0.nupkg
$folderId = uipath orchestrator folders get --query "value[0].Id"
$releaseKey = uipath orchestrator releases post --folder-id $folderId --name "MyProcess" --process-key "MyProcess" --process-version "1.0.0" --query "Key" --output text
$jobId = uipath orchestrator jobs start-jobs --folder-id $folderId --start-info "ReleaseKey=$releaseKey" --query "value[0].Id" --output text
uipath orchestrator jobs get-by-id --folder-id $folderId --key $jobId
```
  • Loading branch information
thschmitt committed Jan 3, 2025
1 parent 1315e06 commit 3687fc9
Show file tree
Hide file tree
Showing 49 changed files with 2,341 additions and 182 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI
on: [push]

env:
UIPATHCLI_BASE_VERSION: "v1.1"
UIPATHCLI_BASE_VERSION: "v2.0"
GO_VERSION: "1.22.2"

jobs:
Expand Down
36 changes: 33 additions & 3 deletions auth/browser_launcher.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
package auth

// BrowserLauncher interface for opening browser windows.
type BrowserLauncher interface {
Open(url string) error
import (
"fmt"
"time"

"github.com/UiPath/uipathcli/utils"
)

// BrowserLauncher tries to open the default browser on the local system.
type BrowserLauncher struct {
Exec utils.ExecProcess
}

func (l BrowserLauncher) Open(url string) error {
cmd := l.openBrowser(url)
err := cmd.Start()
if err != nil {
return err
}
done := make(chan error)
go func() {
done <- cmd.Wait()
}()

select {
case err := <-done:
return err
case <-time.After(5 * time.Second):
return fmt.Errorf("Timed out waiting for browser to start")
}
}

func NewBrowserLauncher() *BrowserLauncher {
return &BrowserLauncher{utils.NewExecProcess()}
}
11 changes: 11 additions & 0 deletions auth/browser_launcher_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build darwin

package auth

import (
"github.com/UiPath/uipathcli/utils"
)

func (l BrowserLauncher) openBrowser(url string) utils.ExecCmd {
return l.Exec.Command("open", url)
}
11 changes: 11 additions & 0 deletions auth/browser_launcher_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build linux

package auth

import (
"github.com/UiPath/uipathcli/utils"
)

func (l BrowserLauncher) openBrowser(url string) utils.ExecCmd {
return l.Exec.Command("xdg-open", url)
}
11 changes: 11 additions & 0 deletions auth/browser_launcher_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build windows

package auth

import (
"github.com/UiPath/uipathcli/utils"
)

func (l BrowserLauncher) openBrowser(url string) utils.ExecCmd {
return l.Exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
}
47 changes: 0 additions & 47 deletions auth/exec_browser_launcher.go

This file was deleted.

30 changes: 15 additions & 15 deletions auth/oauth_authenticator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"runtime"
"strings"
"testing"

"github.com/UiPath/uipathcli/cache"
"github.com/UiPath/uipathcli/utils"
)

func TestOAuthAuthenticatorNotEnabled(t *testing.T) {
Expand All @@ -25,7 +27,7 @@ func TestOAuthAuthenticatorNotEnabled(t *testing.T) {
request := NewAuthenticatorRequest("http:/localhost", map[string]string{})
context := NewAuthenticatorContext("login", config, createIdentityUrl(""), false, false, *request)

authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
authenticator := NewOAuthAuthenticator(cache.NewFileCache(), *NewBrowserLauncher())
result := authenticator.Auth(*context)
if result.Error != "" {
t.Errorf("Expected no error when oauth flow is skipped, but got: %v", result.Error)
Expand All @@ -46,7 +48,7 @@ func TestOAuthAuthenticatorPreservesExistingHeaders(t *testing.T) {
request := NewAuthenticatorRequest("http:/localhost", headers)
context := NewAuthenticatorContext("login", config, createIdentityUrl(""), false, false, *request)

authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
authenticator := NewOAuthAuthenticator(cache.NewFileCache(), *NewBrowserLauncher())
result := authenticator.Auth(*context)
if result.Error != "" {
t.Errorf("Expected no error when oauth flow is skipped, but got: %v", result.Error)
Expand All @@ -65,7 +67,7 @@ func TestOAuthAuthenticatorInvalidConfig(t *testing.T) {
request := NewAuthenticatorRequest("http:/localhost", map[string]string{})
context := NewAuthenticatorContext("login", config, createIdentityUrl(""), false, false, *request)

authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
authenticator := NewOAuthAuthenticator(cache.NewFileCache(), *NewBrowserLauncher())
result := authenticator.Auth(*context)
if result.Error != "Invalid oauth authenticator configuration: Invalid value for clientId: '1'" {
t.Errorf("Expected error with invalid config, but got: %v", result.Error)
Expand Down Expand Up @@ -122,7 +124,7 @@ func TestOAuthFlowIsCached(t *testing.T) {
performLogin(loginUrl, t)
<-resultChannel

authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
authenticator := NewOAuthAuthenticator(cache.NewFileCache(), *NewBrowserLauncher())
result := authenticator.Auth(context)

if result.Error != "" {
Expand Down Expand Up @@ -208,8 +210,15 @@ func TestMissingCodeShowsErrorMessage(t *testing.T) {

func callAuthenticator(context AuthenticatorContext) (url.URL, chan AuthenticatorResult) {
loginChan := make(chan string)
authenticator := NewOAuthAuthenticator(cache.NewFileCache(), NoOpBrowserLauncher{
loginUrlChannel: loginChan,
authenticator := NewOAuthAuthenticator(cache.NewFileCache(), BrowserLauncher{
Exec: utils.NewExecCustomProcess(0, "", "", func(name string, args []string) {
switch runtime.GOOS {
case "windows":
loginChan <- args[1]
default:
loginChan <- args[0]
}
}),
})

resultChannel := make(chan AuthenticatorResult)
Expand Down Expand Up @@ -323,12 +332,3 @@ func (i identityServerFake) writeValidationErrorResponse(response http.ResponseW
response.WriteHeader(400)
_, _ = response.Write([]byte(message))
}

type NoOpBrowserLauncher struct {
loginUrlChannel chan string
}

func (l NoOpBrowserLauncher) Open(url string) error {
l.loginUrlChannel <- url
return nil
}
11 changes: 4 additions & 7 deletions cache/file_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import (
"strconv"
"strings"
"time"

"github.com/UiPath/uipathcli/utils"
)

const cacheFilePermissions = 0600
const cacheDirectoryPermissions = 0700
const cacheDirectory = "uipath"
const separator = "|"

// The FileCache stores data on disk in order to preserve them across
Expand Down Expand Up @@ -63,15 +63,12 @@ func (c FileCache) readValue(key string) (int64, string, error) {
}

func (c FileCache) cacheFilePath(key string) (string, error) {
userCacheDirectory, err := os.UserCacheDir()
cacheDirectory, err := utils.Directories{}.Cache()
if err != nil {
return "", err
}
cacheDirectory := filepath.Join(userCacheDirectory, cacheDirectory)
_ = os.MkdirAll(cacheDirectory, cacheDirectoryPermissions)

hash := sha256.Sum256([]byte(key))
fileName := fmt.Sprintf("%x.cache", hash)
fileName := fmt.Sprintf("%x", hash)
return filepath.Join(cacheDirectory, fileName), nil
}

Expand Down
14 changes: 14 additions & 0 deletions definitions/studio.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
openapi: 3.0.1
info:
title: UiPath Studio
description: UiPath Studio
version: v1
servers:
- url: https://cloud.uipath.com/{organization}/studio_/backend
description: The production url
variables:
organization:
description: The organization name (or id)
default: my-org
paths:
{}
2 changes: 1 addition & 1 deletion executor/http_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func (e HttpExecutor) progressReader(text string, completedText string, reader i
if progress.Completed {
displayText = completedText
}
progressBar.Update(displayText, progress.BytesRead, length, progress.BytesPerSecond)
progressBar.UpdateProgress(displayText, progress.BytesRead, length, progress.BytesPerSecond)
})
return progressReader
}
Expand Down
4 changes: 4 additions & 0 deletions log/debug_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ func (l DebugLogger) LogResponse(response ResponseInfo) {
fmt.Fprint(l.writer, "\n\n\n")
}

func (l DebugLogger) Log(message string) {
fmt.Fprint(l.writer, message)
}

func (l DebugLogger) LogError(message string) {
fmt.Fprint(l.writer, message)
}
Expand Down
3 changes: 3 additions & 0 deletions log/default_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ func (l *DefaultLogger) LogRequest(request RequestInfo) {
func (l DefaultLogger) LogResponse(response ResponseInfo) {
}

func (l DefaultLogger) Log(message string) {
}

func (l DefaultLogger) LogError(message string) {
fmt.Fprint(l.writer, message)
}
Expand Down
1 change: 1 addition & 0 deletions log/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package log
// The Logger interface which is used to provide additional information to the
// user about what operations the CLI is performing.
type Logger interface {
Log(message string)
LogError(message string)
LogRequest(request RequestInfo)
LogResponse(response ResponseInfo)
Expand Down
11 changes: 7 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/UiPath/uipathcli/plugin"
plugin_digitizer "github.com/UiPath/uipathcli/plugin/digitizer"
plugin_orchestrator "github.com/UiPath/uipathcli/plugin/orchestrator"
plugin_studio "github.com/UiPath/uipathcli/plugin/studio"
"github.com/UiPath/uipathcli/utils"
)

Expand All @@ -27,7 +28,7 @@ var embedded embed.FS
func authenticators() []auth.Authenticator {
return []auth.Authenticator{
auth.NewPatAuthenticator(),
auth.NewOAuthAuthenticator(cache.NewFileCache(), auth.NewExecBrowserLauncher()),
auth.NewOAuthAuthenticator(cache.NewFileCache(), *auth.NewBrowserLauncher()),
auth.NewBearerAuthenticator(cache.NewFileCache()),
}
}
Expand Down Expand Up @@ -63,9 +64,11 @@ func main() {
commandline.NewDefinitionFileStore(os.Getenv("UIPATH_DEFINITIONS_PATH"), embedded),
parser.NewOpenApiParser(),
[]plugin.CommandPlugin{
plugin_digitizer.DigitizeCommand{},
plugin_orchestrator.UploadCommand{},
plugin_orchestrator.DownloadCommand{},
plugin_digitizer.NewDigitizeCommand(),
plugin_orchestrator.NewUploadCommand(),
plugin_orchestrator.NewDownloadCommand(),
plugin_studio.NewPackagePackCommand(),
plugin_studio.NewPackageAnalyzeCommand(),
},
),
*configProvider,
Expand Down
Loading

0 comments on commit 3687fc9

Please sign in to comment.