Skip to content

Bug: hooks config fails to parse when mixing single-hook and multi-hook formats in the same azure.yaml #7615

Description

@wbreza

Bug Description

HooksConfig.UnmarshalYAML fails when an �zure.yaml file contains a mix of single-hook (map) and multi-hook (list) formats in the same hooks: block. This is a pre-existing limitation that becomes user-facing now that multi-hook support exists.

Repro Steps

  1. Create an �zure.yaml with hooks using mixed formats — some as single maps, some as lists:
name: my-app
hooks:
  preprovision:
    - run: ./hooks/preprovision.sh
      shell: sh
    - run: ./hooks/preprovision/main.py
  predeploy:
    windows:
      shell: pwsh
      run: 'echo "VITE_API_BASE_URL=$env:API_BASE_URL" > ./src/web/.env.local'
    posix:
      shell: sh
      run: 'echo VITE_API_BASE_URL=\"$API_BASE_URL\" > ./src/web/.env.local'
  1. Run any azd command that loads the project config (e.g., �zd provision)

Expected Behavior

Both hook formats should parse successfully in the same hooks: block:

  • preprovision parsed as a list of 2 hooks
  • predeploy parsed as a single hook with platform overrides

Actual Behavior

ERROR: Your azure.yaml file is invalid.

parsing project file: unable to parse azure.yaml file: failed to unmarshal hooks configuration: yaml: unmarshal errors:
  line 33: cannot unmarshal !!map into []*ext.HookConfig
  line 40: cannot unmarshal !!map into []*ext.HookConfig

Root Cause

HooksConfig.UnmarshalYAML (pkg/ext/hooks_config.go) uses a two-pass approach:

  1. First attempts map[string]*HookConfig (all maps) — fails if ANY hook uses list format
  2. Falls back to map[string][]*HookConfig (all lists) — fails if ANY hook uses map format

Neither pass succeeds when the hooks: block contains a mix of both formats.

Suggested Fix

Replace the two-pass approach with per-key type inspection using yaml.Node or map[string]any, then switch on the YAML node kind for each hook entry:

  • Mapping node → unmarshal as single *HookConfig, wrap into slice
  • Sequence node → unmarshal as []*HookConfig

This would allow any combination of single-hook and multi-hook entries within the same hooks: block.

Workaround

Convert ALL hooks to list format (even single hooks become 1-item lists):

hooks:
  preprovision:
    - run: ./hooks/preprovision.sh
      shell: sh
    - run: ./hooks/preprovision/main.py
  predeploy:
    - windows:
        shell: pwsh
        run: 'echo "VITE_API_BASE_URL=$env:API_BASE_URL" > ./src/web/.env.local'
      posix:
        shell: sh
        run: 'echo VITE_API_BASE_URL=\"$API_BASE_URL\" > ./src/web/.env.local'

Context

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

Fields

No fields configured for Bug.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions