Skip to content

Reusable Workflows

This example demonstrates how to work with reusable workflows, including detection, input/output extraction, and analysis.

Detect Reusable Workflows

go
package main

import (
    "fmt"
    "log"
    "github.com/scagogogo/github-action-parser/pkg/parser"
)

func main() {
    // Parse workflow file
    workflow, err := parser.ParseFile(".github/workflows/reusable.yml")
    if err != nil {
        log.Fatalf("Failed to parse workflow: %v", err)
    }
    
    // Check if it's a reusable workflow
    if parser.IsReusableWorkflow(workflow) {
        fmt.Println("✅ This is a reusable workflow")
        analyzeReusableWorkflow(workflow)
    } else {
        fmt.Println("❌ This is not a reusable workflow")
    }
}

func analyzeReusableWorkflow(workflow *parser.ActionFile) {
    fmt.Printf("Name: %s\n", workflow.Name)
    
    // Extract inputs
    inputs, err := parser.ExtractInputsFromWorkflowCall(workflow)
    if err != nil {
        log.Printf("Failed to extract inputs: %v", err)
    } else {
        fmt.Printf("Inputs: %d\n", len(inputs))
    }
    
    // Extract outputs
    outputs, err := parser.ExtractOutputsFromWorkflowCall(workflow)
    if err != nil {
        log.Printf("Failed to extract outputs: %v", err)
    } else {
        fmt.Printf("Outputs: %d\n", len(outputs))
    }
}

Extract and Display Inputs

go
// Extract and display detailed input information
inputs, err := parser.ExtractInputsFromWorkflowCall(workflow)
if err != nil {
    log.Fatalf("Failed to extract inputs: %v", err)
}

if len(inputs) > 0 {
    fmt.Printf("\n=== Inputs (%d) ===\n", len(inputs))
    for name, input := range inputs {
        fmt.Printf("• %s", name)
        
        if input.Required {
            fmt.Printf(" (required)")
        } else {
            fmt.Printf(" (optional)")
        }
        
        fmt.Printf("\n  Description: %s\n", input.Description)
        
        if input.Default != "" {
            fmt.Printf("  Default: %s\n", input.Default)
        }
        
        if input.Deprecated {
            fmt.Printf("  ⚠️  Deprecated\n")
        }
        
        fmt.Println()
    }
} else {
    fmt.Println("No inputs defined")
}

Extract and Display Outputs

go
// Extract and display detailed output information
outputs, err := parser.ExtractOutputsFromWorkflowCall(workflow)
if err != nil {
    log.Fatalf("Failed to extract outputs: %v", err)
}

if len(outputs) > 0 {
    fmt.Printf("\n=== Outputs (%d) ===\n", len(outputs))
    for name, output := range outputs {
        fmt.Printf("• %s\n", name)
        fmt.Printf("  Description: %s\n", output.Description)
        
        if output.Value != "" {
            fmt.Printf("  Value: %s\n", output.Value)
        }
        
        fmt.Println()
    }
} else {
    fmt.Println("No outputs defined")
}

Find All Reusable Workflows

go
package main

import (
    "fmt"
    "log"
    "path/filepath"
    "github.com/scagogogo/github-action-parser/pkg/parser"
)

func main() {
    // Parse all workflows
    workflows, err := parser.ParseDir(".github/workflows")
    if err != nil {
        log.Fatalf("Failed to parse workflows: %v", err)
    }
    
    fmt.Printf("Scanning %d workflow files...\n\n", len(workflows))
    
    reusableCount := 0
    
    for path, workflow := range workflows {
        if parser.IsReusableWorkflow(workflow) {
            reusableCount++
            analyzeReusableWorkflowDetailed(filepath.Base(path), workflow)
        }
    }
    
    fmt.Printf("\n=== Summary ===\n")
    fmt.Printf("Total workflows: %d\n", len(workflows))
    fmt.Printf("Reusable workflows: %d\n", reusableCount)
    fmt.Printf("Regular workflows: %d\n", len(workflows)-reusableCount)
}

func analyzeReusableWorkflowDetailed(filename string, workflow *parser.ActionFile) {
    fmt.Printf("=== %s ===\n", filename)
    
    if workflow.Name != "" {
        fmt.Printf("Name: %s\n", workflow.Name)
    }
    
    // Analyze inputs
    inputs, err := parser.ExtractInputsFromWorkflowCall(workflow)
    if err != nil {
        fmt.Printf("❌ Failed to extract inputs: %v\n", err)
    } else {
        fmt.Printf("Inputs: %d", len(inputs))
        if len(inputs) > 0 {
            requiredCount := 0
            for _, input := range inputs {
                if input.Required {
                    requiredCount++
                }
            }
            fmt.Printf(" (%d required, %d optional)", requiredCount, len(inputs)-requiredCount)
        }
        fmt.Println()
    }
    
    // Analyze outputs
    outputs, err := parser.ExtractOutputsFromWorkflowCall(workflow)
    if err != nil {
        fmt.Printf("❌ Failed to extract outputs: %v\n", err)
    } else {
        fmt.Printf("Outputs: %d\n", len(outputs))
    }
    
    // Analyze jobs
    fmt.Printf("Jobs: %d\n", len(workflow.Jobs))
    
    fmt.Println()
}

Validate Reusable Workflow Usage

go
package main

import (
    "fmt"
    "log"
    "strings"
    "github.com/scagogogo/github-action-parser/pkg/parser"
)

func main() {
    // Parse all workflows to find reusable workflow usage
    workflows, err := parser.ParseDir(".github/workflows")
    if err != nil {
        log.Fatalf("Failed to parse workflows: %v", err)
    }
    
    // Find reusable workflows and their callers
    reusableWorkflows := make(map[string]*parser.ActionFile)
    callerWorkflows := make(map[string]*parser.ActionFile)
    
    for path, workflow := range workflows {
        if parser.IsReusableWorkflow(workflow) {
            reusableWorkflows[path] = workflow
        } else {
            // Check if this workflow calls any reusable workflows
            for _, job := range workflow.Jobs {
                if job.Uses != "" {
                    callerWorkflows[path] = workflow
                    break
                }
            }
        }
    }
    
    fmt.Printf("Found %d reusable workflows and %d caller workflows\n\n", 
        len(reusableWorkflows), len(callerWorkflows))
    
    // Analyze usage
    for callerPath, callerWorkflow := range callerWorkflows {
        analyzeReusableWorkflowUsage(callerPath, callerWorkflow, reusableWorkflows)
    }
}

func analyzeReusableWorkflowUsage(callerPath string, caller *parser.ActionFile, reusableWorkflows map[string]*parser.ActionFile) {
    fmt.Printf("=== %s ===\n", filepath.Base(callerPath))
    
    for jobID, job := range caller.Jobs {
        if job.Uses == "" {
            continue
        }
        
        fmt.Printf("Job '%s' uses: %s\n", jobID, job.Uses)
        
        // Check if it's a local reusable workflow
        if strings.HasPrefix(job.Uses, "./") {
            localPath := strings.TrimPrefix(job.Uses, "./")
            if reusableWorkflow, exists := reusableWorkflows[localPath]; exists {
                validateReusableWorkflowCall(jobID, job, reusableWorkflow)
            } else {
                fmt.Printf("  ⚠️  Local reusable workflow not found: %s\n", localPath)
            }
        }
        
        // Display inputs passed to reusable workflow
        if len(job.With) > 0 {
            fmt.Printf("  Inputs passed:\n")
            for key, value := range job.With {
                fmt.Printf("    %s: %v\n", key, value)
            }
        }
        
        // Display secrets passed to reusable workflow
        if job.Secrets != nil {
            fmt.Printf("  Secrets: %v\n", job.Secrets)
        }
        
        fmt.Println()
    }
}

func validateReusableWorkflowCall(jobID string, job parser.Job, reusableWorkflow *parser.ActionFile) {
    // Extract expected inputs from reusable workflow
    expectedInputs, err := parser.ExtractInputsFromWorkflowCall(reusableWorkflow)
    if err != nil {
        fmt.Printf("  ❌ Failed to extract expected inputs: %v\n", err)
        return
    }
    
    // Check if all required inputs are provided
    providedInputs := make(map[string]bool)
    for key := range job.With {
        providedInputs[key] = true
    }
    
    missingRequired := []string{}
    extraInputs := []string{}
    
    // Check for missing required inputs
    for name, input := range expectedInputs {
        if input.Required && !providedInputs[name] {
            missingRequired = append(missingRequired, name)
        }
    }
    
    // Check for extra inputs
    for name := range providedInputs {
        if _, exists := expectedInputs[name]; !exists {
            extraInputs = append(extraInputs, name)
        }
    }
    
    // Report validation results
    if len(missingRequired) == 0 && len(extraInputs) == 0 {
        fmt.Printf("  ✅ Input validation passed\n")
    } else {
        if len(missingRequired) > 0 {
            fmt.Printf("  ❌ Missing required inputs: %v\n", missingRequired)
        }
        if len(extraInputs) > 0 {
            fmt.Printf("  ⚠️  Extra inputs (not defined): %v\n", extraInputs)
        }
    }
}

Generate Reusable Workflow Documentation

go
package main

import (
    "fmt"
    "log"
    "os"
    "path/filepath"
    "sort"
    "strings"
    "github.com/scagogogo/github-action-parser/pkg/parser"
)

func main() {
    // Find all reusable workflows
    workflows, err := parser.ParseDir(".github/workflows")
    if err != nil {
        log.Fatalf("Failed to parse workflows: %v", err)
    }
    
    reusableWorkflows := make(map[string]*parser.ActionFile)
    for path, workflow := range workflows {
        if parser.IsReusableWorkflow(workflow) {
            reusableWorkflows[path] = workflow
        }
    }
    
    if len(reusableWorkflows) == 0 {
        fmt.Println("No reusable workflows found")
        return
    }
    
    // Generate documentation
    generateReusableWorkflowDocs(reusableWorkflows)
}

func generateReusableWorkflowDocs(workflows map[string]*parser.ActionFile) {
    fmt.Println("# Reusable Workflows Documentation\n")
    
    // Sort workflows by name
    var paths []string
    for path := range workflows {
        paths = append(paths, path)
    }
    sort.Strings(paths)
    
    for _, path := range paths {
        workflow := workflows[path]
        generateWorkflowDoc(filepath.Base(path), workflow)
    }
}

func generateWorkflowDoc(filename string, workflow *parser.ActionFile) {
    fmt.Printf("## %s\n\n", strings.TrimSuffix(filename, filepath.Ext(filename)))
    
    if workflow.Name != "" {
        fmt.Printf("**Name:** %s\n\n", workflow.Name)
    }
    
    // Extract inputs
    inputs, err := parser.ExtractInputsFromWorkflowCall(workflow)
    if err != nil {
        fmt.Printf("❌ Failed to extract inputs: %v\n\n", err)
    } else if len(inputs) > 0 {
        fmt.Printf("### Inputs\n\n")
        fmt.Printf("| Name | Description | Required | Default |\n")
        fmt.Printf("|------|-------------|----------|----------|\n")
        
        // Sort inputs by name
        var inputNames []string
        for name := range inputs {
            inputNames = append(inputNames, name)
        }
        sort.Strings(inputNames)
        
        for _, name := range inputNames {
            input := inputs[name]
            required := "No"
            if input.Required {
                required = "Yes"
            }
            
            defaultValue := input.Default
            if defaultValue == "" {
                defaultValue = "-"
            }
            
            fmt.Printf("| `%s` | %s | %s | `%s` |\n", 
                name, input.Description, required, defaultValue)
        }
        fmt.Println()
    }
    
    // Extract outputs
    outputs, err := parser.ExtractOutputsFromWorkflowCall(workflow)
    if err != nil {
        fmt.Printf("❌ Failed to extract outputs: %v\n\n", err)
    } else if len(outputs) > 0 {
        fmt.Printf("### Outputs\n\n")
        fmt.Printf("| Name | Description |\n")
        fmt.Printf("|------|-------------|\n")
        
        // Sort outputs by name
        var outputNames []string
        for name := range outputs {
            outputNames = append(outputNames, name)
        }
        sort.Strings(outputNames)
        
        for _, name := range outputNames {
            output := outputs[name]
            fmt.Printf("| `%s` | %s |\n", name, output.Description)
        }
        fmt.Println()
    }
    
    // Usage example
    fmt.Printf("### Usage Example\n\n")
    fmt.Printf("```yaml\n")
    fmt.Printf("jobs:\n")
    fmt.Printf("  call-reusable-workflow:\n")
    fmt.Printf("    uses: ./.github/workflows/%s\n", filename)
    
    if len(inputs) > 0 {
        fmt.Printf("    with:\n")
        for name, input := range inputs {
            if input.Required {
                fmt.Printf("      %s: # Required - %s\n", name, input.Description)
            }
        }
    }
    
    fmt.Printf("```\n\n")
    
    fmt.Println("---\n")
}

Complete Reusable Workflow Analysis Tool

go
package main

import (
    "fmt"
    "log"
    "os"
    "path/filepath"
    "github.com/scagogogo/github-action-parser/pkg/parser"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Println("Usage:")
        fmt.Println("  go run main.go analyze    - Analyze all reusable workflows")
        fmt.Println("  go run main.go validate   - Validate reusable workflow usage")
        fmt.Println("  go run main.go docs       - Generate documentation")
        os.Exit(1)
    }
    
    command := os.Args[1]
    
    switch command {
    case "analyze":
        analyzeAllReusableWorkflows()
    case "validate":
        validateReusableWorkflowUsage()
    case "docs":
        generateDocumentation()
    default:
        fmt.Printf("Unknown command: %s\n", command)
        os.Exit(1)
    }
}

func analyzeAllReusableWorkflows() {
    workflows, err := parser.ParseDir(".github/workflows")
    if err != nil {
        log.Fatalf("Failed to parse workflows: %v", err)
    }
    
    fmt.Println("=== Reusable Workflow Analysis ===\n")
    
    reusableCount := 0
    totalInputs := 0
    totalOutputs := 0
    
    for path, workflow := range workflows {
        if !parser.IsReusableWorkflow(workflow) {
            continue
        }
        
        reusableCount++
        filename := filepath.Base(path)
        
        fmt.Printf("📄 %s\n", filename)
        if workflow.Name != "" {
            fmt.Printf("   Name: %s\n", workflow.Name)
        }
        
        inputs, _ := parser.ExtractInputsFromWorkflowCall(workflow)
        outputs, _ := parser.ExtractOutputsFromWorkflowCall(workflow)
        
        fmt.Printf("   Inputs: %d, Outputs: %d, Jobs: %d\n", 
            len(inputs), len(outputs), len(workflow.Jobs))
        
        totalInputs += len(inputs)
        totalOutputs += len(outputs)
        
        fmt.Println()
    }
    
    fmt.Printf("=== Summary ===\n")
    fmt.Printf("Reusable workflows: %d\n", reusableCount)
    fmt.Printf("Total inputs: %d\n", totalInputs)
    fmt.Printf("Total outputs: %d\n", totalOutputs)
    
    if reusableCount > 0 {
        fmt.Printf("Average inputs per workflow: %.1f\n", float64(totalInputs)/float64(reusableCount))
        fmt.Printf("Average outputs per workflow: %.1f\n", float64(totalOutputs)/float64(reusableCount))
    }
}

func validateReusableWorkflowUsage() {
    // Implementation similar to previous validation example
    fmt.Println("=== Validating Reusable Workflow Usage ===")
    // ... (validation logic)
}

func generateDocumentation() {
    // Implementation similar to previous documentation example
    fmt.Println("=== Generating Reusable Workflow Documentation ===")
    // ... (documentation generation logic)
}

Next Steps

Released under the MIT License.