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

UiPath / uipathcli / 13828601172

13 Mar 2025 06:38AM UTC coverage: 89.972% (-0.1%) from 90.121%
13828601172

push

github

thschmitt
Extend analyze command to support governance files

- Added new parameter `--governance-file` to the
  `uipath studio package analyze` command which takes a path to the
  governance policy containing the Workflow Analyzer rules to check
  during the project validation

- Changed default of the `--stop-on-rule-violation` to true so the
  analyze command fails by default if any rule with Error level was
  violated

- Added new property Severity with a human readable string for the
  severity level of the violation

67 of 84 new or added lines in 2 files covered. (79.76%)

2 existing lines in 1 file now uncovered.

5410 of 6013 relevant lines covered (89.97%)

1.01 hits per line

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

85.51
/plugin/studio/package_analyze_command.go
1
package studio
2

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

19
        "github.com/UiPath/uipathcli/log"
20
        "github.com/UiPath/uipathcli/output"
21
        "github.com/UiPath/uipathcli/plugin"
22
        "github.com/UiPath/uipathcli/utils/directories"
23
        "github.com/UiPath/uipathcli/utils/process"
24
        "github.com/UiPath/uipathcli/utils/visualization"
25
)
26

27
// The PackageAnalyzeCommand runs static code analyis on the project to detect common errors.
28
type PackageAnalyzeCommand struct {
29
        Exec process.ExecProcess
30
}
31

32
func (c PackageAnalyzeCommand) Command() plugin.Command {
1✔
33
        return *plugin.NewCommand("studio").
1✔
34
                WithCategory("package", "Package", "UiPath Studio package-related actions").
1✔
35
                WithOperation("analyze", "Analyze Project", "Runs static code analysis on the project to detect common errors").
1✔
36
                WithParameter("source", plugin.ParameterTypeString, "Path to a project.json file or a folder containing project.json file (default: .)", false).
1✔
37
                WithParameter("stop-on-rule-violation", plugin.ParameterTypeBoolean, "Fail when any rule is violated (default: true)", false).
1✔
38
                WithParameter("treat-warnings-as-errors", plugin.ParameterTypeBoolean, "Treat warnings as errors", false).
1✔
39
                WithParameter("governance-file", plugin.ParameterTypeString, "Pass governance policies containing the Workflow Analyzer rules (default: uipath.policy.default.json)", false)
1✔
40
}
1✔
41

42
func (c PackageAnalyzeCommand) 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

48
        stopOnRuleViolation := c.getBoolParameter("stop-on-rule-violation", true, context.Parameters)
1✔
49
        treatWarningsAsErrors := c.getBoolParameter("treat-warnings-as-errors", false, context.Parameters)
1✔
50
        governanceFile, err := c.getGovernanceFile(context, source)
1✔
51
        if err != nil {
1✔
NEW
52
                return err
×
NEW
53
        }
×
54

55
        exitCode, result, err := c.execute(source, stopOnRuleViolation, treatWarningsAsErrors, governanceFile, context.Debug, logger)
1✔
56
        if err != nil {
2✔
57
                return err
1✔
58
        }
1✔
59

60
        json, err := json.Marshal(result)
1✔
61
        if err != nil {
1✔
62
                return fmt.Errorf("analyze command failed: %v", err)
×
63
        }
×
64
        err = writer.WriteResponse(*output.NewResponseInfo(200, "200 OK", "HTTP/1.1", map[string][]string{}, bytes.NewReader(json)))
1✔
65
        if err != nil {
1✔
66
                return err
×
67
        }
×
68
        if exitCode != 0 {
2✔
69
                return errors.New("")
1✔
70
        }
1✔
71
        return nil
1✔
72
}
73

74
func (c PackageAnalyzeCommand) execute(source string, stopOnRuleViolation bool, treatWarningsAsErrors bool, governanceFile string, debug bool, logger log.Logger) (int, *packageAnalyzeResult, error) {
1✔
75
        jsonResultFilePath, err := c.getTemporaryJsonResultFilePath()
1✔
76
        if err != nil {
1✔
77
                return 1, nil, err
×
78
        }
×
79
        defer os.Remove(jsonResultFilePath)
1✔
80

1✔
81
        args := []string{"package", "analyze", source, "--resultPath", jsonResultFilePath}
1✔
82
        if governanceFile != "" {
2✔
83
                args = append(args, "--governanceFilePath", governanceFile)
1✔
84
        }
1✔
85

86
        projectReader := newStudioProjectReader(source)
1✔
87
        project, err := projectReader.ReadMetadata()
1✔
88
        if err != nil {
1✔
89
                return 1, nil, err
×
90
        }
×
91
        supported, err := project.TargetFramework.IsSupported()
1✔
92
        if !supported {
2✔
93
                return 1, nil, err
1✔
94
        }
1✔
95

96
        uipcli := newUipcli(c.Exec, logger)
1✔
97
        err = uipcli.Initialize(project.TargetFramework)
1✔
98
        if err != nil {
1✔
99
                return 1, nil, err
×
100
        }
×
101

102
        if !debug {
2✔
103
                bar := c.newAnalyzingProgressBar(logger)
1✔
104
                defer close(bar)
1✔
105
        }
1✔
106

107
        exitCode, stdErr, err := c.executeUipcli(uipcli, args, logger)
1✔
108
        if err != nil {
1✔
NEW
109
                return exitCode, nil, err
×
NEW
110
        }
×
111

112
        violations, err := c.readAnalyzeResult(jsonResultFilePath)
1✔
113
        if err != nil {
1✔
114
                return 1, nil, err
×
115
        }
×
116
        errorViolationsFound := c.hasErrorViolations(violations, treatWarningsAsErrors)
1✔
117

1✔
118
        if exitCode != 0 {
2✔
119
                return exitCode, newErrorPackageAnalyzeResult(violations, stdErr), nil
1✔
120
        } else if stopOnRuleViolation && errorViolationsFound {
3✔
121
                return 1, newFailedPackageAnalyzeResult(violations), nil
1✔
122
        } else if errorViolationsFound {
2✔
NEW
123
                return 0, newFailedPackageAnalyzeResult(violations), nil
×
NEW
124
        }
×
125
        return 0, newSucceededPackageAnalyzeResult(violations), nil
1✔
126
}
127

128
func (c PackageAnalyzeCommand) executeUipcli(uipcli *uipcli, args []string, logger log.Logger) (int, string, error) {
1✔
129
        cmd, err := uipcli.Execute(args...)
1✔
130
        if err != nil {
1✔
NEW
131
                return 1, "", err
×
NEW
132
        }
×
133
        stdout, err := cmd.StdoutPipe()
1✔
134
        if err != nil {
1✔
NEW
135
                return 1, "", fmt.Errorf("Could not run analyze command: %v", err)
×
136
        }
×
137
        defer stdout.Close()
1✔
138
        stderr, err := cmd.StderrPipe()
1✔
139
        if err != nil {
1✔
NEW
140
                return 1, "", fmt.Errorf("Could not run analyze command: %v", err)
×
141
        }
×
142
        defer stderr.Close()
1✔
143
        err = cmd.Start()
1✔
144
        if err != nil {
1✔
NEW
145
                return 1, "", fmt.Errorf("Could not run analyze command: %v", err)
×
146
        }
×
147

148
        stderrOutputBuilder := new(strings.Builder)
1✔
149
        stderrReader := io.TeeReader(stderr, stderrOutputBuilder)
1✔
150

1✔
151
        var wg sync.WaitGroup
1✔
152
        wg.Add(3)
1✔
153
        go c.readOutput(stdout, logger, &wg)
1✔
154
        go c.readOutput(stderrReader, logger, &wg)
1✔
155
        go c.wait(cmd, &wg)
1✔
156
        wg.Wait()
1✔
157

1✔
158
        return cmd.ExitCode(), stderrOutputBuilder.String(), nil
1✔
159
}
160

161
func (c PackageAnalyzeCommand) hasErrorViolations(violations []packageAnalyzeViolation, treatWarningsAsErrors bool) bool {
1✔
162
        for _, violation := range violations {
2✔
163
                if violation.Severity == "Error" {
2✔
164
                        return true
1✔
165
                }
1✔
166
                if treatWarningsAsErrors && violation.Severity == "Warning" {
2✔
167
                        return true
1✔
168
                }
1✔
169
        }
170
        return false
1✔
171
}
172

173
func (c PackageAnalyzeCommand) getTemporaryJsonResultFilePath() (string, error) {
1✔
174
        tempDirectory, err := directories.Temp()
1✔
175
        if err != nil {
1✔
176
                return "", err
×
177
        }
×
178
        fileName := c.randomJsonResultFileName()
1✔
179
        return filepath.Join(tempDirectory, fileName), nil
1✔
180
}
181

182
func (c PackageAnalyzeCommand) randomJsonResultFileName() string {
1✔
183
        value, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
1✔
184
        return "analyzeresult-" + value.String() + ".json"
1✔
185
}
1✔
186

187
func (c PackageAnalyzeCommand) readAnalyzeResult(path string) ([]packageAnalyzeViolation, error) {
1✔
188
        file, err := os.Open(path)
1✔
189
        if err != nil && errors.Is(err, os.ErrNotExist) {
2✔
190
                return []packageAnalyzeViolation{}, nil
1✔
191
        }
1✔
192
        if err != nil {
1✔
193
                return []packageAnalyzeViolation{}, fmt.Errorf("Error reading %s file: %v", filepath.Base(path), err)
×
194
        }
×
195
        defer file.Close()
1✔
196
        byteValue, err := io.ReadAll(file)
1✔
197
        if err != nil {
1✔
198
                return []packageAnalyzeViolation{}, fmt.Errorf("Error reading %s file: %v", filepath.Base(path), err)
×
199
        }
×
200

201
        var result analyzeResultJson
1✔
202
        err = json.Unmarshal(byteValue, &result)
1✔
203
        if err != nil {
1✔
204
                return []packageAnalyzeViolation{}, fmt.Errorf("Error parsing %s file: %v", filepath.Base(path), err)
×
205
        }
×
206
        return c.convertToViolations(result), nil
1✔
207
}
208

209
func (c PackageAnalyzeCommand) convertToViolations(json analyzeResultJson) []packageAnalyzeViolation {
1✔
210
        violations := []packageAnalyzeViolation{}
1✔
211
        for _, entry := range json {
2✔
212
                var activityId *packageAnalyzeActivityId
1✔
213
                if entry.ActivityId != nil {
2✔
214
                        activityId = &packageAnalyzeActivityId{
1✔
215
                                Id:    entry.ActivityId.Id,
1✔
216
                                IdRef: entry.ActivityId.IdRef,
1✔
217
                        }
1✔
218
                }
1✔
219
                var item *packageAnalyzeItem
1✔
220
                if entry.Item != nil {
2✔
221
                        item = &packageAnalyzeItem{
1✔
222
                                Name: entry.Item.Name,
1✔
223
                                Type: entry.Item.Type,
1✔
224
                        }
1✔
225
                }
1✔
226
                violation := packageAnalyzeViolation{
1✔
227
                        ErrorCode:           entry.ErrorCode,
1✔
228
                        Description:         entry.Description,
1✔
229
                        RuleName:            entry.RuleName,
1✔
230
                        FilePath:            entry.FilePath,
1✔
231
                        ActivityDisplayName: entry.ActivityDisplayName,
1✔
232
                        WorkflowDisplayName: entry.WorkflowDisplayName,
1✔
233
                        ErrorSeverity:       entry.ErrorSeverity,
1✔
234
                        Severity:            c.convertToSeverity(entry.ErrorSeverity),
1✔
235
                        Recommendation:      entry.Recommendation,
1✔
236
                        DocumentationLink:   entry.DocumentationLink,
1✔
237
                        ActivityId:          activityId,
1✔
238
                        Item:                item,
1✔
239
                }
1✔
240
                violations = append(violations, violation)
1✔
241
        }
242
        return violations
1✔
243
}
244

245
func (c PackageAnalyzeCommand) convertToSeverity(errorSeverity int) string {
1✔
246
        switch errorSeverity {
1✔
NEW
247
        case 0:
×
NEW
248
                return "Off"
×
249
        case 1:
1✔
250
                return "Error"
1✔
251
        case 2:
1✔
252
                return "Warning"
1✔
253
        case 3:
1✔
254
                return "Info"
1✔
NEW
255
        default:
×
NEW
256
                return "Trace"
×
257
        }
258
}
259

260
func (c PackageAnalyzeCommand) wait(cmd process.ExecCmd, wg *sync.WaitGroup) {
1✔
261
        defer wg.Done()
1✔
262
        _ = cmd.Wait()
1✔
263
}
1✔
264

265
func (c PackageAnalyzeCommand) newAnalyzingProgressBar(logger log.Logger) chan struct{} {
1✔
266
        progressBar := visualization.NewProgressBar(logger)
1✔
267
        ticker := time.NewTicker(10 * time.Millisecond)
1✔
268
        cancel := make(chan struct{})
1✔
269
        var percent float64 = 0
1✔
270
        go func() {
2✔
271
                for {
2✔
272
                        select {
1✔
273
                        case <-ticker.C:
1✔
274
                                progressBar.UpdatePercentage("analyzing...  ", percent)
1✔
275
                                percent = percent + 1
1✔
276
                                if percent > 100 {
2✔
277
                                        percent = 0
1✔
278
                                }
1✔
279
                        case <-cancel:
1✔
280
                                ticker.Stop()
1✔
281
                                progressBar.Remove()
1✔
282
                                return
1✔
283
                        }
284
                }
285
        }()
286
        return cancel
1✔
287
}
288

289
func (c PackageAnalyzeCommand) getSource(context plugin.ExecutionContext) (string, error) {
1✔
290
        source := c.getParameter("source", ".", context.Parameters)
1✔
291
        source, _ = filepath.Abs(source)
1✔
292
        fileInfo, err := os.Stat(source)
1✔
293
        if err != nil {
2✔
294
                return "", fmt.Errorf("%s not found", defaultProjectJson)
1✔
295
        }
1✔
296
        if fileInfo.IsDir() {
2✔
297
                source = filepath.Join(source, defaultProjectJson)
1✔
298
        }
1✔
299
        return source, nil
1✔
300
}
301

302
func (c PackageAnalyzeCommand) defaultGovernanceFile(source string) string {
1✔
303
        directory := filepath.Dir(source)
1✔
304
        file := filepath.Join(directory, "uipath.policy.default.json")
1✔
305
        fileInfo, err := os.Stat(file)
1✔
306
        if err != nil || fileInfo.IsDir() {
2✔
307
                return ""
1✔
308
        }
1✔
309
        return file
1✔
310
}
311

312
func (c PackageAnalyzeCommand) getGovernanceFile(context plugin.ExecutionContext, source string) (string, error) {
1✔
313
        governanceFileParam := c.getParameter("governance-file", "", context.Parameters)
1✔
314
        if governanceFileParam == "" {
2✔
315
                return c.defaultGovernanceFile(source), nil
1✔
316
        }
1✔
317

318
        file, _ := filepath.Abs(governanceFileParam)
1✔
319
        fileInfo, err := os.Stat(file)
1✔
320
        if err != nil || fileInfo.IsDir() {
1✔
NEW
321
                return "", fmt.Errorf("%s not found", governanceFileParam)
×
NEW
322
        }
×
323
        return file, nil
1✔
324
}
325

326
func (c PackageAnalyzeCommand) readOutput(output io.Reader, logger log.Logger, wg *sync.WaitGroup) {
1✔
327
        defer wg.Done()
1✔
328
        scanner := bufio.NewScanner(output)
1✔
329
        scanner.Split(bufio.ScanRunes)
1✔
330
        for scanner.Scan() {
2✔
331
                logger.Log(scanner.Text())
1✔
332
        }
1✔
333
}
334

335
func (c PackageAnalyzeCommand) getParameter(name string, defaultValue string, parameters []plugin.ExecutionParameter) string {
1✔
336
        result := defaultValue
1✔
337
        for _, p := range parameters {
2✔
338
                if p.Name == name {
2✔
339
                        if data, ok := p.Value.(string); ok {
2✔
340
                                result = data
1✔
341
                                break
1✔
342
                        }
343
                }
344
        }
345
        return result
1✔
346
}
347

348
func (c PackageAnalyzeCommand) getBoolParameter(name string, defaultValue bool, parameters []plugin.ExecutionParameter) bool {
1✔
349
        result := defaultValue
1✔
350
        for _, p := range parameters {
2✔
351
                if p.Name == name {
2✔
352
                        if data, ok := p.Value.(bool); ok {
2✔
353
                                result = data
1✔
354
                                break
1✔
355
                        }
356
                }
357
        }
358
        return result
1✔
359
}
360

361
func NewPackageAnalyzeCommand() *PackageAnalyzeCommand {
1✔
362
        return &PackageAnalyzeCommand{process.NewExecProcess()}
1✔
363
}
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

© 2025 Coveralls, Inc