Skip to content

Commit f526d89

Browse files
committed
feat: add command workflow execute
1 parent c1a1443 commit f526d89

File tree

3 files changed

+159
-6
lines changed

3 files changed

+159
-6
lines changed

src/__tests__/cli-commands.test.ts

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/* eslint-env jest */
22

3-
import { getVersion, runDoctor, addProjectWithGithubOrgs, printChecklists, printChecks, printWorkflows } from '../cli-commands.js'
3+
import { getVersion, runDoctor, addProjectWithGithubOrgs, printChecklists, printChecks, printWorkflows, executeWorkflow } from '../cli-commands.js'
44
import { getPackageJson } from '../utils.js'
5-
import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIErrorResponse, APIChecklistItem, APICheckItem, APIWorkflowItem } from '../types.js'
6-
import { mockApiHealthResponse, mockAPIProjectResponse, mockAPIGithubOrgResponse, mockAPIChecklistResponse, mockAPICheckResponse, mockAPIWorkflowResponse } from './fixtures.js'
5+
import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIErrorResponse, APIChecklistItem, APICheckItem, APIWorkflowItem, APIWorkflowRunItem } from '../types.js'
6+
import { mockApiHealthResponse, mockAPIProjectResponse, mockAPIGithubOrgResponse, mockAPIChecklistResponse, mockAPICheckResponse, mockAPIWorkflowResponse, mockAPIWorkflowRunResponse } from './fixtures.js'
77
import nock from 'nock'
88

99
const pkg = getPackageJson()
@@ -504,4 +504,86 @@ describe('CLI Commands', () => {
504504
expect(result.messages[0]).toBe('No compliance workflows found')
505505
})
506506
})
507+
508+
describe('executeWorkflow', () => {
509+
let workflowRunResponse: APIWorkflowRunItem
510+
511+
beforeEach(() => {
512+
nock.cleanAll()
513+
514+
// Setup mock workflow run response
515+
workflowRunResponse = { ...mockAPIWorkflowRunResponse }
516+
})
517+
518+
it('should execute a workflow successfully', async () => {
519+
// Mock API call
520+
nock('http://localhost:3000')
521+
.post('/api/v1/workflow/update-stuff/run', { data: { projectId: 123 } })
522+
.reply(202, workflowRunResponse)
523+
524+
// Execute the function
525+
const result = await executeWorkflow('update-stuff', { projectId: 123 })
526+
527+
// Verify the result
528+
expect(result.success).toBe(true)
529+
expect(result.messages).toHaveLength(5) // 5 messages with details
530+
expect(result.messages[0]).toContain('Workflow executed successfully in 2500 ms')
531+
expect(result.messages[1]).toContain('Status: completed')
532+
expect(result.messages[2]).toContain('Started:')
533+
expect(result.messages[3]).toContain('Finished:')
534+
expect(result.messages[4]).toContain('Result:')
535+
expect(nock.isDone()).toBe(true) // Verify all mocked endpoints were called
536+
})
537+
538+
it('Should execute a workflow that was unsuccessful', async () => {
539+
// Mock API call
540+
nock('http://localhost:3000')
541+
.post('/api/v1/workflow/update-stuff/run', { data: { projectId: 123 } })
542+
.reply(202, { ...workflowRunResponse, status: 'failed', result: { message: 'Failed to execute workflow', success: false } })
543+
544+
// Execute the function
545+
const result = await executeWorkflow('update-stuff', { projectId: 123 })
546+
547+
// Verify the result
548+
expect(result.success).toBe(true)
549+
expect(result.messages).toHaveLength(5) // 5 messages with details
550+
expect(result.messages[0]).toContain('Workflow executed unsuccessfully in 2500 ms')
551+
expect(result.messages[1]).toContain('Status: failed')
552+
expect(result.messages[2]).toContain('Started:')
553+
expect(result.messages[3]).toContain('Finished:')
554+
expect(result.messages[4]).toContain('Result:')
555+
expect(nock.isDone()).toBe(true) // Verify all mocked endpoints were called
556+
})
557+
558+
it('should handle API errors gracefully', async () => {
559+
// Mock API error
560+
nock('http://localhost:3000')
561+
.post('/api/v1/workflow/invalid-workflow/run')
562+
.reply(404, { errors: [{ message: 'Workflow not found' }] } as APIErrorResponse)
563+
564+
// Execute the function
565+
const result = await executeWorkflow('invalid-workflow', {})
566+
567+
// Verify the result
568+
expect(result.success).toBe(false)
569+
expect(result.messages[0]).toContain('❌ Failed to execute the workflow')
570+
expect(result.messages).toHaveLength(1)
571+
})
572+
573+
it('should handle network errors gracefully', async () => {
574+
// Mock network error
575+
nock('http://localhost:3000')
576+
.post('/api/v1/workflow/update-stuff/run')
577+
.replyWithError('Network error')
578+
579+
// Execute the function
580+
const result = await executeWorkflow('update-stuff', {})
581+
582+
// Verify the result
583+
expect(result.success).toBe(false)
584+
expect(result.messages[0]).toContain('❌ Failed to execute the workflow')
585+
expect(result.messages[0]).toContain('Network error')
586+
expect(result.messages).toHaveLength(1)
587+
})
588+
})
507589
})

src/cli-commands.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CommandResult } from './types.js'
22
import { isApiAvailable, isApiCompatible, getPackageJson } from './utils.js'
3-
import { getAPIDetails, createProject, addGithubOrgToProject, getAllChecklistItems, getAllChecks, getAllWorkflows } from './api-client.js'
3+
import { getAPIDetails, createProject, addGithubOrgToProject, getAllChecklistItems, getAllChecks, getAllWorkflows, runWorkflow } from './api-client.js'
44

55
const pkg = getPackageJson()
66

@@ -139,3 +139,28 @@ export const printWorkflows = async (): Promise<CommandResult> => {
139139
success
140140
}
141141
}
142+
143+
export const executeWorkflow = async (workflowId: string, data: any): Promise<CommandResult> => {
144+
const messages: string[] = []
145+
let success = true
146+
try {
147+
const results = await runWorkflow(workflowId, data)
148+
const startTime = new Date(results.started)
149+
const endTime = new Date(results.finished)
150+
const duration = endTime.getTime() - startTime.getTime()
151+
152+
messages.push(`Workflow executed ${results.result.success ? 'successfully' : 'unsuccessfully'} in ${duration} ms`)
153+
messages.push(`- Status: ${results.status}`)
154+
messages.push(`- Started: ${startTime}`)
155+
messages.push(`- Finished: ${endTime}`)
156+
messages.push(`- Result: ${results.result.message}`)
157+
} catch (error) {
158+
messages.push(`❌ Failed to execute the workflow: ${error instanceof Error ? error.message : 'Unknown error'}`)
159+
success = false
160+
}
161+
162+
return {
163+
messages,
164+
success
165+
}
166+
}

src/index.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import { Command } from 'commander'
44
// @ts-ignore
55
import { stringToArray } from '@ulisesgascon/string-to-array'
6-
import { handleCommandResult } from './utils.js'
7-
import { getVersion, runDoctor, addProjectWithGithubOrgs, printChecklists, printChecks, printWorkflows } from './cli-commands.js'
6+
import { handleCommandResult, validateData } from './utils.js'
7+
import { getAllWorkflows } from './api-client.js'
8+
import fs from 'fs'
9+
import { getVersion, runDoctor, addProjectWithGithubOrgs, printChecklists, printChecks, printWorkflows, executeWorkflow } from './cli-commands.js'
810

911
const program = new Command()
1012

@@ -35,6 +37,50 @@ workflow
3537
handleCommandResult(result)
3638
})
3739

40+
workflow
41+
.command('execute')
42+
.description('Execute a compliance workflow')
43+
.requiredOption('-w, --workflow <workflowName>', 'Workflow name')
44+
.option('-d, --data <data>', 'Data to pass to the workflow')
45+
.option('-f, --file <file>', 'File containing the data to be parsed')
46+
.action(async (options) => {
47+
// @TODO: Move to utils and include tests when the backend has one workflow that requires additional data
48+
const workflows = await getAllWorkflows()
49+
const workflow = workflows.find((workflow) => workflow.id === options.workflow)
50+
let data: any | undefined
51+
if (!workflow) {
52+
throw new Error(`Invalid workflow name (${options.workflow}). Available workflows: ${workflows.map(w => w.id).join(', ')}`)
53+
}
54+
if (!workflow.isEnabled) {
55+
throw new Error('Workflow is not enabled')
56+
}
57+
// Check if workflow requires additional data and if it is provided or requires collection
58+
if (workflow.isRequiredAdditionalData && (!options.data && !options.file)) {
59+
throw new Error('Workflow does not require additional data. Please remove -d or -f options')
60+
} else if (options.data && options.file) {
61+
throw new Error('Please provide either -d or -f, not both')
62+
} else if (options.data) {
63+
data = options.data
64+
} else if (options.file) {
65+
data = JSON.parse(fs.readFileSync(options.file, 'utf-8'))
66+
}
67+
68+
// If data is provided, validate against JSON Schema
69+
if (data) {
70+
const schema = workflow.schema
71+
if (!schema) {
72+
throw new Error('Workflow does not have a JSON schema')
73+
}
74+
const result = await validateData(data, schema)
75+
if (!result.success) {
76+
throw new Error(`Data validation failed: ${result.messages[0]}`)
77+
}
78+
}
79+
80+
const result = await executeWorkflow(options.workflow, data)
81+
handleCommandResult(result)
82+
})
83+
3884
const checklist = program
3985
.command('compliance-checklist')
4086
.description('Compliance checklist management')

0 commit comments

Comments
 (0)