Validation
This example demonstrates how to validate GitHub Actions and Workflows using the built-in validation features.
Basic Validation
go
package main
import (
"fmt"
"log"
"github.com/scagogogo/github-action-parser/pkg/parser"
)
func main() {
// Parse action file
action, err := parser.ParseFile("action.yml")
if err != nil {
log.Fatalf("Failed to parse action: %v", err)
}
// Create validator and validate
validator := parser.NewValidator()
errors := validator.Validate(action)
if len(errors) == 0 {
fmt.Println("✅ Action is valid!")
} else {
fmt.Printf("❌ Found %d validation errors:\n", len(errors))
for _, err := range errors {
fmt.Printf(" - %s: %s\n", err.Field, err.Message)
}
}
}
Validate Multiple Files
go
package main
import (
"fmt"
"log"
"path/filepath"
"github.com/scagogogo/github-action-parser/pkg/parser"
)
func main() {
// Parse all YAML files in current directory
files, err := parser.ParseDir(".")
if err != nil {
log.Fatalf("Failed to parse directory: %v", err)
}
validator := parser.NewValidator()
totalErrors := 0
for path, action := range files {
fmt.Printf("\n=== Validating %s ===\n", filepath.Base(path))
errors := validator.Validate(action)
if len(errors) == 0 {
fmt.Println("✅ Valid")
} else {
fmt.Printf("❌ %d errors:\n", len(errors))
for _, err := range errors {
fmt.Printf(" - %s: %s\n", err.Field, err.Message)
}
totalErrors += len(errors)
}
}
fmt.Printf("\n=== Summary ===\n")
fmt.Printf("Files checked: %d\n", len(files))
fmt.Printf("Total errors: %d\n", totalErrors)
}
Validation with Detailed Reporting
go
package main
import (
"fmt"
"log"
"os"
"github.com/scagogogo/github-action-parser/pkg/parser"
)
func main() {
if len(os.Args) < 2 {
log.Fatal("Usage: go run main.go <file.yml>")
}
filename := os.Args[1]
// Parse file
action, err := parser.ParseFile(filename)
if err != nil {
log.Fatalf("Failed to parse %s: %v", filename, err)
}
// Validate with detailed reporting
validateWithDetails(filename, action)
}
func validateWithDetails(filename string, action *parser.ActionFile) {
fmt.Printf("=== Validating %s ===\n\n", filename)
validator := parser.NewValidator()
errors := validator.Validate(action)
if len(errors) == 0 {
fmt.Println("✅ File is valid!")
displayFileInfo(action)
return
}
// Group errors by category
fieldErrors := make(map[string][]parser.ValidationError)
for _, err := range errors {
fieldErrors[err.Field] = append(fieldErrors[err.Field], err)
}
fmt.Printf("❌ Found %d validation errors:\n\n", len(errors))
// Display errors by category
for field, errs := range fieldErrors {
fmt.Printf("Field: %s\n", field)
for _, err := range errs {
fmt.Printf(" ❌ %s\n", err.Message)
provideSuggestion(err)
}
fmt.Println()
}
// Provide general suggestions
fmt.Println("💡 General Suggestions:")
provideGeneralSuggestions(action, errors)
}
func provideSuggestion(err parser.ValidationError) {
suggestions := map[string]string{
"name": "Add a descriptive name for your action",
"description": "Add a clear description explaining what your action does",
"runs.using": "Specify a supported runtime: node16, node20, docker, or composite",
"runs.main": "For JavaScript actions, specify the main entry point file",
"runs.image": "For Docker actions, specify the Docker image or Dockerfile",
"runs.steps": "For composite actions, add at least one step",
"on": "Specify at least one trigger event for workflows",
"jobs": "Add at least one job to your workflow",
}
if suggestion, exists := suggestions[err.Field]; exists {
fmt.Printf(" 💡 %s\n", suggestion)
}
}
func provideGeneralSuggestions(action *parser.ActionFile, errors []parser.ValidationError) {
// Suggest based on action type
if action.Runs.Using != "" {
switch action.Runs.Using {
case "composite":
fmt.Println(" - For composite actions, ensure each step has either 'uses' or 'run'")
fmt.Println(" - Consider adding step names for better readability")
case "docker":
fmt.Println(" - For Docker actions, ensure your Dockerfile exists")
fmt.Println(" - Consider specifying an entrypoint if needed")
case "node16", "node20":
fmt.Println(" - For JavaScript actions, ensure your main file exists")
fmt.Println(" - Consider adding pre/post scripts if needed")
}
}
// Suggest based on error patterns
hasRequiredFieldErrors := false
for _, err := range errors {
if err.Field == "name" || err.Field == "description" {
hasRequiredFieldErrors = true
break
}
}
if hasRequiredFieldErrors {
fmt.Println(" - Required fields (name, description) are essential for GitHub Actions")
fmt.Println(" - These help users understand what your action does")
}
// Workflow-specific suggestions
if len(action.Jobs) > 0 {
fmt.Println(" - For workflows, ensure each job has 'runs-on' or 'uses'")
fmt.Println(" - Check that all referenced actions exist and are accessible")
}
}
func displayFileInfo(action *parser.ActionFile) {
fmt.Println("\n📋 File Information:")
if action.Name != "" {
fmt.Printf(" Name: %s\n", action.Name)
}
if action.Description != "" {
fmt.Printf(" Description: %s\n", action.Description)
}
if action.Runs.Using != "" {
fmt.Printf(" Type: %s action\n", action.Runs.Using)
}
if len(action.Jobs) > 0 {
fmt.Printf(" Type: Workflow with %d jobs\n", len(action.Jobs))
}
if len(action.Inputs) > 0 {
fmt.Printf(" Inputs: %d\n", len(action.Inputs))
}
if len(action.Outputs) > 0 {
fmt.Printf(" Outputs: %d\n", len(action.Outputs))
}
}
Batch Validation with Summary
go
package main
import (
"fmt"
"log"
"path/filepath"
"strings"
"github.com/scagogogo/github-action-parser/pkg/parser"
)
func main() {
// Validate all workflows in .github/workflows
validateDirectory(".github/workflows", "Workflows")
// Validate action files in current directory
validateDirectory(".", "Actions")
}
func validateDirectory(dir, category string) {
fmt.Printf("\n=== Validating %s in %s ===\n", category, dir)
files, err := parser.ParseDir(dir)
if err != nil {
log.Printf("Failed to parse %s: %v", dir, err)
return
}
if len(files) == 0 {
fmt.Printf("No YAML files found in %s\n", dir)
return
}
validator := parser.NewValidator()
validFiles := 0
totalErrors := 0
errorsByType := make(map[string]int)
for path, action := range files {
errors := validator.Validate(action)
filename := filepath.Base(path)
if len(errors) == 0 {
fmt.Printf("✅ %s\n", filename)
validFiles++
} else {
fmt.Printf("❌ %s (%d errors)\n", filename, len(errors))
totalErrors += len(errors)
// Count error types
for _, err := range errors {
errorsByType[err.Field]++
}
}
}
// Display summary
fmt.Printf("\n📊 %s Summary:\n", category)
fmt.Printf(" Total files: %d\n", len(files))
fmt.Printf(" Valid files: %d\n", validFiles)
fmt.Printf(" Files with errors: %d\n", len(files)-validFiles)
fmt.Printf(" Total errors: %d\n", totalErrors)
if len(errorsByType) > 0 {
fmt.Printf("\n🔍 Most common errors:\n")
for field, count := range errorsByType {
fmt.Printf(" %s: %d occurrences\n", field, count)
}
}
}
Custom Validation Rules
go
package main
import (
"fmt"
"strings"
"github.com/scagogogo/github-action-parser/pkg/parser"
)
// Custom validator with additional rules
type CustomValidator struct {
*parser.Validator
}
func NewCustomValidator() *CustomValidator {
return &CustomValidator{
Validator: parser.NewValidator(),
}
}
func (cv *CustomValidator) ValidateWithCustomRules(action *parser.ActionFile) []parser.ValidationError {
// Run standard validation first
errors := cv.Validator.Validate(action)
// Add custom validation rules
errors = append(errors, cv.validateNaming(action)...)
errors = append(errors, cv.validateSecurity(action)...)
errors = append(errors, cv.validateBestPractices(action)...)
return errors
}
func (cv *CustomValidator) validateNaming(action *parser.ActionFile) []parser.ValidationError {
var errors []parser.ValidationError
// Check action name follows conventions
if action.Name != "" {
if !strings.Contains(strings.ToLower(action.Name), "action") && len(action.Jobs) == 0 {
errors = append(errors, parser.ValidationError{
Field: "name",
Message: "Action name should contain 'Action' for clarity",
})
}
if len(action.Name) > 50 {
errors = append(errors, parser.ValidationError{
Field: "name",
Message: "Action name should be 50 characters or less",
})
}
}
return errors
}
func (cv *CustomValidator) validateSecurity(action *parser.ActionFile) []parser.ValidationError {
var errors []parser.ValidationError
// Check for hardcoded secrets in steps
for jobID, job := range action.Jobs {
for i, step := range job.Steps {
if step.Run != "" {
if strings.Contains(strings.ToLower(step.Run), "password") ||
strings.Contains(strings.ToLower(step.Run), "token") {
errors = append(errors, parser.ValidationError{
Field: fmt.Sprintf("jobs.%s.steps[%d].run", jobID, i),
Message: "Avoid hardcoding secrets in run commands",
})
}
}
}
}
return errors
}
func (cv *CustomValidator) validateBestPractices(action *parser.ActionFile) []parser.ValidationError {
var errors []parser.ValidationError
// Check for step names
for jobID, job := range action.Jobs {
unnamedSteps := 0
for _, step := range job.Steps {
if step.Name == "" {
unnamedSteps++
}
}
if unnamedSteps > len(job.Steps)/2 {
errors = append(errors, parser.ValidationError{
Field: fmt.Sprintf("jobs.%s.steps", jobID),
Message: "Consider adding names to steps for better readability",
})
}
}
// Check for action versioning
for jobID, job := range action.Jobs {
for i, step := range job.Steps {
if step.Uses != "" && strings.Contains(step.Uses, "actions/") {
if !strings.Contains(step.Uses, "@v") && !strings.Contains(step.Uses, "@main") {
errors = append(errors, parser.ValidationError{
Field: fmt.Sprintf("jobs.%s.steps[%d].uses", jobID, i),
Message: "Consider pinning action to a specific version",
})
}
}
}
}
return errors
}
func main() {
action, err := parser.ParseFile("action.yml")
if err != nil {
log.Fatal(err)
}
// Use custom validator
validator := NewCustomValidator()
errors := validator.ValidateWithCustomRules(action)
if len(errors) == 0 {
fmt.Println("✅ Action passes all validation rules!")
} else {
fmt.Printf("Found %d issues:\n", len(errors))
for _, err := range errors {
fmt.Printf(" - %s: %s\n", err.Field, err.Message)
}
}
}
Validation in CI/CD
Here's an example of how to use validation in a CI/CD pipeline:
go
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/scagogogo/github-action-parser/pkg/parser"
)
func main() {
exitCode := 0
// Validate all workflow files
if err := validateWorkflows(); err != nil {
fmt.Printf("❌ Workflow validation failed: %v\n", err)
exitCode = 1
}
// Validate action files
if err := validateActions(); err != nil {
fmt.Printf("❌ Action validation failed: %v\n", err)
exitCode = 1
}
if exitCode == 0 {
fmt.Println("✅ All validations passed!")
}
os.Exit(exitCode)
}
func validateWorkflows() error {
workflows, err := parser.ParseDir(".github/workflows")
if err != nil {
return err
}
validator := parser.NewValidator()
hasErrors := false
for path, workflow := range workflows {
errors := validator.Validate(workflow)
if len(errors) > 0 {
fmt.Printf("❌ %s:\n", filepath.Base(path))
for _, err := range errors {
fmt.Printf(" - %s: %s\n", err.Field, err.Message)
}
hasErrors = true
}
}
if hasErrors {
return fmt.Errorf("workflow validation failed")
}
return nil
}
func validateActions() error {
// Look for action.yml or action.yaml files
actionFiles := []string{"action.yml", "action.yaml"}
validator := parser.NewValidator()
hasErrors := false
for _, filename := range actionFiles {
if _, err := os.Stat(filename); os.IsNotExist(err) {
continue
}
action, err := parser.ParseFile(filename)
if err != nil {
return fmt.Errorf("failed to parse %s: %w", filename, err)
}
errors := validator.Validate(action)
if len(errors) > 0 {
fmt.Printf("❌ %s:\n", filename)
for _, err := range errors {
fmt.Printf(" - %s: %s\n", err.Field, err.Message)
}
hasErrors = true
}
}
if hasErrors {
return fmt.Errorf("action validation failed")
}
return nil
}
Next Steps
- Learn about Reusable Workflows
- Explore Utility Functions for advanced processing
- Check out the API Reference for detailed validation documentation