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" + ] +}