Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 57 additions & 14 deletions cli/azd/pkg/ext/hooks_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,77 @@

package ext

import "fmt"
import (
"fmt"

"gopkg.in/yaml.v3"
)

// HooksConfig is an alias for map of hook names to slices of hook configurations.
// It supports unmarshalling both legacy single-hook and newer multi-hook formats.
type HooksConfig map[string][]*HookConfig

// UnmarshalYAML converts hook configuration from YAML, supporting both single-hook configuration
// and multiple-hooks configuration.
//
// Each hook entry is independently parsed as either a single HookConfig (mapping node) or a list
// of HookConfigs (sequence node), allowing mixed formats within the same hooks: block.
func (ch *HooksConfig) UnmarshalYAML(unmarshal func(any) error) error {
var legacyConfig map[string]*HookConfig
// Unmarshal into map[string]any so each value retains its Go representation:
// YAML mapping → map[string]any
// YAML sequence → []any
// YAML null → nil
var raw map[string]any
if err := unmarshal(&raw); err != nil {
return fmt.Errorf("failed to unmarshal hooks configuration: %w", err)
}

if err := unmarshal(&legacyConfig); err == nil {
newConfig := HooksConfig{}
result := make(HooksConfig, len(raw))

for key, value := range legacyConfig {
newConfig[key] = []*HookConfig{value}
}
for key, val := range raw {
switch val.(type) {
case nil:
// A null YAML value (e.g. "preprovision:" with no body).
// Preserve with a nil entry so downstream validation can report it.
result[key] = []*HookConfig{nil}

*ch = newConfig
return nil
}
case map[string]any:
// Single hook configuration (a YAML mapping).
encoded, encErr := yaml.Marshal(val)
if encErr != nil {
return fmt.Errorf("failed to unmarshal hook %q: %w", key, encErr)
}

var newConfig map[string][]*HookConfig
if err := unmarshal(&newConfig); err != nil {
return fmt.Errorf("failed to unmarshal hooks configuration: %w", err)
var single HookConfig
if err := yaml.Unmarshal(encoded, &single); err != nil {
return fmt.Errorf("failed to unmarshal hook %q: %w", key, err)
}

result[key] = []*HookConfig{&single}

case []any:
// List of hook configurations (a YAML sequence).
encoded, encErr := yaml.Marshal(val)
if encErr != nil {
return fmt.Errorf("failed to unmarshal hook %q: %w", key, encErr)
}

var list []*HookConfig
if err := yaml.Unmarshal(encoded, &list); err != nil {
return fmt.Errorf("failed to unmarshal hook %q: %w", key, err)
}

result[key] = list

default:
return fmt.Errorf(
"failed to unmarshal hook %q: expected mapping or sequence, got %T",
key, val,
)
}
}

*ch = newConfig
*ch = result

return nil
}
Expand Down
Loading
Loading