// Package gitops implements config-as-code for repo-backed workloads: a // dockerfile/static workload can read a small .tinyforge.yml from its own repo // that declares a subset of its deploy config. The package is deliberately // decoupled from the store and source plugins — it takes a RepoRef (repo // coords + a decrypted token) and a live source_config blob, and returns a // validated merged config + a field-level drift report. It never writes to the // database and never decides to deploy. // // v1 scope (see plans/gitops/PLAN.md): only source_config-resident fields are // overlayable, and the set is source-aware (dockerfile: port/healthcheck/ // deploy_strategy; static: deploy_strategy). env/faces live in separate stores // and are intentionally out of v1; the typed ApplyPlan reserves their slots. package gitops import ( "bytes" "errors" "fmt" "io" "gopkg.in/yaml.v3" ) // Spec is the parsed shape of a .tinyforge.yml file (v1). type Spec struct { Version int `yaml:"version"` Deploy DeploySpec `yaml:"deploy"` } // DeploySpec carries the overlayable deploy fields. Pointers so an omitted key // is distinguishable from a zero value — only present (non-nil) fields are // applied or drift-compared, so an absent key never clears live config. type DeploySpec struct { Port *int `yaml:"port"` Healthcheck *string `yaml:"healthcheck"` DeployStrategy *string `yaml:"deploy_strategy"` } // ParseSpec decodes a .tinyforge.yml body. Unknown keys are rejected // (KnownFields) so a typo or an unsupported field — e.g. someone trying to // declare env/faces in v1 — surfaces as an error instead of being silently // dropped. Only version 1 is accepted. func ParseSpec(data []byte) (Spec, error) { var s Spec dec := yaml.NewDecoder(bytes.NewReader(data)) dec.KnownFields(true) if err := dec.Decode(&s); err != nil { if errors.Is(err, io.EOF) { return Spec{}, fmt.Errorf("gitops: empty .tinyforge.yml") } return Spec{}, fmt.Errorf("gitops: parse .tinyforge.yml: %w", err) } if s.Version != 1 { return Spec{}, fmt.Errorf("gitops: unsupported version %d (want 1)", s.Version) } return s, nil }