diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26387899..a155bdd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,6 +138,7 @@ jobs: cache: true - run: test -z "$(gofmt -l cmd internal third_party)" - run: go vet ./... + - run: go vet -C internal/incusplacement/starlarkexec ./... - run: go build -trimpath ./... derivative: diff --git a/internal/incusplacement/race_off.go b/internal/incusplacement/race_off.go new file mode 100644 index 00000000..f16a8aab --- /dev/null +++ b/internal/incusplacement/race_off.go @@ -0,0 +1,5 @@ +//go:build !race + +package incusplacement + +const raceEnabled = false diff --git a/internal/incusplacement/race_on.go b/internal/incusplacement/race_on.go new file mode 100644 index 00000000..316879fc --- /dev/null +++ b/internal/incusplacement/race_on.go @@ -0,0 +1,5 @@ +//go:build race + +package incusplacement + +const raceEnabled = true diff --git a/internal/incusplacement/starlarkexec/execute.go b/internal/incusplacement/starlarkexec/execute.go new file mode 100644 index 00000000..4980a59b --- /dev/null +++ b/internal/incusplacement/starlarkexec/execute.go @@ -0,0 +1,271 @@ +// Nested module so packing tests can import go.starlark.net without +// changing the root go.mod that pins the provider derivative. +package starlarkexec + +import ( + "errors" + "fmt" + "strconv" + "strings" + + "github.com/lxc/incus/v7/shared/api" + apiScriptlet "github.com/lxc/incus/v7/shared/api/scriptlet" + "github.com/lxc/incus/v7/shared/scriptlet" + "go.starlark.net/starlark" +) + +// The compile-time names Incus v6.0.6 / v7 InstancePlacementCompile injects. +// get_instances does not take pending; get_instances_count does. +var placementPredeclared = []string{ + "log_info", + "log_warn", + "log_error", + "set_target", + "get_cluster_member_resources", + "get_cluster_member_state", + "get_instance_resources", + "get_instances", + "get_instances_count", + "get_cluster_members", + "get_project", +} + +// ClusterSnapshot is the Incus view the scriptlet reads. MemoryTotalBytes is +// RAM only; swap is not a schedulable placement input. +type ClusterSnapshot struct { + PoolName string + Members []MemberSnapshot +} + +type MemberSnapshot struct { + Name string + Config map[string]string + MemoryTotalBytes uint64 + CPUTotal uint64 + LoadAverage float64 + PoolTotalBytes uint64 + PoolUsedBytes uint64 + Instances []InstanceSnapshot + // PendingCount is get_instances_count(project, location, true): current + // instances plus InstanceCreate operations. It is not extras-only; the + // script subtracts len(instances) to size creates that have no record yet. + PendingCount int +} + +type InstanceSnapshot struct { + Name string + MemoryLimitMiB int +} + +type PlacementRequest struct { + Project string + Name string + MemorySize uint64 + RootDiskSize uint64 +} + +type PlacementOutcome struct { + Target string + Failed bool + FailMessage string + Logs []string +} + +// Execute compiles and runs a rendered placement scriptlet against a mocked +// Incus cluster. Builtins match Incus instance-placement signatures so a +// strings.Contains test cannot hide a live call-shape or packing bug. +func Execute(script string, request PlacementRequest, cluster ClusterSnapshot) (PlacementOutcome, error) { + membersByName := make(map[string]MemberSnapshot, len(cluster.Members)) + candidates := make([]*api.ClusterMember, 0, len(cluster.Members)) + for _, member := range cluster.Members { + if member.Name == "" { + return PlacementOutcome{}, fmt.Errorf("cluster snapshot member is missing a name") + } + if _, duplicate := membersByName[member.Name]; duplicate { + return PlacementOutcome{}, fmt.Errorf("cluster snapshot repeats member %q", member.Name) + } + membersByName[member.Name] = member + config := api.ConfigMap{} + for key, value := range member.Config { + config[key] = value + } + candidates = append(candidates, &api.ClusterMember{ + ServerName: member.Name, + ClusterMemberPut: api.ClusterMemberPut{Config: config}, + }) + } + + var outcome PlacementOutcome + setTarget := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var memberName string + if err := starlark.UnpackArgs(b.Name(), args, kwargs, "member_name", &memberName); err != nil { + return nil, err + } + if _, found := membersByName[memberName]; !found { + return nil, fmt.Errorf("Invalid member name: %s", memberName) + } + outcome.Target = memberName + return starlark.None, nil + } + getResources := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var memberName string + if err := starlark.UnpackArgs(b.Name(), args, kwargs, "member_name", &memberName); err != nil { + return nil, err + } + member, err := lookupMember(membersByName, memberName) + if err != nil { + return nil, err + } + return scriptlet.StarlarkMarshal(api.Resources{ + CPU: api.ResourcesCPU{Total: member.CPUTotal}, + Memory: api.ResourcesMemory{Total: member.MemoryTotalBytes}, + }) + } + getState := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var memberName string + if err := starlark.UnpackArgs(b.Name(), args, kwargs, "member_name", &memberName); err != nil { + return nil, err + } + member, err := lookupMember(membersByName, memberName) + if err != nil { + return nil, err + } + poolName := cluster.PoolName + if poolName == "" { + poolName = "gha-lvm" + } + return scriptlet.StarlarkMarshal(api.ClusterMemberState{ + SysInfo: api.ClusterMemberSysInfo{LoadAverages: []float64{member.LoadAverage}}, + StoragePools: map[string]api.StoragePoolState{ + poolName: {ResourcesStoragePool: api.ResourcesStoragePool{ + Space: api.ResourcesStoragePoolSpace{ + Used: member.PoolUsedBytes, Total: member.PoolTotalBytes, + }, + }}, + }, + }) + } + getInstanceResources := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + if err := starlark.UnpackArgs(b.Name(), args, kwargs); err != nil { + return nil, err + } + return scriptlet.StarlarkMarshal(apiScriptlet.InstanceResources{ + MemorySize: request.MemorySize, RootDiskSize: request.RootDiskSize, + }) + } + getInstances := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var project, location string + if err := starlark.UnpackArgs(b.Name(), args, kwargs, "project??", &project, "location??", &location); err != nil { + return nil, err + } + member, err := lookupMember(membersByName, location) + if err != nil { + return nil, err + } + instances := make([]api.Instance, 0, len(member.Instances)) + for _, instance := range member.Instances { + instances = append(instances, api.Instance{ + Name: instance.Name, Project: project, Location: location, + ExpandedConfig: api.ConfigMap{ + "limits.memory": strconv.Itoa(instance.MemoryLimitMiB) + "MiB", + }, + }) + } + return scriptlet.StarlarkMarshal(instances) + } + getInstancesCount := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var project, location string + var includePending bool + if err := starlark.UnpackArgs( + b.Name(), args, kwargs, "project??", &project, "location??", &location, "pending??", &includePending, + ); err != nil { + return nil, err + } + member, err := lookupMember(membersByName, location) + if err != nil { + return nil, err + } + count := len(member.Instances) + if includePending { + count = member.PendingCount + } + return scriptlet.StarlarkMarshal(count) + } + logger := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) { + var builder strings.Builder + for _, arg := range args { + text, err := strconv.Unquote(arg.String()) + if err != nil { + text = arg.String() + } + builder.WriteString(text) + } + outcome.Logs = append(outcome.Logs, b.Name()+": "+builder.String()) + return starlark.None, nil + } + unsupported := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + return nil, fmt.Errorf("%s is not part of the fleet placement harness", b.Name()) + } + + program, err := scriptlet.Compile("instance_placement", script, placementPredeclared) + if err != nil { + return PlacementOutcome{}, err + } + thread := &starlark.Thread{Name: "instance_placement"} + globals, err := program.Init(thread, starlark.StringDict{ + "log_info": starlark.NewBuiltin("log_info", logger), + "log_warn": starlark.NewBuiltin("log_warn", logger), + "log_error": starlark.NewBuiltin("log_error", logger), + "set_target": starlark.NewBuiltin("set_target", setTarget), + "get_cluster_member_resources": starlark.NewBuiltin("get_cluster_member_resources", getResources), + "get_cluster_member_state": starlark.NewBuiltin("get_cluster_member_state", getState), + "get_instance_resources": starlark.NewBuiltin("get_instance_resources", getInstanceResources), + "get_instances": starlark.NewBuiltin("get_instances", getInstances), + "get_instances_count": starlark.NewBuiltin("get_instances_count", getInstancesCount), + "get_cluster_members": starlark.NewBuiltin("get_cluster_members", unsupported), + "get_project": starlark.NewBuiltin("get_project", unsupported), + }) + if err != nil { + return PlacementOutcome{}, fmt.Errorf("Failed initializing: %w", err) + } + placement := globals["instance_placement"] + if placement == nil { + return PlacementOutcome{}, errors.New("Scriptlet missing instance_placement function") + } + requestValue, err := scriptlet.StarlarkMarshal(apiScriptlet.InstancePlacement{ + InstancesPost: api.InstancesPost{Name: request.Name}, + Project: request.Project, + }) + if err != nil { + return PlacementOutcome{}, err + } + candidatesValue, err := scriptlet.StarlarkMarshal(candidates) + if err != nil { + return PlacementOutcome{}, err + } + returned, err := starlark.Call(thread, placement, nil, []starlark.Tuple{ + {starlark.String("request"), requestValue}, + {starlark.String("candidate_members"), candidatesValue}, + }) + if err != nil { + var evalErr *starlark.EvalError + if errors.As(err, &evalErr) { + outcome.Failed = true + outcome.FailMessage = evalErr.Error() + return outcome, nil + } + return outcome, err + } + if returned.Type() != "NoneType" { + return PlacementOutcome{}, fmt.Errorf("Failed with unexpected return value: %v", returned) + } + return outcome, nil +} + +func lookupMember(members map[string]MemberSnapshot, name string) (MemberSnapshot, error) { + member, found := members[name] + if !found { + return MemberSnapshot{}, fmt.Errorf("Invalid member name: %s", name) + } + return member, nil +} diff --git a/internal/incusplacement/starlarkexec/execute_test.go b/internal/incusplacement/starlarkexec/execute_test.go new file mode 100644 index 00000000..fae5e060 --- /dev/null +++ b/internal/incusplacement/starlarkexec/execute_test.go @@ -0,0 +1,183 @@ +package starlarkexec + +import ( + "fmt" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/NDDev-OpenNetwork/github-actions/internal/config" + "github.com/NDDev-OpenNetwork/github-actions/internal/incusplacement" +) + +const ( + hostRAMBytes = 16 * 1024 * 1024 * 1024 + eightGiBBytes = 8192 * 1024 * 1024 + rootDiskBytes = 50 * 1024 * 1024 * 1024 + poolBytes = 200 * 1024 * 1024 * 1024 +) + +func TestRenderedPlacementCompilesAgainstIncusBuiltinNames(t *testing.T) { + script := renderExample(t) + outcome, err := Execute(script, eightGiBRequest(), fourEmptyMembers()) + if err != nil { + t.Fatal(err) + } + if outcome.Failed || outcome.Target != "gha-runner-1" { + t.Fatalf("empty cluster refused an 8GiB worker: %#v", outcome) + } +} + +func TestSpreadFourGiBWorkersLeaveNoEightGiBSlot(t *testing.T) { + script := renderExample(t) + // Four hosts, each holding 8 GiB of 4 GiB workers. Packing onto remaining + // RAM is the live policy; spreading that occupancy is the counterexample + // that used to starve Almaty 8 GiB jobs. + outcome, err := Execute(script, eightGiBRequest(), membersWithFourGiBOccupancy([]int{8192, 8192, 8192, 8192})) + if err != nil { + t.Fatal(err) + } + if !outcome.Failed || !strings.Contains(outcome.FailMessage, "insufficient-memory") { + t.Fatalf("spread 4GiB occupancy still placed an 8GiB worker: %#v", outcome) + } +} + +func TestPackedFourGiBWorkersLeaveAnEightGiBSlot(t *testing.T) { + script := renderExample(t) + outcome, err := Execute(script, eightGiBRequest(), membersWithFourGiBOccupancy([]int{12288, 12288, 8192, 0})) + if err != nil { + t.Fatal(err) + } + if outcome.Failed || outcome.Target != "gha-runner-4" { + t.Fatalf("packed occupancy did not land the 8GiB worker on the empty member: %#v", outcome) + } +} + +func TestSysinfoSwapDoesNotBecomeSchedulableRAM(t *testing.T) { + script := renderExample(t) + cluster := membersWithFourGiBOccupancy([]int{8192, 8192, 8192, 8192}) + for i := range cluster.Members { + cluster.Members[i].LoadAverage = 0 + } + outcome, err := Execute(script, eightGiBRequest(), cluster) + if err != nil { + t.Fatal(err) + } + if !outcome.Failed { + t.Fatalf("placement treated something other than resources.memory.total as RAM: %#v", outcome) + } +} + +func TestMaintenancePlacesOnClosedEmptyMember(t *testing.T) { + script := renderExample(t) + cluster := fourEmptyMembers() + for i := range cluster.Members { + cluster.Members[i].Config["user.gha_pressure.state"] = "closed" + } + job, err := Execute(script, eightGiBRequest(), cluster) + if err != nil { + t.Fatal(err) + } + if !job.Failed { + t.Fatalf("closed gate still placed a job worker: %#v", job) + } + build, err := Execute(script, PlacementRequest{ + Project: "gha-fleet", Name: "gha-image-builder-stage", + MemorySize: eightGiBBytes, RootDiskSize: rootDiskBytes, + }, cluster) + if err != nil { + t.Fatal(err) + } + if build.Failed || build.Target == "" { + t.Fatalf("maintenance placement did not use the drained closed member: %#v", build) + } +} + +func TestPendingCreateWithoutRecordReservesMaxWorker(t *testing.T) { + script := renderExample(t) + cluster := fourEmptyMembers() + cluster.Members[0].PendingCount = 1 + outcome, err := Execute(script, eightGiBRequest(), cluster) + if err != nil { + t.Fatal(err) + } + if outcome.Failed || outcome.Target == "gha-runner-1" { + t.Fatalf("pending create on runner-1 still took the 8GiB worker: %#v", outcome) + } +} + +func TestWrongProjectIsANoOp(t *testing.T) { + script := renderExample(t) + outcome, err := Execute(script, PlacementRequest{ + Project: "not-fleet", Name: "worker-1", + MemorySize: eightGiBBytes, RootDiskSize: rootDiskBytes, + }, fourEmptyMembers()) + if err != nil { + t.Fatal(err) + } + if outcome.Failed || outcome.Target != "" { + t.Fatalf("foreign project must return without set_target: %#v", outcome) + } +} + +func renderExample(t *testing.T) string { + t.Helper() + _, current, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller failed") + } + cfg, err := config.Load(filepath.Join(filepath.Dir(current), "../../../config/example-runner-1.yaml")) + if err != nil { + t.Fatal(err) + } + script, err := incusplacement.Render(cfg) + if err != nil { + t.Fatal(err) + } + return script +} + +func eightGiBRequest() PlacementRequest { + return PlacementRequest{ + Project: "gha-fleet", Name: "gha-job-almaty", + MemorySize: eightGiBBytes, RootDiskSize: rootDiskBytes, + } +} + +func fourEmptyMembers() ClusterSnapshot { + return membersWithFourGiBOccupancy([]int{0, 0, 0, 0}) +} + +func membersWithFourGiBOccupancy(committedMiB []int) ClusterSnapshot { + members := make([]MemberSnapshot, len(committedMiB)) + for index, committed := range committedMiB { + if committed%4096 != 0 { + panic("occupancy must be a multiple of the 4GiB worker class") + } + count := committed / 4096 + instances := make([]InstanceSnapshot, 0, count) + for n := 0; n < count; n++ { + instances = append(instances, InstanceSnapshot{ + Name: fmt.Sprintf("gha-worker-%d-%d", index+1, n+1), MemoryLimitMiB: 4096, + }) + } + members[index] = MemberSnapshot{ + Name: fmt.Sprintf("gha-runner-%d", index+1), + Config: pressureOpen(), + MemoryTotalBytes: hostRAMBytes, + CPUTotal: 8, + PoolTotalBytes: poolBytes, + Instances: instances, + PendingCount: count, + } + } + return ClusterSnapshot{PoolName: "gha-lvm", Members: members} +} + +func pressureOpen() map[string]string { + return map[string]string{ + "user.gha_pressure.schema": "1", + "user.gha_pressure.state": "open", + } +} diff --git a/internal/incusplacement/starlarkexec/go.mod b/internal/incusplacement/starlarkexec/go.mod new file mode 100644 index 00000000..bc35aaea --- /dev/null +++ b/internal/incusplacement/starlarkexec/go.mod @@ -0,0 +1,19 @@ +// Nested module: packing tests import go.starlark.net here so the root +// go.mod/go.sum stay identical to the pinned provider derivative. +module github.com/NDDev-OpenNetwork/github-actions/internal/incusplacement/starlarkexec + +go 1.26.7 + +require ( + github.com/NDDev-OpenNetwork/github-actions v0.0.0 + github.com/lxc/incus/v7 v7.4.0 + go.starlark.net v0.0.0-20260708150628-5395d018f003 +) + +require ( + go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect + golang.org/x/sys v0.47.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/NDDev-OpenNetwork/github-actions => ../../.. diff --git a/internal/incusplacement/starlarkexec/go.sum b/internal/incusplacement/starlarkexec/go.sum new file mode 100644 index 00000000..3d82257e --- /dev/null +++ b/internal/incusplacement/starlarkexec/go.sum @@ -0,0 +1,20 @@ +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/lxc/incus/v7 v7.4.0 h1:gZPEQvFVOeeSVhsnZVObcrSCLVfnowBP3VM8Fr5QaAQ= +github.com/lxc/incus/v7 v7.4.0/go.mod h1:drJHddxdr1men9MS1VYrdjDWBeiI92yL//Aq4oAIPCY= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +go.starlark.net v0.0.0-20260708150628-5395d018f003 h1:cAxcqHgW8fnmT0cEBU3TzvVYHIFt8IIGDMWUF6rImk4= +go.starlark.net v0.0.0-20260708150628-5395d018f003/go.mod h1:Iue6g6iirlfLoVi/DYCi5/x0h/bAOuWF3dULTKpt2Vo= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= +go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/incusplacement/starlarkexec_hook_test.go b/internal/incusplacement/starlarkexec_hook_test.go new file mode 100644 index 00000000..57fd2b06 --- /dev/null +++ b/internal/incusplacement/starlarkexec_hook_test.go @@ -0,0 +1,46 @@ +package incusplacement + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" +) + +// TestStarlarkExecutionHarness runs the nested module. `go test ./...` from +// the repository root skips nested modules, and that skip is what would let a +// packing regression land without executing Starlark. +func TestStarlarkExecutionHarness(t *testing.T) { + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller failed") + } + dir := filepath.Join(filepath.Dir(file), "starlarkexec") + args := []string{"test", "-count=1", "."} + if raceEnabled { + args = []string{"test", "-race", "-count=1", "."} + } + cmd := exec.Command("go", args...) + cmd.Dir = dir + cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("starlarkexec tests: %v\n%s", err, output) + } +} + +func TestStarlarkExecutionHarnessVet(t *testing.T) { + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller failed") + } + dir := filepath.Join(filepath.Dir(file), "starlarkexec") + cmd := exec.Command("go", "vet", ".") + cmd.Dir = dir + cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("starlarkexec vet: %v\n%s", err, output) + } +} diff --git a/internal/repositorycontract/derivative_boundaries_test.go b/internal/repositorycontract/derivative_boundaries_test.go index 88cefce0..db0ebbe0 100644 --- a/internal/repositorycontract/derivative_boundaries_test.go +++ b/internal/repositorycontract/derivative_boundaries_test.go @@ -65,3 +65,23 @@ func TestWorkflowResolvesTheProviderBoundaryItEnforces(t *testing.T) { t.Fatal("observer-only providerretry package still invalidates the external provider artifact") } } + +func TestRootModuleDoesNotRequireStarlark(t *testing.T) { + content, err := os.ReadFile(filepath.Join(repositoryRoot, "go.mod")) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(content), "go.starlark.net") { + t.Fatal("root go.mod requires go.starlark.net; that is provider-derivative drift") + } +} + +func TestWorkflowVetsTheNestedStarlarkHarness(t *testing.T) { + content, err := os.ReadFile(filepath.Join(repositoryRoot, ".github", "workflows", "ci.yml")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(content), "go vet -C internal/incusplacement/starlarkexec") { + t.Fatal("static CI no longer vets the nested Starlark placement harness") + } +}