ChangeWorkflow
A ChangeWorkflow is the declared series of stages a change moves through on its way from where it is authored to everywhere it must land, with the conditions that must hold before a change may enter each stage. It is the definition — the pattern. A ChangeOrder is the invocation: one named change moving through it.
The order is a partial order: sequential between stages, parallel within one. A stage is a wave — everything in it may be promoted in any order, or all at once — and the waves are ordered. A fleet of a thousand clusters cannot be a sequence of a thousand steps.
It is a Unit, not a first-class entity
A ChangeWorkflow is a config Unit written in the Kubernetes Resource Model's shape: an apiVersion carrying a group and version, a kind, a name under metadata, and the rest under spec. Being KRM, it is an ordinary Kubernetes/YAML Unit — the default toolchain. Nothing in Kubernetes reconciles it; KRM is the shape, not the destination.
apiVersion: confighub.com/v1
kind: ChangeWorkflow
metadata:
name: acme-main-line
spec:
stages:
- name: base
whereSpace: "Labels.Stage = 'Base'"
- name: dev
whereSpace: "Labels.Stage = 'Development'"
- name: staging
whereSpace: "Labels.Stage = 'Staging'"
prerequisites:
- released
- name: prod
whereSpace: "Labels.Stage = 'Production'"
prerequisites:
- released
- healthy
final:
prerequisites:
- released
- healthy
Being a Unit means it is versioned for free — every edit is a Revision, diffable and restorable — that it is editable with every existing surface on day one, and that it is itself subject to change management. It also means there is no referential integrity: a renamed or deleted Space leaves a dangling selector, and a mistyped one silently matches nothing.
Because it is found by resource type rather than by location, it may live in any Space. Two constraints: it must not be deployable — a Space with no release Target — and it should not sit in a component's base Space, or it gets cloned into every variant.
Stages and prerequisites
whereSpace is a literal where expression over Spaces, so stage membership is by selector rather than by an enumerated list: a variant added to a stage later is covered without editing the definition. The standard Space labels — Stage, Environment, Region — are what stages select on.
A stage's membership is an intersection of three terms: its own selector, the component of the Space the ChangeOrder lives in, and that ChangeOrder's InScopeSpaceIDs. Two of the three come from the invocation, so the same definition resolves to different Spaces under different ChangeOrders — which is what lets a workflow Unit be cloned to give another component the same shape of rollout. The component is appended to every stage's selector, so a whereSpace that names Labels.Component itself is refused rather than conjoined: restating it either changes nothing or makes the stage select no Space at all, and a stage that promotes into nothing reports nothing wrong.
prerequisites are entry gates: what must hold to get into a stage, evaluated over every Space in the stage before it. Two are recognized:
released— that Space has published a Release carrying the change.healthy— that Space's live status, written back by the GitOps operator, reports Synced / Succeeded / Healthy.
Having taken the change is checked whatever is declared; the prerequisites are checks on top of that. They are reads of existing status, evaluated at the moment a promotion is attempted and never cached from promotion time — so a stage that has degraded since it was promoted does not open the gate ahead of it.
healthy implies released, since live status only exists downstream of a Release. A Space with no release Target can never satisfy either.
final.prerequisites are evaluated against the last stage and say when the rollout is completed, rather than when a hop may happen — a reading no promotion makes, there being no hop left to gate once the change is in the last stage.
The definition does not name where the change starts. The base is the Space its ChangeOrder is created in, so the starting point is stated once, by the invocation, and there is no second declaration to keep coherent with it.
How a ChangeOrder picks one up
cub changeorder create --change-workflow <unit> records an annotation naming the workflow Unit and the revision in force. That revision is pinned for the ChangeOrder's lifetime, so editing the workflow part way through a rollout cannot change the rules a change already started under.
Sequencing is enforced by the client in this phase: cub variant promote reads the pinned definition, checks the gates, and refuses a hop the workflow does not permit.
As mentioned above, each stage's membership is the intersection of the workflow's selectors for that stage, the component, and the InScopeSpaceIDs. This is resolved on promotion by the client, and used to determine the current stage of the workflow.
See tracking and promoting changes for the workflow in practice.