package config import ( "crypto/sha256" "encoding/hex" "fmt" "path/filepath" "strings" "gopkg.in/yaml.v3" ) const pipelineDefaultOwnershipSource = "default" // PipelineProfileProvenance identifies the profile selected while resolving a // pipeline. It contains only non-secret selection metadata. type PipelineProfileProvenance struct { Name string Source string OverlayPath string } // SelectedPipelineProfile reports the profile used to resolve cfg, if any. func SelectedPipelineProfile(cfg *PipelineConfig) (*PipelineProfileProvenance, bool) { if cfg == nil || cfg.resolution == nil || cfg.resolution.selectedProfile == nil { return nil, false } selection := cfg.resolution.selectedProfile return &PipelineProfileProvenance{Name: selection.name, Source: selection.source, OverlayPath: selection.overlayPath}, true } // EffectivePipelineDigest reports the deterministic secret-free digest for a // resolved pipeline. func EffectivePipelineDigest(cfg *PipelineConfig) string { if cfg == nil || cfg.resolution == nil { return "" } return cfg.resolution.effectiveDigest } func finalizePipelineResolution(cfg *PipelineConfig) error { if cfg == nil || cfg.resolution == nil { return fmt.Errorf("pipeline resolution metadata is required") } declared := make(map[string][]string, len(cfg.resolution.ownership)) for _, ownership := range cfg.resolution.ownership { declared[ownership.path] = append([]string(nil), ownership.sources...) } data, err := yaml.Marshal(cfg) if err != nil { return fmt.Errorf("serialize normalized effective pipeline: %w", err) } document, err := parseCompositionBytes("normalized effective pipeline", data) if err != nil { return err } records, err := document.semanticRecords() if err != nil { return err } ownership := make([]pipelineFieldOwnership, 0, len(records)) for _, record := range records { sources := declared[record.Path] if len(sources) == 0 { sources = []string{pipelineDefaultOwnershipSource} } ownership = append(ownership, pipelineFieldOwnership{ path: record.Path, sources: append([]string(nil), sources...), }) } cfg.resolution.ownership = ownership return recomputePipelineEffectiveDigest(cfg) } // recomputePipelineEffectiveDigest is the single package-owned hook for // refreshing provenance after later resolution expands concrete pipeline // values. Composition declarations and runtime provenance are not serialized. func recomputePipelineEffectiveDigest(cfg *PipelineConfig) error { if cfg == nil || cfg.resolution == nil { return fmt.Errorf("pipeline resolution metadata is required") } digestConfig := *cfg if cfg.Notarius != nil && cfg.resolution.logicalNotariusCaptured { notarius := *cfg.Notarius notarius.ConfigPath = cfg.resolution.logicalNotariusConfig notarius.WorkingDirectory = cfg.resolution.logicalNotariusWorking digestConfig.Notarius = ¬arius } data, err := yaml.Marshal(&digestConfig) if err != nil { return fmt.Errorf("serialize normalized effective pipeline: %w", err) } document, err := parseCompositionBytes("normalized effective pipeline", data) if err != nil { return err } canonical, err := document.canonicalDigestInput() if err != nil { return err } digest := sha256.Sum256(canonical) cfg.resolution.effectiveDigest = hex.EncodeToString(digest[:]) return nil } // captureLogicalNotariusPaths retains normalized user-facing path semantics // before runtime resolution makes relative paths depend on the checkout or // installation directory. Runtime paths remain absolute; provenance does not. func captureLogicalNotariusPaths(cfg *PipelineConfig) { if cfg == nil || cfg.resolution == nil || cfg.Notarius == nil { return } configPath := normalizeLogicalFilesystemPath(cfg.Notarius.ConfigPath) workingDirectory := normalizeLogicalFilesystemPath(cfg.Notarius.WorkingDirectory) if cfg.Notarius.Enabled && workingDirectory == "" && configPath != "" { workingDirectory = normalizeLogicalFilesystemPath(filepath.Dir(configPath)) } cfg.resolution.logicalNotariusConfig = configPath cfg.resolution.logicalNotariusWorking = workingDirectory cfg.resolution.logicalNotariusCaptured = true } func normalizeLogicalFilesystemPath(value string) string { value = strings.TrimSpace(value) if value == "" { return "" } return filepath.ToSlash(filepath.Clean(value)) }