generated from mathias/template-go-web
oathcandidate/ is a separate Go module (mirrors swedsl's own oath/testdata/selfcandidate pattern, keeping var-go's transitive deps out of the deployed atlas binary) whose Build() parses the committed .gitea/workflows/cd.yml and checks the "oath" job exists and invokes cmd/vargo-gate. TDD: passes against the real file, fails closed on a fixture missing the job. Rewires the oath CI job to go-run vargo-gate from its real module path (git.d-ma.be/mathias/swedsl/oath/cmd/vargo-gate@oath/v0.28.0, unblocked by swedsl#35/#38) against VARGO_CANDIDATE_DIR=oathcandidate, instead of checking out swedsl and gating its hardcoded toy fixture. Private-module auth via a short-lived GIT_ASKPASS script (token never in argv, never written to git config, matches act_runner's env:-block-with-secrets gotcha). Discovered along the way: var-go's parser needs single-line, period-separated oath sentences with no Given/When/Then/And keyword stripping — this repo's older oaths (incl. #1) used an unverified multi-line keyword-prefixed style. #8's oath uses the proven format. Still not required by branch protection pending a real-PR confirmation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
109 lines
3.4 KiB
Go
109 lines
3.4 KiB
Go
// 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 "<no such job>"
|
|
}); 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 := "<no such job>"
|
|
if ok {
|
|
foundJob = job
|
|
}
|
|
foundCmd := cmd
|
|
if !ok || !strings.Contains(run, cmd) {
|
|
foundCmd = "<not invoked>"
|
|
}
|
|
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
|
|
}
|