Add semantic configuration profile comparison

This commit is contained in:
2026-08-30 15:10:38 +00:00
parent dde7f76ecb
commit 4c57ace2f6
13 changed files with 847 additions and 25 deletions

View File

@@ -46,7 +46,20 @@ type pipelineProfileDeclaration struct {
overlay string
}
func loadComposedPipeline(path string, opts PipelineLoadOptions) (*PipelineConfig, error) {
// pipelineCompositionSources retains one validated root source set. Each
// selected profile is resolved from a cloned base document so callers can
// safely compare or otherwise resolve multiple profiles without rereading or
// mutating the source set.
type pipelineCompositionSources struct {
rootPath string
envelope pipelineCompositionEnvelope
imports []loadedPipelineImport
overlays []loadedPipelineProfileOverlay
overlaysLoaded bool
base *compositionDocument
}
func loadPipelineCompositionSources(path string) (*pipelineCompositionSources, error) {
rootPath, err := filepath.Abs(path)
if err != nil {
return nil, fmt.Errorf("resolve root pipeline path %q: %w", path, err)
@@ -72,14 +85,6 @@ func loadComposedPipeline(path string, opts PipelineLoadOptions) (*PipelineConfi
if err != nil {
return nil, err
}
selection, err := selectPipelineProfile(envelope, opts)
if err != nil {
return nil, err
}
overlays, err := loadPipelineProfileOverlays(rootPath, envelope.profiles, imports)
if err != nil {
return nil, err
}
documents := make([]*compositionDocument, 0, len(imports)+1)
documents = append(documents, baseRoot)
for _, imported := range imports {
@@ -89,8 +94,47 @@ func loadComposedPipeline(path string, opts PipelineLoadOptions) (*PipelineConfi
if err != nil {
return nil, err
}
return &pipelineCompositionSources{
rootPath: rootPath,
envelope: envelope,
imports: imports,
base: merged,
}, nil
}
func (sources *pipelineCompositionSources) loadOverlays() error {
if sources == nil {
return fmt.Errorf("pipeline composition sources are required")
}
if sources.overlaysLoaded {
return nil
}
overlays, err := loadPipelineProfileOverlays(sources.rootPath, sources.envelope.profiles, sources.imports)
if err != nil {
return err
}
sources.overlays = overlays
sources.overlaysLoaded = true
return nil
}
func (sources *pipelineCompositionSources) resolve(opts PipelineLoadOptions) (*PipelineConfig, error) {
if sources == nil || sources.base == nil {
return nil, fmt.Errorf("pipeline composition sources are required")
}
if !sources.overlaysLoaded {
return nil, fmt.Errorf("pipeline profile overlays have not been loaded")
}
selection, err := selectPipelineProfile(sources.envelope, opts)
if err != nil {
return nil, err
}
merged := &compositionDocument{
root: cloneCompositionNode(sources.base.root),
sources: append([]string(nil), sources.base.sources...),
}
if selection != nil {
overlay, ok := loadedProfileOverlay(overlays, selection.name)
overlay, ok := loadedProfileOverlay(sources.overlays, selection.name)
if !ok {
return nil, fmt.Errorf("selected profile %q overlay was not loaded", selection.name)
}
@@ -106,7 +150,7 @@ func loadComposedPipeline(path string, opts PipelineLoadOptions) (*PipelineConfi
return nil, err
}
var cfg PipelineConfig
if err := decodeStrictYAMLFromReader("pipeline", rootPath, strings.NewReader(string(rendered)), &cfg); err != nil {
if err := decodeStrictYAMLFromReader("pipeline", sources.rootPath, strings.NewReader(string(rendered)), &cfg); err != nil {
return nil, fmt.Errorf("assembled pipeline sources %s: %w", formatCompositionSources(merged.sources), err)
}
records, err := merged.semanticRecords()
@@ -114,11 +158,11 @@ func loadComposedPipeline(path string, opts PipelineLoadOptions) (*PipelineConfi
return nil, err
}
metadata := &pipelineResolutionMetadata{
rootPath: rootPath,
rootPath: sources.rootPath,
sources: append([]string(nil), merged.sources...),
selectedProfile: selection,
}
for _, imported := range imports {
for _, imported := range sources.imports {
metadata.imports = append(metadata.imports, imported.path)
}
for _, record := range records {