Use a kind from another Go service
About 742 wordsAbout 2 min
2026-10-06
A second Go service, such as a release bot, wants to evaluate the same policies as the deploy gate and act on the decisions, but it doesn't import the deploy gate's Go package. By the end of this guide it builds the DeployApproval kind from code that sigil gen go generated from the exported kind file, evaluates policies with it, and reads each result with a type switch, with a CI check that fails when the generated code goes stale.
You need the kind file the defining host exports, deploy_approval.sigil (see Build a host binary), and the sigil binary. The layout used here:
releasebot/
├── main.go
├── approval/
│ └── kind.go # generated by sigil gen go
└── policies/
├── deploy_approval.sigil
└── payments.sigilGenerate the package
Add the module the generated code imports:
go get github.com/spechtlabs/sigilGenerate the code into its own package, from a go:generate directive next to the code that uses it:
//go:generate sigil gen go --package approval --out approval/kind.go policies/deploy_approval.sigil$ go generate ./...
✓ wrote approval/kind.goThe package declares the kind's enums, struct types and input as Go types, a payload struct per decision, a handle per decision and reason, and NewKind. What maps to what is in the sigil gen go reference. Run go generate again whenever the kind file changes; the file is left untouched when it's current.
Build the kind
NewKind takes the implementations of the kind's host functions. DeployApproval declares fn split(string, string) -> list<string>, so approval.Funcs has a Split field. Build the kind once, at package level:
var Deploy = approval.NewKind(approval.Funcs{
Split: func(s, sep string) ([]string, error) { return strings.Split(s, sep), nil },
})Every field of Funcs must be set. A nil field makes NewKind panic when the program starts, naming the field:
panic: approval.NewKind: no implementation for host functions: Funcs.SplitEvaluate a policy
Deploy is a *policy.Kind[approval.Input], so loading and evaluating work as in Embed Sigil in a Go service. The kind exports the kind file it was generated from byte for byte, so a bundle that holds deploy_approval.sigil next to the policies loads:
//go:embed policies
var files embed.FS
policies, err := fs.Sub(files, "policies")
if err != nil {
log.Fatal(err)
}
p, err := Deploy.Load(policies, "payments.production")
if err != nil {
log.Fatal(err)
}
res, err := p.Eval(ctx, approval.Input{
Service: approval.Service{Name: "ledger", Tier: approval.TierCritical},
Actor: approval.Actor{Name: "ana", Teams: []string{"payments/sre"}},
})Read the result
Every decision has a payload type of its own, so a type switch on res.Value() has one case per decision, and a switch on res.Why() compares the reason against the generated handles:
switch d := res.Value().(type) {
case approval.ApproveData:
fmt.Println("approved, bake", d.Bake)
case approval.ReviewData:
fmt.Println("needs review by", d.Approvers)
case approval.DenyData:
fmt.Println("denied")
}
switch res.Why() {
case approval.ApprovePaymentsSre:
fmt.Println("payments SRE approved it")
case approval.DenyNoRuleMatched:
fmt.Println("no rule matched")
}For a policy that approves with approve(reason: payments_sre, bake: 2h):
approved, bake 2h0m0s
payments SRE approved itCheck err before reading the result: after a failed evaluation the result holds the kind's default. Handle failed evaluations covers each error.
Keep the code current in CI
--check compares the generated file with what the kind file generates now, and fails when they differ:
sigil gen go --check --package approval --out approval/kind.go policies/deploy_approval.sigil✗ approval/kind.go is stale: it doesn't match the code policies/deploy_approval.sigil generates
regenerate it with `sigil gen go --package approval --out approval/kind.go policies/deploy_approval.sigil`Run it next to sigil check in the job that checks the policies; see Check policies in CI.
When gen go refuses the kind file
A kind file a host exported generates as it is. A hand-written one can declare things in an order Go's policy.NewKind can't reproduce, and gen go then names each declaration and says how to rewrite it:
deploy_approval.sigil:3:6: error: a Go kind can't declare type Release here
|
3 | type Release {
| ^^^^^^^
= help: a Go kind declares struct types in the order its inputs, host functions and payloads first use them; declare them as Service, Release, which changes no policyReordering declarations changes no policy, so it doesn't need a new kind version. Every case gen go refuses is listed in the reference.
