Skip to content

ChangeOrder

A ChangeOrder names one defined set of changes in a Space and follows it through promotion into the Units and Revisions of other Spaces. It answers "did this change reach that variant?", which no per-Unit revision number can: numbering is per Unit.

A ChangeSet bounds a change and locks the Units it is open on. A ChangeOrder names the change itself, and is the only identity a change keeps as it crosses a Space boundary. The two compose: a ChangeOrder can adopt a ChangeSet's end Tag as its boundary, and neither requires the other.

A ChangeOrder is not required, and it does not perform promotion. It supplies the identity, the revision range, and the target set; the existing upgrade and resolve paths do the work.

Where it lives, and what it fixes

A ChangeOrder resides in the Space holding the changes to promote — its base. That Space is what says which Units are in scope, which is why the start of the interval can be derived rather than described Unit by Unit.

It is created after the revisions it names, at the moment you decide to promote them. Creating one derives, per Unit of its Space:

  • The Links of its UpdateType (UpgradeUnit, the clone lineage, by default, or MergeUnits) whose downstream Space is in scope. A MergeUnits ChangeOrder skips a Unit nothing follows by a MergeUnits Link, since it changes the Units that are merged from rather than adding any.
  • The start — the revision each downstream has already taken, read from those Links' merge cursors. If they disagree, that is an error naming the Unit and the values, rather than a different range being silently promoted to each target.
  • The end — the Unit's head revision, or the revision a supplied boundary Tag marks on it.
  • Three Tags of its own, suffixed -co-start, -co-end, and -co-publish, and the ChangeOrder stamped onto the revisions in the interval. The third is its release Tag. UserID records who created it.

Every Unit is marked

In git, a commit names a tree, and the tree names a version of every file in the repository. ConfigHub has no tree: each Unit has its own revision history. So for a ChangeOrder to say what a Space is after the change, its end Tag marks a revision of every Unit of each Space the change is in, and its start Tag each Unit's prior state. The Units the change changed are the ones where the two differ. ConfigHub/YAML Units, which hold the configuration of entities such as Links, are not marked for now.

A Unit the change does not change has both Tags on one revision, chosen so that the change moves nothing it did not change:

  • Where its followers are: the revision the Spaces in scope that follow it have already taken. Promoting the change then finds nothing to merge into them.
  • Where it is released, when that is later. A Release of the change then leaves the Unit as it is deployed, and its followers take the difference as revisions from before the change, as their PriorRevisions says.
  • Where it is released, when nothing follows it, or at its head when it has never been released.

PriorRevisions on the ChangeOrder decides this, and is the default for its promotions. Include, the default, Skip, and Error mark as above, and differ in what a promotion does with a changed Unit's revisions from before the change (below). All marks every Unit the change does not change at its head instead, so promoting the change brings every Unit up to the Space it takes from. It is fixed at creation, since the ChangeOrder's own Space is marked then.

SkippedUnits records the Units of its own Space it carries no revisions of, and why of each, so a ChangeOrder narrower than you expected says so.

Where it is headed

InScopeSpaceIDs is where the change is headed: a list of Spaces, not a standing query. A query re-asked on every read would take in a Space that arrived weeks after the change had been released everywhere and put the ChangeOrder back to InProgress with nothing having changed. What is recorded is the answer, which can be worked out in three ways:

  • Listed by the client, with --in-scope-space. A client can compute the list however it likes.
  • Selected by the server, from WhereSpace, a where expression over Spaces, and SpaceFilterID, a Filter over Spaces, which are ANDed when both are set. The server evaluates the selection with the permissions of whoever writes it and records the Spaces it picks. It is evaluated when the ChangeOrder is created, when either field changes, and when an update asks for it with --refresh-spaces, which is how Spaces that have come to match since are taken in. While a selection is set, the list is the server's to write. cub changeorder create --component <component> adds ComponentID = '<component-id>' to WhereSpace, which selects the Spaces of a component.
  • Left empty, in which case wherever the ChangeOrder's Links reach when its scope is derived is recorded instead.

For a ChangeOrder that follows Links, every Space between its own and a Space in scope must be in scope too, since a change is propagated hop by hop; a list or selection that skips one is refused. Editing the list, or a selection that moves it, re-derives what the ChangeOrder covers, since where each Unit's interval begins is decided by the revision the Spaces in scope have already taken.

Propagating by invocation

A ChangeOrder with UpdateType Invoke propagates the other way round. There is no upstream revision to merge and no lineage to follow: the change is one Invocation, run in each Space in scope, and the ChangeOrder is created before any of it has happened. That fits routine rollouts where every variant receives the same operation, such as bumping an image tag, setting a label, or applying a policy, and where the variants need not be clones of one another at all.

  • InvocationID names what to run, and Parameters supplies the values for a parameterized Invocation's declared parameters: one set for the whole ChangeOrder, so every Space receives the same change.
  • WhereUnit and UnitFilterID narrow which Units of each Space are covered; empty covers every Unit.
  • All of these are fixed at creation, since they are what the change is.

Promoting it into a Space runs the Invocation on the covered Units there and marks the Revisions it made with the ChangeOrder's Tags. Every other Unit of the Space, whether the selection left it out or the Invocation changed nothing in it, is marked where it is released, or at its head if it has never been released. Everything else — promoting by Stage, the prerequisites, releasing with --revision ChangeOrder:<slug>, State, and undoing — reads the same as for a ChangeOrder that follows Links. Unlike the Spaces in scope, the Unit selection is asked again on every read: a Unit added to a Space after the change was made there has not had the Invocation run on it, and counts against that Space until it does.

A ChangeOrder with UpdateType Insert, Upsert, or TransformPaths names a change to Units outside its scope that the Units of the Spaces in scope follow by Links of that type: a Unit holding the image tag of a container repository, in a Space of its own, that each variant's workload reads through a TransformPaths Link. It usually resides in the component's base, like its other ChangeOrders. The Spaces in scope are its targets, and the Units their Links point at in Spaces outside the scope are its sources.

  • Creating one marks each source, in whatever Space it is: the end Tag on its head, or the revision a supplied boundary Tag marks, and the start Tag on the oldest revision any Link in scope has taken. Targets at different revisions are not an error, since these Links recompute the downstream value from the state they are pointed at rather than merging a range. Marking a source needs the same permission as tagging any revision of it.
  • It is refused when a Link in scope is AutoUpdate, or has already resolved past the end.
  • InScopeSpaceIDs is required, listed or selected, since the Links leave from the Spaces in scope; there is no graph from the ChangeOrder's own Space for their reach to stand in for. It is also what tells targets from sources: a Space in scope is a target, never a source.
  • Promoting it into a Space resolves that Space's Links with the ChangeOrder, reading each source at the revision the end Tag marks. Nothing is cloned, and no Space waits for another.
  • Its own Space does not have the change by holding the ChangeOrder, as with Invoke, and SkippedUnits names sources rather than Units of its own Space.

Governed by a ChangeWorkflow

ChangeWorkflowID names the ChangeWorkflow the ChangeOrder is promoted under. It is set at creation and fixed afterwards, and the server copies the workflow's definition onto the ChangeOrder, in ChangeWorkflow, when it is set. Every promotion of the ChangeOrder, and every publish for it, is judged against that copy, so editing the workflow later changes nothing about a rollout already under way. A ChangeOrder with no workflow is promoted ungated.

Stage records the Stage the change has reached, read against that copy, and becomes Completed once the last Stage satisfies the workflow's Final prerequisites. PromotionOverrides records each promotion forced past gates that did not hold: who forced it, when, into which Stage, why, and which gates failed. PromotionFailures records each promotion that did not complete: who ran it, when, the Stage it was headed for, and each Space it failed or was blocked in, with the Space's error or reason and each failed Unit's error. Both keep the most recent entries.

Promoting and undoing

A promotion marks itself: the start Tag on the target's head before the merge, the end Tag on the revision the merge arrives at, and the ChangeOrder on every revision it creates. That makes each hop an inspectable fact and makes it undoable in one step however many revisions the range produced. A target that already holds the change makes no revision, and both Tags go on its head, which is how a Space where the change was made first reads as having taken it.

A target has to be at or short of the ChangeOrder's start. In the Space the ChangeOrder was created in, the start is read from the targets' merge cursors, so they are at it. One Space further down they can be short of it: the start Tag went on each Unit's head when the ChangeOrder was promoted into the Space above, after whatever that Space wrote on its own. PriorRevisions decides what happens to those revisions: the ChangeOrder's, or the promotion's --prior-revisions. Include merges them first, as revisions that do not carry the ChangeOrder, and puts the start Tag after them. Skip promotes only the ChangeOrder's range, and the Link moves past the revisions it skipped, so they do not arrive later; a range can still be taken explicitly with cub unit update --merge-source --merge-base --merge-end. Error refuses. All merges them as Include does. A target already past the start is refused regardless, with the revisions it has taken named.

Once a Space's writes are done, every Unit of it the change did not change is marked, as above. That includes the Units a variant created for itself rather than cloning from upstream, such as a database Unit that only one variant has: no change from upstream reaches them, so they are marked where they are. The variants downstream neither take the variant's edits to them nor clone the ones they do not have, unless PriorRevisions is All. Updating Units one at a time with cub unit update --upgrade --change-order marks only the Units it writes, and places the release Tag; the rest of the Space is marked by a promotion with cub variant promote.

A promotion copies the variant's Links as well, independently of these rules: a Link whose source Unit has a copy downstream gets one too, whatever PriorRevisions says. Links are not yet part of what a ChangeOrder marks.

Promotion is idempotent. A Unit already carrying the end Tag has taken the change, whatever has happened to it since, and is passed over — which is what lets a promotion that landed partway be run again to finish it.

Setting AbortedReason aborts a ChangeOrder: the record that the change is not coming to the Spaces still waiting for it. An aborted ChangeOrder is refused by every promotion. Undoing one requires the abort, mints a third Tag (-co-restore), and cannot be taken back once anything has been restored — so a ChangeOrder travels in one direction, and a change to promote after an undoing is a new ChangeOrder.

Releasing it

A Release of a ChangeOrder, cub release publish --revision ChangeOrder:<slug>, is published at its release Tag, ReleaseTagID (<slug>-co-publish). The release Tag marks the releasable Units of each Space the change is in, those with an empty ProviderType or OCI, at the revision the end Tag marks. A Release of the change therefore delivers the whole Space as the change left it, and leaves every Unit the change did not change as it is deployed.

Unlike the end Tag, the release Tag moves, until a Release in the Space has been published at it:

  • It follows resolve. When the resolve processor makes a revision on top of the one the release Tag marks, such as a ConfigMap rendered from an AppConfig Unit the change updated, the release Tag moves to it. The end Tag stays where the change landed. An edit, an invocation, or an upgrade is somebody's own change, and does not move it.
  • It is moved by hand with cub unit tag ChangeOrder:<slug> --move --revision <revision>, to adopt a fix made in review. It cannot move before the revision the end Tag marks or the Unit's last released revision, nor to a revision with validation errors.

ReleasePriorRevisions says what a Release of the ChangeOrder does with a Unit's revisions from before the change: those after its last released revision, up to the one the start Tag marks. They are there when an earlier change went downstream from the Space without being released in it. Include, the default, releases them; Error refuses, naming them and the ChangeOrder each belongs to. cub release publish --prior-revisions overrides it for one Release.

Where it has got to

ResolvedSpaceIDs, ReleasedSpaceIDs, Releases, RestoredSpaceIDs, ReleasedRestoredSpaceIDs and State are derived when the ChangeOrder is read, not recorded as it travels, so they cannot go stale.

Releases names, for each Space in ReleasedSpaceIDs, the Release that released the change there: the earliest published Release of the Space that carries it, as SpaceID, ReleaseID and ReleaseNum. It is the Release the promotion gates read, and cub changeorder get shows its number beside each released Space. A Space whose carrying Release was withdrawn has no entry.

State reduces them to one word: New until a Space other than its own has taken it (for Invoke and the Link types that leave its scope, until any Space has), InProgress while some have and some have not, Resolved once every Space in scope has, and Released once every Space in scope has released what it took. A Space with no release Target has nothing to release: it is never in ReleasedSpaceIDs, and it does not hold Released back. The ChangeOrder's own Space is no exception either way: with a release Target, it has to release the change like any other. AbortedReason overrides all of it with Aborted, and the undoing overrides that in turn with Restored and RestoreReleased.

A Space is measured on the Units the change changed and the Units that follow them, not on every Unit its Tags mark. A Space downstream is short of the change, and not resolved, when it is missing a Unit the change adds, such as one the base created after its variants were cloned; a Unit it does not have that the change only changed is one it does without.

All of them are queryable: ResolvedSpaceIDs ? '<space-id>' asks whether one Space has it, LEN(ResolvedSpaceIDs) how many do, and Releases.*.ReleaseID = '<release-id>' which ChangeOrders a Release released.

Deleting a ChangeOrder takes its marks back — it is removed from every revision carrying it, and its own Tags are removed and deleted. A boundary Tag it only adopted is never touched. A Release published at its release Tag refers to that Tag, so deleting the ChangeOrder is refused until that Release is deleted.

See tracking and promoting changes for the workflow, and ChangeWorkflow for the definition that gives a ChangeOrder its stages.