Skip to content

Commit 92ef1ef

Browse files
committed
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 <bjp@apple.com>
1 parent 73ae696 commit 92ef1ef

8 files changed

Lines changed: 294 additions & 19 deletions

File tree

links.md

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,228 @@ Relation links are used to add some context to certain events
594594
}
595595
}
596596
```
597+
## Cross-Domain Linking with `domainId`
598+
599+
### The Problem
600+
601+
`contextId` requires the publisher to know the parent event's context ID.
602+
If the parent is not a CDEvent, there is no context ID to reference.
603+
604+
For example, GitHub does not emit CDEvents today. A build event triggered by a
605+
GitHub PR cannot use `contextId` to link back to that PR — there is no CDEvent
606+
context ID to know. Without `domainId`, the only option is to bury that
607+
causality in `customData`, where it is unstructured and non-queryable.
608+
609+
`domainId` provides a first-class way to express causality and relationships
610+
across system boundaries, regardless of whether the referenced system emits
611+
CDEvents.
612+
613+
This is a transitional mechanism: as more systems adopt CDEvents, `domainId`
614+
links naturally migrate to `contextId` links. It is a bridge, not a
615+
permanent replacement.
616+
617+
### Event-to-Event vs Event-to-Resource Linking
618+
619+
`contextId` and `domainId` represent two fundamentally different linking models.
620+
621+
`contextId` is **event-to-event**: it points to a single, specific CDEvent by
622+
its unique context ID. One event references exactly one other event.
623+
624+
`domainId` is **many-to-many**: a `domainId` URN acts as a container. Many
625+
CDEvents can reference the same external resource, and a single CDEvent can
626+
reference many external resources. There is no requirement that any CDEvent
627+
know about the others referencing the same `domainId`.
628+
629+
```plaintext
630+
contextId — event-to-event (1:1)
631+
632+
+--------------------+ contextId +--------------------+
633+
| CDEvent |<----- "abc-123" ------| CDEvent |
634+
| id: "abc-123" | | build.started |
635+
| change.merged | | links: [{ |
636+
+--------------------+ | target: { |
637+
| contextId: |
638+
| "abc-123" |
639+
| } |
640+
| }] |
641+
+--------------------+
642+
643+
644+
domainId — many-to-many (N events, M resources)
645+
646+
Many events referencing one resource (domainId as container/grouping key):
647+
648+
cdevents:github:xibz:repo:pr:42
649+
(external resource — container)
650+
^
651+
|
652+
+--------------------+--------------------+
653+
| | |
654+
+----------+-------+ +---------+---------+ +------+-----------+
655+
| CDEvent | | CDEvent | | CDEvent |
656+
| build.started | | testrun.started | | service.deployed |
657+
| domainId: | | domainId: | | domainId: |
658+
| cdevents:github: | | cdevents:github: | | cdevents:github: |
659+
| :repo:pr:42 | | :repo:pr:42 | | :repo:pr:42 |
660+
+------------------+ +-------------------+ +------------------+
661+
662+
663+
One event referencing many resources (fan-out):
664+
665+
+----------------------------------+
666+
| CDEvent |
667+
| service.deployed |
668+
| links: [ +-----> cdevents:github:xibz:repo:pr:42
669+
| { domainId: |
670+
| cdevents:github:...:pr:42 }, +-----> cdevents:jira:xibz:project:issue:12
671+
| { domainId: |
672+
| cdevents:jira:...:issue:12 },+-----> cdevents:circleci:xibz:pipeline:execution:789
673+
| { domainId: |
674+
| cdevents:circleci:...:789 } |
675+
| ] |
676+
+----------------------------------+
677+
```
678+
679+
A consumer querying by `cdevents:github:xibz:repo:pr:42` gets back every CDEvent
680+
that referenced that resource — build, test, deploy — without any single event
681+
needing to know about the others. A single event can simultaneously express
682+
causality across multiple external systems by listing multiple `domainId` links,
683+
covering fan-out scenarios where one action triggers work across several systems.
684+
685+
### CDEvents Domain IDs
686+
687+
`domainId` values are URNs following this format:
688+
689+
```
690+
cdevents:<service>:<namespace>:<instance>:<type>:<resource id>
691+
```
692+
693+
| Segment | Description |
694+
|---------------|----------------------------------------------------------------------------------------------------------------|
695+
| `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. |
696+
| `namespace` | The org, account, or tenant within the service |
697+
| `instance` | The specific instance or environment within the namespace |
698+
| `type` | A governed resource type. Must be one of the values defined in [Common Resource Types](#common-resource-types) |
699+
| `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. |
700+
701+
Examples:
702+
703+
- GitHub PR: `cdevents:github:xibz:repo:pr:42`
704+
- Jira ticket: `cdevents:jira:xibz:project:issue:12345`
705+
- Datadog alert: `cdevents:datadog:prod:monitor:alert:98765`
706+
707+
### Common Resource Types
708+
709+
The `type` segment is a governed field. Producers MUST use one of the following
710+
values. This ensures interoperability — consumers can match and query by `type`
711+
without handling variations like `pull_request`, `PR`, or `pullrequest`.
712+
713+
| Type | Description | `resource id` example |
714+
|---------------|------------------------------------------------------------------------------------|-------------------------------|
715+
| `pr` | A pull or merge request | `42` (PR number) |
716+
| `commit` | A source code commit | `abc123def456` (commit SHA) |
717+
| `issue` | An issue or ticket in a tracking system | `1234` (issue number) |
718+
| `branch` | A source code branch | `main`, `feature/my-branch` |
719+
| `tag` | A source code tag or release | `v1.2.3` |
720+
| `definition` | A named, reusable pipeline or workflow template | `my-pipeline` |
721+
| `execution` | A single run of a build, pipeline, workflow, or deployment | `789` (run number) |
722+
| `artifact` | A build artifact (binary, container image, package, etc.) | `myapp:1.0.0` |
723+
| `environment` | A target deployment environment | `production`, `staging` |
724+
| `alert` | A monitoring or observability alert | `98765` (alert ID) |
725+
726+
If a resource does not fit any of the above types, it SHOULD be proposed for
727+
addition to this list before using a custom value.
728+
729+
### Usage in Relation Links
730+
731+
`domainId` can be used in place of `contextId` in the `source` and `target`
732+
fields of a `RELATION` link. Both embedded and standalone relation links support
733+
this.
734+
735+
**Example: Build triggered by a GitHub PR**
736+
737+
```json
738+
{
739+
"context": {
740+
"id": "build-event-789",
741+
"chainId": "d0be0005-cca7-4175-8fe3-f64d2f27bc01"
742+
},
743+
"links": [
744+
{
745+
"linkType": "RELATION",
746+
"linkKind": "triggeredBy",
747+
"target": {
748+
"domainId": "cdevents:github:xibz:repo:pr:42"
749+
}
750+
}
751+
]
752+
}
753+
```
754+
755+
**Example: Rollback pipeline triggered by a Datadog alert**
756+
757+
```json
758+
{
759+
"links": [
760+
{
761+
"linkType": "RELATION",
762+
"linkKind": "triggeredBy",
763+
"target": {
764+
"domainId": "cdevents:datadog:prod:monitor:alert:98765"
765+
}
766+
}
767+
]
768+
}
769+
```
770+
771+
**Example: Deployment failure with full cross-domain causality**
772+
773+
A deployment failure can link back to its causes across multiple systems,
774+
without requiring any system to know another system's internal context IDs:
775+
776+
```json
777+
{
778+
"context": { "id": "deploy-event-999" },
779+
"links": [
780+
{
781+
"linkType": "RELATION",
782+
"linkKind": "causedBy",
783+
"target": {
784+
"domainId": "cdevents:circleci:xibz:pipeline:execution:789"
785+
}
786+
},
787+
{
788+
"linkType": "RELATION",
789+
"linkKind": "causedBy",
790+
"target": {
791+
"domainId": "cdevents:github:xibz:repo:commit:abc123def456"
792+
}
793+
},
794+
{
795+
"linkType": "RELATION",
796+
"linkKind": "causedBy",
797+
"target": {
798+
"domainId": "cdevents:github:xibz:repo:pr:42"
799+
}
800+
}
801+
]
802+
}
803+
```
804+
805+
Consumers can query directly by `domainId` URN without parsing `customData` or
806+
needing to know the context IDs of external systems.
807+
808+
### When to Use `contextId` vs `domainId`
809+
810+
| Scenario | Use |
811+
|----------|-----|
812+
| Linking to another CDEvent whose context ID is known | `contextId` |
813+
| Linking to a system that does not emit CDEvents | `domainId` |
814+
| Linking to a CDEvent but context ID is not available | `domainId` as a fallback |
815+
816+
Each system uses what it knows: `contextId` for events within the CDEvents
817+
ecosystem, and `domainId` URNs for anything outside it.
818+
597819
### Scalability
598820

599821
Scalability is one of the bigger goals in this proposal and we wanted to ensure

schemas/links/embeddedlinkend.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@
1515
"contextId": {
1616
"type": "string",
1717
"minLength": 1
18+
},
19+
"domainId": {
20+
"type": "string",
21+
"format": "uri-reference"
1822
}
1923
},
20-
"required": [
21-
"contextId"
24+
"anyOf": [
25+
{ "required": ["contextId"] },
26+
{ "required": ["domainId"] }
2227
]
2328
},
2429
"tags": {

schemas/links/embeddedlinkpath.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@
1515
"contextId": {
1616
"type": "string",
1717
"minLength": 1
18+
},
19+
"domainId": {
20+
"type": "string",
21+
"format": "uri-reference"
1822
}
1923
},
20-
"required": [
21-
"contextId"
24+
"anyOf": [
25+
{ "required": ["contextId"] },
26+
{ "required": ["domainId"] }
2227
]
2328
},
2429
"tags": {

schemas/links/embeddedlinkrelation.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,16 @@
1919
"contextId": {
2020
"type": "string",
2121
"minLength": 1
22+
},
23+
"domainId": {
24+
"type": "string",
25+
"format": "uri-reference"
2226
}
23-
}
27+
},
28+
"anyOf": [
29+
{ "required": ["contextId"] },
30+
{ "required": ["domainId"] }
31+
]
2432
},
2533
"tags": {
2634
"type": "object",

schemas/links/linkend.json

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,15 @@
2525
"contextId": {
2626
"type": "string",
2727
"minLength": 1
28+
},
29+
"domainId": {
30+
"type": "string",
31+
"format": "uri-reference"
2832
}
2933
},
30-
"required": [
31-
"contextId"
34+
"anyOf": [
35+
{ "required": ["contextId"] },
36+
{ "required": ["domainId"] }
3237
]
3338
},
3439
"end": {
@@ -38,10 +43,15 @@
3843
"contextId": {
3944
"type": "string",
4045
"minLength": 1
46+
},
47+
"domainId": {
48+
"type": "string",
49+
"format": "uri-reference"
4150
}
4251
},
43-
"required": [
44-
"contextId"
52+
"anyOf": [
53+
{ "required": ["contextId"] },
54+
{ "required": ["domainId"] }
4555
]
4656
},
4757
"tags": {

schemas/links/linkpath.json

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@
2222
"contextId": {
2323
"type": "string",
2424
"minLength": 1
25+
},
26+
"domainId": {
27+
"type": "string",
28+
"format": "uri-reference"
2529
}
2630
},
27-
"required": [
28-
"contextId"
31+
"anyOf": [
32+
{ "required": ["contextId"] },
33+
{ "required": ["domainId"] }
2934
]
3035
},
3136
"to": {
@@ -34,10 +39,15 @@
3439
"contextId": {
3540
"type": "string",
3641
"minLength": 1
42+
},
43+
"domainId": {
44+
"type": "string",
45+
"format": "uri-reference"
3746
}
3847
},
39-
"required": [
40-
"contextId"
48+
"anyOf": [
49+
{ "required": ["contextId"] },
50+
{ "required": ["domainId"] }
4151
]
4252
},
4353
"tags": {

schemas/links/linkrelation.json

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,15 @@
2828
"contextId": {
2929
"type": "string",
3030
"minLength": 1
31+
},
32+
"domainId": {
33+
"type": "string",
34+
"format": "uri-reference"
3135
}
3236
},
33-
"required": [
34-
"contextId"
37+
"anyOf": [
38+
{ "required": ["contextId"] },
39+
{ "required": ["domainId"] }
3540
]
3641
},
3742
"target": {
@@ -41,10 +46,15 @@
4146
"contextId": {
4247
"type": "string",
4348
"minLength": 1
49+
},
50+
"domainId": {
51+
"type": "string",
52+
"format": "uri-reference"
4453
}
4554
},
46-
"required": [
47-
"contextId"
55+
"anyOf": [
56+
{ "required": ["contextId"] },
57+
{ "required": ["domainId"] }
4858
]
4959
},
5060
"tags": {

0 commit comments

Comments
 (0)