11package cli
22
33import (
4+ "encoding/json"
45 "errors"
56 "fmt"
67 "os"
@@ -163,6 +164,7 @@ type CompileConfig struct {
163164 Zizmor bool // Run zizmor security scanner on generated .lock.yml files
164165 Poutine bool // Run poutine security scanner on generated .lock.yml files
165166 Actionlint bool // Run actionlint linter on generated .lock.yml files
167+ JSONOutput bool // Output validation results as JSON
166168}
167169
168170// CompilationStats tracks the results of workflow compilation
@@ -173,6 +175,22 @@ type CompilationStats struct {
173175 FailedWorkflows []string // Names of workflows that failed compilation
174176}
175177
178+ // ValidationError represents a single validation error or warning
179+ type ValidationError struct {
180+ Type string `json:"type"`
181+ Message string `json:"message"`
182+ Line int `json:"line,omitempty"`
183+ }
184+
185+ // ValidationResult represents the validation result for a single workflow
186+ type ValidationResult struct {
187+ Workflow string `json:"workflow"`
188+ Valid bool `json:"valid"`
189+ Errors []ValidationError `json:"errors"`
190+ Warnings []ValidationError `json:"warnings"`
191+ CompiledFile string `json:"compiled_file,omitempty"`
192+ }
193+
176194// validateCompileConfig validates the configuration flags before compilation
177195// This is extracted for faster testing without full compilation
178196func validateCompileConfig (config CompileConfig ) error {
@@ -216,12 +234,16 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) {
216234 zizmor := config .Zizmor
217235 poutine := config .Poutine
218236 actionlint := config .Actionlint
237+ jsonOutput := config .JSONOutput
219238
220- compileLog .Printf ("Starting workflow compilation: files=%d, validate=%v, watch=%v, noEmit=%v, dependabot=%v, zizmor=%v, poutine=%v, actionlint=%v" , len (markdownFiles ), validate , watch , noEmit , dependabot , zizmor , poutine , actionlint )
239+ compileLog .Printf ("Starting workflow compilation: files=%d, validate=%v, watch=%v, noEmit=%v, dependabot=%v, zizmor=%v, poutine=%v, actionlint=%v, jsonOutput=%v " , len (markdownFiles ), validate , watch , noEmit , dependabot , zizmor , poutine , actionlint , jsonOutput )
221240
222241 // Track compilation statistics
223242 stats := & CompilationStats {}
224243
244+ // Track validation results for JSON output
245+ var validationResults []ValidationResult
246+
225247 // Validate configuration
226248 if err := validateCompileConfig (config ); err != nil {
227249 return nil , err
@@ -286,45 +308,94 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) {
286308 var errorMessages []string
287309 for _ , markdownFile := range markdownFiles {
288310 stats .Total ++
311+
312+ // Initialize validation result for this workflow
313+ result := ValidationResult {
314+ Workflow : markdownFile ,
315+ Valid : true ,
316+ Errors : []ValidationError {},
317+ Warnings : []ValidationError {},
318+ }
319+
289320 // Resolve workflow ID or file path to actual file path
290321 compileLog .Printf ("Resolving workflow file: %s" , markdownFile )
291322 resolvedFile , err := resolveWorkflowFile (markdownFile , verbose )
292323 if err != nil {
293324 errMsg := fmt .Sprintf ("failed to resolve workflow '%s': %v" , markdownFile , err )
294- fmt .Fprintln (os .Stderr , console .FormatErrorMessage (errMsg ))
325+ if ! jsonOutput {
326+ fmt .Fprintln (os .Stderr , console .FormatErrorMessage (errMsg ))
327+ }
295328 errorMessages = append (errorMessages , err .Error ())
296329 errorCount ++
297330 stats .Errors ++
298331 stats .FailedWorkflows = append (stats .FailedWorkflows , markdownFile )
332+
333+ // Add to validation results
334+ result .Valid = false
335+ result .Errors = append (result .Errors , ValidationError {
336+ Type : "resolution_error" ,
337+ Message : err .Error (),
338+ })
339+ validationResults = append (validationResults , result )
299340 continue
300341 }
301342 compileLog .Printf ("Resolved to: %s" , resolvedFile )
343+
344+ // Update result with resolved file name
345+ result .Workflow = filepath .Base (resolvedFile )
346+ lockFile := strings .TrimSuffix (resolvedFile , ".md" ) + ".lock.yml"
347+ if ! noEmit {
348+ result .CompiledFile = lockFile
349+ }
302350
303351 // Parse workflow file to get data
304352 compileLog .Printf ("Parsing workflow file: %s" , resolvedFile )
305353 workflowData , err := compiler .ParseWorkflowFile (resolvedFile )
306354 if err != nil {
307355 errMsg := fmt .Sprintf ("failed to parse workflow file %s: %v" , resolvedFile , err )
308- fmt .Fprintln (os .Stderr , console .FormatErrorMessage (errMsg ))
356+ if ! jsonOutput {
357+ fmt .Fprintln (os .Stderr , console .FormatErrorMessage (errMsg ))
358+ }
309359 errorMessages = append (errorMessages , err .Error ())
310360 errorCount ++
311361 stats .Errors ++
312362 stats .FailedWorkflows = append (stats .FailedWorkflows , filepath .Base (resolvedFile ))
363+
364+ // Add to validation results
365+ result .Valid = false
366+ result .Errors = append (result .Errors , ValidationError {
367+ Type : "parse_error" ,
368+ Message : err .Error (),
369+ })
370+ validationResults = append (validationResults , result )
313371 continue
314372 }
315373 workflowDataList = append (workflowDataList , workflowData )
316374
317375 compileLog .Printf ("Starting compilation of %s" , resolvedFile )
318- if err := CompileWorkflowDataWithValidation (compiler , workflowData , resolvedFile , verbose , zizmor && ! noEmit , poutine && ! noEmit , actionlint && ! noEmit , strict , validate && ! noEmit ); err != nil {
376+ if err := CompileWorkflowDataWithValidation (compiler , workflowData , resolvedFile , verbose && ! jsonOutput , zizmor && ! noEmit , poutine && ! noEmit , actionlint && ! noEmit , strict , validate && ! noEmit ); err != nil {
319377 // Always put error on a new line and don't wrap with "failed to compile workflow"
320- fmt .Fprintln (os .Stderr , err .Error ())
378+ if ! jsonOutput {
379+ fmt .Fprintln (os .Stderr , err .Error ())
380+ }
321381 errorMessages = append (errorMessages , err .Error ())
322382 errorCount ++
323383 stats .Errors ++
324384 stats .FailedWorkflows = append (stats .FailedWorkflows , filepath .Base (resolvedFile ))
385+
386+ // Add to validation results
387+ result .Valid = false
388+ result .Errors = append (result .Errors , ValidationError {
389+ Type : "compilation_error" ,
390+ Message : err .Error (),
391+ })
392+ validationResults = append (validationResults , result )
325393 continue
326394 }
327395 compiledCount ++
396+
397+ // Add successful validation result
398+ validationResults = append (validationResults , result )
328399 }
329400
330401 // Get warning count from compiler
@@ -371,8 +442,17 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) {
371442 // Note: Instructions are only written by the init command
372443 // The compile command should not write instruction files
373444
374- // Print summary
375- printCompilationSummary (stats )
445+ // Output JSON if requested
446+ if jsonOutput {
447+ jsonBytes , err := json .MarshalIndent (validationResults , "" , " " )
448+ if err != nil {
449+ return workflowDataList , fmt .Errorf ("failed to marshal JSON: %w" , err )
450+ }
451+ fmt .Println (string (jsonBytes ))
452+ } else {
453+ // Print summary for text output
454+ printCompilationSummary (stats )
455+ }
376456
377457 // Return error if any compilations failed
378458 if errorCount > 0 {
@@ -445,26 +525,63 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) {
445525 var successCount int
446526 for _ , file := range mdFiles {
447527 stats .Total ++
528+
529+ // Initialize validation result for this workflow
530+ result := ValidationResult {
531+ Workflow : filepath .Base (file ),
532+ Valid : true ,
533+ Errors : []ValidationError {},
534+ Warnings : []ValidationError {},
535+ }
536+
537+ lockFile := strings .TrimSuffix (file , ".md" ) + ".lock.yml"
538+ if ! noEmit {
539+ result .CompiledFile = lockFile
540+ }
541+
448542 // Parse workflow file to get data
449543 workflowData , err := compiler .ParseWorkflowFile (file )
450544 if err != nil {
451- fmt .Fprintln (os .Stderr , console .FormatErrorMessage (fmt .Sprintf ("failed to parse workflow file %s: %v" , file , err )))
545+ if ! jsonOutput {
546+ fmt .Fprintln (os .Stderr , console .FormatErrorMessage (fmt .Sprintf ("failed to parse workflow file %s: %v" , file , err )))
547+ }
452548 errorCount ++
453549 stats .Errors ++
454550 stats .FailedWorkflows = append (stats .FailedWorkflows , filepath .Base (file ))
551+
552+ // Add to validation results
553+ result .Valid = false
554+ result .Errors = append (result .Errors , ValidationError {
555+ Type : "parse_error" ,
556+ Message : err .Error (),
557+ })
558+ validationResults = append (validationResults , result )
455559 continue
456560 }
457561 workflowDataList = append (workflowDataList , workflowData )
458562
459- if err := CompileWorkflowDataWithValidation (compiler , workflowData , file , verbose , zizmor && ! noEmit , poutine && ! noEmit , actionlint && ! noEmit , strict , validate && ! noEmit ); err != nil {
563+ if err := CompileWorkflowDataWithValidation (compiler , workflowData , file , verbose && ! jsonOutput , zizmor && ! noEmit , poutine && ! noEmit , actionlint && ! noEmit , strict , validate && ! noEmit ); err != nil {
460564 // Print the error to stderr (errors from CompileWorkflow are already formatted)
461- fmt .Fprintln (os .Stderr , err .Error ())
565+ if ! jsonOutput {
566+ fmt .Fprintln (os .Stderr , err .Error ())
567+ }
462568 errorCount ++
463569 stats .Errors ++
464570 stats .FailedWorkflows = append (stats .FailedWorkflows , filepath .Base (file ))
571+
572+ // Add to validation results
573+ result .Valid = false
574+ result .Errors = append (result .Errors , ValidationError {
575+ Type : "compilation_error" ,
576+ Message : err .Error (),
577+ })
578+ validationResults = append (validationResults , result )
465579 continue
466580 }
467581 successCount ++
582+
583+ // Add successful validation result
584+ validationResults = append (validationResults , result )
468585 }
469586
470587 // Get warning count from compiler
@@ -535,8 +652,17 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) {
535652 // Note: Instructions are only written by the init command
536653 // The compile command should not write instruction files
537654
538- // Print summary
539- printCompilationSummary (stats )
655+ // Output JSON if requested
656+ if jsonOutput {
657+ jsonBytes , err := json .MarshalIndent (validationResults , "" , " " )
658+ if err != nil {
659+ return workflowDataList , fmt .Errorf ("failed to marshal JSON: %w" , err )
660+ }
661+ fmt .Println (string (jsonBytes ))
662+ } else {
663+ // Print summary for text output
664+ printCompilationSummary (stats )
665+ }
540666
541667 // Return error if any compilations failed
542668 if errorCount > 0 {
0 commit comments