dev-pod-api-build/internal/k8s/workflow_templates_test.go
2026-04-16 04:16:36 +00:00

381 lines
9.4 KiB
Go

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())
}
})
}
}