From bf8f0e0cb294825a189c703ac0141916270aa0a1 Mon Sep 17 00:00:00 2001 From: Dadisi Sanyika Date: Wed, 29 Jul 2026 09:08:21 -0700 Subject: [PATCH] feat(cd): add migration subject and events A migration is a versioned change applied to a stateful resource - a database schema, the data it holds, or a configuration store. What distinguishes it from other versioned work is that undoing it may not be possible. For a service, recovery is a deployment of an earlier artifact: another forward action, always available. For stored state it may not be, since dropped columns and deleted rows do not come back by running the same step again. A consumer that automates recovery, or that decides whether to page a human, needs to know which case it is at the moment the event arrives, and that cannot be inferred from a versioned run in general nor from the tool that emitted it. reversible declares that per occurrence. When absent, reversibility is undeclared and consumers MUST NOT read the absence as true. Three predicates: queued, started, finished. A reversal, where possible, is itself a later migration carrying the earlier version in toVersion, so no reverted predicate is defined. A migration that finds nothing to apply emits nothing, keeping the subject an occurrence model. toVersion is required and fromVersion is optional: a producer always knows the version it applied, but frequently does not know the version it overwrote without querying prior state. The change object is reused from the existing change subject. Event versions start at 0.1.0-draft. Signed-off-by: Dadisi Sanyika --- .spellcheck-en-custom.txt | 1 + conformance/migration_finished.json | 63 +++++++++++ conformance/migration_queued.json | 62 +++++++++++ conformance/migration_started.json | 62 +++++++++++ continuous-deployment.md | 85 ++++++++++++++ schemas/migrationfinished.json | 167 ++++++++++++++++++++++++++++ schemas/migrationqueued.json | 154 +++++++++++++++++++++++++ schemas/migrationstarted.json | 154 +++++++++++++++++++++++++ 8 files changed, 748 insertions(+) create mode 100644 conformance/migration_finished.json create mode 100644 conformance/migration_queued.json create mode 100644 conformance/migration_started.json create mode 100644 schemas/migrationfinished.json create mode 100644 schemas/migrationqueued.json create mode 100644 schemas/migrationstarted.json diff --git a/.spellcheck-en-custom.txt b/.spellcheck-en-custom.txt index 4bb487d..7c3735b 100644 --- a/.spellcheck-en-custom.txt +++ b/.spellcheck-en-custom.txt @@ -60,6 +60,7 @@ schemas schemauri specversion src +stateful subjectid taskrun testcase diff --git a/conformance/migration_finished.json b/conformance/migration_finished.json new file mode 100644 index 0000000..8950047 --- /dev/null +++ b/conformance/migration_finished.json @@ -0,0 +1,63 @@ +{ + "context": { + "specversion": "0.6.0-draft", + "id": "271069a8-fc18-44f1-b38f-9d70a1695819", + "chainId": "4c8cb7dd-3448-41de-8768-eec704e2829b", + "source": "/gitops/k8s/migrations", + "type": "dev.cdevents.migration.finished.0.1.0-draft", + "timestamp": "2026-07-27T17:31:00Z", + "schemaUri": "https://myorg.com/schema/custom", + "links": [ + { + "linkType": "RELATION", + "linkKind": "TRIGGER", + "target": { + "contextId": "5328c37f-bb7e-4bb7-84ea-9f5f85e4a7ce" + }, + "tags": { + "foo1": "bar", + "foo2": "bar" + } + }, + { + "linkType": "PATH", + "from": { + "contextId": "5328c37f-bb7e-4bb7-84ea-9f5f85e4a7ce" + }, + "tags": { + "foo1": "bar", + "foo2": "bar" + } + }, + { + "linkType": "END", + "from": { + "contextId": "5328c37f-bb7e-4bb7-84ea-9f5f85e4a7ce" + }, + "tags": { + "foo1": "bar", + "foo2": "bar" + } + } + ] + }, + "subject": { + "id": "V12__add_order_status", + "source": "/gitops/k8s/migrations", + "content": { + "target": { + "id": "orders-db", + "source": "/prod/postgres/orders", + "type": "database" + }, + "fromVersion": "11", + "toVersion": "12", + "reversible": false, + "change": { + "id": "a8f3c21", + "source": "/scm/github/orders-svc" + }, + "outcome": "success" + } + } +} diff --git a/conformance/migration_queued.json b/conformance/migration_queued.json new file mode 100644 index 0000000..0a499df --- /dev/null +++ b/conformance/migration_queued.json @@ -0,0 +1,62 @@ +{ + "context": { + "specversion": "0.6.0-draft", + "id": "271069a8-fc18-44f1-b38f-9d70a1695819", + "chainId": "4c8cb7dd-3448-41de-8768-eec704e2829b", + "source": "/gitops/k8s/migrations", + "type": "dev.cdevents.migration.queued.0.1.0-draft", + "timestamp": "2026-07-27T17:31:00Z", + "schemaUri": "https://myorg.com/schema/custom", + "links": [ + { + "linkType": "RELATION", + "linkKind": "TRIGGER", + "target": { + "contextId": "5328c37f-bb7e-4bb7-84ea-9f5f85e4a7ce" + }, + "tags": { + "foo1": "bar", + "foo2": "bar" + } + }, + { + "linkType": "PATH", + "from": { + "contextId": "5328c37f-bb7e-4bb7-84ea-9f5f85e4a7ce" + }, + "tags": { + "foo1": "bar", + "foo2": "bar" + } + }, + { + "linkType": "END", + "from": { + "contextId": "5328c37f-bb7e-4bb7-84ea-9f5f85e4a7ce" + }, + "tags": { + "foo1": "bar", + "foo2": "bar" + } + } + ] + }, + "subject": { + "id": "V12__add_order_status", + "source": "/gitops/k8s/migrations", + "content": { + "target": { + "id": "orders-db", + "source": "/prod/postgres/orders", + "type": "database" + }, + "fromVersion": "11", + "toVersion": "12", + "reversible": false, + "change": { + "id": "a8f3c21", + "source": "/scm/github/orders-svc" + } + } + } +} diff --git a/conformance/migration_started.json b/conformance/migration_started.json new file mode 100644 index 0000000..1a32732 --- /dev/null +++ b/conformance/migration_started.json @@ -0,0 +1,62 @@ +{ + "context": { + "specversion": "0.6.0-draft", + "id": "271069a8-fc18-44f1-b38f-9d70a1695819", + "chainId": "4c8cb7dd-3448-41de-8768-eec704e2829b", + "source": "/gitops/k8s/migrations", + "type": "dev.cdevents.migration.started.0.1.0-draft", + "timestamp": "2026-07-27T17:31:00Z", + "schemaUri": "https://myorg.com/schema/custom", + "links": [ + { + "linkType": "RELATION", + "linkKind": "TRIGGER", + "target": { + "contextId": "5328c37f-bb7e-4bb7-84ea-9f5f85e4a7ce" + }, + "tags": { + "foo1": "bar", + "foo2": "bar" + } + }, + { + "linkType": "PATH", + "from": { + "contextId": "5328c37f-bb7e-4bb7-84ea-9f5f85e4a7ce" + }, + "tags": { + "foo1": "bar", + "foo2": "bar" + } + }, + { + "linkType": "END", + "from": { + "contextId": "5328c37f-bb7e-4bb7-84ea-9f5f85e4a7ce" + }, + "tags": { + "foo1": "bar", + "foo2": "bar" + } + } + ] + }, + "subject": { + "id": "V12__add_order_status", + "source": "/gitops/k8s/migrations", + "content": { + "target": { + "id": "orders-db", + "source": "/prod/postgres/orders", + "type": "database" + }, + "fromVersion": "11", + "toVersion": "12", + "reversible": false, + "change": { + "id": "a8f3c21", + "source": "/scm/github/orders-svc" + } + } + } +} diff --git a/continuous-deployment.md b/continuous-deployment.md index fba55e9..5551fdd 100644 --- a/continuous-deployment.md +++ b/continuous-deployment.md @@ -16,10 +16,13 @@ Continuous Deployment (CD) events are related to continuous deployment pipelines This specification defines two subjects in this stage: `environment` and `service`. The term `service` is used to represent a running Artifact. A `service` can represent a binary that is running, a daemon, an application, a docker container. The term `environment` represent any platform which has all the means to run a `service`. +Deployments also change resources that hold state. A [`migration`](#migration) is a versioned change applied to a stateful resource, such as a database schema, the data it holds, or a configuration store. + | Subject | Description | Predicates | |---------|-------------|------------| | [`environment`](#environment) | An environment where to run services | [`created`](#environment-created), [`modified`](#environment-modified), [`deleted`](#environment-deleted)| | [`service`](#service) | A service | [`deployed`](#service-deployed), [`upgraded`](#service-upgraded), [`rolledback`](#service-rolledback), [`removed`](#service-removed), [`published`](#service-published)| +| [`migration`](#migration) | A versioned change applied to a stateful resource | [`queued`](#migration-queued), [`started`](#migration-started), [`finished`](#migration-finished)| ### `environment` @@ -43,6 +46,30 @@ A `service` can represent for example a binary that is running, a daemon, an app | environment | `Object` ([`environment`](#environment)) | Reference for the environment where the service runs | `{"id": "1234"}`, `{"id": "maven123, "source": "tekton-dev-123"}` | | artifactId | `Purl` | Identifier of the artifact deployed with this service | `pkg:oci/myapp@sha256%3A0b31b1c02ff458ad9b7b81cbdf8f028bd54699fa151f221d1e8de6817db93427`, `pkg:golang/mygit.com/myorg/myapp@234fd47e07d1004f0aed9c` | +### `migration` + +A [`migration`](#migration) is a versioned change applied to a stateful resource: a database schema, the data it holds, or a configuration store. It runs to completion, and `toVersion` identifies the version it applied. + +What distinguishes a `migration` from other versioned work is that undoing it may not be possible. For a `service`, recovery is a deployment of an earlier artifact — another forward action, always available. For stored state it may not be: dropped columns and deleted rows do not come back by running the same step again. `reversible` declares, per occurrence, whether a clean reversal of this particular change exists. + +When `reversible` is absent, reversibility is undeclared. A consumer MUST NOT read an absent `reversible` as `true`. Producers able to determine it, such as tools that keep an explicit undo script alongside each change, SHOULD declare it. + +A reversal, where one is possible, is itself a `migration` — a later occurrence carrying the earlier version in `toVersion`. There is therefore no `reverted` predicate. Applying a change against a temporary copy to check it first is a [`testCaseRun`](testing-events.md#testcaserun), not a `migration`. + +A `migration` that finds nothing to apply emits nothing. + +| Field | Type | Description | Examples | +|-------|------|-------------|----------| +| id | `String` | See [id](spec.md#id-subject)| `V12__add_order_status` | +| source | `URI-Reference` | See [source](spec.md#source-subject) | `/gitops/k8s/migrations` | +| target | `Object` | The stateful resource that was changed. `type` names its kind. | `{"id": "orders-db", "source": "/prod/postgres/orders", "type": "database"}` | +| fromVersion | `String` | The version the target held beforehand, when the producer knows it | `11` | +| toVersion | `String` | The version this migration applied | `12` | +| reversible | `Boolean` | Whether a clean reversal of this change exists | `false` | +| change | `Object` ([`change`](source-code-version-control.md#change)) | The change that authored this migration, when one exists | `{"id": "a8f3c21", "source": "/scm/github/orders-svc"}` | +| outcome | `String (enum)` | outcome of a finished `migration` | `success`, `failure`, `cancel`, or `error` | +| errors | `String` | In case of error, canceled, or failed migration, provides details about the failure | `Constraint violation on orders.status` | + ## Events ### [`environment created`](conformance/environment_created.json) @@ -161,3 +188,61 @@ This event represents an existing instance of a service that has an accessible U | id | `String` | See [id](spec.md#id-subject)| `service/myapp`, `daemonset/myapp` | ✅ | | source | `URI-Reference` | See [source](spec.md#source-subject) | | | | environment | `Object` ([`environment`](#environment)) | Reference for the environment where the service runs | `{"id": "1234"}`, `{"id": "maven123, "source": "tekton-dev-123"}` | ✅ | + +### [`migration queued`](conformance/migration_queued.json) + +A migration has been accepted for execution. No changes have been applied to the target yet. + +- Event Type: __`dev.cdevents.migration.queued.0.1.0-draft`__ +- Predicate: queued +- Subject: [`migration`](#migration) + +| Field | Type | Description | Examples | Required | +|-------|------|-------------|----------|----------------------------| +| id | `String` | See [id](spec.md#id-subject)| `V12__add_order_status` | ✅ | +| source | `URI-Reference` | [source](spec.md#source) from the context | | | +| target | `Object` | The stateful resource to be changed | `{"id": "orders-db", "source": "/prod/postgres/orders", "type": "database"}` | ✅ | +| fromVersion | `String` | The version the target holds beforehand, when known | `11` | | +| toVersion | `String` | The version this migration will apply | `12` | ✅ | +| reversible | `Boolean` | Whether a clean reversal of this change exists | `false` | | +| change | `Object` ([`change`](source-code-version-control.md#change)) | The change that authored this migration | `{"id": "a8f3c21", "source": "/scm/github/orders-svc"}` | | + +### [`migration started`](conformance/migration_started.json) + +A migration has begun applying changes to the target. + +- Event Type: __`dev.cdevents.migration.started.0.1.0-draft`__ +- Predicate: started +- Subject: [`migration`](#migration) + +| Field | Type | Description | Examples | Required | +|-------|------|-------------|----------|----------------------------| +| id | `String` | See [id](spec.md#id-subject)| `V12__add_order_status` | ✅ | +| source | `URI-Reference` | [source](spec.md#source) from the context | | | +| target | `Object` | The stateful resource being changed | `{"id": "orders-db", "source": "/prod/postgres/orders", "type": "database"}` | ✅ | +| fromVersion | `String` | The version the target held beforehand, when known | `11` | | +| toVersion | `String` | The version this migration is applying | `12` | ✅ | +| reversible | `Boolean` | Whether a clean reversal of this change exists | `false` | | +| change | `Object` ([`change`](source-code-version-control.md#change)) | The change that authored this migration | `{"id": "a8f3c21", "source": "/scm/github/orders-svc"}` | | + +### [`migration finished`](conformance/migration_finished.json) + +A migration has terminated, successfully or not. + +When `reversible` is `false` and `outcome` is `success`, the change is a commitment that cannot be cleanly undone; recovery must be a new forward migration. + +- Event Type: __`dev.cdevents.migration.finished.0.1.0-draft`__ +- Predicate: finished +- Subject: [`migration`](#migration) + +| Field | Type | Description | Examples | Required | +|-------|------|-------------|----------|----------------------------| +| id | `String` | See [id](spec.md#id-subject)| `V12__add_order_status` | ✅ | +| source | `URI-Reference` | [source](spec.md#source) from the context | | | +| target | `Object` | The stateful resource that was changed | `{"id": "orders-db", "source": "/prod/postgres/orders", "type": "database"}` | ✅ | +| fromVersion | `String` | The version the target held beforehand, when known | `11` | | +| toVersion | `String` | The version this migration applied | `12` | ✅ | +| reversible | `Boolean` | Whether a clean reversal of this change exists | `false` | | +| change | `Object` ([`change`](source-code-version-control.md#change)) | The change that authored this migration | `{"id": "a8f3c21", "source": "/scm/github/orders-svc"}` | | +| outcome | `String (enum)` | outcome of a finished `migration` | `success`, `failure`, `cancel`, or `error` | `success`, `failure`, `cancel`, `error` | +| errors | `String` | In case of error, canceled, or failed migration, provides details about the failure | `Constraint violation on orders.status` | | diff --git a/schemas/migrationfinished.json b/schemas/migrationfinished.json new file mode 100644 index 0000000..8316052 --- /dev/null +++ b/schemas/migrationfinished.json @@ -0,0 +1,167 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cdevents.dev/0.6.0-draft/schema/migration-finished-event", + "properties": { + "context": { + "properties": { + "specversion": { + "type": "string", + "minLength": 1 + }, + "id": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "minLength": 1, + "format": "uri-reference" + }, + "type": { + "type": "string", + "enum": [ + "dev.cdevents.migration.finished.0.1.0-draft" + ], + "default": "dev.cdevents.migration.finished.0.1.0-draft" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "schemaUri": { + "type": "string", + "minLength": 1, + "format": "uri" + }, + "chainId": { + "type": "string", + "minLength": 1 + }, + "links": { + "$ref": "links/embeddedlinksarray" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "specversion", + "id", + "source", + "type", + "timestamp" + ] + }, + "subject": { + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "minLength": 1, + "format": "uri-reference" + }, + "content": { + "properties": { + "target": { + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "minLength": 1, + "format": "uri-reference" + }, + "type": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "type" + ] + }, + "fromVersion": { + "type": "string" + }, + "toVersion": { + "type": "string" + }, + "reversible": { + "type": "boolean" + }, + "change": { + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "minLength": 1, + "format": "uri-reference" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id" + ] + }, + "outcome": { + "type": "string", + "enum": [ + "success", + "failure", + "cancel", + "error" + ] + }, + "errors": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "target", + "toVersion", + "outcome" + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "content" + ] + }, + "customData": { + "oneOf": [ + { + "type": "object" + }, + { + "type": "string", + "contentEncoding": "base64" + } + ] + }, + "customDataContentType": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "context", + "subject" + ] +} diff --git a/schemas/migrationqueued.json b/schemas/migrationqueued.json new file mode 100644 index 0000000..5cd5a38 --- /dev/null +++ b/schemas/migrationqueued.json @@ -0,0 +1,154 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cdevents.dev/0.6.0-draft/schema/migration-queued-event", + "properties": { + "context": { + "properties": { + "specversion": { + "type": "string", + "minLength": 1 + }, + "id": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "minLength": 1, + "format": "uri-reference" + }, + "type": { + "type": "string", + "enum": [ + "dev.cdevents.migration.queued.0.1.0-draft" + ], + "default": "dev.cdevents.migration.queued.0.1.0-draft" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "schemaUri": { + "type": "string", + "minLength": 1, + "format": "uri" + }, + "chainId": { + "type": "string", + "minLength": 1 + }, + "links": { + "$ref": "links/embeddedlinksarray" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "specversion", + "id", + "source", + "type", + "timestamp" + ] + }, + "subject": { + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "minLength": 1, + "format": "uri-reference" + }, + "content": { + "properties": { + "target": { + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "minLength": 1, + "format": "uri-reference" + }, + "type": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "type" + ] + }, + "fromVersion": { + "type": "string" + }, + "toVersion": { + "type": "string" + }, + "reversible": { + "type": "boolean" + }, + "change": { + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "minLength": 1, + "format": "uri-reference" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id" + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "target", + "toVersion" + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "content" + ] + }, + "customData": { + "oneOf": [ + { + "type": "object" + }, + { + "type": "string", + "contentEncoding": "base64" + } + ] + }, + "customDataContentType": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "context", + "subject" + ] +} diff --git a/schemas/migrationstarted.json b/schemas/migrationstarted.json new file mode 100644 index 0000000..628fccd --- /dev/null +++ b/schemas/migrationstarted.json @@ -0,0 +1,154 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cdevents.dev/0.6.0-draft/schema/migration-started-event", + "properties": { + "context": { + "properties": { + "specversion": { + "type": "string", + "minLength": 1 + }, + "id": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "minLength": 1, + "format": "uri-reference" + }, + "type": { + "type": "string", + "enum": [ + "dev.cdevents.migration.started.0.1.0-draft" + ], + "default": "dev.cdevents.migration.started.0.1.0-draft" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "schemaUri": { + "type": "string", + "minLength": 1, + "format": "uri" + }, + "chainId": { + "type": "string", + "minLength": 1 + }, + "links": { + "$ref": "links/embeddedlinksarray" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "specversion", + "id", + "source", + "type", + "timestamp" + ] + }, + "subject": { + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "minLength": 1, + "format": "uri-reference" + }, + "content": { + "properties": { + "target": { + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "minLength": 1, + "format": "uri-reference" + }, + "type": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "type" + ] + }, + "fromVersion": { + "type": "string" + }, + "toVersion": { + "type": "string" + }, + "reversible": { + "type": "boolean" + }, + "change": { + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "minLength": 1, + "format": "uri-reference" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id" + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "target", + "toVersion" + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "content" + ] + }, + "customData": { + "oneOf": [ + { + "type": "object" + }, + { + "type": "string", + "contentEncoding": "base64" + } + ] + }, + "customDataContentType": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "context", + "subject" + ] +}