• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

UiPath / uipathcli / 12594758457

03 Jan 2025 08:13AM UTC coverage: 90.328% (-0.1%) from 90.453%
12594758457

Pull #135

github

thschmitt
Add support to analyze and package studio projects

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.
```

```
{
  "... (continued)
Pull Request #135: Add support to analyze and package studio projects

698 of 818 new or added lines in 25 files covered. (85.33%)

2 existing lines in 1 file now uncovered.

4931 of 5459 relevant lines covered (90.33%)

1.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.22
/plugin/studio/package_pack_command.go
1
package studio
2

3
import (
4
        "bufio"
5
        "bytes"
6
        "encoding/json"
7
        "errors"
8
        "fmt"
9
        "io"
10
        "os"
11
        "path/filepath"
12
        "strings"
13
        "sync"
14
        "time"
15

16
        "github.com/UiPath/uipathcli/log"
17
        "github.com/UiPath/uipathcli/output"
18
        "github.com/UiPath/uipathcli/plugin"
19
        "github.com/UiPath/uipathcli/utils"
20
)
21

22
const defaultProjectJson = "project.json"
23

24
// The PackagePackCommand packs a project into a single NuGet package
25
type PackagePackCommand struct {
26
        Exec utils.ExecProcess
27
}
28

29
func (c PackagePackCommand) Command() plugin.Command {
1✔
30
        return *plugin.NewCommand("studio").
1✔
31
                WithCategory("package", "Package", "UiPath Studio package-related actions").
1✔
32
                WithOperation("pack", "Package Project", "Packs a project into a single package").
1✔
33
                WithParameter("source", plugin.ParameterTypeString, "Path to a project.json file or a folder containing project.json file", true).
1✔
34
                WithParameter("destination", plugin.ParameterTypeString, "The output folder", true).
1✔
35
                WithParameter("package-version", plugin.ParameterTypeString, "The package version", false).
1✔
36
                WithParameter("auto-version", plugin.ParameterTypeBoolean, "Auto-generate package version", false).
1✔
37
                WithParameter("output-type", plugin.ParameterTypeString, "Force the output to a specific type", false).
1✔
38
                WithParameter("split-output", plugin.ParameterTypeBoolean, "Enables the output split to runtime and design libraries", false).
1✔
39
                WithParameter("release-notes", plugin.ParameterTypeString, "Add release notes", false)
1✔
40
}
1✔
41

42
func (c PackagePackCommand) Execute(context plugin.ExecutionContext, writer output.OutputWriter, logger log.Logger) error {
1✔
43
        source, err := c.getSource(context)
1✔
44
        if err != nil {
2✔
45
                return err
1✔
46
        }
1✔
47
        destination, err := c.getDestination(context)
1✔
48
        if err != nil {
1✔
NEW
49
                return err
×
NEW
50
        }
×
51
        packageVersion := c.getParameter("package-version", context.Parameters)
1✔
52
        autoVersion := c.getBoolParameter("auto-version", context.Parameters)
1✔
53
        outputType := c.getParameter("output-type", context.Parameters)
1✔
54
        splitOutput := c.getBoolParameter("split-output", context.Parameters)
1✔
55
        releaseNotes := c.getParameter("release-notes", context.Parameters)
1✔
56
        params := newPackagePackParams(source, destination, packageVersion, autoVersion, outputType, splitOutput, releaseNotes)
1✔
57

1✔
58
        result, err := c.execute(*params, context.Debug, logger)
1✔
59
        if err != nil {
1✔
NEW
60
                return err
×
NEW
61
        }
×
62

63
        json, err := json.Marshal(result)
1✔
64
        if err != nil {
1✔
NEW
65
                return fmt.Errorf("pack command failed: %v", err)
×
NEW
66
        }
×
67
        return writer.WriteResponse(*output.NewResponseInfo(200, "200 OK", "HTTP/1.1", map[string][]string{}, bytes.NewReader(json)))
1✔
68
}
69

70
func (c PackagePackCommand) execute(params packagePackParams, debug bool, logger log.Logger) (*packagePackResult, error) {
1✔
71
        if !debug {
2✔
72
                bar := c.newPackagingProgressBar(logger)
1✔
73
                defer close(bar)
1✔
74
        }
1✔
75

76
        args := []string{"package", "pack", params.Source, "--output", params.Destination}
1✔
77
        if params.PackageVersion != "" {
1✔
NEW
78
                args = append(args, "--version", params.PackageVersion)
×
NEW
79
        }
×
80
        if params.AutoVersion {
2✔
81
                args = append(args, "--autoVersion")
1✔
82
        }
1✔
83
        if params.OutputType != "" {
2✔
84
                args = append(args, "--outputType", params.OutputType)
1✔
85
        }
1✔
86
        if params.SplitOutput {
2✔
87
                args = append(args, "--splitOutput")
1✔
88
        }
1✔
89
        if params.ReleaseNotes != "" {
2✔
90
                args = append(args, "--releaseNotes", params.ReleaseNotes)
1✔
91
        }
1✔
92

93
        projectReader := newStudioProjectReader(params.Source)
1✔
94

1✔
95
        uipcli := newUipcli(c.Exec, logger)
1✔
96
        cmd, err := uipcli.Execute(projectReader.GetTargetFramework(), args...)
1✔
97
        if err != nil {
1✔
NEW
98
                return nil, err
×
NEW
99
        }
×
100
        stdout, err := cmd.StdoutPipe()
1✔
101
        if err != nil {
1✔
NEW
102
                return nil, fmt.Errorf("Could not run pack command: %v", err)
×
NEW
103
        }
×
104
        defer stdout.Close()
1✔
105
        stderr, err := cmd.StderrPipe()
1✔
106
        if err != nil {
1✔
NEW
107
                return nil, fmt.Errorf("Could not run pack command: %v", err)
×
NEW
108
        }
×
109
        defer stderr.Close()
1✔
110
        err = cmd.Start()
1✔
111
        if err != nil {
1✔
NEW
112
                return nil, fmt.Errorf("Could not run pack command: %v", err)
×
NEW
113
        }
×
114

115
        stderrOutputBuilder := new(strings.Builder)
1✔
116
        stderrReader := io.TeeReader(stderr, stderrOutputBuilder)
1✔
117

1✔
118
        var wg sync.WaitGroup
1✔
119
        wg.Add(3)
1✔
120
        go c.readOutput(stdout, logger, &wg)
1✔
121
        go c.readOutput(stderrReader, logger, &wg)
1✔
122
        go c.wait(cmd, &wg)
1✔
123
        wg.Wait()
1✔
124

1✔
125
        project, err := projectReader.ReadMetadata()
1✔
126
        if err != nil {
1✔
NEW
127
                return nil, err
×
NEW
128
        }
×
129

130
        exitCode := cmd.ExitCode()
1✔
131
        var result *packagePackResult
1✔
132
        if exitCode == 0 {
2✔
133
                nupkgFile := c.findNupkg(params.Destination)
1✔
134
                version := c.extractVersion(nupkgFile)
1✔
135
                result = newSucceededPackagePackResult(
1✔
136
                        filepath.Join(params.Destination, nupkgFile),
1✔
137
                        project.Name,
1✔
138
                        project.Description,
1✔
139
                        project.ProjectId,
1✔
140
                        version)
1✔
141
        } else {
2✔
142
                result = newFailedPackagePackResult(
1✔
143
                        stderrOutputBuilder.String(),
1✔
144
                        &project.Name,
1✔
145
                        &project.Description,
1✔
146
                        &project.ProjectId)
1✔
147
        }
1✔
148
        return result, nil
1✔
149
}
150

151
func (c PackagePackCommand) findNupkg(destination string) string {
1✔
152
        newestFile := ""
1✔
153
        newestTime := time.Time{}
1✔
154

1✔
155
        files, _ := os.ReadDir(destination)
1✔
156
        for _, file := range files {
2✔
157
                extension := filepath.Ext(file.Name())
1✔
158
                if strings.EqualFold(extension, ".nupkg") {
2✔
159
                        fileInfo, _ := file.Info()
1✔
160
                        time := fileInfo.ModTime()
1✔
161
                        if time.After(newestTime) {
2✔
162
                                newestTime = time
1✔
163
                                newestFile = file.Name()
1✔
164
                        }
1✔
165
                }
166
        }
167
        return newestFile
1✔
168
}
169

170
func (c PackagePackCommand) extractVersion(nupkgFile string) string {
1✔
171
        parts := strings.Split(nupkgFile, ".")
1✔
172
        len := len(parts)
1✔
173
        if len < 4 {
2✔
174
                return ""
1✔
175
        }
1✔
176
        return fmt.Sprintf("%s.%s.%s", parts[len-4], parts[len-3], parts[len-2])
1✔
177
}
178

179
func (c PackagePackCommand) wait(cmd utils.ExecCmd, wg *sync.WaitGroup) {
1✔
180
        defer wg.Done()
1✔
181
        _ = cmd.Wait()
1✔
182
}
1✔
183

184
func (c PackagePackCommand) newPackagingProgressBar(logger log.Logger) chan struct{} {
1✔
185
        progressBar := utils.NewProgressBar(logger)
1✔
186
        ticker := time.NewTicker(10 * time.Millisecond)
1✔
187
        cancel := make(chan struct{})
1✔
188
        var percent float64 = 0
1✔
189
        go func() {
2✔
190
                for {
2✔
191
                        select {
1✔
192
                        case <-ticker.C:
1✔
193
                                progressBar.UpdatePercentage("packaging...  ", percent)
1✔
194
                                percent = percent + 1
1✔
195
                                if percent > 100 {
2✔
196
                                        percent = 0
1✔
197
                                }
1✔
198
                        case <-cancel:
1✔
199
                                ticker.Stop()
1✔
200
                                progressBar.Remove()
1✔
201
                                return
1✔
202
                        }
203
                }
204
        }()
205
        return cancel
1✔
206
}
207

208
func (c PackagePackCommand) getSource(context plugin.ExecutionContext) (string, error) {
1✔
209
        source := c.getParameter("source", context.Parameters)
1✔
210
        if source == "" {
1✔
NEW
211
                return "", errors.New("source is not set")
×
NEW
212
        }
×
213
        source, _ = filepath.Abs(source)
1✔
214
        fileInfo, err := os.Stat(source)
1✔
215
        if err != nil {
2✔
216
                return "", fmt.Errorf("%s not found", defaultProjectJson)
1✔
217
        }
1✔
218
        if fileInfo.IsDir() {
2✔
219
                source = filepath.Join(source, defaultProjectJson)
1✔
220
        }
1✔
221
        return source, nil
1✔
222
}
223

224
func (c PackagePackCommand) getDestination(context plugin.ExecutionContext) (string, error) {
1✔
225
        destination := c.getParameter("destination", context.Parameters)
1✔
226
        if destination == "" {
1✔
NEW
227
                return "", errors.New("destination is not set")
×
NEW
228
        }
×
229
        destination, _ = filepath.Abs(destination)
1✔
230
        return destination, nil
1✔
231
}
232

233
func (c PackagePackCommand) readOutput(output io.Reader, logger log.Logger, wg *sync.WaitGroup) {
1✔
234
        defer wg.Done()
1✔
235
        scanner := bufio.NewScanner(output)
1✔
236
        scanner.Split(bufio.ScanRunes)
1✔
237
        for scanner.Scan() {
2✔
238
                logger.Log(scanner.Text())
1✔
239
        }
1✔
240
}
241

242
func (c PackagePackCommand) getParameter(name string, parameters []plugin.ExecutionParameter) string {
1✔
243
        result := ""
1✔
244
        for _, p := range parameters {
2✔
245
                if p.Name == name {
2✔
246
                        if data, ok := p.Value.(string); ok {
2✔
247
                                result = data
1✔
248
                                break
1✔
249
                        }
250
                }
251
        }
252
        return result
1✔
253
}
254

255
func (c PackagePackCommand) getBoolParameter(name string, parameters []plugin.ExecutionParameter) bool {
1✔
256
        result := false
1✔
257
        for _, p := range parameters {
2✔
258
                if p.Name == name {
2✔
259
                        if data, ok := p.Value.(bool); ok {
2✔
260
                                result = data
1✔
261
                                break
1✔
262
                        }
263
                }
264
        }
265
        return result
1✔
266
}
267

268
func NewPackagePackCommand() *PackagePackCommand {
1✔
269
        return &PackagePackCommand{utils.NewExecProcess()}
1✔
270
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc