repositories / bugabinga.net
bugabinga.net
personal infrastructure for bugabinga!
owned by admin
services/luci/internal/ciconfig/ciconfig_test.go
Rawpackage ciconfig
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/sblinch/kdl-go/document"
)
func TestRepositoryCIConfig(t *testing.T) {
repoRoot, err := filepath.Abs(filepath.Join("..", "..", "..", ".."))
if err != nil {
t.Fatal(err)
}
cfg, err := Load(repoRoot)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
want := map[string]bool{
"go-test": false,
"go-race": false,
"go-staticcheck": false,
"go-vuln": false,
"rust-fmt": false,
"rust-clippy": false,
"rust-test": false,
"shell": false,
"infra-tofu-route": false,
"infra-quadlets": false,
}
for _, job := range cfg.Jobs {
if _, ok := want[job.Name]; !ok {
continue
}
if job.Name == "shell" && job.SourceFile == "shell.kdl" || (job.Name == "infra-tofu-route" || job.Name == "infra-quadlets") && job.SourceFile == "infra.kdl" || job.Name != "shell" && job.Name != "infra-tofu-route" && job.Name != "infra-quadlets" && job.SourceFile == "go.kdl" {
want[job.Name] = true
}
}
for name, found := range want {
if !found {
t.Errorf("repository CI job %q missing", name)
}
}
for _, job := range cfg.Jobs {
if job.SourceFile != "go.kdl" && job.SourceFile != "shell.kdl" && job.SourceFile != "infra.kdl" {
continue
}
switch {
case strings.HasPrefix(job.Name, "go-"):
if job.Name != "go-vuln" && (len(job.Run) != 2 || job.Trigger.Push == nil || !contains(job.Trigger.Push.Include, "scripts/local/go.sh") || !contains(job.Trigger.Push.Include, "scripts/local/check-staged.sh")) {
t.Errorf("repository CI job %q is missing Go automation coverage: %#v", job.Name, job)
}
for _, command := range job.Run {
if strings.Contains(command, "cd ") || !strings.Contains(command, "scripts/local/go.sh") || !strings.Contains(command, "GOTOOLCHAIN=local") || !strings.Contains(command, "GOMODCACHE=$PWD/.go-modcache") {
t.Errorf("repository CI command %q must run bounded Go tooling from the workspace root", command)
}
}
case strings.HasPrefix(job.Name, "rust-"):
if job.Image != "docker.io/library/rust:1.98.0-bookworm" || job.Trigger.Push == nil || !contains(job.Trigger.Push.Include, "services/luigit/**") || !contains(job.Trigger.Push.Include, "scripts/local/rust.sh") {
t.Errorf("repository CI job %q is missing Rust automation coverage: %#v", job.Name, job)
}
if job.Name == "rust-fmt" {
if len(job.Caches) != 0 || len(job.Run) != 1 || !strings.Contains(job.Run[0], "rustup component add rustfmt") {
t.Errorf("repository CI fmt job must install rustfmt without a cache: %#v", job)
}
} else if len(job.Caches) != 1 || job.Caches[0].Path != ".cargo-home" {
t.Errorf("repository CI job %q must cache Cargo state: %#v", job.Name, job)
}
for _, command := range job.Run {
if strings.Contains(command, "cd ") || strings.Contains(command, "apk ") || !strings.Contains(command, "scripts/local/rust.sh") {
t.Errorf("repository CI command %q must use Rust tooling from the workspace root", command)
}
}
case strings.HasPrefix(job.Name, "infra-"):
if job.SourceFile != "infra.kdl" || job.Trigger.Push == nil || !contains(job.Trigger.Push.Include, "scripts/local/verify-infra.sh") || !contains(job.Trigger.Push.Include, "scripts/local/test-verify-infra.sh") || job.Name == "infra-tofu-route" && !contains(job.Trigger.Push.Include, "modules/klops/**") {
t.Errorf("repository infrastructure CI job is incomplete: %#v", job)
}
if job.Name == "infra-tofu-route" && (len(job.Run) != 2 || strings.Contains(job.Run[1], "tofu version") || !strings.Contains(job.Run[1], "just verify infra tofu-route")) {
t.Errorf("repository OpenTofu CI job must delegate diagnostics to verification: %#v", job)
}
if job.Name == "infra-quadlets" && (len(job.Run) != 2 || strings.Contains(job.Run[0], "dnf") || !strings.Contains(job.Run[0], "install-just.sh") || !contains(job.Trigger.Push.Include, "scripts/local/install-just.sh")) {
t.Errorf("repository Quadlet CI job must bootstrap pinned Just without dnf: %#v", job)
}
case job.Name == "shell":
if job.SourceFile != "shell.kdl" || job.Image != "docker.io/library/alpine:3.22.0" || job.Trigger.Push == nil || !contains(job.Trigger.Push.Include, "**/*.sh") || !contains(job.Trigger.Push.Include, "*.sh") || len(job.Run) != 3 || !strings.Contains(job.Run[0], "apk --root $PWD/.apk --initdb") || !strings.Contains(job.Run[0], "add --no-cache bash git shfmt shellcheck") || !strings.Contains(job.Run[1], "shellcheck --version") || !strings.Contains(job.Run[2], "scripts/local/shell-check.sh read") {
t.Errorf("repository shell CI job is incomplete: %#v", job)
}
for _, command := range job.Run {
if strings.Contains(command, "go.sh") || strings.Contains(command, "tofu") {
t.Errorf("repository shell CI command %q must not run Go or OpenTofu", command)
}
}
}
}
}
func TestRepositoryCIPathFilters(t *testing.T) {
repoRoot, err := filepath.Abs(filepath.Join("..", "..", "..", ".."))
if err != nil {
t.Fatal(err)
}
cfg, err := Load(repoRoot)
if err != nil {
t.Fatal(err)
}
assertMatches := func(path string, want ...string) {
t.Helper()
got := map[string]bool{}
for _, job := range cfg.Jobs {
if job.MatchesPush("refs/heads/trunk", []string{path}) {
got[job.Name] = true
}
}
for _, name := range want {
if !got[name] {
t.Errorf("change %q did not select %q: %#v", path, name, got)
}
delete(got, name)
}
if len(got) != 0 {
t.Errorf("change %q selected unexpected jobs: %#v", path, got)
}
}
assertMatches("services/luci/cmd/luci/main.go", "go-test", "go-race", "go-staticcheck")
assertMatches("services/toad/cmd/toad/main.go", "go-test", "go-race", "go-staticcheck", "infra-tofu-route")
assertMatches("services/luigit/src/main.rs", "rust-fmt", "rust-clippy", "rust-test", "infra-tofu-route")
assertMatches("scripts/local/go.sh", "go-test", "go-race", "go-staticcheck", "shell")
assertMatches("scripts/local/rust.sh", "rust-fmt", "rust-clippy", "rust-test", "shell")
assertMatches("scripts/local/shell-check.sh", "shell")
assertMatches("scripts/local/with space.sh", "shell")
assertMatches(".ci/go.kdl", "go-test", "go-race", "go-staticcheck", "rust-fmt", "rust-clippy", "rust-test")
assertMatches(".ci/shell.kdl", "go-test", "go-race", "go-staticcheck", "rust-fmt", "rust-clippy", "rust-test", "shell")
assertMatches(".ci/infra.kdl", "go-test", "go-race", "go-staticcheck", "rust-fmt", "rust-clippy", "rust-test", "infra-tofu-route", "infra-quadlets")
assertMatches("modules/klops/quadlets/caddy.container", "infra-tofu-route", "infra-quadlets")
assertMatches("services/toad/deploy/gateway.Caddyfile", "go-test", "go-race", "go-staticcheck", "infra-tofu-route")
assertMatches("scripts/local/install-just.sh", "infra-quadlets", "shell")
assertMatches("README.md")
}
func TestParseDurableMounts(t *testing.T) {
cfg, err := ParseFiles(map[string]string{"ci.kdl": `job "deploy" {
image "alpine"
run "true"
mount "auth" "registry" "/root/.config/containers"
mount "cache" "build" "/var/cache/build"
}`})
if err != nil || len(cfg.Jobs[0].Mounts) != 2 {
t.Fatalf("mounts=%#v err=%v", cfg, err)
}
for _, source := range []string{
`mount "host" "name" "/target"`,
`mount "auth" "../name" "/target"`,
`mount "auth" "name" "target"`,
`mount "cache" "one" "/same" mount "auth" "two" "/same/path"`,
`mount "cache" "one" "/work/cache"`,
} {
_, err := ParseFiles(map[string]string{"ci.kdl": `job "bad" { image "alpine" run "true" ` + source + ` }`})
if err == nil {
t.Fatalf("accepted %s", source)
}
}
}
func contains(values []string, want string) bool {
for _, value := range values {
if value == want {
return true
}
}
return false
}
func TestLoadReadsCiFilesLexicographically(t *testing.T) {
repo := t.TempDir()
mustWrite(t, filepath.Join(repo, ".ci", "20-second.kdl"), `
job "second" {
image "alpine"
run "echo second"
}
`)
mustWrite(t, filepath.Join(repo, ".ci", "10-first.kdl"), `
job "first" {
image "alpine"
run "echo first"
}
`)
cfg, err := Load(repo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if got := []string{cfg.Jobs[0].Name, cfg.Jobs[1].Name}; got[0] != "first" || got[1] != "second" {
t.Fatalf("job order = %#v", got)
}
}
func TestParseFilesTracksDeterministicJobSource(t *testing.T) {
cfg, err := ParseFiles(map[string]string{
"20-second.kdl": "job \"second\" {\n image \"alpine\"\n run \"echo second\"\n}",
"10-first.kdl": "job \"first\" {\n image \"alpine\"\n run \"echo first\"\n}",
})
if err != nil {
t.Fatal(err)
}
if got, want := cfg.Jobs[0].Name, "first"; got != want {
t.Fatalf("first job=%q", got)
}
if got, want := cfg.Jobs[0].SourceFile, "10-first.kdl"; got != want {
t.Fatalf("source file=%q", got)
}
if got := cfg.Jobs[0].Source; !strings.Contains(got, `job "first"`) || !strings.Contains(got, `run "echo first"`) {
t.Fatalf("source=%q", got)
}
}
func TestLoadRejectsDuplicateJobs(t *testing.T) {
repo := t.TempDir()
mustWrite(t, filepath.Join(repo, ".ci", "a.kdl"), `
job "check" {
image "alpine"
run "echo a"
}
`)
mustWrite(t, filepath.Join(repo, ".ci", "b.kdl"), `
job "check" {
image "alpine"
run "echo b"
}
`)
_, err := Load(repo)
if err == nil {
t.Fatal("duplicate job accepted")
}
if got := err.Error(); got != "duplicate job \"check\"" {
t.Fatalf("unexpected error: %s", got)
}
}
func TestLoadParsesTriggerAndMatrix(t *testing.T) {
repo := t.TempDir()
mustWrite(t, filepath.Join(repo, ".ci", "ci.kdl"), `
job "test" {
image "alpine:${version}"
run "echo ${version}"
trigger {
push {
branch "main"
tag "v*"
}
schedule "0 3 * * *"
}
matrix {
version "3.20" "3.21"
}
}
`)
cfg, err := Load(repo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
job := cfg.Jobs[0]
if job.Trigger.Push == nil || len(job.Trigger.Push.Branches) != 1 || job.Trigger.Push.Branches[0] != "main" || len(job.Trigger.Push.Tags) != 1 || job.Trigger.Push.Tags[0] != "v*" || job.Trigger.Schedule == nil {
t.Fatalf("trigger = %#v", job.Trigger)
}
if len(job.Matrix.Axes) != 1 || job.Matrix.Axes[0].Name != "version" || len(job.Matrix.Axes[0].Values) != 2 || job.Matrix.Axes[0].Values[1] != "3.21" {
t.Fatalf("matrix = %#v", job.Matrix)
}
}
func TestLoadParsesOptInPushPathsAndSchedule(t *testing.T) {
repo := t.TempDir()
mustWrite(t, filepath.Join(repo, ".ci", "ci.kdl"), `job "test" {
image "alpine"
run "echo ok"
trigger {
push {
branch "trunk"
include "src/**"
exclude "src/generated/**"
}
schedule "*/15 * * * *"
}
}
job "manual" {
image "alpine"
run "echo manual"
}
`)
cfg, err := Load(repo)
if err != nil {
t.Fatal(err)
}
push := cfg.Jobs[0].Trigger.Push
if push == nil || len(push.Include) != 1 || len(push.Exclude) != 1 || cfg.Jobs[0].Trigger.Schedule == nil {
t.Fatalf("trigger=%#v", cfg.Jobs[0].Trigger)
}
if cfg.Jobs[1].Trigger.Push != nil || cfg.Jobs[1].Trigger.Schedule != nil {
t.Fatalf("manual trigger=%#v", cfg.Jobs[1].Trigger)
}
}
func TestScheduleMatchesUTCMinute(t *testing.T) {
minute := time.Date(2026, 3, 4, 3, 0, 45, 0, time.FixedZone("local", 3600))
if !ScheduleMatches("0 2 * * *", minute) {
t.Fatal("UTC minute schedule did not match")
}
}
func TestParseScheduleBoundsMalformedInput(t *testing.T) {
_, err := ParseSchedule(strings.Repeat("1", 70000) + " 0 * * *")
if err == nil || len(err.Error()) > 100 {
t.Fatalf("err=%v", err)
}
}
func TestLoadRejectsOversizedMatrix(t *testing.T) {
repo := t.TempDir()
mustWrite(t, filepath.Join(repo, ".ci", "ci.kdl"), `job "large" {
image "alpine"
run "echo ok"
matrix {
a "1" "2" "3" "4" "5" "6" "7" "8" "9"
b "1" "2" "3" "4" "5" "6" "7" "8"
}
}
`)
if _, err := Load(repo); err == nil {
t.Fatal("oversized matrix accepted")
}
}
func TestLoadRejectsBadTriggerAndMatrix(t *testing.T) {
cases := map[string]string{
"bad-schedule": `
job "test" {
image "alpine"
run "echo ok"
trigger {
schedule "@daily"
}
}
`,
"timezone-schedule": `
job "test" {
image "alpine"
run "echo ok"
trigger {
schedule "TZ=UTC 0 0 * *"
}
}
`,
"push-property": `
job "test" {
image "alpine"
run "echo ok"
trigger {
push branch="trunk"
}
}
`,
"duplicate-schedule": `
job "test" {
image "alpine"
run "echo ok"
trigger {
schedule "0 3 * * *"
schedule "0 4 * * *"
}
}
`,
"bad-path-glob": `
job "test" {
image "alpine"
run "echo ok"
trigger {
push {
include "["
}
}
}
`,
"bad-trigger-child": `
job "test" {
image "alpine"
run "echo ok"
trigger { timer true }
}
`,
"bad-push-type": `
job "test" {
image "alpine"
run "echo ok"
trigger { push "yes" }
}
`,
"bad-push-args": `
job "test" {
image "alpine"
run "echo ok"
trigger { push true false }
}
`,
"bad-branch-args": `
job "test" {
image "alpine"
run "echo ok"
trigger { branch "main" "dev" }
}
`,
"bad-tag-args": `
job "test" {
image "alpine"
run "echo ok"
trigger { tag "v*" "x*" }
}
`,
"numeric-matrix-value": `
job "test" {
image "alpine"
run "echo ok"
matrix { node 22 }
}
`,
"duplicate-axis": `
job "test" {
image "alpine"
run "echo ok"
matrix { arch "amd64" arch "arm64" }
}
`,
"empty-axis": `
job "test" {
image "alpine"
run "echo ok"
matrix { arch }
}
`,
}
for name, content := range cases {
t.Run(name, func(t *testing.T) {
repo := t.TempDir()
mustWrite(t, filepath.Join(repo, ".ci", "ci.kdl"), content)
if _, err := Load(repo); err == nil {
t.Fatal("bad config accepted")
}
})
}
}
func TestLoadRejectsBadFilesAndJobShape(t *testing.T) {
if _, err := Load("["); err == nil {
t.Fatal("bad glob accepted")
}
repo := t.TempDir()
if err := os.MkdirAll(filepath.Join(repo, ".ci"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.Symlink("missing", filepath.Join(repo, ".ci", "missing.kdl")); err != nil {
t.Fatal(err)
}
if _, err := Load(repo); err == nil {
t.Fatal("broken symlink accepted")
}
cases := map[string]string{
"invalid-kdl": `job "x" {`,
"top-level": `pipeline "x" {}`,
"job-no-arg": `job { image "alpine" run "echo ok" }`,
"unknown-job-node": `job "x" { image "alpine" run "echo ok" artifact "x" }`,
"bad-image-args": `job "x" { image "alpine" "edge" run "echo ok" }`,
"bad-run-args": `job "x" { image "alpine" run }`,
"missing-image": `job "x" { run "echo ok" }`,
"missing-run": `job "x" { image "alpine" }`,
"secret-no-arg": `job "x" { image "alpine" run "echo ok" secret env="TOKEN" }`,
"secret-both": `job "x" { image "alpine" run "echo ok" secret "TOKEN" env="TOKEN" file="token" }`,
"secret-neither": `job "x" { image "alpine" run "echo ok" secret "TOKEN" }`,
}
for name, content := range cases {
t.Run(name, func(t *testing.T) {
repo := t.TempDir()
mustWrite(t, filepath.Join(repo, ".ci", "ci.kdl"), content)
if _, err := Load(repo); err == nil {
t.Fatal("bad config accepted")
}
})
}
}
func TestLoadRejectsMultilineBadJobShapes(t *testing.T) {
cases := map[string]string{
"job-no-arg": `job {
image "alpine"
run "echo ok"
}
`,
"unknown-job-node": `job "x" {
image "alpine"
run "echo ok"
artifact "x"
}
`,
"bad-image-args": `job "x" {
image "alpine" "edge"
run "echo ok"
}
`,
"bad-run-args": `job "x" {
image "alpine"
run
}
`,
"missing-image": `job "x" {
run "echo ok"
}
`,
"missing-run": `job "x" {
image "alpine"
}
`,
"secret-no-arg": `job "x" {
image "alpine"
run "echo ok"
secret env="TOKEN"
}
`,
"secret-both": `job "x" {
image "alpine"
run "echo ok"
secret "TOKEN" env="TOKEN" file="token"
}
`,
"secret-neither": `job "x" {
image "alpine"
run "echo ok"
secret "TOKEN"
}
`,
"bad-trigger-branch": `job "x" {
image "alpine"
run "echo ok"
trigger {
branch "main" "dev"
}
}
`,
"bad-trigger-tag": `job "x" {
image "alpine"
run "echo ok"
trigger {
tag "v*" "x*"
}
}
`,
"bad-trigger-push": `job "x" {
image "alpine"
run "echo ok"
trigger {
push true false
}
}
`,
"bad-trigger-push-type": `job "x" {
image "alpine"
run "echo ok"
trigger {
push "yes"
}
}
`,
"bad-trigger-child": `job "x" {
image "alpine"
run "echo ok"
trigger {
timer true
}
}
`,
"bad-matrix-duplicate": `job "x" {
image "alpine"
run "echo ok"
matrix {
arch "amd64"
arch "arm64"
}
}
`,
"bad-matrix-empty": `job "x" {
image "alpine"
run "echo ok"
matrix {
arch
}
}
`,
"unsafe-secret-file": `job "x" {
image "alpine"
run "echo ok"
secret "KEY" file="../key"
}
`,
"bad-cache": `job "x" {
image "alpine"
run "echo ok"
cache "deps" {
path "../deps"
key { file "lock" }
}
}
`,
"bad-publish": `job "x" {
image "alpine"
run "echo ok"
publish "site" { from "dist" }
}
`,
"registry-host-image": `job "x" {
image "alpine"
run "echo ok"
publish "registry" {
image "app:latest"
to "registry.invalid/app:latest"
}
}
`,
"registry-missing-archive": `job "x" {
image "alpine"
run "echo ok"
publish "registry" { to "registry.invalid/app:latest" }
}
`,
}
for name, content := range cases {
t.Run(name, func(t *testing.T) {
repo := t.TempDir()
mustWrite(t, filepath.Join(repo, ".ci", "ci.kdl"), content)
if _, err := Load(repo); err == nil {
t.Fatal("bad config accepted")
}
})
}
}
func TestRegistryImagePublishHasMigrationError(t *testing.T) {
_, err := ParseFiles(map[string]string{"ci.kdl": `job "x" {
image "alpine"
run "true"
publish "registry" {
image "app:latest"
to "registry.invalid/app:latest"
}
}`})
if err == nil || !strings.Contains(err.Error(), "write an OCI archive") {
t.Fatalf("err=%v", err)
}
}
func TestLowLevelParserHelpers(t *testing.T) {
if nodeName(nil) != "" || nodeName(&document.Node{}) != "" {
t.Fatal("nil node name not empty")
}
if _, err := oneStringArg(&document.Node{}, "x"); err == nil {
t.Fatal("oneStringArg accepted missing arg")
}
if _, err := oneBoolArg(&document.Node{}, "x"); err == nil {
t.Fatal("oneBoolArg accepted missing arg")
}
if stringValue(nil) != "" {
t.Fatal("nil stringValue not empty")
}
}
func TestLoadParsesNumericMatrixValue(t *testing.T) {
repo := t.TempDir()
mustWrite(t, filepath.Join(repo, ".ci", "ci.kdl"), `job "x" {
image "alpine"
run "echo ok"
matrix {
node 22
}
}
`)
cfg, err := Load(repo)
if err != nil {
t.Fatal(err)
}
if cfg.Jobs[0].Matrix.Axes[0].Values[0] != "22" {
t.Fatalf("matrix = %#v", cfg.Jobs[0].Matrix)
}
}
func TestLoadParsesCoreJobFields(t *testing.T) {
repo := t.TempDir()
mustWrite(t, filepath.Join(repo, ".ci", "ci.kdl"), `
job "test" {
image "docker.io/library/node:${node}-alpine"
run "npm ci"
run "npm test"
secret "HCLOUD_TOKEN" env="HCLOUD_TOKEN"
secret "DEPLOY_KEY" file=".secrets/deploy_key"
cache "npm" {
path ".npm"
key {
file "package-lock.json"
}
}
publish "site" {
from "dist"
to "docs/"
}
}
`)
cfg, err := Load(repo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
job := cfg.Jobs[0]
if job.Name != "test" || job.Image != "docker.io/library/node:${node}-alpine" {
t.Fatalf("job = %#v", job)
}
if len(job.Run) != 2 || job.Run[0] != "npm ci" || job.Run[1] != "npm test" {
t.Fatalf("run = %#v", job.Run)
}
if len(job.Secrets) != 2 || job.Secrets[0].Name != "HCLOUD_TOKEN" || job.Secrets[0].Env != "HCLOUD_TOKEN" || job.Secrets[1].File != ".secrets/deploy_key" {
t.Fatalf("secrets = %#v", job.Secrets)
}
if len(job.Caches) != 1 || job.Caches[0].Name != "npm" || job.Caches[0].Path != ".npm" || len(job.Caches[0].KeyFiles) != 1 {
t.Fatalf("caches = %#v", job.Caches)
}
if len(job.Publishes) != 1 || job.Publishes[0].Adapter != "site" || job.Publishes[0].From != "dist" || job.Publishes[0].To != "docs/" {
t.Fatalf("publishes = %#v", job.Publishes)
}
}
func mustWrite(t *testing.T, path string, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
func TestConfigJobMissing(t *testing.T) {
cfg := Config{Jobs: []Job{{Name: "check"}}}
if job, ok := cfg.Job("missing"); ok || job.Name != "" {
t.Fatalf("missing job = %#v %v", job, ok)
}
}
func TestTriggerMatching(t *testing.T) {
all := &Push{}
if !(Job{Name: "check", Trigger: Trigger{Push: all}}).MatchesPush("refs/heads/anything", nil) {
t.Fatal("empty branch patterns did not match")
}
if !(Job{Name: "check", Trigger: Trigger{Push: all}}).MatchesPush("refs/tags/v1", nil) {
t.Fatal("empty tag patterns did not match")
}
if (Job{Name: "check", Trigger: Trigger{Push: &Push{Branches: []string{"main"}}}}).MatchesPush("refs/tags/v1", nil) {
t.Fatal("branch-only trigger matched tag")
}
if (Job{Name: "check", Trigger: Trigger{Push: &Push{Tags: []string{"v*"}}}}).MatchesPush("refs/heads/main", nil) {
t.Fatal("tag-only trigger matched branch")
}
job := Job{Name: "check", Trigger: Trigger{Push: &Push{Branches: []string{"main", "release/*"}, Tags: []string{"v*"}, Include: []string{"src/**"}, Exclude: []string{"src/generated/**"}}}}
if !job.MatchesPush("refs/heads/main", []string{"src/main/app.go"}) {
t.Fatal("included nested path did not match")
}
if job.MatchesPush("refs/heads/main", []string{"src/generated/app.go"}) {
t.Fatal("excluded path matched")
}
if !job.MatchesPush("refs/tags/v1.0.0", []string{"src/app.go"}) {
t.Fatal("tag did not match")
}
if job.MatchesPush("refs/heads/feature/x", []string{"src/app.go"}) {
t.Fatal("feature branch matched")
}
if (Job{Name: "manual"}).MatchesPush("refs/heads/main", nil) {
t.Fatal("manual-only job matched push")
}
}
func TestManualLookupIgnoresTriggers(t *testing.T) {
cfg := Config{Jobs: []Job{{Name: "deploy"}}}
job, ok := cfg.Job("deploy")
if !ok || job.Name != "deploy" {
t.Fatalf("manual lookup failed: %#v %v", job, ok)
}
}
func TestMatrixExpansionSingleJob(t *testing.T) {
expanded := (Job{Name: "check"}).ExpandMatrix()
if len(expanded) != 1 || expanded[0].Name != "check" || len(expanded[0].Values) != 0 {
t.Fatalf("expanded = %#v", expanded)
}
}
func TestMatrixExpansionOrder(t *testing.T) {
job := Job{
Name: "test",
Matrix: Matrix{Axes: []Axis{
{Name: "node", Values: []string{"22", "24"}},
{Name: "distro", Values: []string{"fedora43", "debian13"}},
}},
}
expanded := job.ExpandMatrix()
wantNames := []string{
"test[node=22,distro=fedora43]",
"test[node=22,distro=debian13]",
"test[node=24,distro=fedora43]",
"test[node=24,distro=debian13]",
}
for i, want := range wantNames {
if expanded[i].Name != want {
t.Fatalf("expanded[%d].Name = %q, want %q", i, expanded[i].Name, want)
}
}
if expanded[2].Values["node"] != "24" || expanded[2].Values["distro"] != "fedora43" {
t.Fatalf("expanded[2].Values = %#v", expanded[2].Values)
}
}
func TestValidateSecretPaths(t *testing.T) {
job := Job{Name: "deploy", Secrets: []Secret{{Name: "HCLOUD_TOKEN", Env: "HCLOUD_TOKEN"}}}
paths, err := job.SecretPaths("/data/ci", "bugabinga.net")
if err != nil {
t.Fatalf("SecretPaths() error = %v", err)
}
want := filepath.Join("/data/ci", "secrets", "bugabinga.net", "deploy", "HCLOUD_TOKEN")
if len(paths) != 1 || paths["HCLOUD_TOKEN"] != want {
t.Fatalf("paths = %#v", paths)
}
bad := Job{Name: "../deploy", Secrets: []Secret{{Name: "TOKEN", Env: "TOKEN"}}}
if _, err := bad.SecretPaths("/data/ci", "bugabinga.net"); err == nil {
t.Fatal("unsafe job name accepted")
}
}
func TestRejectsSecretEnvironmentCollisions(t *testing.T) {
for name, source := range map[string]string{
"reserved": `job "check" {
image "alpine"
run "true"
secret "token" env="CI_RUN_ID"
}`,
"duplicate": `job "check" {
image "alpine"
run "true"
secret "first" env="TOKEN"
secret "second" env="TOKEN"
}`,
"matrix": `job "check" {
image "alpine"
run "true"
matrix {
foo-bar "x"
foo_bar "y"
}
}`,
} {
t.Run(name, func(t *testing.T) {
if _, err := ParseFiles(map[string]string{"ci.kdl": source}); err == nil {
t.Fatal("invalid environment accepted")
}
})
}
}