Docs · Glidepath
Real-World Example: Complete Build→Test→Deploy→Release Flow
This example traces a single nodejs-demo-app commit through a complete CI/CD flow, showing exact OTEL spans and CDEvents at each stage.
Scenario
- Application: nodejs-demo-app
- Git Event: Push to
mainbranch at2025-08-06T14:32:00Z - Git SHA:
abc123def456789(first 12 chars:abc123def456) - App Namespace:
app-demo-cicd - Image Repo:
ghcr.io/myorg/nodejs-demo-app
Generated IDs (at Build Start)
# These are generated by otel_flow_root_begin() and never change for the entire flow:
TRACE_ID="7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c"
FLOW_ROOT_SPAN_ID="4c9e2a7d1f3b8e5c"
CHAIN_ID="c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b"
FLOW_START_TIME="2025-08-06T14:32:00.000000000Z"
# Traceparent format (W3C):
TRACEPARENT="00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01"
BUILD Stage
Build PipelineRun Creation
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
name: build-nodejs-demo-app-8xk2j
namespace: app-demo-cicd
spec:
pipelineRef:
resolver: cluster
params:
- name: name
value: build
params:
- name: git-url
value: "https://github.com/myorg/nodejs-demo-app.git"
- name: git-revision
value: "abc123def456789"
- name: image-repo
value: "ghcr.io/myorg/nodejs-demo-app"
Task: start-flow-root-span
Step 1: generate-chain-id
printf '%s' "$(uuidgen)" > /tekton/results/chain-id
# Result: c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b
Step 2: start-span (otel-flow-root-start StepAction)
# Executes:
otel_flow_root_begin
# Outputs:
# span-id result: 4c9e2a7d1f3b8e5c
# start-time result: 2025-08-06T14:32:00.000000000Z
# traceparent result: 00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01
Task: start-build-stage-span
# Executes:
otel_stage_span_begin "00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01"
# Outputs:
BUILD_STAGE_SPAN_ID="9f2d7a3e1c8b5f4e"
BUILD_STAGE_START_TIME="2025-08-06T14:32:05.123456789Z"
Task: build-image
Step 1: resolve-build-config
# Resolves from config
CONTAINERFILE_PATH="Containerfile"
IMAGE_REF="ghcr.io/myorg/nodejs-demo-app:abc123def456"
SPAN_START_TIME="2025-08-06T14:32:06.234567890Z"
Step 2: build-and-push-arm64 / build-and-push-amd64
# Two kaniko runs, one per arch (no bash/otel-cli available in either) - kaniko has no
# multi-arch/manifest-list capability of its own, so each push goes to its own
# throwaway -arm64/-amd64-suffixed tag:
# ghcr.io/myorg/nodejs-demo-app:abc123def456-arm64
# ghcr.io/myorg/nodejs-demo-app:abc123def456-amd64
Step 3: publish-multiarch-manifest
# crane combines the two arch-suffixed images above into one real OCI image index
# under the actual (unsuffixed) tag: ghcr.io/myorg/nodejs-demo-app:abc123def456
# Writes image-digest to /tekton/results/image-digest
# Result: sha256:abc123def456789abcdef123456789abcdef (the INDEX digest, not either
# single-arch image's own digest)
Step 4: emit-image-ref-and-span
# Executes:
source "${PLATFORM_LIB}/otel.sh"
otel_task_span_send "build-image" \
"00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01" \
"9f2d7a3e1c8b5f4e" \
"2025-08-06T14:32:06.234567890Z" \
"2025-08-06T14:32:45.345678901Z" \
"Succeeded"
# Calls otel-cli:
# otel-cli span --service platform-cicd --name build-image \
# --force-trace-id 7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c \
# --force-span-id <random-8-hex-chars> \
# --force-parent-span-id 9f2d7a3e1c8b5f4e \
# --start 2025-08-06T14:32:06.234567890Z \
# --end 2025-08-06T14:32:45.345678901Z \
# --status-code ok
OTEL Span Sent to Collector:
// OpenTelemetry protocol format (simplified)
span {
trace_id: hex!("7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c")
span_id: hex!("a3f5c1e9d2b7f4a8") // Random 8-byte value
parent_span_id: hex!("9f2d7a3e1c8b5f4e")
name: "build-image"
start_time_unix_nano: 1722955926234567890
end_time_unix_nano: 1722955965345678901
status {
code: STATUS_CODE_OK
}
attributes {
key: "service.name"
value { string_value: "platform-cicd" }
}
}
Build Pipeline Finally Block
Task: start-build-stage-span (already ran, see above)
Task: end-build-stage-span
# Executes (otel-span-send StepAction):
otel_span_send \
"stage:build" \
"00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01" \
"9f2d7a3e1c8b5f4e" \
"2025-08-06T14:32:05.123456789Z" \
"2025-08-06T14:32:50.456789012Z" \
"Succeeded" \
"platform.chain_id=c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b,platform.stage=build"
# otel-cli call:
# otel-cli span --service platform-cicd --name stage:build \
# --force-trace-id 7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c \
# --force-span-id 9f2d7a3e1c8b5f4e \
# --force-parent-span-id 4c9e2a7d1f3b8e5c \
# --start 2025-08-06T14:32:05.123456789Z \
# --end 2025-08-06T14:32:50.456789012Z \
# --status-code ok \
# --attrs platform.chain_id=c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b,platform.stage=build
OTEL Build Stage Span:
span {
trace_id: hex!("7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c")
span_id: hex!("9f2d7a3e1c8b5f4e") // Use the pre-minted ID
parent_span_id: hex!("4c9e2a7d1f3b8e5c") // Parent is flow-root
name: "stage:build"
start_time_unix_nano: 1722955925123456789
end_time_unix_nano: 1722955970456789012
status { code: STATUS_CODE_OK }
attributes {
key: "service.name"
value { string_value: "platform-cicd" }
key: "platform.chain_id"
value { string_value: "c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b" }
key: "platform.stage"
value { string_value: "build" }
}
}
Task: send-cdevent (artifact.published)
Environment variables set:
CDEVENTS_BROKER_URL="http://el-cdevents-broker.platform-system.svc.cluster.local:8080"
NAMESPACE="app-demo-cicd"
TEKTON_PIPELINE_RUN="build-nodejs-demo-app-8xk2j"
PLATFORM_CHAIN_ID="c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b"
PLATFORM_TRACEPARENT="00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01"
PLATFORM_FLOW_START_TIME="2025-08-06T14:32:00.000000000Z"
CDEvent ID calculation:
event_id=$(printf '%s' "build-nodejs-demo-app-8xk2j:dev.cdevents.artifact.published.0.3.0" \
| sha256sum | cut -d' ' -f1 | cut -c1-20)
# Result: "a2f8c3e1d5b9e7a2"
CDEvent Payload (POST to broker):
{
"context": {
"version": "0.4.1",
"id": "a2f8c3e1d5b9e7a2",
"source": "/platform-cicd/app-demo-cicd/build-nodejs-demo-app-8xk2j",
"type": "dev.cdevents.artifact.published.0.3.0",
"timestamp": "2025-08-06T14:32:52.000000Z",
"chainId": "c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b"
},
"subject": {
"id": "build-nodejs-demo-app-8xk2j",
"source": "/platform-cicd/app-demo-cicd/build-nodejs-demo-app-8xk2j",
"type": "artifact",
"content": {
"name": "nodejs-demo-app",
"version": "abc123def456",
"uri": "ghcr.io/myorg/nodejs-demo-app@sha256:abc123def456789abcdef123456789abcdef"
}
},
"customData": {
"platform": {
"traceparent": "00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01",
"flow_start_time": "2025-08-06T14:32:00.000000000Z"
}
},
"customDataContentType": "application/json"
}
Task: end-flow-root-span (runs in finally)
# NOT called yet - wait for final stage completion
# (In Phase 1, flow-root ends at deploy; Phase 2 will move this to release)
TEST Stage (Triggered by Build’s CDEvent)
Trigger Receives Event
apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
name: cdevents-broker
namespace: platform-system
spec:
triggers:
- name: test-trigger
when:
- name: event-type
value: "dev.cdevents.artifact.published.*"
template:
ref: test-trigger-template
---
apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerTemplate
metadata:
name: test-trigger-template
spec:
params:
- name: pipelineRun-name
value: "test-$(body.context.id)"
resourcetemplates:
- apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
name: $(tt.params.pipelineRun-name)
namespace: $(body.subject.content.namespace)
spec:
pipelineRef:
name: test
params:
- name: flow-traceparent
value: "$(body.customData.platform.traceparent)"
- name: chain-id
value: "$(body.context.chainId)"
- name: flow-start-time
value: "$(body.customData.platform.flow_start_time)"
- name: git-url
value: "$(body.subject.content.git_url)"
- name: git-revision
value: "$(body.subject.content.git_revision)"
- name: image-ref
value: "$(body.subject.content.uri)"
Test PipelineRun Created
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
name: test-a2f8c3e1d5b9e7a2 # Named from CDEvent ID
namespace: app-demo-cicd
spec:
pipelineRef:
name: test
params:
- name: flow-traceparent
value: "00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01"
- name: chain-id
value: "c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b"
- name: flow-start-time
value: "2025-08-06T14:32:00.000000000Z"
- name: git-url
value: "https://github.com/myorg/nodejs-demo-app.git"
- name: git-revision
value: "abc123def456789"
- name: image-ref
value: "ghcr.io/myorg/nodejs-demo-app@sha256:abc123def456789abcdef123456789abcdef"
Task: start-stage-span
# Executes:
otel_stage_span_begin "00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01"
# Outputs:
TEST_STAGE_SPAN_ID="e7b4f2d8c1a5e9f3" # Fresh span ID
TEST_STAGE_START_TIME="2025-08-06T14:33:15.567890123Z"
Task: clone-repo
source "${PLATFORM_LIB}/otel.sh"
# Start time of this task
CLONE_START="2025-08-06T14:33:20.123456789Z"
# After git clone completes
CLONE_END="2025-08-06T14:33:24.234567890Z"
otel_task_span_send "clone-repo" \
"00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01" \
"e7b4f2d8c1a5e9f3" \
"2025-08-06T14:33:20.123456789Z" \
"2025-08-06T14:33:24.234567890Z" \
"Succeeded"
OTEL Clone Span (nested under test stage):
span {
trace_id: hex!("7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c")
span_id: hex!("f1e9d4c2b8a3e5f7") // Random fresh ID
parent_span_id: hex!("e7b4f2d8c1a5e9f3") // Parent is test stage
name: "clone-repo"
start_time_unix_nano: 1722956000123456789
end_time_unix_nano: 1722956004234567890
status { code: STATUS_CODE_OK }
}
Task: run-tests
# Test execution
RUN_START="2025-08-06T14:33:25.345678901Z"
RUN_END="2025-08-06T14:33:45.456789012Z"
otel_task_span_send "run-tests" \
"00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01" \
"e7b4f2d8c1a5e9f3" \
"2025-08-06T14:33:25.345678901Z" \
"2025-08-06T14:33:45.456789012Z" \
"Succeeded"
Test Pipeline Finally Block
Task: end-stage-span
otel_span_send \
"stage:test" \
"00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01" \
"e7b4f2d8c1a5e9f3" \
"2025-08-06T14:33:15.567890123Z" \
"2025-08-06T14:33:48.567890123Z" \
"Succeeded" \
"platform.chain_id=c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b,platform.stage=test"
Task: send-cdevent (testcaserun.finished)
Environment:
TEKTON_PIPELINE_RUN="test-a2f8c3e1d5b9e7a2"
PLATFORM_CHAIN_ID="c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b" # Same!
PLATFORM_TRACEPARENT="00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01" # Same!
PLATFORM_FLOW_START_TIME="2025-08-06T14:32:00.000000000Z" # Same!
TEKTON_STATUS="Succeeded"
CDEvent ID:
event_id=$(printf '%s' "test-a2f8c3e1d5b9e7a2:dev.cdevents.testcaserun.finished.0.3.0" \
| sha256sum | cut -d' ' -f1 | cut -c1-20)
# Result: "b3e9d7a2c8f1e5b4"
CDEvent Payload:
{
"context": {
"version": "0.4.1",
"id": "b3e9d7a2c8f1e5b4",
"source": "/platform-cicd/app-demo-cicd/test-a2f8c3e1d5b9e7a2",
"type": "dev.cdevents.testcaserun.finished.0.3.0",
"timestamp": "2025-08-06T14:33:50.000000Z",
"chainId": "c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b"
},
"subject": {
"id": "test-a2f8c3e1d5b9e7a2",
"source": "/platform-cicd/app-demo-cicd/test-a2f8c3e1d5b9e7a2",
"type": "testCaseRun",
"content": {
"outcome": "success",
"testSuite": "nodejs-demo-app",
"testName": "unit-tests",
"testResult": {
"passes": 42,
"failures": 0,
"skipped": 2
}
}
},
"customData": {
"platform": {
"traceparent": "00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01",
"flow_start_time": "2025-08-06T14:32:00.000000000Z"
}
},
"customDataContentType": "application/json"
}
DEPLOY Stage (Triggered by Test’s CDEvent)
Deploy PipelineRun Created
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
name: deploy-b3e9d7a2c8f1e5b4 # Named from test CDEvent ID
namespace: app-demo-cicd
spec:
pipelineRef:
name: deploy
params:
- name: flow-traceparent
value: "00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01"
- name: chain-id
value: "c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b"
- name: flow-start-time
value: "2025-08-06T14:32:00.000000000Z"
Task: start-stage-span
otel_stage_span_begin "00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01"
# Outputs:
DEPLOY_STAGE_SPAN_ID="d9c5e1f8a2b7f3e6" # Different, same trace
DEPLOY_STAGE_START_TIME="2025-08-06T14:34:10.678901234Z"
Task: deploy
otel_task_span_send "deploy" \
"00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01" \
"d9c5e1f8a2b7f3e6" \
"2025-08-06T14:34:15.789012345Z" \
"2025-08-06T14:34:35.890123456Z" \
"Succeeded"
Deploy Pipeline Finally Block
Task: end-stage-span
otel_span_send \
"stage:deploy" \
"00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01" \
"d9c5e1f8a2b7f3e6" \
"2025-08-06T14:34:10.678901234Z" \
"2025-08-06T14:34:40.901234567Z" \
"Succeeded" \
"platform.chain_id=c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b,platform.stage=deploy"
Task: send-cdevent (service.deployed)
{
"context": {
"version": "0.4.1",
"id": "c4f1e8d2a9b5f3e7",
"source": "/platform-cicd/app-demo-cicd/deploy-b3e9d7a2c8f1e5b4",
"type": "dev.cdevents.service.deployed.0.3.0",
"timestamp": "2025-08-06T14:34:42.000000Z",
"chainId": "c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b"
},
"subject": {
"id": "deploy-b3e9d7a2c8f1e5b4",
"source": "/platform-cicd/app-demo-cicd/deploy-b3e9d7a2c8f1e5b4",
"type": "service",
"content": {
"service": "nodejs-demo-app",
"environment": "dev",
"deployment": "nodejs-demo-app",
"namespace": "app-demo-dev",
"imageSha": "sha256:abc123def456789abcdef123456789abcdef"
}
},
"customData": {
"platform": {
"traceparent": "00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01",
"flow_start_time": "2025-08-06T14:32:00.000000000Z"
}
}
}
Task: end-flow-root-span (FINAL - Phase 1)
# Called from deploy's finally block (Phase 1 endpoint)
# Phase 2+ will move this to release stage's finally
otel_span_send \
"build" \
"00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01" \
"" \ # Empty = use flow-root's span ID
"2025-08-06T14:32:00.000000000Z" \
"2025-08-06T14:34:42.000000000Z" \
"Succeeded"
OTEL Flow-Root Span (Complete):
span {
trace_id: hex!("7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c")
span_id: hex!("4c9e2a7d1f3b8e5c") // Use flow-root's own ID
parent_span_id: null // No parent
name: "build"
start_time_unix_nano: 1722955920000000000 // 14:32:00
end_time_unix_nano: 1722956082000000000 // 14:34:42
duration: 162 seconds (2m 42s) ← ENTIRE FLOW
status { code: STATUS_CODE_OK }
}
Complete Tempo Trace View
Span Hierarchy (as seen in Tempo UI)
Trace: 7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c
Duration: 2m 42s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[build] 14:32:00 → 14:34:42 (2m 42s) [root]
├─ [stage:build] 14:32:05 → 14:32:50 (45s) [stage span]
│ └─ [build-image] 14:32:06 → 14:32:45 (39s) [task span]
│
├─ [stage:test] 14:33:15 → 14:33:48 (33s) [stage span]
│ ├─ [clone-repo] 14:33:20 → 14:33:24 (4s) [task span]
│ └─ [run-tests] 14:33:25 → 14:33:45 (20s) [task span]
│
└─ [stage:deploy] 14:34:10 → 14:34:40 (30s) [stage span]
└─ [deploy] 14:34:15 → 14:34:35 (20s) [task span]
Span Attributes in Tempo
All spans carry:
{
"service.name": "platform-cicd",
"trace_id": "7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c"
}
Stage spans additionally carry:
{
"platform.chain_id": "c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b",
"platform.stage": "build" | "test" | "deploy"
}
CDEvents Audit Trail
Chain of events sent to broker:
1. artifact.published (from build)
Event ID: a2f8c3e1d5b9e7a2
├─ chainId: c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b
├─ traceparent: 00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01
└─ flow_start_time: 2025-08-06T14:32:00.000000000Z
↓ Triggers test PipelineRun
2. testcaserun.finished (from test)
Event ID: b3e9d7a2c8f1e5b4
├─ chainId: c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b (same)
├─ traceparent: 00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01 (same)
└─ flow_start_time: 2025-08-06T14:32:00.000000000Z (same)
↓ Triggers deploy PipelineRun
3. service.deployed (from deploy)
Event ID: c4f1e8d2a9b5f3e7
├─ chainId: c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b (same)
├─ traceparent: 00-7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c-4c9e2a7d1f3b8e5c-01 (same)
└─ flow_start_time: 2025-08-06T14:32:00.000000000Z (same)
Key Invariants Verified
✅ Trace ID is constant across all stages
- Flow-root: 7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c
- Build stage: 7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c (same)
- Test stage: 7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c (same)
- Deploy stage: 7f2e4a8c1d6b5f3e2c9a4d8f1b6e3a7c (same)
✅ Chain ID is constant
- Generated at build start: c4b8e2f5-7d1a-4c9f-b3e6-2a8d5f1c9e4b
- Carried through all CDEvents
- Never changes
✅ Flow start time is constant
- Set at build: 2025-08-06T14:32:00.000000000Z
- Carried through all CDEvents
- Used for flow duration calculation: 14:32:00 → 14:34:42 = 162 seconds
✅ PipelineRun naming is deterministic
- build-
- test-<hash(build-event-id)>[0:20]
- deploy-<hash(test-event-id)>[0:20]
✅ Stage spans are siblings
- All parent to flow-root (4c9e2a7d1f3b8e5c)
- Build stage: parent=4c9e2a7d1f3b8e5c, span=9f2d7a3e1c8b5f4e
- Test stage: parent=4c9e2a7d1f3b8e5c, span=e7b4f2d8c1a5e9f3
- Deploy stage: parent=4c9e2a7d1f3b8e5c, span=d9c5e1f8a2b7f3e6
✅ Task spans nest correctly under stage spans
- build-image: parent=9f2d7a3e1c8b5f4e (build stage)
- clone-repo: parent=e7b4f2d8c1a5e9f3 (test stage)
- run-tests: parent=e7b4f2d8c1a5e9f3 (test stage)
- deploy: parent=d9c5e1f8a2b7f3e6 (deploy stage)