Fix profile loading with ConfigKit history
All checks were successful
ci/woodpecker/push/verify Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful

This commit is contained in:
2026-09-07 13:10:29 +00:00
parent 3ce198197d
commit ed4623bc41
6 changed files with 130 additions and 45 deletions

View File

@@ -46,16 +46,6 @@ func loadPipelineWithConfigKit(path string, opts PipelineLoadOptions) (*Pipeline
if err != nil {
return nil, describePipelineLoadError(err)
}
if profile != "" {
base, err := loadPipelineConfigKitResult(path, "")
if err != nil {
return nil, describePipelineLoadError(err)
}
baseConfig := base.Config
if err := rejectDuplicateBaseDefinitions(&baseConfig, base.Metadata(), base.Sources()); err != nil {
return nil, fmt.Errorf("load pipeline config: %w", err)
}
}
cfg := result.Config
metadata, err := pipelineMetadataFromConfigKit(&cfg, result, opts)
if err != nil {
@@ -266,7 +256,7 @@ func validatePipelineConfigKit(cfg *PipelineConfig, validation *configkit.Valida
func pipelineMetadataFromConfigKit(cfg *PipelineConfig, result *configkit.Result[PipelineConfig], opts PipelineLoadOptions) (*pipelineResolutionMetadata, error) {
sources := result.Sources()
metadata := result.Metadata()
if err := rejectDuplicateBaseDefinitions(cfg, metadata, sources); err != nil {
if err := rejectDuplicateBaseDefinitions(metadata, sources); err != nil {
return nil, err
}
resolved := &pipelineResolutionMetadata{
@@ -345,34 +335,30 @@ func ownershipSources(origin configkit.Origin, pathsByID map[string]string) []st
return sources
}
func rejectDuplicateBaseDefinitions(cfg *PipelineConfig, metadata configkit.Metadata, sources []configkit.SourceInfo) error {
func rejectDuplicateBaseDefinitions(metadata configkit.Metadata, sources []configkit.SourceInfo) error {
pathsByID := make(map[string]string, len(sources))
for _, source := range sources {
pathsByID[source.ID] = source.Path
}
var declarations []struct {
kind string
key string
path configkit.Path
}
if cfg.Scriptorium != nil {
for key := range cfg.Scriptorium.Artifacts {
declarations = append(declarations, struct {
kind, key string
path configkit.Path
}{"artifact", key, configkit.Keys("scriptorium", "artifacts", key)})
// Enumerate retained declarations, not just surviving effective keys. Loading
// the base separately would also resolve credentials removed by the profile.
// ConfigKit returns canonical JSON Pointers in deterministic order.
for _, path := range metadata.HistoricalPaths() {
parts := strings.Split(path.String(), "/")
if len(parts) != 4 || parts[1] != "scriptorium" {
continue
}
for key := range cfg.Scriptorium.ArtifactFamilies {
declarations = append(declarations, struct {
kind, key string
path configkit.Path
}{"artifact family", key, configkit.Keys("scriptorium", "artifact_families", key)})
var kind string
switch parts[2] {
case "artifacts":
kind = "artifact"
case "artifact_families":
kind = "artifact family"
default:
continue
}
}
sort.Slice(declarations, func(i, j int) bool { return declarations[i].path.String() < declarations[j].path.String() })
for _, declaration := range declarations {
seen := map[string]struct{}{}
for _, change := range metadata.History(declaration.path) {
for _, change := range metadata.History(path) {
if change.Origin.Layer != configkit.LayerBase {
continue
}
@@ -386,7 +372,8 @@ func rejectDuplicateBaseDefinitions(cfg *PipelineConfig, metadata configkit.Meta
claims = append(claims, pathsByID[sourceID])
}
sort.Strings(claims)
return fmt.Errorf("pipeline Scriptorium %s %q is declared by multiple base files: %s", declaration.kind, declaration.key, strings.Join(claims, ", "))
key := strings.NewReplacer("~1", "/", "~0", "~").Replace(parts[3])
return fmt.Errorf("pipeline Scriptorium %s %q is declared by multiple base files: %s", kind, key, strings.Join(claims, ", "))
}
}
return nil

View File

@@ -88,6 +88,35 @@ whisperx:
}
}
func TestPipelineProfileDoesNotRequireUnusedBaseCredentials(t *testing.T) {
const production = "NARRATIO_PROFILE_UNUSED_PROD_KEY"
const testingKey = "NARRATIO_PROFILE_TEST_KEY"
t.Setenv(production, "")
if err := os.Unsetenv(production); err != nil {
t.Fatal(err)
}
t.Setenv(testingKey, "synthetic-test-key")
for _, credential := range []string{testingKey, "null"} {
t.Run(credential, func(t *testing.T) {
dir := t.TempDir()
root := writePipelineFixture(t, dir, "pipeline.yml", "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\naudita:\n llm_api_key: "+production+"\n")
writePipelineFixture(t, dir, "profiles/testing.yml", "audita:\n llm_api_key: "+credential+"\n")
profile := "testing"
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Profile: &profile})
if err != nil {
t.Fatal(err)
}
if credential == "null" {
if cfg.Audita.LLMAPIKey != nil {
t.Fatal("profile did not remove the base credential")
}
} else if cfg.Audita.LLMAPIKey == nil || cfg.Audita.LLMAPIKey.EnvName() != testingKey || !cfg.Audita.LLMAPIKey.IsResolved() {
t.Fatal("profile credential was not resolved")
}
})
}
}
func TestPipelineConfigKitAllowsOneBaseDeclarationWithProfileOverride(t *testing.T) {
dir := t.TempDir()
writePipelineFixture(t, dir, "pipeline.yml", `configkit: