From 76bb4cefcf1a46161cd10cd62ae4325833db260c Mon Sep 17 00:00:00 2001 From: Mikolaj Matuszny Date: Fri, 19 Jun 2026 11:48:42 +0200 Subject: [PATCH 1/3] BUILD-11567 Fold CI Metrics step-summary into one collapsible block per job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror of the producer-hook change in github-runners-infra#425. The post-job hook now emits a single
fold per job — collapsed carries a digest of the headline numbers, anomalies (OOM / throttling) lead with a [!] prefix, and the cache is a one-line-per-entry list instead of a 6-column table. Presentation only; JSON output and schema_version 3 are unchanged. This is the hand-synced copy of infra/applications/github-runners/hooks/job-completed.sh; the two files' presentation regions are byte-identical (the template byte-identity test in github-runners-infra enforces it). --- ci-metrics/job-completed.sh | 107 +++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 43 deletions(-) diff --git a/ci-metrics/job-completed.sh b/ci-metrics/job-completed.sh index d4857175..5428862a 100755 --- a/ci-metrics/job-completed.sh +++ b/ci-metrics/job-completed.sh @@ -370,12 +370,14 @@ summary_target="${GITHUB_STEP_SUMMARY:-/dev/null}" # This rendering mirrors _rci_cpu_cell in report-ci-metrics/lib.sh; the expected output per tier # is pinned by the "_rci_cpu_cell() denominator preference" spec — keep both in sync if changed. cpu_avg_cores="" +cpu_avg_pct="" # set when a denominator exists; drives both the table % and the digest line if [[ -n "$cpu_usage_seconds" && -n "$duration_seconds" ]]; then cpu_avg_cores=$(awk -v u="$cpu_usage_seconds" -v d="$duration_seconds" \ 'BEGIN{ if (d>0) printf "%.2f", u/d }') fi if [[ -n "$cpu_avg_cores" && -n "$cpu_limit_cores" && -n "$cpu_avg_utilization" ]]; then - v_cpu_avg="${cpu_avg_cores} / $(round2 "$cpu_limit_cores") cores ($(pct0 "$cpu_avg_utilization")%)" + cpu_avg_pct=$(pct0 "$cpu_avg_utilization") + v_cpu_avg="${cpu_avg_cores} / $(round2 "$cpu_limit_cores") cores (${cpu_avg_pct}%)" elif [[ -n "$cpu_avg_cores" && -n "$cpu_request_cores" ]] \ && awk -v r="$cpu_request_cores" 'BEGIN{exit !(r+0>0)}'; then cpu_avg_pct=$(awk -v c="$cpu_avg_cores" -v r="$cpu_request_cores" \ @@ -434,8 +436,46 @@ if [[ "$memory_oom_kill" =~ ^[0-9]+$ ]] && (( memory_oom_kill > 0 )); then show_oom=1 fi +# ---------- Step summary: one collapsible CI Metrics block per job ---------- +# Folded by default to keep a multi-job matrix summary compact; the line carries a digest +# so the common case needs no expand. Title is plain "CI Metrics" to stay distinct from the +# aggregated CI Metrics table emitted by ci-github-actions' report-ci-metrics action. + +# Digest: middot-joined token per available metric; n/a metrics are dropped. Anomaly tokens (OOM / +# throttling) lead and trigger a "[!]" prefix so they show while collapsed. Net is always present. +digest_parts=() +(( show_oom )) && digest_parts+=("OOM kill ×${memory_oom_kill}") +if (( show_throttled )); then + if [[ -n "$cpu_throttle_rate" ]]; then + digest_parts+=("throttled $(pct0 "$cpu_throttle_rate")%") + else + digest_parts+=("throttled ${cpu_throttled_seconds}s") + fi +fi +if [[ -n "$cpu_avg_pct" ]]; then + digest_parts+=("CPU ${cpu_avg_pct}%") +elif [[ -n "$cpu_avg_cores" ]]; then + digest_parts+=("CPU ${cpu_avg_cores} cores") +fi +if [[ -n "$memory_peak_utilization" ]]; then + digest_parts+=("Mem $(pct0 "$memory_peak_utilization")%") +elif [[ -n "$memory_peak_bytes" ]]; then + digest_parts+=("Mem $(fmt_bytes "$memory_peak_bytes")") +fi +[[ -n "$disk_utilization" ]] && digest_parts+=("Disk $(pct0 "$disk_utilization")%") +digest_parts+=("Net $(fmt_bytes "$net_rx_total")↓ $(fmt_bytes "$net_tx_total")↑") + +digest="" +for part in "${digest_parts[@]}"; do + digest+="${digest:+ · }${part}" +done +summary_line="CI Metrics — ${digest}" +(( show_oom || show_throttled )) && summary_line="[!] ${summary_line}" + +# Open the fold and emit the metric table. A blank line after is required for the +# Markdown table inside
to render on GitHub. { - printf '## CI Metrics\n' + printf '
%s\n\n' "$summary_line" printf '| Metric | Value |\n' printf '|---|---|\n' printf '| CPU avg | %s |\n' "$v_cpu_avg" @@ -446,19 +486,12 @@ fi (( show_oom )) && printf '| OOM kills | %s |\n' "$memory_oom_kill" } >> "$summary_target" 2>/dev/null || true -# ---------- Cache section ---------- -# Render one row per ${CI_METRICS_DIR}/cache-*.json entry that we previously parsed and folded into $cache_json. -# Skipped silently when no rows. -# The "Hit" column is three-state: yes (exact), partial (), no. -# "Size Saved" is only meaningful when `saved == true` (cache action will persist the post-step size); otherwise the on-disk path size -# doesn't correspond to anything saved, so show n/a. +# ---------- Cache list (inside the same fold) ---------- +# One line per ${CI_METRICS_DIR}/cache-*.json entry; skipped when none. if [[ "$cache_json" != "[]" ]] && command -v jq >/dev/null 2>&1; then - # Fields joined by ASCII Unit Separator (US, 0x1f). - # Both sides reference the byte via escapes (jq accepts the \uXXXX form, bash uses $'\xNN') so this file stays YAML-safe when - # runner.yaml.gotmpl reads it verbatim into an init-container body. - # Booleans emit as "true"/"false" strings so jq's `//` doesn't swallow `false`. - # Numeric sizes emit as digits or "" (null/missing); the bash side maps "" → "n/a". - # Fail-open on jq error. + # Fields joined by ASCII Unit Separator ( / $'\x1f'); escaped so the file stays YAML-safe + # when runner.yaml.gotmpl embeds it verbatim. Booleans as strings so jq `//` keeps `false`. + # Numeric sizes are digits or "" (null/missing). Fail-open on jq error. cache_rows=$(jq -r --argjson c "$cache_json" -n ' $c | sort_by(.step // "") @@ -477,48 +510,36 @@ if [[ "$cache_json" != "[]" ]] && command -v jq >/dev/null 2>&1; then if [[ -n "$cache_rows" ]]; then { - printf '\n### Cache\n' - printf '| Key | Hit | Backend | Size Restored | Saved | Size Saved |\n' - printf '|---|---|---|---|---|---|\n' - # `|| [[ -n "$c_key" ]]` is the canonical idiom for a final line lacking a trailing newline. `jq -r` always emits `\n` today, - # but the guard keeps us safe if that ever changes. + printf '\n**Cache**\n' + # `|| [[ -n "$c_key" ]]` guards a final line without a trailing newline. while IFS=$'\x1f' read -r c_key c_hit c_rkey c_backend c_size_r c_saved c_size_e || [[ -n "$c_key" ]]; do - # Hit column: three-state. + # status: hit (+restored size when known) / partial () / miss if [[ "$c_hit" == "true" ]]; then - v_hit="yes" + v_status="hit" + [[ "$c_size_r" =~ ^[0-9]+$ ]] && v_status="hit ($(fmt_bytes "$c_size_r"))" elif [[ -n "$c_rkey" ]]; then - v_hit="partial (${c_rkey})" + v_status="partial (${c_rkey})" else - v_hit="no" + v_status="miss" fi - # Sizes: only format if numeric. - if [[ "$c_size_r" =~ ^[0-9]+$ ]]; then - v_size_r=$(fmt_bytes "$c_size_r") - else - v_size_r="n/a" - fi - # Size Saved is only meaningful when `saved == true`. Otherwise size-bytes-at-end reflects on-disk size at job end, not what - # got persisted, so we render "n/a". The Saved column stays three-state: "yes" / "no" / "n/a" (missing/null), so a rendered - # row distinguishes "cache action skipped save" from "we don't know whether it saved" — don't collapse them. + # "saved " only when the action persisted it; size-at-end is otherwise just the + # job-end on-disk size, which corresponds to nothing saved. + v_saved="" if [[ "$c_saved" == "true" ]]; then - v_saved="yes" if [[ "$c_size_e" =~ ^[0-9]+$ ]]; then - v_size_e=$(fmt_bytes "$c_size_e") + v_saved=", saved $(fmt_bytes "$c_size_e")" else - v_size_e="n/a" + v_saved=", saved" fi - elif [[ "$c_saved" == "false" ]]; then - v_saved="no" - v_size_e="n/a" - else - v_saved="n/a" - v_size_e="n/a" fi - printf '| %s | %s | %s | %s | %s | %s |\n' \ - "$c_key" "$v_hit" "$c_backend" "$v_size_r" "$v_saved" "$v_size_e" + # shellcheck disable=SC2016 # backticks are literal Markdown code-span, not command substitution + printf -- '- `%s` — %s, %s%s\n' "$c_key" "$v_status" "$c_backend" "$v_saved" done <<< "$cache_rows" } >> "$summary_target" 2>/dev/null || true fi fi +# Close the fold (blank line so the preceding Markdown block terminates). +printf '\n
\n' >> "$summary_target" 2>/dev/null || true + exit 0 From 2cdebc7e8c6e605df5dcc87cd145bcfbc24b9ae2 Mon Sep 17 00:00:00 2001 From: Mikolaj Matuszny Date: Fri, 19 Jun 2026 12:30:48 +0200 Subject: [PATCH 2/3] BUILD-11567 Drop redundant explanatory comment from CI Metrics summary block --- ci-metrics/job-completed.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ci-metrics/job-completed.sh b/ci-metrics/job-completed.sh index 5428862a..8416ea31 100755 --- a/ci-metrics/job-completed.sh +++ b/ci-metrics/job-completed.sh @@ -437,12 +437,6 @@ if [[ "$memory_oom_kill" =~ ^[0-9]+$ ]] && (( memory_oom_kill > 0 )); then fi # ---------- Step summary: one collapsible CI Metrics block per job ---------- -# Folded by default to keep a multi-job matrix summary compact; the line carries a digest -# so the common case needs no expand. Title is plain "CI Metrics" to stay distinct from the -# aggregated CI Metrics table emitted by ci-github-actions' report-ci-metrics action. - -# Digest: middot-joined token per available metric; n/a metrics are dropped. Anomaly tokens (OOM / -# throttling) lead and trigger a "[!]" prefix so they show while collapsed. Net is always present. digest_parts=() (( show_oom )) && digest_parts+=("OOM kill ×${memory_oom_kill}") if (( show_throttled )); then From 47263e155d42e8c78ed3b13abeee5206868188ce Mon Sep 17 00:00:00 2001 From: Mikolaj Matuszny Date: Fri, 19 Jun 2026 14:20:19 +0200 Subject: [PATCH 3/3] BUILD-11567 Surface cache status in the digest; label the cache detail lines Mirror of github-runners-infra: cache token in the collapsed and self-describing per-entry detail lines in the fold. --- ci-metrics/job-completed.sh | 130 ++++++++++++++++++++---------------- 1 file changed, 72 insertions(+), 58 deletions(-) diff --git a/ci-metrics/job-completed.sh b/ci-metrics/job-completed.sh index 8416ea31..2b262091 100755 --- a/ci-metrics/job-completed.sh +++ b/ci-metrics/job-completed.sh @@ -437,6 +437,73 @@ if [[ "$memory_oom_kill" =~ ^[0-9]+$ ]] && (( memory_oom_kill > 0 )); then fi # ---------- Step summary: one collapsible CI Metrics block per job ---------- + +# Parse the cache entries first: one ${CI_METRICS_DIR}/cache-*.json each, sorted by step. A job can +# have several (e.g. maven + npm), so we derive both a headline cache token and the per-entry fold +# detail from the same parse. Fields joined by ASCII Unit Separator ( / $'\x1f'); escaped so the +# file stays YAML-safe when runner.yaml.gotmpl embeds it verbatim. Booleans as strings so jq `//` +# keeps `false`. Numeric sizes are digits or "" (null/missing). Fail-open on jq error. +cache_rows="" +if [[ "$cache_json" != "[]" ]] && command -v jq >/dev/null 2>&1; then + cache_rows=$(jq -r --argjson c "$cache_json" -n ' + $c + | sort_by(.step // "") + | .[] + | [ + (.key // ""), + (if .cache_hit == true then "true" else "false" end), + (.restore_key_hit // ""), + (.backend // "unknown"), + (if (.size_bytes_restored|type) == "number" then (.size_bytes_restored|tostring) else "" end), + (if .saved == true then "true" elif .saved == false then "false" else "" end), + (if (.size_bytes_at_end|type) == "number" then (.size_bytes_at_end|tostring) else "" end) + ] + | join("\u001f") + ' 2>/dev/null) || cache_rows="" +fi + +# Single pass over the parsed rows: count hit/miss for the headline, remember the lone entry's short +# status for the 1-entry case, and build the labeled fold detail lines. +cache_n=0 cache_hits=0 cache_misses=0 cache_one_short="" cache_detail="" +if [[ -n "$cache_rows" ]]; then + # `|| [[ -n "$c_key" ]]` guards a final line without a trailing newline. + while IFS=$'\x1f' read -r c_key c_hit c_rkey c_backend c_size_r c_saved c_size_e || [[ -n "$c_key" ]]; do + cache_n=$((cache_n + 1)) + # status: hit (partial restore-key counts as a hit) / miss. Detail keeps the restored size. + if [[ "$c_hit" == "true" ]]; then + cache_hits=$((cache_hits + 1)) + short="hit"; [[ "$c_size_r" =~ ^[0-9]+$ ]] && short="hit ($(fmt_bytes "$c_size_r"))" + v_status="hit"; [[ "$c_size_r" =~ ^[0-9]+$ ]] && v_status="hit (restored $(fmt_bytes "$c_size_r"))" + elif [[ -n "$c_rkey" ]]; then + cache_hits=$((cache_hits + 1)) + short="partial"; v_status="partial (${c_rkey})" + else + cache_misses=$((cache_misses + 1)) + short="miss"; v_status="miss" + fi + cache_one_short="$short" + # "saved " only when the action persisted it; size-at-end is otherwise just the + # job-end on-disk size, which corresponds to nothing saved. + v_saved="" + if [[ "$c_saved" == "true" ]]; then + v_saved=", saved"; [[ "$c_size_e" =~ ^[0-9]+$ ]] && v_saved=", saved $(fmt_bytes "$c_size_e")" + fi + # shellcheck disable=SC2016 # backticks are literal Markdown code-span, not command substitution + cache_detail+=$(printf -- '- `%s` — %s, backend %s%s' "$c_key" "$v_status" "$c_backend" "$v_saved")$'\n' + done <<< "$cache_rows" +fi + +# Headline cache token: lone entry shows its status (+restored size on a hit); multiple entries show +# hit/miss counts. Glanceable while collapsed; the fold carries key/backend/saved detail. +cache_token="" +if (( cache_n == 1 )); then + cache_token="cache ${cache_one_short}" +elif (( cache_n > 1 )); then + cache_token="cache ${cache_hits} hit, ${cache_misses} miss" +fi + +# Digest: middot-joined token per available metric; n/a metrics are dropped. Anomaly tokens (OOM / +# throttling) lead and trigger a "[!]" prefix so they show while collapsed. Net is always present. digest_parts=() (( show_oom )) && digest_parts+=("OOM kill ×${memory_oom_kill}") if (( show_throttled )); then @@ -458,6 +525,7 @@ elif [[ -n "$memory_peak_bytes" ]]; then fi [[ -n "$disk_utilization" ]] && digest_parts+=("Disk $(pct0 "$disk_utilization")%") digest_parts+=("Net $(fmt_bytes "$net_rx_total")↓ $(fmt_bytes "$net_tx_total")↑") +[[ -n "$cache_token" ]] && digest_parts+=("$cache_token") digest="" for part in "${digest_parts[@]}"; do @@ -466,8 +534,8 @@ done summary_line="CI Metrics — ${digest}" (( show_oom || show_throttled )) && summary_line="[!] ${summary_line}" -# Open the fold and emit the metric table. A blank line after is required for the -# Markdown table inside
to render on GitHub. +# Open the fold and emit the metric table, then the per-entry cache detail. A blank line after +#
is required for the Markdown table inside
to render on GitHub. { printf '
%s\n\n' "$summary_line" printf '| Metric | Value |\n' @@ -478,62 +546,8 @@ summary_line="CI Metrics — ${digest}" printf '| Network total | %s |\n' "$v_net" (( show_throttled )) && printf '| CPU throttled | %s |\n' "$v_cpu_throttled" (( show_oom )) && printf '| OOM kills | %s |\n' "$memory_oom_kill" + [[ -n "$cache_detail" ]] && printf '\n**Cache**\n%s' "$cache_detail" + printf '\n
\n' } >> "$summary_target" 2>/dev/null || true -# ---------- Cache list (inside the same fold) ---------- -# One line per ${CI_METRICS_DIR}/cache-*.json entry; skipped when none. -if [[ "$cache_json" != "[]" ]] && command -v jq >/dev/null 2>&1; then - # Fields joined by ASCII Unit Separator ( / $'\x1f'); escaped so the file stays YAML-safe - # when runner.yaml.gotmpl embeds it verbatim. Booleans as strings so jq `//` keeps `false`. - # Numeric sizes are digits or "" (null/missing). Fail-open on jq error. - cache_rows=$(jq -r --argjson c "$cache_json" -n ' - $c - | sort_by(.step // "") - | .[] - | [ - (.key // ""), - (if .cache_hit == true then "true" else "false" end), - (.restore_key_hit // ""), - (.backend // "unknown"), - (if (.size_bytes_restored|type) == "number" then (.size_bytes_restored|tostring) else "" end), - (if .saved == true then "true" elif .saved == false then "false" else "" end), - (if (.size_bytes_at_end|type) == "number" then (.size_bytes_at_end|tostring) else "" end) - ] - | join("\u001f") - ' 2>/dev/null) || cache_rows="" - - if [[ -n "$cache_rows" ]]; then - { - printf '\n**Cache**\n' - # `|| [[ -n "$c_key" ]]` guards a final line without a trailing newline. - while IFS=$'\x1f' read -r c_key c_hit c_rkey c_backend c_size_r c_saved c_size_e || [[ -n "$c_key" ]]; do - # status: hit (+restored size when known) / partial () / miss - if [[ "$c_hit" == "true" ]]; then - v_status="hit" - [[ "$c_size_r" =~ ^[0-9]+$ ]] && v_status="hit ($(fmt_bytes "$c_size_r"))" - elif [[ -n "$c_rkey" ]]; then - v_status="partial (${c_rkey})" - else - v_status="miss" - fi - # "saved " only when the action persisted it; size-at-end is otherwise just the - # job-end on-disk size, which corresponds to nothing saved. - v_saved="" - if [[ "$c_saved" == "true" ]]; then - if [[ "$c_size_e" =~ ^[0-9]+$ ]]; then - v_saved=", saved $(fmt_bytes "$c_size_e")" - else - v_saved=", saved" - fi - fi - # shellcheck disable=SC2016 # backticks are literal Markdown code-span, not command substitution - printf -- '- `%s` — %s, %s%s\n' "$c_key" "$v_status" "$c_backend" "$v_saved" - done <<< "$cache_rows" - } >> "$summary_target" 2>/dev/null || true - fi -fi - -# Close the fold (blank line so the preceding Markdown block terminates). -printf '\n
\n' >> "$summary_target" 2>/dev/null || true - exit 0