381 lines
9.4 KiB
Go
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())
|
|
}
|
|
})
|
|
}
|
|
}
|