yq: Merge YAML with the * Operator
In yq, the * operator merges two maps, recursively combining their keys; later values override earlier ones.
Config overlays are everywhere in CI: a base file plus an environment-specific override. yq spells deep merge as multiplication, with modifiers that change how arrays and nulls behave.
What it does
The * operator deep-merges two maps. By default, scalar values on the right replace those on the left and arrays are replaced (not concatenated). Modifiers tune this: *+ appends arrays, *? only sets keys that do not already exist, *d deep-merges and deletes.
Common usage
# merge overlay.yaml onto base.yaml
yq '. *= load("overlay.yaml")' base.yaml
# append arrays instead of replacing them
yq '. *+= load("overlay.yaml")' base.yaml
# only fill in keys that are missing
yq '. *?= load("defaults.yaml")' values.yamlMerge modifiers
| Operator | Behavior |
|---|---|
| * | Deep-merge; right wins, arrays replaced |
| *+ | Deep-merge; arrays concatenated |
| *? | Only set keys not already present |
| *n | Merge but keep nulls from the right out |
| *= | Update-assign form of any of the above |
| load("f.yaml") | Load another file as a value to merge |
In CI
A base values.yaml plus an environment overlay merges cleanly with yq -i '. *= load("prod-overrides.yaml")' values.yaml. Remember arrays are replaced by default; if you want to add a sidecar container rather than overwrite the list, use *+.
Common errors in CI
Expecting arrays to combine but seeing the base list vanish means you used * (replace) instead of *+ (append). "Error: cannot load file ..." from load() means a wrong path relative to the runner working directory. Merging requires the values in scope, so prefer load() inside eval, or use eval-all for file-to-file merges; mixing the two patterns is a frequent mistake.