// Package oathcandidate supplies cad-atlas's own real var-go candidate (cad-atlas#8): // steps that gate its own CI-workflow oath by actually parsing the committed // .gitea/workflows/cd.yml, not a stub that hardcodes an unrelated toy vocabulary. // var-go injects and owns the gate across the subprocess boundary (SubprocessGate, // ADR-0003), so this package supplies only the prose->behaviour binding and never a // verdict — it cannot self-certify. package oathcandidate import ( "os" "path/filepath" "strings" oath "git.d-ma.be/mathias/swedsl/oath" "gopkg.in/yaml.v3" ) // workflowState is the candidate's domain: the job names and concatenated step-run // scripts parsed out of one Gitea Actions workflow file. type workflowState struct { jobNames map[string]bool jobRuns map[string]string // job name -> every step's `run:` script, concatenated } type workflowFile struct { Jobs map[string]struct { Steps []struct { Run string `yaml:"run"` } `yaml:"steps"` } `yaml:"jobs"` } // Build returns cad-atlas's candidate registry. cmd/vargo-gate runs the generated // harness with cwd = this module's own directory (SubprocessGate's // cmd.Dir = candidateModuleDir contract) — one level under the cad-atlas repo root // in cad-atlas's real layout — so a workflow path in the oath text like // ".gitea/workflows/cd.yml" is read relative to "..". func Build() *oath.Registry[workflowState] { reg := oath.NewRegistry[workflowState]() if err := reg.Stimulus(`the CI workflow file {string} is parsed`, func(_ workflowState, path string) workflowState { return parseWorkflow(path) }); err != nil { panic(err) } if err := reg.Sensor(`it defines a job named {string}`, func(s workflowState, name string) string { if s.jobNames[name] { return name } return "" }); err != nil { panic(err) } // Checks what the workflow file can actually attest to: the job's run script // invokes the gate binary. The "var-go/oath" commit-status context string // itself lives in vargo-gate's Go code, not the YAML — not something this // file-level check can see, so it isn't what's asserted here. if err := reg.Sensor(`the job named {string} invokes {string}`, func(s workflowState, job, cmd string) (string, string) { run, ok := s.jobRuns[job] foundJob := "" if ok { foundJob = job } foundCmd := cmd if !ok || !strings.Contains(run, cmd) { foundCmd = "" } return foundJob, foundCmd }); err != nil { panic(err) } return reg } // parseWorkflow reads and parses a Gitea Actions workflow file relative to the // repo root (see Build's doc comment for the cwd contract). A read or parse // failure returns an empty state — every sensor then observes "not found", // which fails the gate closed rather than silently skipping the check. func parseWorkflow(repoRelativePath string) workflowState { state := workflowState{jobNames: map[string]bool{}, jobRuns: map[string]string{}} data, err := os.ReadFile(filepath.Join("..", repoRelativePath)) if err != nil { return state } var wf workflowFile if err := yaml.Unmarshal(data, &wf); err != nil { return state } for name, job := range wf.Jobs { state.jobNames[name] = true var runs strings.Builder for _, step := range job.Steps { runs.WriteString(step.Run) runs.WriteString("\n") } state.jobRuns[name] = runs.String() } return state }