# Writing changes that generalize across variants

Many changes in ConfigHub are not applied to just a single unit. A [function](./functions.md) invocation runs across whatever selection you give it, the resulting change propagates to the [variants](./variants.md) downstream, and stored invocations are re-run later against data that has changed since. The same invocation therefore has to work against units with different contents, different collection ordering (e.g., sometimes patches append to arrays rather than insert), and sometimes different resource names.

Everything in this guide already applies to bulk execution today. It matters more as the number of variants grows, because an invocation that only works against the data in front of you fails silently in the variants where it does not match.

## Refer to things by identity, not position

The most common reason an invocation stops working elsewhere is a hardcoded index.

```
# Fragile: whichever container happens to be first in this unit
cub function set --space prod --unit backend \
    set-string-path apps/v1/Deployment "spec.template.spec.containers.0.image" ghcr.io/acme/api:v2

# Portable: the container named "api", wherever it appears
cub function set --space prod --unit backend \
    set-container-image api ghcr.io/acme/api:v2
```

`containers.0` selects the element in position 0 of that unit's container list. A variant that adds a sidecar, or lists its containers in a different order, has a different container in that position, and writing to it succeeds because position 0 exists. Nothing fails; the wrong container is modified. Selecting by name is stable under both reordering and insertion, because ConfigHub matches the element rather than counting to it.

Three forms of the same rule:

- **Prefer a purpose-built function** - `set-container-image`, `set-env-var`, `set-container-resources` — over a generic path setter like `set-string-path`. These already select their target by merge key.
- **When a path is unavoidable, use one that selects.** ConfigHub paths can match by [merge key](../background/concepts/mutation-sources.md): `spec.template.spec.containers.?name=api.image` selects the container named `api`.
- **Keep arguments parameters rather than data.** A function whose argument is a list computed from one unit's current contents only applies to that unit. Arguments that name what to change are portable; arguments carrying a specific unit's data are not.

Similar rules apply when writing functions using CEL, YQ, and Starlark.

## Merge keys are identity, so avoid renaming them

Like [Kubernetes strategic merge patch](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/#use-a-strategic-merge-patch-to-update-a-deployment) and [kustomize](https://kustomize.io), ConfigHub matches array elements by merge key — the container name, the volume name. A name in a declarative model identifies the element and usually describes its role, so changing it makes the element a different one as far as the merge engine and every stored invocation are concerned.

Renaming a container from `web` to `frontend` in a downstream variant means every stored invocation that refers to `web` no longer matches anything in that variant. Those invocations do not fail; they find nothing to change. Merge key changes across variants also break patches. Avoid renaming merge keys. If a variant genuinely needs a differently-purposed container, adding one and removing the old one states that directly. When you do have to rename, treat it as a breaking change for that variant: invocations naming the old value stop reaching it, and they need to be updated.

## Plan for execution on units with different numbers of matching paths

Variants could contain more or fewer resources, containers, ports, volumes, etc.

- **Search for all relevant occurrences.** Use the `*` wildcard or a function that searches by value, such as `set-image-reference-by-uri`. 
- **Select only the relevant occurrences.** Don't assume that the specified path only occurs once. Use `WhereResource` or expressions in CEL, YQ, or Starlark to ensure the function invocation doesn't touch paths it shouldn't.

In a template, such as a Helm chart, correct variation is sometimes implemented by explicit conditional logic, but often is pushed into careful curation of input values for each variant across values overlay files in variant-specific directories -- implicit rather than explicit reasons. In ConfigHub, it is recommended that high-level variant characteristics, such as "highly-available" or "pci-compliant", that determine detailed configuration settings should be made explicit via labels or annotations in ConfigHub and/or Kubernetes resources, and then unit and/or resource filters can be used to target function invocations appropriately. 

## Dry-run across the variants before committing a change

`--dry-run` accepts the same selection that bulk execution does, so you can check an invocation against every variant before it becomes part of the history:

```
cub function set --space "*" --where "Slug = 'deployment'" --dry-run -o mutations \
    set-container-image web ghcr.io/acme/app:v2
```

Two things in that output are worth reading:

- **Variants that report no mutation.** The invocation matched nothing there, possibly because the container is absent or renamed. This is the case that is hardest to notice later, because it produces no error and no change.
- **Variants that changed something unexpected**, or more paths than intended. That generally means the invocation is not selective enough.

Either can be fixed while the change is still a proposal, by correcting the invocation or by correcting the variant that has drifted from the shape the invocation expects.

## Why an invocation that changes nothing is not an error

Most functions do not return an error when they find nothing to operate on. That is deliberate: it is what allows one invocation to run across a bulk selection, or across variants shaped differently from the one it was written against, without the caller enumerating which units and resources it applies to.

We may add a mode that returns errors in such cases in the future.

## Protecting the values a variant owns

The other half of a change reaching the right variants is a change not overwriting the values a variant sets deliberately. ConfigHub records, per path, whether the value there is a local override, and a merge does not overwrite one. See [Mutation Sources](../background/concepts/mutation-sources.md) for the mechanism and for adjusting [protection](../background/concepts/mutation-sources.md#protection) per path.

Two points that matter when the change is made by automation rather than by hand:

- **A variant's own value is only preserved if someone said so.** Protection is opt-in: the change that sets it passes `--protect`, or `cub unit set-protection --protect` marks the path afterwards. A script applying a value it did not choose passes neither, and the path stays open to the next upgrade — which is what you want for it and not for the variant's own decisions.
- **Protecting a path is a standing decision, not a one-time one.** Once a path is protected, upstream changes to it are reported rather than applied. `cub unit conflicts` lists what was withheld, and lets you apply the changes you want.

## Checklist

Before an invocation becomes part of the history:

1. Does it select what it changes by name, rather than by position?
2. Is there a purpose-built function for this change?
3. If it takes a path, does the path select by merge key?
4. Does it select precisely the resources and paths that should be affected?
5. Has it been dry-run across `--space "*"`, and were the variants reporting no mutation accounted for?
6. Does it rename a merge key that other invocations refer to?
7. If it is setting a value the variant owns, does it pass `--protect` — and if it is automation applying a value decided elsewhere, does it leave the flag off?
