Making Changes
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, grouping, undoing, and merging changes within one variant. Publishing Releases covers making them live. Taking a named change out of a base and through dev, staging, and production, including review and approval along the way, 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. Opening a ChangeSet changes nothing about the units: its start Tag goes on each unit's current head revision, which is the state to go back to, and no revision is created.
cub unit update --patch --space acme-dev --filter acme-home/acme-app --changeset acme-home/release-v452
cub variant changeset open does the same for the units of a variant: every unit not already in a change set, or those --where selects among them.
cub variant changeset open acme-dev acme-home/release-v452
A ChangeSet can be opened on units in more than one Space, and records each Space it was opened in, in InScopeSpaceIDs.
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 Units. Taking the first unit out of a ChangeSet closes the whole of it, so take every unit it is open on out of it, in every Space: a unit left in a closed ChangeSet cannot be written until it is taken out.
cub unit update --patch --space "*" --where "ChangeSet.Slug = 'release-v452'" --changeset -
cub variant changeset close does that for you:
cub variant changeset close acme-dev acme-home/release-v452
A ChangeSet locks the Units it is open on, in whichever Spaces they are. It does not follow the change into other Spaces as it is promoted: that is a ChangeOrder's job, described in tracking and promoting changes. To promote or release what a ChangeSet changed, close it and create a ChangeOrder that ends at its end Tag, as shown in defining the change.
Reviewing a Change Set
An open ChangeSet is reviewed before it is closed, the way a pull request is reviewed before it is merged. Its page in the UI, under Explorers > ChangeSets, shows what it changed in each unit, who has approved it, and whose review it is waiting for.
Ask for a review when you create the ChangeSet, or later:
cub changeset create --space acme-home release-v452 --description "Release 452" --request-review alice
cub changeset update --space acme-home release-v452 --request-review bob
cub changeset update --space acme-home release-v452 --withdraw-review-request bob
A request is not cleared when the review is given: whether a reviewer has reviewed is read from the Attestations they recorded. An approval of the ChangeSet covers the latest revision it made of each unit while it is open, and its end once it is closed, so a change made after an approval leaves that approval out of date:
cub variant approve acme-dev --all --revision ChangeSet:acme-home/release-v452
cub changeset get shows who created the ChangeSet and whose reviews have been requested.
Gating the close with a ChangeWorkflow
A ChangeSet created with --change-workflow is governed by that ChangeWorkflow: it closes only when every prerequisite of the workflow's Final holds in every Space the ChangeSet was opened in. The workflow has no Stages, since a ChangeSet is not promoted, and its Final can name Validated, custom prerequisites, and attestation prerequisites, such as an approval by someone who did not write the change. The workflow is copied onto the ChangeSet when it is created, and cannot be changed afterwards.
cub changeworkflow create --space acme-workflows release-review --from-stdin <<EOF
Final:
Prerequisites: [Validated, reviewed]
AttestationPrerequisites:
- Name: reviewed
Description: one approval by someone who did not write the change
EOF
cub changeset create --space acme-home release-v452 --change-workflow acme-workflows/release-review
The gates are evaluated when the ChangeSet closes, against the latest revision it made of each unit. A Space where it changed nothing satisfies every gate. --dry-run reports the gates without closing anything. A close the gates refuse takes no unit out of the ChangeSet, and names each gate that does not hold, in each Space where it does not:
cub variant changeset close acme-dev acme-home/release-v452 --dry-run
The unit write that closes a governed ChangeSet cannot also change the unit's configuration, since the gates were evaluated on the revisions the ChangeSet made. Make the change first, then close it.
To close it although a gate does not hold, give a reason. Forcing it needs Edit permission on the ChangeSet, and who forced it, when, why, and which gates did not hold are recorded in its CloseOverride:
cub variant changeset close acme-dev acme-home/release-v452 --force-reason "approved in the incident channel"
cub unit update --changeset - takes --force-close with --force-reason to the same effect. The ChangeSet's page in the UI disables Close while a gate does not hold, lists the gates and the Spaces each does not hold in, and offers Force close with a reason.
When a unit in a Space of a Component that requires a ChangeWorkflow joins a ChangeSet, the ChangeSet has to be governed by one, and the workflow has to be one the Component allows. An upload into such a Space takes --change-workflow for the ChangeSet it records its writes in, and leaves a governed ChangeSet open for review rather than closing it:
cub variant upload --component acme --variant dev --change-workflow acme-workflows/release-review ./rendered/
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; 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 --display-name "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 --display-name "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 --display-name "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. 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 (cub variant approve --revision Tag:<tag>), 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 --display-name "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, and a Release that creates its Tag owns that one. Those are not added or removed by hand: cub unit tag refuses a Tag a ChangeSet, a ChangeOrder, or a Release owns. The exception is a ChangeOrder's release Tag, which cub unit tag ChangeOrder:<slug> --move moves to adopt a later revision before the change is released; see releasing each stage. --move works on any other Tag too, and refuses a Unit the Tag does not already mark.
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
--changeset lists the revisions made in a ChangeSet, in whichever units it was opened on:
cub revision list --changeset acme-home/release-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.