# Change Workflow

Changing configuration and making it live are two separate steps in ConfigHub. You edit Units freely — nothing you do to a Unit reaches a cluster. Configuration goes live only when you publish a [Release](../background/entities/release.md), which a GitOps operator then pulls.

That separation is what this workflow is built on. Between the change and the release sit the things you want there: grouping, validation, review, and approval.

This guide covers making a change and releasing it within one [variant](../background/concepts/variant.md). Taking a named change out of a base and through dev, staging, and production is [promotion](./promotion.md).

## Selecting Units

To make a configuration change and then release it, first identify a set of [units](../background/entities/unit.md). Typically it will be a [variant](../background/concepts/variant.md), corresponding to a [Space](../background/entities/space.md), or set of variants, deployed to a single Target cluster.

Use a [where expression](../background/concepts/filters.md) or a saved [Filter](../background/entities/filter.md) to select the Spaces.

See the [guide about managing sets of units](./unit-sets.md) for more details.

## Change Sets

Create a [ChangeSet](../background/entities/changeset.md) with a Description. ChangeSets make it possible to group multiple related changes, in sequence and across units. ChangeSets can be used to refer to changes when restoring previous configurations and when merging configurations across units and "rebasing" prior changes on top of more recent changes. Each ChangeSet has a start [tag](../background/entities/tag.md) and an end tag.

```
cub changeset create --space acme-home release-v452 --description "Release 452: Fix X. New feature Y. Upgrade Z."
```

Use [bulk patch](../background/concepts/bulk-operations.md) on the filtered set of units to add them to the change set, which "opens" it. A change set acts as a kind of lock, so if any units are in another change set, it will need to be removed (closed) first. If successful, this will create a new revision for each unit of the change set, whether the configuration data is changed or not, so set the LastChangeDescription on this update to refer to the ChangeSet.

```
cub unit update --patch --space acme-dev --filter acme-home/acme-app --changeset acme-home/release-v452 --change-desc "Starting Release 452 rollout"
```

Now you can make modifications to the filtered Units, such as [upgrades](../background/concepts/variant.md), and the [revisions](../background/entities/revision.md) will be part of the change set. If you invoke functions, the change set ID needs to be passed to the function invoke API, or the change will be rejected.

```
cub function set --space acme-dev --changeset acme-home/release-v452 --filter acme-home/acme-app --change-desc "Update image ref to v452" set-container-image-reference ":v452"
```

To close the changeset, the units need to be patched again with a null ChangeSetID. That will add the ChangeSet EndTagID to the Head Revision of each of the filtered Units.

```
cub unit update --patch --space acme-dev --filter acme-home/acme-app --changeset -
```

A ChangeSet names a change within one Space and locks the Units it is open on. It does not follow the change into other Spaces. That is a [ChangeOrder](../background/entities/changeorder.md)'s job — see [tracking and promoting changes](./promotion.md).

## Review and Approval

Someone may need to review and approve the changes, if a `vet-approvedby` Trigger has been installed in the spaces containing the units. Approving clears the [Validation Error](../background/concepts/gates.md) for the revision approved and no other, so a later change is gated again without anyone re-arming anything.

To approve a whole variant — which is usually what a review is of, since the change arrived across the Space — use `cub variant approve`:

```
cub variant approve acme-dev
```

By default it approves the units with a Target, which is what a Release of that Space would publish. A [base](../background/concepts/base.md) has no Target, so pass `--all` to approve one:

```
cub variant approve acme-base --all
```

`--where` narrows the selection further, and `--revision` approves a revision other than each unit's head — for reviewing a change that has since been superseded:

```
cub variant approve acme-prod --where "Slug LIKE 'deployment-%'"
cub variant approve acme-prod --revision LastReleasedRevisionNum
```

Approving waits for the Space's Triggers to finish evaluating before returning, because publishing is usually what comes next and a publish issued while evaluation is pending fails on the transient `awaiting/triggers` gate rather than on anything real. Pass `--no-wait` to skip the wait.

For a change spanning more than one Space, or a subset of a Space's units, approve by ChangeSet instead. Send the name and space of the ChangeSet to the desired approver(s). The bulk approve API accepts `ChangeSet:` with the change set ID for the `revision` parameter, which approves the revision marked with the ChangeSet's end tag for each of the filtered Units.

```
cub unit approve --space acme-dev --filter acme-home/acme-app --revision ChangeSet:acme-home/release-v452
```

## Release

Publishing a Release is what makes the change live. A Release bundles every Unit in a Space assigned to that Space's release [Target](../background/entities/target.md), captured at a point in time, and serves it as an immutable OCI artifact for your GitOps operator to pull.

Releases are published per Space, so you generally release a whole [variant](../background/concepts/variant.md) at once:

```
cub release publish acme-dev
```

To bundle each Unit at the revision carrying a particular tag rather than its head revision, pass `--revision`:

```
cub release publish --revision v452 acme-dev
```

`--revision` also takes a ChangeSet or [ChangeOrder](../background/entities/changeorder.md) boundary, which is how a whole change is released rather than a revision number that means something different in every Unit:

```
cub release publish --revision ChangeSet:acme-home/release-v452 acme-dev
```

A Unit with an outstanding [Validation Error](../background/concepts/gates.md) is not ready to be released, and blocks the publish. This is not terminal — the check re-runs the Unit's triggers, so once the underlying problem is fixed the publish succeeds on the next attempt. Validation Errors are evaluated against the revision being bundled, so `--revision v452` checks the gates on the `v452` revisions specifically.

Publishing advances each bundled Unit's `LastReleasedRevisionNum` to the revision captured in the bundle. `UnreleasedChanges` compares a Unit's head revision against it, so it tells you what has changed since the last publish.

See [Publishing Releases](./releases.md) for the full command reference and [integrating with GitOps operators](./gitops.md) for how the bundle reaches the cluster.

## Undoing a Change Set

If you need to undo a ChangeSet, a bulk patch with a `restore` parameter of "Before:ChangeSet:" and the change set ID will restore the prior revisions. Restoring creates new head revisions; approve them and publish a new Release to roll the rollback out. You could tag those new restored head revisions using the bulk tag API by passing "HeadRevisionNum" for the `Revision` string in the request.

```
cub tag create --space acme-home rollback-v452 --description "Rollback release 452"
cub unit update --patch --space acme-dev --filter acme-home/acme-app --restore "Before:ChangeSet:acme-home/release-v452" --tag acme-home/rollback-v452
```

Restoring takes each Unit's content back wholesale: everything after the restored revision goes with it. When there are later changes you want to keep, revert the range instead.

## Reverting changes

A restore goes back to a revision. A **revert** takes out one range of revisions and keeps everything that came after it, which is what you want when the change to undo is not the most recent one.

Reverting uses the same merge machinery as [merging and rebasing](#merging-and-rebasing), with the range running backwards. Normally `--merge-base` names the revision before the range and `--merge-end` the last revision in it. Swap them and the diff the merge carries is the inverse: what those revisions did is what gets taken away.

A reversed range requires `--squash`. The default walked form re-runs the revisions in `(base, end]`, which is empty when the range runs backwards — it would report success and change nothing, a silence that reads as a revert that worked. So it is refused:

```
merge base revision 7 is after merge end revision 5; reversing the range to undo it requires --squash
```

Bracket the change with [tags](../background/entities/tag.md) so the range can be named from both sides without hardcoding revision numbers:

```
cub tag create --space acme-home tls-pre --description "Before the TLS annotation change"
cub unit tag --space acme-dev --filter acme-home/acme-app --revision HeadRevisionNum acme-home/tls-pre

cub function set --space acme-dev --filter acme-home/acme-app set-annotation acme.io/tls "strict"

cub tag create --space acme-home tls-post --description "After the TLS annotation change"
cub unit tag --space acme-dev --filter acme-home/acme-app --revision HeadRevisionNum acme-home/tls-post
```

Then revert it with the two tags in the other order:

```
cub unit update --patch --space acme-dev --filter acme-home/acme-app \
    --merge-source Self --squash \
    --merge-base "Tag:acme-home/tls-post" \
    --merge-end "Tag:acme-home/tls-pre" \
    --change-desc "Revert the TLS annotation change"
```

Only the paths that range touched are taken back. Changes made after `tls-post` — including changes to other fields of the same resource — survive.

A ChangeSet or a [ChangeOrder](../background/entities/changeorder.md) already brackets a range, so neither needs tags of its own. Reverting one is the same command with its boundaries reversed:

```
cub unit update --patch --space acme-dev --filter acme-home/acme-app \
    --merge-source Self --squash \
    --merge-base "ChangeSet:acme-home/release-v452" \
    --merge-end "Before:ChangeSet:acme-home/release-v452" \
    --change-desc "Revert release 452"
```

Two rules decide what a revert may take back:

- **[Protection](./variants.md#protecting-a-variants-own-values) does not stand in the way.** Protection is about content arriving from another Unit — an upgrade, a merge from elsewhere. A `--merge-source Self` revert is the Unit acting on its own history, so a protected field is the caller's own and the revert may take it back.
- **A guard does.** A guard names why a value is what it is, and an operation carrying no clearance for that reason does not write the path. Pass `--clearance` naming the guard's reason to revert a guarded path. See [`cub unit set-guard`](../developer/cli/cub_unit_set-guard.md).

Like a restore, a revert only changes configuration data. Approve the new revisions and publish a Release to make the revert live.

To undo a change that has already been promoted into other Spaces, revert it there too — or, if the change was named by a ChangeOrder, use [`cub variant demote`](./promotion.md#undoing-a-rollout-abort-and-demote), which does the same thing Space by Space and records that it did.

## Merging and rebasing

In ConfigHub, revisions and clones are somewhat analogous to git commits and branches, but are somewhat different since each Unit is kind of like its own repo with its own commit history, and ConfigHub also enforces a linear revision history, which is generally more suitable and convenient for managing granular configuration units. But there are some cases where merging changes from other units or prior revisions is necessary or desirable.

Examples:

1. Prior revisions were restored, as described above, but there were subsequent changes after the changeset that was undone. You can update the units with `merge_source=Self` and the change set or other revision range for the revisions to rebase on top of the restored revision.
2. Changes have been made since the last Release that shouldn't go out yet, but an urgent change needs to be made. The last released revision can be restored, then the urgent change made and published, and the reverted changes patched back in when it's time to release them, using the same approach as in (1).
3. Clone a configuration unit and make changes for an upcoming release. When it's time to roll out, merge the changes with the upstream.

```
cub unit update --patch --space acme-dev --filter acme-home/acme-app --merge-source Self --merge-base Before:ChangeSet:acme-home/release-v452 --merge-end ChangeSet:acme-home/release-v452 --change-desc "Reapply release 452"
```

A merge walks the range it is given, revision by revision, re-running the functions each revision recorded where it can, and records one revision here per source revision that has an effect. `--squash` merges the range as one rebased diff in one revision instead. A merge that cannot apply part of what it brought records the withheld change rather than dropping or forcing it; `cub unit conflicts` lists what is outstanding, and applies or dismisses it. Conflicts are described in the [variants guide](./variants.md#conflicts-what-a-merge-could-not-apply); the merge engine itself is documented in [advanced merging techniques](./advanced-merging.md).

## Tags

ChangeSets use "managed" Tags, but [Tags](../background/entities/tag.md) can also be set independently, on named revisions such as HeadRevisionNum and LastReleasedRevisionNum. It is a good idea to tag any revisions being approved and released that aren't part of a ChangeSet, and then use the Tag when approving, when publishing (`cub release publish --revision <tag>`), and when restoring (using "Before:Tag:" and the tag ID).

```
cub tag create --space acme-home upgrade-251023 --description "Upgrade from base"
cub unit update --patch --space acme-dev --filter acme-home/acme-app --upgrade # could also use --tag here
cub unit tag --space acme-dev --filter acme-home/acme-app acme-home/upgrade-251023
```

A [ChangeOrder](../background/entities/changeorder.md) mints Tags of its own, which are how a change is tracked across Spaces. Those are not moved by hand: `cub unit tag` refuses a Tag a ChangeSet or a ChangeOrder owns.

## Listing ChangeSets and Tags

Use the organization-level Revision List API to list Revisions associated with ChangeSet or Tag. For ChangeSets, you can use the ChangeSet's StartTagID or EndTagID (if closed) via where `Tags ? '<tag-id>'`, or filter by the ChangeSetID using where `ChangeSetID = '<changeset-id>'`.

```
cub revision list --space acme-dev --filter acme-home/acme-app --tag acme-home/rollback-v452
```

For a particular Unit, all Revisions in the ChangeSet can be listed using where `ChangeSetID = '<changeset-id>'` using the existing unit-level Revision List API.

```
cub revision list --space acme-dev frontend --where "ChangeSet.Slug = 'release-v452'"
```

## Beyond one variant

Everything above happens in one Space. A change that has to reach dev, then staging, then every production variant is a rollout: it needs an identity that survives crossing a Space boundary, a declared order for the hops, and a way to ask where it has got to. See [tracking and promoting changes](./promotion.md).
