package k8s import ( "os" "path/filepath" "runtime" "strings" "testing" "gopkg.in/yaml.v3" ) func workflowTemplatesDir() string { _, thisFile, _, _ := runtime.Caller(0) return filepath.Join(filepath.Dir(thisFile), "..", "..", "..", "..", "docker", "dev-golden", "forgejo-workflow-templates") } type workflowDoc struct { Name string `yaml:"name"` On map[string]interface{} `yaml:"on"` Jobs map[string]interface{} `yaml:"jobs"` } func readWorkflowYAML(t *testing.T, name string) workflowDoc { t.Helper() data, err := os.ReadFile(filepath.Join(workflowTemplatesDir(), name)) if err != nil { t.Fatalf("reading %s: %v", name, err) } var doc workflowDoc if err := yaml.Unmarshal(data, &doc); err != nil { t.Fatalf("parsing %s: %v", name, err) } return doc } func jobFromWorkflow(t *testing.T, doc workflowDoc, jobName string) map[string]interface{} { t.Helper() job, ok := doc.Jobs[jobName] if !ok { t.Fatalf("missing %s job", jobName) } jobMap, ok := job.(map[string]interface{}) if !ok { t.Fatalf("job %s is not a map", jobName) } return jobMap } func stepsFromJob(t *testing.T, job map[string]interface{}) []interface{} { t.Helper() steps, ok := job["steps"].([]interface{}) if !ok || len(steps) == 0 { t.Fatal("missing steps") } return steps } func stepNames(steps []interface{}) []string { var names []string for _, s := range steps { step := s.(map[string]interface{}) if name, ok := step["name"].(string); ok { names = append(names, name) } if uses, ok := step["uses"].(string); ok { names = append(names, uses) } } return names } func assertRunsOnSelfHosted(t *testing.T, job map[string]interface{}) { t.Helper() runsOn, ok := job["runs-on"].([]interface{}) if !ok { t.Fatal("missing runs-on") } for _, r := range runsOn { if r == "self-hosted" { return } } t.Error("expected 'self-hosted' in runs-on") } func assertStepsContain(t *testing.T, names []string, required []string) { t.Helper() for _, req := range required { found := false for _, name := range names { if strings.Contains(name, req) { found = true break } } if !found { t.Errorf("missing required step containing %q", req) } } } func TestWorkflowTemplatesExist(t *testing.T) { dir := workflowTemplatesDir() expected := []string{ "claude-implement.yml", "claude-review.yml", "ci-build.yml", } for _, name := range expected { path := filepath.Join(dir, name) if _, err := os.Stat(path); os.IsNotExist(err) { t.Errorf("missing workflow template: %s", path) } } } func TestClaudeImplementWorkflow(t *testing.T) { doc := readWorkflowYAML(t, "claude-implement.yml") t.Run("name", func(t *testing.T) { if doc.Name != "Claude Implement" { t.Errorf("expected name 'Claude Implement', got %v", doc.Name) } }) t.Run("trigger", func(t *testing.T) { ic, ok := doc.On["issue_comment"].(map[string]interface{}) if !ok { t.Fatal("missing issue_comment trigger") } types, ok := ic["types"].([]interface{}) if !ok || len(types) == 0 { t.Fatal("missing types for issue_comment") } if types[0] != "created" { t.Errorf("expected type 'created', got %v", types[0]) } }) t.Run("job_condition", func(t *testing.T) { impl := jobFromWorkflow(t, doc, "implement") ifCond, ok := impl["if"].(string) if !ok { t.Fatal("missing 'if' condition") } if !strings.Contains(ifCond, "@claude implement") { t.Error("'if' condition should check for '@claude implement'") } if !strings.Contains(ifCond, "pull_request == null") { t.Error("'if' condition should exclude PR comments") } }) t.Run("runs_on_self_hosted", func(t *testing.T) { impl := jobFromWorkflow(t, doc, "implement") assertRunsOnSelfHosted(t, impl) }) t.Run("has_required_steps", func(t *testing.T) { impl := jobFromWorkflow(t, doc, "implement") steps := stepsFromJob(t, impl) assertStepsContain(t, stepNames(steps), []string{ "actions/checkout@v4", "Configure git", "Extract task", "Run Claude Code", "Create branch and push", "Create pull request", }) }) t.Run("permissions", func(t *testing.T) { impl := jobFromWorkflow(t, doc, "implement") perms, ok := impl["permissions"].(map[string]interface{}) if !ok { t.Fatal("missing permissions") } if perms["contents"] != "write" { t.Error("expected contents: write permission") } if perms["pull-requests"] != "write" { t.Error("expected pull-requests: write permission") } }) } func TestClaudeReviewWorkflow(t *testing.T) { doc := readWorkflowYAML(t, "claude-review.yml") t.Run("name", func(t *testing.T) { if doc.Name != "Claude Review" { t.Errorf("expected name 'Claude Review', got %v", doc.Name) } }) t.Run("trigger", func(t *testing.T) { ic, ok := doc.On["issue_comment"].(map[string]interface{}) if !ok { t.Fatal("missing issue_comment trigger") } types, ok := ic["types"].([]interface{}) if !ok || len(types) == 0 { t.Fatal("missing types for issue_comment") } if types[0] != "created" { t.Errorf("expected type 'created', got %v", types[0]) } }) t.Run("job_condition", func(t *testing.T) { review := jobFromWorkflow(t, doc, "review") ifCond, ok := review["if"].(string) if !ok { t.Fatal("missing 'if' condition") } if !strings.Contains(ifCond, "@claude review") { t.Error("'if' condition should check for '@claude review'") } if !strings.Contains(ifCond, "pull_request != null") { t.Error("'if' condition should require PR context") } }) t.Run("runs_on_self_hosted", func(t *testing.T) { review := jobFromWorkflow(t, doc, "review") assertRunsOnSelfHosted(t, review) }) t.Run("has_required_steps", func(t *testing.T) { review := jobFromWorkflow(t, doc, "review") steps := stepsFromJob(t, review) assertStepsContain(t, stepNames(steps), []string{ "actions/checkout@v4", "Fetch base branch", "Run Claude Code review", "Post review comment", }) }) t.Run("checkout_uses_pr_ref", func(t *testing.T) { review := jobFromWorkflow(t, doc, "review") steps := stepsFromJob(t, review) for _, s := range steps { step := s.(map[string]interface{}) if uses, ok := step["uses"].(string); ok && strings.Contains(uses, "checkout") { with, ok := step["with"].(map[string]interface{}) if !ok { t.Fatal("checkout step missing 'with' for PR ref") } ref, _ := with["ref"].(string) if ref == "" { t.Error("checkout step should specify ref for PR branch") } return } } t.Error("no checkout step found") }) t.Run("permissions", func(t *testing.T) { review := jobFromWorkflow(t, doc, "review") perms, ok := review["permissions"].(map[string]interface{}) if !ok { t.Fatal("missing permissions") } if perms["pull-requests"] != "write" { t.Error("expected pull-requests: write permission") } }) } func TestCIBuildWorkflow(t *testing.T) { doc := readWorkflowYAML(t, "ci-build.yml") t.Run("name", func(t *testing.T) { if doc.Name != "CI Build" { t.Errorf("expected name 'CI Build', got %v", doc.Name) } }) t.Run("trigger", func(t *testing.T) { push, ok := doc.On["push"].(map[string]interface{}) if !ok { t.Fatal("missing push trigger") } branches, ok := push["branches"].([]interface{}) if !ok || len(branches) == 0 { t.Fatal("missing branches for push trigger") } if branches[0] != "main" { t.Errorf("expected branch 'main', got %v", branches[0]) } }) t.Run("job_exists", func(t *testing.T) { jobFromWorkflow(t, doc, "build") }) t.Run("runs_on_self_hosted", func(t *testing.T) { build := jobFromWorkflow(t, doc, "build") assertRunsOnSelfHosted(t, build) }) t.Run("has_checkout_and_build_steps", func(t *testing.T) { build := jobFromWorkflow(t, doc, "build") steps := stepsFromJob(t, build) names := stepNames(steps) hasCheckout := false hasBuild := false for _, n := range names { if strings.Contains(n, "checkout") { hasCheckout = true } if strings.Contains(strings.ToLower(n), "build") { hasBuild = true } } if !hasCheckout { t.Error("missing checkout step") } if !hasBuild { t.Error("missing build step") } }) t.Run("build_detects_multiple_languages", func(t *testing.T) { build := jobFromWorkflow(t, doc, "build") steps := stepsFromJob(t, build) for _, s := range steps { step := s.(map[string]interface{}) name, _ := step["name"].(string) if !strings.Contains(strings.ToLower(name), "build") { continue } run, ok := step["run"].(string) if !ok { t.Fatal("build step missing run command") } for _, marker := range []string{"Cargo.toml", "go.mod", "package.json"} { if !strings.Contains(run, marker) { t.Errorf("build step should detect %s", marker) } } return } t.Error("no build step found with run command") }) } func TestWorkflowTemplatesAreValidYAML(t *testing.T) { dir := workflowTemplatesDir() entries, err := os.ReadDir(dir) if err != nil { t.Fatalf("reading templates dir: %v", err) } for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yml") { continue } t.Run(entry.Name(), func(t *testing.T) { data, err := os.ReadFile(filepath.Join(dir, entry.Name())) if err != nil { t.Fatalf("reading %s: %v", entry.Name(), err) } var doc workflowDoc if err := yaml.Unmarshal(data, &doc); err != nil { t.Fatalf("invalid YAML in %s: %v", entry.Name(), err) } if doc.Name == "" { t.Errorf("%s missing 'name' field", entry.Name()) } if doc.Jobs == nil { t.Errorf("%s missing 'jobs' field", entry.Name()) } }) } }