From 249e081f633d134c90f52d58f5e715b34586d27b Mon Sep 17 00:00:00 2001 From: xibz Date: Thu, 16 Apr 2026 16:23:21 -0500 Subject: [PATCH] update(links): Relax link schemas to support domain-level identifier This change updates all link schemas (START, END, RELATION, and embedded variants) to allow references to either a CDEvent contextId, a domainId, or both. Previously, links could only reference event context IDs. This limited cross-system connectivity and encouraged embedding execution identifiers in customData purely for graph reconstruction. By allowing domainId alongside contextId: - Links can represent relationships between domain executions (e.g., pipelinerun) as well as individual events. - Connectivity metadata no longer needs to be embedded in event payloads. - Chain-first modeling constraints are relaxed, enabling relation-first graph modeling. - The change remains backward compatible. At least one of contextId or domainId is now required for link endpoints. AdditionalProperties are restricted to prevent schema drift. This preserves existing semantics while improving flexibility and reducing customData pollution. Signed-off-by: xibz --- links.md | 222 ++++++++++++++++++++++++ schemas/links/embeddedlinkend.json | 9 +- schemas/links/embeddedlinkpath.json | 9 +- schemas/links/embeddedlinkrelation.json | 10 +- schemas/links/linkend.json | 18 +- schemas/links/linkpath.json | 18 +- schemas/links/linkrelation.json | 18 +- schemas/links/linkstart.json | 9 +- 8 files changed, 294 insertions(+), 19 deletions(-) diff --git a/links.md b/links.md index 2a35fff..4e4b9d0 100644 --- a/links.md +++ b/links.md @@ -594,6 +594,228 @@ Relation links are used to add some context to certain events } } ``` +## Cross-Domain Linking with `domainId` + +### The Problem + +`contextId` requires the publisher to know the parent event's context ID. +If the parent is not a CDEvent, there is no context ID to reference. + +For example, GitHub does not emit CDEvents today. A build event triggered by a +GitHub PR cannot use `contextId` to link back to that PR — there is no CDEvent +context ID to know. Without `domainId`, the only option is to bury that +causality in `customData`, where it is unstructured and non-queryable. + +`domainId` provides a first-class way to express causality and relationships +across system boundaries, regardless of whether the referenced system emits +CDEvents. + +This is a transitional mechanism: as more systems adopt CDEvents, `domainId` +links naturally migrate to `contextId` links. It is a bridge, not a +permanent replacement. + +### Event-to-Event vs Event-to-Resource Linking + +`contextId` and `domainId` represent two fundamentally different linking models. + +`contextId` is **event-to-event**: it points to a single, specific CDEvent by +its unique context ID. One event references exactly one other event. + +`domainId` is **many-to-many**: a `domainId` URN acts as a container. Many +CDEvents can reference the same external resource, and a single CDEvent can +reference many external resources. There is no requirement that any CDEvent +know about the others referencing the same `domainId`. + +```plaintext +contextId — event-to-event (1:1) + + +--------------------+ contextId +--------------------+ + | CDEvent |<----- "abc-123" ------| CDEvent | + | id: "abc-123" | | build.started | + | change.merged | | links: [{ | + +--------------------+ | target: { | + | contextId: | + | "abc-123" | + | } | + | }] | + +--------------------+ + + +domainId — many-to-many (N events, M resources) + + Many events referencing one resource (domainId as container/grouping key): + + urn:github:xibz:repo:pr:42 + (external resource — container) + ^ + | + +--------------------+--------------------+ + | | | + +----------+-------+ +---------+---------+ +------+-----------+ + | CDEvent | | CDEvent | | CDEvent | + | build.started | | testrun.started | | service.deployed | + | domainId: | | domainId: | | domainId: | + | urn:github:... | | urn:github:... | | urn:github:... | + | :repo:pr:42 | | :repo:pr:42 | | :repo:pr:42 | + +------------------+ +-------------------+ +------------------+ + + + One event referencing many resources (fan-out): + + +----------------------------------+ + | CDEvent | + | service.deployed | + | links: [ +-----> urn:github:xibz:repo:pr:42 + | { domainId: | + | urn:github:...:pr:42 }, +-----> urn:jira:xibz:project:issue:1234 + | { domainId: | + | urn:jira:...:issue:1234 }, +-----> urn:circleci:xibz:pipeline:execution:789 + | { domainId: | + | urn:circleci:...:789 } | + | ] | + +----------------------------------+ +``` + +A consumer querying by `urn:github:xibz:repo:pr:42` gets back every CDEvent +that referenced that resource — build, test, deploy — without any single event +needing to know about the others. A single event can simultaneously express +causality across multiple external systems by listing multiple `domainId` links, +covering fan-out scenarios where one action triggers work across several systems. + +### CDEvents Domain IDs + +`domainId` values are URNs following this format: + +``` +cdevents::::: +``` + +| Segment | Description | +|---------------|----------------------------------------------------------------------------------------------------------------| +| `service` | A governed identifier for the system or tool. Must match a known entry in the CDEvents service registry or a shared identifier agreed upon by producers and consumers (e.g. `github`, `jira`, `datadog`). Free-form values are not permitted. | +| `namespace` | The org, account, or tenant within the service | +| `instance` | The specific instance or environment within the namespace | +| `type` | A governed resource type. Must be one of the values defined in [Common Resource Types](#common-resource-types) | +| `resource id` | The publicly exposed identifier that end users see for this resource (e.g. a PR number, commit SHA, or ticket number). Must not be an internal or opaque system-generated ID. | + +Examples: + +- GitHub PR: `cdevents:github:xibz:repo:pr:42` +- Jira ticket: `cdevents:jira:xibz:project:issue:12345` +- Datadog alert: `cdevents:datadog:prod:monitor:alert:98765` + +### Common Resource Types + +The `type` segment is a governed field. Producers MUST use one of the following +values. This ensures interoperability — consumers can match and query by `type` +without handling variations like `pull_request`, `PR`, or `pullrequest`. + +| Type | Description | `resource id` example | +|---------------|------------------------------------------------------------------------------------|-------------------------------| +| `pr` | A pull or merge request | `42` (PR number) | +| `commit` | A source code commit | `abc123def456` (commit SHA) | +| `issue` | An issue or ticket in a tracking system | `1234` (issue number) | +| `branch` | A source code branch | `main`, `feature/my-branch` | +| `tag` | A source code tag or release | `v1.2.3` | +| `definition` | A named, reusable pipeline or workflow template | `my-pipeline` | +| `execution` | A single run of a build, pipeline, workflow, or deployment | `789` (run number) | +| `artifact` | A build artifact (binary, container image, package, etc.) | `myapp:1.0.0` | +| `environment` | A target deployment environment | `production`, `staging` | +| `alert` | A monitoring or observability alert | `98765` (alert ID) | + +If a resource does not fit any of the above types, it SHOULD be proposed for +addition to this list before using a custom value. + +### Usage in Relation Links + +`domainId` can be used in place of `contextId` in the `source` and `target` +fields of a `RELATION` link. Both embedded and standalone relation links support +this. + +**Example: Build triggered by a GitHub PR** + +```json +{ + "context": { + "id": "build-event-789", + "chainId": "d0be0005-cca7-4175-8fe3-f64d2f27bc01" + }, + "links": [ + { + "linkType": "RELATION", + "linkKind": "triggeredBy", + "target": { + "domainId": "urn:github:xibz:repo:pr:42" + } + } + ] +} +``` + +**Example: Rollback pipeline triggered by a Datadog alert** + +```json +{ + "links": [ + { + "linkType": "RELATION", + "linkKind": "triggeredBy", + "target": { + "domainId": "urn:datadog:prod:monitor:alert:98765" + } + } + ] +} +``` + +**Example: Deployment failure with full cross-domain causality** + +A deployment failure can link back to its causes across multiple systems, +without requiring any system to know another system's internal context IDs: + +```json +{ + "context": { "id": "deploy-event-999" }, + "links": [ + { + "linkType": "RELATION", + "linkKind": "causedBy", + "target": { + "domainId": "urn:circleci:xibz:pipeline:execution:789" + } + }, + { + "linkType": "RELATION", + "linkKind": "causedBy", + "target": { + "domainId": "urn:github:xibz:repo:commit:abc123def456" + } + }, + { + "linkType": "RELATION", + "linkKind": "causedBy", + "target": { + "domainId": "urn:github:xibz:repo:pr:42" + } + } + ] +} +``` + +Consumers can query directly by `domainId` URN without parsing `customData` or +needing to know the context IDs of external systems. + +### When to Use `contextId` vs `domainId` + +| Scenario | Use | +|----------|-----| +| Linking to another CDEvent whose context ID is known | `contextId` | +| Linking to a system that does not emit CDEvents | `domainId` | +| Linking to a CDEvent but context ID is not available | `domainId` as a fallback | + +Each system uses what it knows: `contextId` for events within the CDEvents +ecosystem, and `domainId` URNs for anything outside it. + ### Scalability Scalability is one of the bigger goals in this proposal and we wanted to ensure diff --git a/schemas/links/embeddedlinkend.json b/schemas/links/embeddedlinkend.json index f9a4bf4..ac4da25 100644 --- a/schemas/links/embeddedlinkend.json +++ b/schemas/links/embeddedlinkend.json @@ -15,10 +15,15 @@ "contextId": { "type": "string", "minLength": 1 + }, + "domainId": { + "type": "string", + "format": "uri-reference" } }, - "required": [ - "contextId" + "anyOf": [ + { "required": ["contextId"] }, + { "required": ["domainId"] } ] }, "tags": { diff --git a/schemas/links/embeddedlinkpath.json b/schemas/links/embeddedlinkpath.json index d783aa4..60e8688 100644 --- a/schemas/links/embeddedlinkpath.json +++ b/schemas/links/embeddedlinkpath.json @@ -15,10 +15,15 @@ "contextId": { "type": "string", "minLength": 1 + }, + "domainId": { + "type": "string", + "format": "uri-reference" } }, - "required": [ - "contextId" + "anyOf": [ + { "required": ["contextId"] }, + { "required": ["domainId"] } ] }, "tags": { diff --git a/schemas/links/embeddedlinkrelation.json b/schemas/links/embeddedlinkrelation.json index aa5d438..7d5954f 100644 --- a/schemas/links/embeddedlinkrelation.json +++ b/schemas/links/embeddedlinkrelation.json @@ -19,8 +19,16 @@ "contextId": { "type": "string", "minLength": 1 + }, + "domainId": { + "type": "string", + "format": "uri-reference" } - } + }, + "anyOf": [ + { "required": ["contextId"] }, + { "required": ["domainId"] } + ] }, "tags": { "type": "object", diff --git a/schemas/links/linkend.json b/schemas/links/linkend.json index 6bc3904..6bf975d 100644 --- a/schemas/links/linkend.json +++ b/schemas/links/linkend.json @@ -25,10 +25,15 @@ "contextId": { "type": "string", "minLength": 1 + }, + "domainId": { + "type": "string", + "format": "uri-reference" } }, - "required": [ - "contextId" + "anyOf": [ + { "required": ["contextId"] }, + { "required": ["domainId"] } ] }, "end": { @@ -38,10 +43,15 @@ "contextId": { "type": "string", "minLength": 1 + }, + "domainId": { + "type": "string", + "format": "uri-reference" } }, - "required": [ - "contextId" + "anyOf": [ + { "required": ["contextId"] }, + { "required": ["domainId"] } ] }, "tags": { diff --git a/schemas/links/linkpath.json b/schemas/links/linkpath.json index 53da853..e0fc885 100644 --- a/schemas/links/linkpath.json +++ b/schemas/links/linkpath.json @@ -22,10 +22,15 @@ "contextId": { "type": "string", "minLength": 1 + }, + "domainId": { + "type": "string", + "format": "uri-reference" } }, - "required": [ - "contextId" + "anyOf": [ + { "required": ["contextId"] }, + { "required": ["domainId"] } ] }, "to": { @@ -34,10 +39,15 @@ "contextId": { "type": "string", "minLength": 1 + }, + "domainId": { + "type": "string", + "format": "uri-reference" } }, - "required": [ - "contextId" + "anyOf": [ + { "required": ["contextId"] }, + { "required": ["domainId"] } ] }, "tags": { diff --git a/schemas/links/linkrelation.json b/schemas/links/linkrelation.json index d597264..98ed727 100644 --- a/schemas/links/linkrelation.json +++ b/schemas/links/linkrelation.json @@ -28,10 +28,15 @@ "contextId": { "type": "string", "minLength": 1 + }, + "domainId": { + "type": "string", + "format": "uri-reference" } }, - "required": [ - "contextId" + "anyOf": [ + { "required": ["contextId"] }, + { "required": ["domainId"] } ] }, "target": { @@ -41,10 +46,15 @@ "contextId": { "type": "string", "minLength": 1 + }, + "domainId": { + "type": "string", + "format": "uri-reference" } }, - "required": [ - "contextId" + "anyOf": [ + { "required": ["contextId"] }, + { "required": ["domainId"] } ] }, "tags": { diff --git a/schemas/links/linkstart.json b/schemas/links/linkstart.json index 1a60021..462cd50 100644 --- a/schemas/links/linkstart.json +++ b/schemas/links/linkstart.json @@ -25,10 +25,15 @@ "contextId": { "type": "string", "minLength": 1 + }, + "domainId": { + "type": "string", + "format": "uri-reference" } }, - "required": [ - "contextId" + "anyOf": [ + { "required": ["contextId"] }, + { "required": ["domainId"] } ] }, "tags": {