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, 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. Taking a named change out of a base and through dev, staging, and production is promotion.
Selecting Units
To make a configuration change and then release it, first identify a set of units. Typically it will be a variant, corresponding to a Space, or set of variants, deployed to a single Target cluster.
Use a where expression or a saved Filter to select the Spaces.
See the guide about managing sets of units for more details.
Change Sets
Create a ChangeSet 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 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 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, and the revisions 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's job — see tracking and promoting changes.
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 Apply Gate 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 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, 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 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 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 Apply Gate 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. Gates 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 for the full command reference and integrating with GitOps operators 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, 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 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 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 does not stand in the way. Protection is about content arriving from another Unit — an upgrade, a merge from elsewhere. A
--merge-source Selfrevert 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
--clearancenaming the guard's reason to revert a guarded path. Seecub unit set-guard.
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, 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:
- 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=Selfand the change set or other revision range for the revisions to rebase on top of the restored revision. - 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).
- 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; the merge engine itself is documented in advanced merging techniques.
Tags
ChangeSets use "managed" Tags, but Tags 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 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.