Skip to content

Add !merge tag for positional sequence merge - #900

Draft
ndeloof wants to merge 1 commit into
compose-spec:mainfrom
ndeloof:positional-sequence-merge
Draft

Add !merge tag for positional sequence merge#900
ndeloof wants to merge 1 commit into
compose-spec:mainfrom
ndeloof:positional-sequence-merge

Conversation

@ndeloof

@ndeloof ndeloof commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Note

Prototype / draft for discussion.

What

Add a !merge YAML tag that makes an override sequence merge positionally
(element by element, by index) instead of the default append.

Today sequences can only be appended (default / mergeToSequence) or fully
replaced (!override, or the override rule for command/entrypoint/…).
There is no way to change a single element of a list in place. !merge fills
that gap:

# compose.yaml
services:
  app:
    command: [server, --port, "8080", --verbose]

# compose.override.yaml
services:
  app:
    command: !merge
      - {}        # keep base[0]  -> server
      - {}        # keep base[1]  -> --port
      - "9090"    # replace base[2]
                  # base[3] --verbose kept (override shorter)
# result: [server, --port, "9090", --verbose]

Semantics, per index i:

  • a no-op element (- {} or - null) keeps base[i];
  • otherwise override[i] is deep-merged with base[i] (deep-merge for
    mappings, replace for scalars);
  • override longer → the sequence is extended; base longer → the tail is kept.

The tag is ignored on anything that is not a sequence, and the default
behavior is unchanged when the tag is absent (opt-in, non-breaking). !merge
takes precedence over both the default append and the per-field merge rules,
since it is an explicit user request.

How

Mirrors the existing !reset/!override machinery:

  • loader/reset.goResetProcessor records the paths of !merge-tagged
    sequences and strips the tag so the node decodes as a plain sequence.
  • loader/loader.go — passes those paths to the merge step.
  • override/merge.go — new MergeWithPositionalPaths / mergePositional
    performs the positional merge at the recorded paths. Merge/MergeYaml stay
    as-is (wrappers), so the public API is unchanged.

Tests double as documentation: override/merge_positional_test.go (semantics)
and loader/merge_positional_test.go (the tag end to end).

Known limitations (prototype)

  • Positional paths are not threaded through the special mergers (build,
    networks, depends_on, …), so a !merge nested under one of those fields
    is not honored yet.
  • A !merge sequence reused through a YAML anchor is not cached/replayed at the
    reuse site.

Both are addressable follow-ups if the approach is accepted.

🤖 Generated with Claude Code

Sequences are otherwise always appended (or fully replaced) when merging an
override file, so there is no way to change a single element of a list
in-place. The new `!merge` tag, applicable to a sequence in an override file
(ignored elsewhere), merges the sequence element by element by index instead:

- a no-op element (`- {}` or `- null`) leaves the base element untouched, so an
  override can target one element while keeping the others;
- any other element is deep-merged with the base element at the same index
  (deep-merge for mappings, replace for scalars);
- length mismatches extend or preserve the tail as expected.

The loader (ResetProcessor) records the paths of `!merge`-tagged sequences and
strips the tag, then override.MergeWithPositionalPaths performs the positional
merge at those paths — taking precedence over the default append and the
per-field merge rules, since it is an explicit user request. The default
behavior is unchanged when the tag is absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant