Have a Tekton Pipeline POST a small JSON payload to riptide-collector at the
end of each PipelineRun. The endpoint is shared with Jenkins and any other
CI (POST /webhooks/pipeline) — Tekton callers identify themselves via
"source": "tekton".
Add a finally: task to your Pipeline that always runs (regardless of
prior task success/failure) and POSTs the riptide payload. This is the
idiomatic Tekton way to do post-run notifications without coupling to Tekton
Triggers or CloudEvents.
tekton/tasks/riptide-notify.yaml:
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: riptide-notify
spec:
params:
- name: pipeline-name
description: Tekton Pipeline metadata.name
- name: run-id
description: PipelineRun metadata.name
- name: aggregate-status
description: $(tasks.status) from the calling Pipeline (Succeeded/Failed/Completed)
- name: commit-sha
description: git SHA built / deployed
- name: started-at
description: PipelineRun status.startTime (ISO 8601 UTC)
- name: finished-at
description: PipelineRun status.completionTime (ISO 8601 UTC)
- name: riptide-url
default: https://riptide-collector.example.com/webhooks/pipeline
steps:
- name: post
image: registry.access.redhat.com/ubi9/ubi-minimal:latest
env:
- name: RIPTIDE_TOKEN
valueFrom:
secretKeyRef:
name: riptide-token
key: token
script: |
#!/bin/sh
# Notification is best-effort: a slow or unavailable riptide-collector
# must NEVER fail the PipelineRun. We deliberately do not `set -e` and
# exit 0 at the end regardless of the curl outcome.
set -u
microdnf install -y --nodocs jq curl >/dev/null 2>&1 || {
echo "riptide-notify: could not install curl/jq, skipping"; exit 0; }
# Tekton's $(tasks.status) is one of: Succeeded, Failed, Completed, None
STATUS="$(params.aggregate-status)"
BODY=$(jq -n \
--arg src "tekton" \
--arg pn "$(params.pipeline-name)" \
--arg rid "$(params.run-id)" \
--arg phase "COMPLETED" \
--arg st "$STATUS" \
--arg sha "$(params.commit-sha)" \
--arg sa "$(params.started-at)" \
--arg fa "$(params.finished-at)" \
'{source:$src, pipeline_name:$pn, run_id:$rid, phase:$phase,
status:$st, commit_sha:$sha, started_at:$sa, finished_at:$fa}')
# --connect-timeout caps TCP/TLS handshake; --max-time caps the full
# request. One quick retry handles transient blips. Any non-2xx is
# logged and ignored — the PipelineRun result is unaffected.
HTTP_CODE=$(curl -sS -o /tmp/riptide.out -w '%{http_code}' \
--connect-timeout 3 --max-time 10 --retry 1 --retry-delay 1 \
-X POST "$(params.riptide-url)" \
-H "Authorization: Bearer ${RIPTIDE_TOKEN}" \
-H "Content-Type: application/json" \
--data "$BODY") || HTTP_CODE="000"
case "$HTTP_CODE" in
2*) echo "riptide-notify: OK (http=${HTTP_CODE})" ;;
000)
# Connect/timeout/DNS — we never reached riptide-collector.
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo "!! WARNING: RIPTIDE-COLLECTOR UNREACHABLE !!"
echo "!! url=$(params.riptide-url)"
echo "!! reason=connect/timeout/DNS (curl exit non-zero, http=000)!!"
echo "!! pipeline result is UNAFFECTED — this is best-effort !!"
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
;;
*)
# Reached the server but it returned an error status.
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo "!! WARNING: RIPTIDE-COLLECTOR REJECTED THE EVENT !!"
echo "!! http=${HTTP_CODE} url=$(params.riptide-url)"
echo "!! body (first 500 bytes):"
head -c 500 /tmp/riptide.out 2>/dev/null || true; echo
echo "!! pipeline result is UNAFFECTED — this is best-effort !!"
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
;;
esac
exit 0In any Pipeline you want measured, add a finally: block:
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: payments-api-deploy
spec:
params:
- name: commit-sha
tasks:
- name: build
taskRef: { name: build }
params:
- name: commit-sha
value: $(params.commit-sha)
- name: deploy
runAfter: [build]
taskRef: { name: deploy }
finally:
- name: notify-riptide
taskRef: { name: riptide-notify }
# Hard ceiling so a hung pod can never delay the PipelineRun.
# curl budget is ~14s (connect 3s + max 10s + 1 retry); 30s adds
# headroom for pod startup without making humans wait.
timeout: "30s"
params:
- name: pipeline-name
value: $(context.pipeline.name)
- name: run-id
value: $(context.pipelineRun.name)
- name: aggregate-status
value: $(tasks.status)
- name: commit-sha
value: $(params.commit-sha)
- name: started-at
value: $(context.pipelineRun.startTime)
- name: finished-at
value: $(context.pipelineRun.completionTime)Each team has its own bearer (the platform team hands it out — see
docs/onboarding-a-team.md). Use the team's jenkins entry from
team-keys.json — the same key covers /webhooks/pipeline for both
Jenkins and Tekton. ArgoCD / Bitbucket keys are rejected here (strict
source binding). Create a Secret per team in their pipeline namespace:
oc -n <pipeline-namespace> create secret generic riptide-token \
--from-literal=token='<RAW_TOKEN_FOR_THIS_TEAM>'The token identifies the team to riptide; do not share across teams.
SELECT delivery_id, source, pipeline_name, run_id, status, duration_seconds,
team
FROM pipeline_events
WHERE source = 'tekton' AND pipeline_name = '<your pipeline>'
ORDER BY created_at DESC
LIMIT 5;finally: runs once per PipelineRun regardless of upstream failures, with
the data we want already in scope ($(tasks.status), $(context.pipelineRun.*)).
Tekton CloudEvents would force riptide to learn the CloudEvents envelope; the
finally approach keeps the wire format identical to Jenkins so the same
endpoint serves both.