mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 15:28:03 +00:00
The validation owned its own push trigger and paths filter, which made it the one build-and-test workflow outside the on-pr.yml plus reusable-*.yml pair the rest of CI uses. Rename it to reusable-telemetry-validation.yml, take workflow_call, and let on-pr.yml decide when it runs. on-pr.yml gains a second changed-files list for the telemetry paths. Its existing `go` flag is true for nearly every pull request, so gating on `go` alone would run a self-hosted build and a 30-minute cluster on all of them. The configure step no longer repeats the telemetry option. A command-line define is written to the cache before the toolchain runs and wins over it, so it could turn telemetry on with the package never fetched. build-xrpld reads the value the build actually configured out of CMakeCache.txt and publishes it as a job output; validate-telemetry runs only when that says telemetry is on, and every value CMake does not accept as a boolean fails the job rather than skipping silently.
447 lines
21 KiB
YAML
447 lines
21 KiB
YAML
# Telemetry Validation CI Workflow
|
|
#
|
|
# Builds rippled, runs the multi-node workload harness, validates all telemetry
|
|
# data, and runs performance benchmarks.
|
|
#
|
|
# The build runs whatever the caller's tree configures, so a telemetry-off tree
|
|
# is compiled here too. Only the validation is gated: a build with the tracing
|
|
# compiled out emits no spans and no metrics to assert on.
|
|
#
|
|
# The caller decides only whether to start this at all. on-pr.yml runs it when
|
|
# telemetry-relevant paths changed.
|
|
#
|
|
# The workflow is intentionally heavyweight (builds rippled, starts Docker
|
|
# services, runs a multi-node cluster) — it validates the full telemetry
|
|
# stack end-to-end rather than individual unit tests.
|
|
#
|
|
# Architecture: three jobs to leverage cached dependencies:
|
|
# 0. linux-image-tag — reads the CI image tag from the build matrix so this
|
|
# workflow cannot drift onto a different compiler than the main CI.
|
|
# 1. build-xrpld — runs on a self-hosted runner inside the same container
|
|
# image the main CI uses. This ensures Conan packages are fetched from
|
|
# the XRPLF remote instead of built from source, and ccache hits the
|
|
# remote cache. It also reports whether the configured build has telemetry
|
|
# compiled in.
|
|
# 2. validate-telemetry — runs on ubuntu-latest (which has Docker) to
|
|
# launch the telemetry stack (OTel collector, Prometheus, Tempo, etc.)
|
|
# and validate the full pipeline end-to-end, against the binary job 1
|
|
# uploaded. Skipped when job 1 reports telemetry off.
|
|
|
|
name: Telemetry Validation
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
run_benchmark:
|
|
description: "Run performance benchmarks."
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
concurrency:
|
|
group: telemetry-validation-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
|
|
env:
|
|
BUILD_DIR: build
|
|
|
|
jobs:
|
|
# ── Job 0: Resolve the CI image tag ────────────────────────────────
|
|
# The tag is pinned once, alongside the build matrix, in linux.json. Reading
|
|
# it here rather than hardcoding a second copy means this workflow always
|
|
# builds in the same image (and therefore the same compiler) as the main CI.
|
|
# A hardcoded copy silently went stale and left this job on gcc 13 after the
|
|
# rest of CI moved to gcc 15, which broke the build on code the main CI
|
|
# compiled fine.
|
|
linux-image-tag:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
tag: ${{ steps.tag.outputs.tag }}
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Read nix image tag
|
|
id: tag
|
|
run: echo "tag=$(jq -r .image_tag .github/scripts/strategy-matrix/linux.json)" >>"${GITHUB_OUTPUT}"
|
|
|
|
# ── Job 1: Build xrpld in the same container the main CI uses ──────
|
|
# This ensures Conan binary packages are fetched from the XRPLF remote
|
|
# (matching package IDs) and ccache hits the remote compilation cache.
|
|
build-xrpld:
|
|
name: Build xrpld
|
|
needs: linux-image-tag
|
|
outputs:
|
|
telemetry: ${{ steps.telemetry.outputs.enabled }}
|
|
runs-on: [self-hosted, Linux, X64, heavy]
|
|
container: ghcr.io/xrplf/xrpld/nix-debian:${{ needs.linux-image-tag.outputs.tag }}
|
|
timeout-minutes: 60
|
|
env:
|
|
CCACHE_NAMESPACE: telemetry-validation
|
|
CCACHE_REMOTE_ONLY: true
|
|
CCACHE_REMOTE_STORAGE: http://cache.dev.ripplex.io:8080|layout=bazel
|
|
CCACHE_SLOPPINESS: include_file_ctime,include_file_mtime
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Prepare runner
|
|
uses: XRPLF/actions/prepare-runner@c00c22ada3bd6bcda48fcb0d62fbbab49fec8a0f
|
|
with:
|
|
enable_ccache: ${{ github.repository_owner == 'XRPLF' }}
|
|
|
|
- name: Print build environment
|
|
uses: XRPLF/actions/print-build-env@59dec886e4afb05a1724443af08baccbc045b574
|
|
|
|
- name: Get number of processors
|
|
uses: XRPLF/actions/get-nproc@cf0433aa74563aead044a1e395610c96d65a37cf
|
|
id: nproc
|
|
with:
|
|
subtract: 2
|
|
|
|
# The nix image ships several toolchains, so CC/CXX must be set
|
|
# explicitly for Conan to detect the intended one. gcc matches the
|
|
# debian gcc-release config the main CI builds.
|
|
- name: Set compiler environment
|
|
uses: ./.github/actions/set-compiler-env
|
|
with:
|
|
compiler: gcc
|
|
|
|
- name: Setup Conan
|
|
uses: ./.github/actions/setup-conan
|
|
|
|
- name: Build dependencies
|
|
uses: ./.github/actions/build-deps
|
|
with:
|
|
build_nproc: ${{ steps.nproc.outputs.nproc }}
|
|
build_type: Release
|
|
log_verbosity: verbose
|
|
|
|
# The telemetry option is deliberately not repeated here. Conan owns it and
|
|
# the generated toolchain hands its value to CMake. A command-line define
|
|
# is written to the cache before the toolchain runs and wins over it, so it
|
|
# could turn telemetry on with the package never fetched.
|
|
- name: Configure CMake
|
|
working-directory: ${{ env.BUILD_DIR }}
|
|
run: |
|
|
cmake \
|
|
-G Ninja \
|
|
-DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \
|
|
-DCMAKE_BUILD_TYPE=Release \
|
|
..
|
|
|
|
# A build with tracing compiled out emits nothing to assert on, so the
|
|
# validation job below gates on this. Read from the configured build, so
|
|
# this file holds no copy of the setting.
|
|
#
|
|
# Every value CMake does not accept as a boolean fails the job, including a
|
|
# missing entry. A gate that guessed would skip the validation silently,
|
|
# which is the one failure it must not have.
|
|
- name: Read whether telemetry is compiled in
|
|
id: telemetry
|
|
working-directory: ${{ env.BUILD_DIR }}
|
|
run: |
|
|
value=$(sed -n 's/^telemetry:[^=]*=//p' CMakeCache.txt)
|
|
case "${value}" in
|
|
True | TRUE | true | ON | on | YES | yes | Y | y | 1) enabled=true ;;
|
|
False | FALSE | false | OFF | off | NO | no | N | n | 0) enabled=false ;;
|
|
*)
|
|
echo "telemetry reads '${value}' in CMakeCache.txt, which is not" >&2
|
|
echo "a boolean. conan install sets it through the toolchain." >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
echo "telemetry compiled in: ${enabled}"
|
|
echo "enabled=${enabled}" >>"${GITHUB_OUTPUT}"
|
|
|
|
- name: Build xrpld
|
|
working-directory: ${{ env.BUILD_DIR }}
|
|
env:
|
|
BUILD_NPROC: ${{ steps.nproc.outputs.nproc }}
|
|
run: |
|
|
cmake \
|
|
--build . \
|
|
--config Release \
|
|
--parallel "${BUILD_NPROC}" \
|
|
--target xrpld
|
|
|
|
- name: Show ccache statistics
|
|
if: ${{ github.repository_owner == 'XRPLF' }}
|
|
run: ccache --show-stats -vv
|
|
|
|
- name: Upload xrpld binary
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: xrpld-telemetry
|
|
path: ${{ env.BUILD_DIR }}/xrpld
|
|
retention-days: 1
|
|
if-no-files-found: error
|
|
|
|
# ── Job 2: Run telemetry validation on ubuntu-latest (has Docker) ──
|
|
validate-telemetry:
|
|
name: Telemetry Stack Validation
|
|
needs: build-xrpld
|
|
# Reuses the binary job 1 built and uploaded, so a skip here costs no build.
|
|
if: needs.build-xrpld.outputs.telemetry == 'true'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Install Python dependencies
|
|
run: pip3 install -r docker/telemetry/workload/requirements.txt
|
|
|
|
- name: Download xrpld binary
|
|
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
|
with:
|
|
name: xrpld-telemetry
|
|
path: ${{ env.BUILD_DIR }}
|
|
|
|
- name: Make binaries and scripts executable
|
|
run: |
|
|
chmod +x ${{ env.BUILD_DIR }}/xrpld
|
|
chmod +x docker/telemetry/workload/*.sh
|
|
|
|
- name: Run full telemetry validation
|
|
id: validation
|
|
env:
|
|
RPC_RATE: "50"
|
|
RPC_DURATION: "120"
|
|
TX_TPS: "5"
|
|
TX_DURATION: "120"
|
|
RUN_BENCHMARK: ${{ inputs.run_benchmark }}
|
|
run: |
|
|
# The four rate/duration flags below are inert:
|
|
# run-full-validation.sh parses them and never reads them. Load shape
|
|
# comes from the default --profile full-validation. They are still
|
|
# passed so the flags stay exercised if they are ever wired up.
|
|
ARGS="--xrpld ${{ env.BUILD_DIR }}/xrpld"
|
|
ARGS="$ARGS --rpc-rate $RPC_RATE"
|
|
ARGS="$ARGS --rpc-duration $RPC_DURATION"
|
|
ARGS="$ARGS --tx-tps $TX_TPS"
|
|
ARGS="$ARGS --tx-duration $TX_DURATION"
|
|
if [ "$RUN_BENCHMARK" = "true" ]; then
|
|
ARGS="$ARGS --with-benchmark"
|
|
fi
|
|
docker/telemetry/workload/run-full-validation.sh $ARGS
|
|
# continue-on-error allows subsequent steps (artifact upload,
|
|
# summary printing) to run even if validation fails. The final
|
|
# "Check validation result" step re-checks steps.validation.outcome
|
|
# (the pre-continue-on-error result) and fails the job properly.
|
|
continue-on-error: true
|
|
|
|
- name: Upload validation reports
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: telemetry-validation-reports
|
|
path: /tmp/xrpld-validation/reports/
|
|
retention-days: 30
|
|
|
|
# Keyed on the validation step's own outcome, not job status. The step
|
|
# above sets continue-on-error, so the job is not failing at this point
|
|
# and `if: failure()` never fires -- which silently skipped these logs on
|
|
# every failed run, and they are the only record of why a node did not
|
|
# reach consensus.
|
|
#
|
|
# stdout.log matters as much as debug.log: a node that dies before its
|
|
# log sink opens writes no debug.log at all, so stdout is the only place
|
|
# its reason survives. A run that timed out at 4/5 nodes was left
|
|
# undiagnosable because that file was not collected.
|
|
- name: Upload node logs
|
|
if: always() && steps.validation.outcome != 'success'
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: xrpld-node-logs
|
|
path: |
|
|
/tmp/xrpld-validation/node*/debug.log
|
|
/tmp/xrpld-validation/node*/stdout.log
|
|
/tmp/xrpld-validation/*.log
|
|
retention-days: 7
|
|
if-no-files-found: warn
|
|
|
|
- name: Print validation summary
|
|
if: always()
|
|
run: |
|
|
REPORT="/tmp/xrpld-validation/reports/validation-report.json"
|
|
if [ -f "$REPORT" ]; then
|
|
echo "## Telemetry Validation Results" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "" >>"$GITHUB_STEP_SUMMARY"
|
|
TOTAL=$(jq '.summary.total' "$REPORT")
|
|
PASSED=$(jq '.summary.passed' "$REPORT")
|
|
FAILED=$(jq '.summary.failed' "$REPORT")
|
|
echo "| Metric | Value |" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "|--------|-------|" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "| Total Checks | $TOTAL |" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "| Passed | $PASSED |" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "| Failed | $FAILED |" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "" >>"$GITHUB_STEP_SUMMARY"
|
|
if [ "$FAILED" -gt 0 ]; then
|
|
echo "### Failed Checks" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "" >>"$GITHUB_STEP_SUMMARY"
|
|
jq -r '.checks[] | select(.passed == false) | "- **\(.name)**: \(.message)"' "$REPORT" >>"$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
fi
|
|
|
|
# Publishes captured OTel timings + regression report to the Step Summary.
|
|
# When the committed baseline is a placeholder AND the capture is
|
|
# complete, emits a fenced JSON block that can be copy-pasted directly
|
|
# into baselines/baseline-timings.json. An incomplete capture is named as
|
|
# such and its JSON withheld — see the comment on that branch below.
|
|
# When the baseline is populated, summarises the top regressions so the
|
|
# PR author sees the failure reason without downloading artifacts.
|
|
- name: Print regression summary
|
|
if: always()
|
|
run: |
|
|
set -euo pipefail
|
|
TIMINGS="/tmp/xrpld-validation/reports/timings.json"
|
|
REGRESSION="/tmp/xrpld-validation/reports/regression-report.json"
|
|
BASELINE="docker/telemetry/workload/baselines/baseline-timings.json"
|
|
|
|
if [ ! -f "$TIMINGS" ]; then
|
|
echo "## Regression Gate: no timings captured" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "::warning::capture_timings.py did not produce timings.json — regression gate was not evaluated."
|
|
exit 0
|
|
fi
|
|
|
|
if [ ! -f "$BASELINE" ]; then
|
|
echo "## Regression Gate: baseline file missing" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "::error::baselines/baseline-timings.json not found in checkout"
|
|
exit 1
|
|
fi
|
|
|
|
# NOTE: do NOT use `jq -e` here. With -e, jq exits non-zero when the
|
|
# filter's result is boolean false — which is the normal case for a
|
|
# populated (non-placeholder) baseline — and that would be
|
|
# misreported as a parse failure. Plain `jq -r` exits 0 on any valid
|
|
# JSON, so a real non-zero exit genuinely means malformed JSON.
|
|
IS_PLACEHOLDER=$(jq -r '.placeholder == true or (.metrics | length == 0)' "$BASELINE") || {
|
|
echo "::error::Failed to parse baseline JSON"
|
|
exit 1
|
|
}
|
|
|
|
# Whether the capture is usable as baseline material is the capture's
|
|
# own verdict, carried in the artifact by capture_timings.py, which
|
|
# computes it against --min-capture-ratio. It is READ here, never
|
|
# re-derived: a second copy of the ratio rule in shell would be a
|
|
# second source of truth and would drift from the producer.
|
|
#
|
|
# `// false` covers both an artifact written before the block existed
|
|
# and a truncated one. Neither can prove it is complete, so neither is
|
|
# offered — the whole point is that a degraded capture must not look
|
|
# like a good one. Same `jq -r` reasoning as the baseline parse above.
|
|
CAPTURE_COMPLETE=$(jq -r '.capture.complete // false' "$TIMINGS") || {
|
|
echo "::error::Failed to parse timings JSON"
|
|
exit 1
|
|
}
|
|
if [ "$(jq -r 'has("capture")' "$TIMINGS")" = "true" ]; then
|
|
CAPTURE_COUNTS=$(jq -r '"\(.capture.captured)/\(.capture.declared)"' "$TIMINGS")
|
|
CAPTURE_SHORTFALL="only **$CAPTURE_COUNTS** declared metrics came back, below the capture's own minimum ratio"
|
|
else
|
|
CAPTURE_COUNTS="unknown"
|
|
CAPTURE_SHORTFALL="the artifact carries no \`capture\` block, so it predates completeness reporting and cannot state what it captured"
|
|
fi
|
|
|
|
echo "## OTel Timings Regression Gate" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "" >>"$GITHUB_STEP_SUMMARY"
|
|
|
|
if [ "$IS_PLACEHOLDER" = "true" ] && [ "$CAPTURE_COMPLETE" != "true" ]; then
|
|
# The placeholder path is the ONLY route to a committed baseline,
|
|
# which makes it the one place an incomplete capture does lasting
|
|
# damage: pasted in, it silently narrows the gate to the keys that
|
|
# happened to come back. So the JSON is withheld rather than
|
|
# printed with a caveat — a warning above a copyable block is
|
|
# still a copyable block. The counts are shown so the reader knows
|
|
# how thin it was, and the artifact is still uploaded for anyone
|
|
# who needs to inspect it deliberately.
|
|
echo "### Baseline NOT refreshable from this run" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "The committed baseline is a placeholder, so this run would" \
|
|
"normally print a block to paste into" \
|
|
"\`baselines/baseline-timings.json\`. It is withheld because" \
|
|
"$CAPTURE_SHORTFALL, so the JSON may describe metrics that" \
|
|
"were never measured. Pasting it would narrow the gate to" \
|
|
"whichever keys were captured, with nothing reporting that" \
|
|
"it had narrowed." >>"$GITHUB_STEP_SUMMARY"
|
|
echo "" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "Fix the capture first — the usual cause is Prometheus not" \
|
|
"being scraped for long enough, or nodes not reaching" \
|
|
"consensus — then re-run. See the \`timings.json\` artifact's" \
|
|
"\`capture\` block for the exact counts." >>"$GITHUB_STEP_SUMMARY"
|
|
echo "::error::Timing capture is incomplete ($CAPTURE_COUNTS metrics) — no baseline block printed. Do not refresh the baseline from this run."
|
|
elif [ "$IS_PLACEHOLDER" = "true" ]; then
|
|
echo "### Paste into \`baselines/baseline-timings.json\`" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "The committed baseline is a placeholder. Open a PR replacing" \
|
|
"its contents with the JSON block below to activate the" \
|
|
"regression gate. The capture is complete" \
|
|
"($CAPTURE_COUNTS declared metrics)." >>"$GITHUB_STEP_SUMMARY"
|
|
echo "" >>"$GITHUB_STEP_SUMMARY"
|
|
echo '```json' >>"$GITHUB_STEP_SUMMARY"
|
|
cat "$TIMINGS" >>"$GITHUB_STEP_SUMMARY"
|
|
echo '```' >>"$GITHUB_STEP_SUMMARY"
|
|
elif [ -f "$REGRESSION" ]; then
|
|
# Existence is not readability: a truncated report satisfies -f,
|
|
# and the `|| =0` fallbacks below would then render a clean table
|
|
# of zeros for a run that compared nothing. Same trap the note
|
|
# above the baseline parse warns about, so check the shape first.
|
|
SUMMARY_OK=$(jq -r 'if (.summary | type) == "object" then "yes" else "no" end' \
|
|
"$REGRESSION" 2>/dev/null) || SUMMARY_OK=no
|
|
if [ "$SUMMARY_OK" != "yes" ]; then
|
|
echo "## Regression Gate: report unreadable" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "\`$REGRESSION\` exists but carries no \`summary\` object, so no" \
|
|
"table can be rendered. The pass/fail above still comes from the" \
|
|
"comparator's exit code." >>"$GITHUB_STEP_SUMMARY"
|
|
echo "::error::Regression report is present but has no summary object"
|
|
else
|
|
# No `jq -e`: it exits non-zero when a field is legitimately 0 or
|
|
# false, which the fallbacks would silently turn into 0 as well.
|
|
REGR_COUNT=$(jq -r '.summary.regressions // 0' "$REGRESSION")
|
|
IMPR_COUNT=$(jq -r '.summary.improvements // 0' "$REGRESSION")
|
|
TOTAL=$(jq -r '.summary.total // 0' "$REGRESSION")
|
|
MISSING_COUNT=$(jq -r '.summary.missing_in_current // 0' "$REGRESSION")
|
|
COMPARED=$(jq -r '.summary.compared // 0' "$REGRESSION")
|
|
# `total` is every key in the report, i.e. the union of the
|
|
# baseline and this run, so it is not what was gated. The
|
|
# comparator reports `compared` for that; do not derive it from
|
|
# total minus missing, because a key can also be skipped for
|
|
# being new or for having no data on either side.
|
|
echo "| Stat | Count |" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "|------|-------|" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "| Metrics in report | $TOTAL |" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "| Metrics compared | $COMPARED |" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "| Not captured this run | $MISSING_COUNT |" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "| Regressions | $REGR_COUNT |" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "| Improvements | $IMPR_COUNT |" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "" >>"$GITHUB_STEP_SUMMARY"
|
|
if [ "$MISSING_COUNT" -gt 0 ]; then
|
|
echo "::warning::$MISSING_COUNT baseline metric(s) were not captured this run, so they were not gated"
|
|
fi
|
|
if [ "$REGR_COUNT" -gt 0 ]; then
|
|
echo "### Regressions" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "| Metric | Baseline | Current | Δ | % | Unit |" >>"$GITHUB_STEP_SUMMARY"
|
|
echo "|--------|---------:|--------:|--:|--:|------|" >>"$GITHUB_STEP_SUMMARY"
|
|
jq -r '.metrics[] | select(.regressed) | "| \(.key) | \(.baseline) | \(.current) | \(.delta) | \(.pct_change)% | \(.unit) |"' \
|
|
"$REGRESSION" >>"$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
- name: Cleanup
|
|
if: always()
|
|
run: |
|
|
docker/telemetry/workload/run-full-validation.sh --cleanup 2>/dev/null || true
|
|
|
|
- name: Check validation result
|
|
if: steps.validation.outcome == 'failure'
|
|
run: |
|
|
echo "Telemetry validation failed. Check the uploaded reports for details."
|
|
exit 1
|