From bc67d4afd8f96dda239cd1bbaed1f867dd1efd7d Mon Sep 17 00:00:00 2001 From: chinmay jain Date: Tue, 11 Aug 2026 15:13:43 +0530 Subject: [PATCH 1/4] feat(notify): scope run/jobs lookups to github.repository with optional run-token --- README.md | 6 +++++- actions/notify/action.yml | 26 ++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8cb76d8..13b54cc 100644 --- a/README.md +++ b/README.md @@ -315,9 +315,10 @@ card above. | `color` | no | `''` | Alert-card bar color (hex, e.g. `#F9A825`). Only takes effect in alert mode. Overrides the result-derived green/red. | | `status` | no | `''` | When set, the alert card's first field renders as **Status** with this text in place of the **Version** field. Only takes effect in alert mode. | | `channel` | no | `slack` | Chat channel; only `slack` is implemented today. | -| `github-token` | no | `${{ github.token }}` | Token used to read the PR/commit, the workflow run, and (on failure) the run's jobs. The default job token covers a same-repo deploy; for a cross-org deploy pass a token minted in the caller from a GitHub App with read access to `target-repo`. | +| `github-token` | no | `${{ github.token }}` | Token used to read the PR/commit that `target-repo`/`target-sha` describes. The default job token covers a same-repo deploy; for a cross-org deploy pass a token minted in the caller from a GitHub App with read access to `target-repo`. | | `target-repo` | no | `${{ github.repository }}` | `owner/repo` whose PR/commit the card describes. Override for a cross-repo deploy. | | `target-sha` | no | `${{ github.sha }}` | Exact built/deployed commit SHA used to resolve the PR/commit. | +| `run-token` | no | falls back to `github-token` | Token used ONLY for the `/actions/runs/{id}` + `/jobs` lookups that populate `DURATION` and `FAILED AT`. Runs always live in the caller's repo (`github.repository`), so a cross-org `github-token` scoped to `target-repo` can't read them. Pass `${{ github.token }}` when overriding `github-token` cross-org; otherwise leave unset. | Example — same-repo deploy (token/target default to this repo): @@ -360,6 +361,9 @@ Example — cross-org deploy (card describes a PR/commit in another repo): github-token: ${{ steps.app-token.outputs.token }} target-repo: other-org/other-repo target-sha: ${{ needs.build.outputs.app_sha }} + # github-token is scoped to other-org/other-repo and can't read this + # repo's Actions API — pass the job's own token for DURATION + FAILED AT. + run-token: ${{ github.token }} ``` ## enforce — inputs & outputs diff --git a/actions/notify/action.yml b/actions/notify/action.yml index 3e665e8..4aa9b93 100644 --- a/actions/notify/action.yml +++ b/actions/notify/action.yml @@ -80,6 +80,17 @@ inputs: commit shown on the card. Defaults to this workflow's commit. required: false default: ${{ github.sha }} + run-token: + description: >- + Token used ONLY for the /actions/runs/{id} + /jobs API calls that + populate DURATION and FAILED AT. Defaults to `github-token` (fine when + that token has `actions: read` on the current repo). Override when + `github-token` is a cross-org token minted for `target-repo` and thus + has no access to the current repo's Actions API: pass + `${{ github.token }}` so the run metadata is read with the job's own + token. Requires `actions: read` on the notify job. + required: false + default: "" runs: using: composite @@ -98,9 +109,11 @@ runs: WEBHOOK: ${{ inputs.webhook-url }} TARGET_REPO: ${{ inputs.target-repo }} TARGET_SHA: ${{ inputs.target-sha }} + RUN_REPO: ${{ github.repository }} RUN_ID: ${{ github.run_id }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_TOKEN: ${{ inputs.github-token }} + RUN_TOKEN: ${{ inputs.run-token }} run: | set -uo pipefail @@ -109,6 +122,11 @@ runs: exit 0 fi + # Runs live in github.repository (RUN_REPO), even when target-repo is + # a cross-org repo for PR attribution. Fall the run-token back to the + # PR-attribution token so single-repo callers keep working unchanged. + RUN_TOKEN="${RUN_TOKEN:-$GH_TOKEN}" + # Status icon only. Header emoji conveys success/failure — no separate # subtitle line under it. icon=":x:"; is_success=0 @@ -200,10 +218,10 @@ runs: # -- Duration: pull run_started_at, compute elapsed vs now. # Falls back silently if the API call fails. duration="" - run_json="$(gh api "repos/${TARGET_REPO}/actions/runs/${RUN_ID}" 2>/dev/null || echo '{}')" + run_json="$(GH_TOKEN="$RUN_TOKEN" gh api "repos/${RUN_REPO}/actions/runs/${RUN_ID}" 2>/dev/null || echo '{}')" started_at="$(jq -r '.run_started_at // .created_at // empty' <<<"$run_json")" if [ -z "$started_at" ]; then - echo "::warning::notify: /actions/runs/${RUN_ID} returned no run_started_at. Check that the notify job grants 'actions: read' — DURATION will not render." + echo "::warning::notify: /actions/runs/${RUN_ID} in ${RUN_REPO} returned no run_started_at. Check that the notify job grants 'actions: read' and (for cross-org callers) run-token has access — DURATION will not render." fi if [ -n "$started_at" ]; then started_epoch="$(date -u -d "$started_at" +%s 2>/dev/null || echo '')" @@ -227,9 +245,9 @@ runs: # always human-meaningful and match what users see in the Actions UI. failed_step="" if [ "$is_success" -eq 0 ]; then - jobs_json="$(gh api "repos/${TARGET_REPO}/actions/runs/${RUN_ID}/jobs" 2>/dev/null || echo '{}')" + jobs_json="$(GH_TOKEN="$RUN_TOKEN" gh api "repos/${RUN_REPO}/actions/runs/${RUN_ID}/jobs" 2>/dev/null || echo '{}')" if [ "$(jq -r '(.jobs // []) | length' <<<"$jobs_json")" = "0" ]; then - echo "::warning::notify: /actions/runs/${RUN_ID}/jobs returned no jobs. Check that the notify job grants 'actions: read' — FAILED AT will not render." + echo "::warning::notify: /actions/runs/${RUN_ID}/jobs in ${RUN_REPO} returned no jobs. Check that the notify job grants 'actions: read' and (for cross-org callers) run-token has access — FAILED AT will not render." fi failed_step="$(jq -r '[.jobs[]? | select(.conclusion == "failure")] | .[0].name // ""' <<<"$jobs_json")" fi From d0b56346f3252afc496d9bebb2350f33a989c01c Mon Sep 17 00:00:00 2001 From: chinmay jain Date: Tue, 11 Aug 2026 15:35:37 +0530 Subject: [PATCH 2/4] fix(notify): don't use ${{ }} syntax inside input description --- actions/notify/action.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/actions/notify/action.yml b/actions/notify/action.yml index 4aa9b93..5cd0589 100644 --- a/actions/notify/action.yml +++ b/actions/notify/action.yml @@ -86,9 +86,9 @@ inputs: populate DURATION and FAILED AT. Defaults to `github-token` (fine when that token has `actions: read` on the current repo). Override when `github-token` is a cross-org token minted for `target-repo` and thus - has no access to the current repo's Actions API: pass - `${{ github.token }}` so the run metadata is read with the job's own - token. Requires `actions: read` on the notify job. + has no access to the current repo's Actions API: pass the job's own + `github.token` so the run metadata is read with it. Requires + `actions: read` on the notify job. required: false default: "" From 99331c566db5a15a817133d08c25e7d82f4f35df Mon Sep 17 00:00:00 2001 From: chinmay jain Date: Tue, 11 Aug 2026 15:47:36 +0530 Subject: [PATCH 3/4] feat(notify): always render grid so FAILED AT survives an unresolvable target-sha --- actions/notify/action.yml | 169 +++++++++++++++++++------------------- 1 file changed, 84 insertions(+), 85 deletions(-) diff --git a/actions/notify/action.yml b/actions/notify/action.yml index 5cd0589..78567d8 100644 --- a/actions/notify/action.yml +++ b/actions/notify/action.yml @@ -280,94 +280,93 @@ runs: url_display="$(printf '%s' "$DEPLOYED_URL" | sed -E 's|^https?://||; s|/+$||')" fi - # Fallback path: neither PR nor commit resolved → single-line message. - if [ -z "$commit_subject" ] && [ -z "$pr_num" ]; then - payload="$(jq -n \ - --arg text "${icon} ${LABEL} ${version} | 📅 ${built_utc} UTC | <${RUN_URL}|view run>" \ - '{ text: $text }')" - else - # -- Single full-width mrkdwn section with monospace-padded labels. - # Slack's section.fields is a 2-col grid ~50/50; the value column is - # only ~30 chars, which wrapped long commit subjects and PR refs. - # Wrapping each label in inline code (`) forces monospace, so we can - # right-pad to a fixed 9-char column ("FAILED AT" is the longest at - # 9). That preserves the aligned-column look while giving the value - # column the full card width — long values no longer wrap. - # `-r` is required so the joined newlines survive raw to the outer jq. - rows_text="$(jq -n -r \ - --arg is_success "$is_success" \ - --arg failed_step "$failed_step" \ - --arg subject "$commit_subject" \ - --arg pr_label "$pr_label" --arg pr_url "$pr_url" \ - --arg author "$commit_author" \ - --arg short_sha "$short_sha" --arg commit_url "$commit_url" \ - --arg duration "$duration" \ - --arg built "$built_utc" \ - --arg url_display "$url_display" --arg url_full "$DEPLOYED_URL" ' - def esc: gsub("&";"&") | gsub("<";"<") | gsub(">";">"); - # MAINTENANCE RISK. The padlabel scheme below is empirically tuned to - # Slacks Lato font at current rendering sizes any Slack renderer - # change and column alignment drifts silently (visible only on - # screenshot review, not in any test signal). If durability matters - # more than fidelity, the safe fallback is a plain bold-then-value - # render: replace padlabel(k) + v with "*" + k + "*\n" + v. - # `label` is a jq reserved keyword (label/break), so name this `padlabel`. - # Two-space padding scheme for column alignment in Slack Lato: - # 1. Em space (\u2003 = 1 em) \u2014 pad to 12 chars total. - # 2. NBSP (\u00a0 = ~0.25 em) \u2014 add N per label = label length. - # In Slack Lato, bold letter width is ~0.75 em, em space is 1 em, NBSP - # is ~0.25 em. Total row width = L*0.75 + (12-L)*1 + L*0.25 = 12 em, - # constant across all label lengths. Pure em-space padding drifted - # because bold letter (0.75) < em (1), so longer labels came out - # narrower \u2014 visible as DURATION (8 chars) drifting left of PR (2). - # The per-label NBSP tail scales with L to make up the missing 0.25 em - # per bold letter, so DURATION gets the largest NBSP tail. - # NOTE: no apostrophes in these comments the outer shell context - # is a single-quoted jq expression and an apostrophe would close it. - def padlabel(k): - (k | length) as $L - # Empirical shave: COMMIT / AUTHOR / DURATION are all-caps single - # words dominated by wide bold letters (M, D, U, R, A, O, N), so - # under the analytical formula their rendered width overshoots - # the reference column formed by the other labels. Drop 2 NBSPs - # from those three to pull their values back into the same X. - | (if k == "COMMIT" or k == "AUTHOR" or k == "DURATION" then 2 elif k == "PR" or k == "FAILED AT" then -2 else 0 end) as $shave - | "*" + k + "*" - + "\u2003" * (if 12 - $L > 0 then 12 - $L else 0 end) - + "\u00a0" * ($L - $shave); - def row(k; v): padlabel(k) + v; + # Always render the grid card — every row is individually conditional, + # so an unresolved target-sha just skips COMMIT + PR while FAILED AT + + # HASH + DURATION + DATE + the status header keep the failure signal + # visible. Prior behavior fell back to a single-line message that + # swallowed FAILED AT precisely when engineers needed it most. + + # -- Single full-width mrkdwn section with monospace-padded labels. + # Slack's section.fields is a 2-col grid ~50/50; the value column is + # only ~30 chars, which wrapped long commit subjects and PR refs. + # Wrapping each label in inline code (`) forces monospace, so we can + # right-pad to a fixed 9-char column ("FAILED AT" is the longest at + # 9). That preserves the aligned-column look while giving the value + # column the full card width — long values no longer wrap. + # `-r` is required so the joined newlines survive raw to the outer jq. + rows_text="$(jq -n -r \ + --arg is_success "$is_success" \ + --arg failed_step "$failed_step" \ + --arg subject "$commit_subject" \ + --arg pr_label "$pr_label" --arg pr_url "$pr_url" \ + --arg author "$commit_author" \ + --arg short_sha "$short_sha" --arg commit_url "$commit_url" \ + --arg duration "$duration" \ + --arg built "$built_utc" \ + --arg url_display "$url_display" --arg url_full "$DEPLOYED_URL" ' + def esc: gsub("&";"&") | gsub("<";"<") | gsub(">";">"); + # MAINTENANCE RISK. The padlabel scheme below is empirically tuned to + # Slacks Lato font at current rendering sizes any Slack renderer + # change and column alignment drifts silently (visible only on + # screenshot review, not in any test signal). If durability matters + # more than fidelity, the safe fallback is a plain bold-then-value + # render: replace padlabel(k) + v with "*" + k + "*\n" + v. + # `label` is a jq reserved keyword (label/break), so name this `padlabel`. + # Two-space padding scheme for column alignment in Slack Lato: + # 1. Em space (\u2003 = 1 em) \u2014 pad to 12 chars total. + # 2. NBSP (\u00a0 = ~0.25 em) \u2014 add N per label = label length. + # In Slack Lato, bold letter width is ~0.75 em, em space is 1 em, NBSP + # is ~0.25 em. Total row width = L*0.75 + (12-L)*1 + L*0.25 = 12 em, + # constant across all label lengths. Pure em-space padding drifted + # because bold letter (0.75) < em (1), so longer labels came out + # narrower \u2014 visible as DURATION (8 chars) drifting left of PR (2). + # The per-label NBSP tail scales with L to make up the missing 0.25 em + # per bold letter, so DURATION gets the largest NBSP tail. + # NOTE: no apostrophes in these comments the outer shell context + # is a single-quoted jq expression and an apostrophe would close it. + def padlabel(k): + (k | length) as $L + # Empirical shave: COMMIT / AUTHOR / DURATION are all-caps single + # words dominated by wide bold letters (M, D, U, R, A, O, N), so + # under the analytical formula their rendered width overshoots + # the reference column formed by the other labels. Drop 2 NBSPs + # from those three to pull their values back into the same X. + | (if k == "COMMIT" or k == "AUTHOR" or k == "DURATION" then 2 elif k == "PR" or k == "FAILED AT" then -2 else 0 end) as $shave + | "*" + k + "*" + + "\u2003" * (if 12 - $L > 0 then 12 - $L else 0 end) + + "\u00a0" * ($L - $shave); + def row(k; v): padlabel(k) + v; - ( if $is_success == "0" and $failed_step != "" then - [ row("FAILED AT"; ($failed_step|esc)) ] + ( if $is_success == "0" and $failed_step != "" then + [ row("FAILED AT"; ($failed_step|esc)) ] + else [] end ) + + ( if $subject != "" then [ row("COMMIT"; ($subject|esc)) ] else [] end ) + + ( if $pr_label != "" then + [ row("PR"; (if $pr_url != "" then "<" + $pr_url + "|" + $pr_label + ">" else $pr_label end)) ] else [] end ) - + ( if $subject != "" then [ row("COMMIT"; ($subject|esc)) ] else [] end ) - + ( if $pr_label != "" then - [ row("PR"; (if $pr_url != "" then "<" + $pr_url + "|" + $pr_label + ">" else $pr_label end)) ] - else [] end ) - + [ row("AUTHOR"; ($author|esc)) ] - + [ row("HASH"; (if $commit_url != "" then "<" + $commit_url + "|`" + $short_sha + "`>" else "`" + $short_sha + "`" end)) ] - + ( if $duration != "" then [ row("DURATION"; $duration) ] else [] end ) - + [ row("DATE"; ($built + " UTC")) ] - + ( if $is_success == "1" and $url_display != "" then - [ row("URL"; "<" + $url_full + "|" + $url_display + ">") ] - else [] end ) - | join("\n\n") - ')" + + [ row("AUTHOR"; ($author|esc)) ] + + [ row("HASH"; (if $commit_url != "" then "<" + $commit_url + "|`" + $short_sha + "`>" else "`" + $short_sha + "`" end)) ] + + ( if $duration != "" then [ row("DURATION"; $duration) ] else [] end ) + + [ row("DATE"; ($built + " UTC")) ] + + ( if $is_success == "1" and $url_display != "" then + [ row("URL"; "<" + $url_full + "|" + $url_display + ">") ] + else [] end ) + | join("\n\n") + ')" - # Header emoji conveys status; no separate context/subtitle block. - payload="$(jq -n \ - --arg icon "$icon" --arg label "$LABEL" --arg version "$version" \ - --arg runurl "$RUN_URL" \ - --arg rows_text "$rows_text" ' - { blocks: - [ { type:"header", text:{ type:"plain_text", text:($icon + " " + $label + " " + $version), emoji:true } }, - { type:"section", text:{ type:"mrkdwn", text: $rows_text } }, - { type:"actions", elements:[ - { type:"button", - text:{ type:"plain_text", text:"View run" }, - url: $runurl } ] } ] - }')" - fi + # Header emoji conveys status; no separate context/subtitle block. + payload="$(jq -n \ + --arg icon "$icon" --arg label "$LABEL" --arg version "$version" \ + --arg runurl "$RUN_URL" \ + --arg rows_text "$rows_text" ' + { blocks: + [ { type:"header", text:{ type:"plain_text", text:($icon + " " + $label + " " + $version), emoji:true } }, + { type:"section", text:{ type:"mrkdwn", text: $rows_text } }, + { type:"actions", elements:[ + { type:"button", + text:{ type:"plain_text", text:"View run" }, + url: $runurl } ] } ] + }')" # -f makes curl fail on HTTP >=400 so Slack payload rejections surface in # the log instead of being silently reported as a successful send. From 5a1ee793a9bdfbb99d924c970a3ac2099020607b Mon Sep 17 00:00:00 2001 From: chinmay jain Date: Tue, 11 Aug 2026 15:55:54 +0530 Subject: [PATCH 4/4] Revert "feat(notify): always render grid so FAILED AT survives an unresolvable target-sha" This reverts commit 99331c566db5a15a817133d08c25e7d82f4f35df. --- actions/notify/action.yml | 169 +++++++++++++++++++------------------- 1 file changed, 85 insertions(+), 84 deletions(-) diff --git a/actions/notify/action.yml b/actions/notify/action.yml index 78567d8..5cd0589 100644 --- a/actions/notify/action.yml +++ b/actions/notify/action.yml @@ -280,93 +280,94 @@ runs: url_display="$(printf '%s' "$DEPLOYED_URL" | sed -E 's|^https?://||; s|/+$||')" fi - # Always render the grid card — every row is individually conditional, - # so an unresolved target-sha just skips COMMIT + PR while FAILED AT + - # HASH + DURATION + DATE + the status header keep the failure signal - # visible. Prior behavior fell back to a single-line message that - # swallowed FAILED AT precisely when engineers needed it most. - - # -- Single full-width mrkdwn section with monospace-padded labels. - # Slack's section.fields is a 2-col grid ~50/50; the value column is - # only ~30 chars, which wrapped long commit subjects and PR refs. - # Wrapping each label in inline code (`) forces monospace, so we can - # right-pad to a fixed 9-char column ("FAILED AT" is the longest at - # 9). That preserves the aligned-column look while giving the value - # column the full card width — long values no longer wrap. - # `-r` is required so the joined newlines survive raw to the outer jq. - rows_text="$(jq -n -r \ - --arg is_success "$is_success" \ - --arg failed_step "$failed_step" \ - --arg subject "$commit_subject" \ - --arg pr_label "$pr_label" --arg pr_url "$pr_url" \ - --arg author "$commit_author" \ - --arg short_sha "$short_sha" --arg commit_url "$commit_url" \ - --arg duration "$duration" \ - --arg built "$built_utc" \ - --arg url_display "$url_display" --arg url_full "$DEPLOYED_URL" ' - def esc: gsub("&";"&") | gsub("<";"<") | gsub(">";">"); - # MAINTENANCE RISK. The padlabel scheme below is empirically tuned to - # Slacks Lato font at current rendering sizes any Slack renderer - # change and column alignment drifts silently (visible only on - # screenshot review, not in any test signal). If durability matters - # more than fidelity, the safe fallback is a plain bold-then-value - # render: replace padlabel(k) + v with "*" + k + "*\n" + v. - # `label` is a jq reserved keyword (label/break), so name this `padlabel`. - # Two-space padding scheme for column alignment in Slack Lato: - # 1. Em space (\u2003 = 1 em) \u2014 pad to 12 chars total. - # 2. NBSP (\u00a0 = ~0.25 em) \u2014 add N per label = label length. - # In Slack Lato, bold letter width is ~0.75 em, em space is 1 em, NBSP - # is ~0.25 em. Total row width = L*0.75 + (12-L)*1 + L*0.25 = 12 em, - # constant across all label lengths. Pure em-space padding drifted - # because bold letter (0.75) < em (1), so longer labels came out - # narrower \u2014 visible as DURATION (8 chars) drifting left of PR (2). - # The per-label NBSP tail scales with L to make up the missing 0.25 em - # per bold letter, so DURATION gets the largest NBSP tail. - # NOTE: no apostrophes in these comments the outer shell context - # is a single-quoted jq expression and an apostrophe would close it. - def padlabel(k): - (k | length) as $L - # Empirical shave: COMMIT / AUTHOR / DURATION are all-caps single - # words dominated by wide bold letters (M, D, U, R, A, O, N), so - # under the analytical formula their rendered width overshoots - # the reference column formed by the other labels. Drop 2 NBSPs - # from those three to pull their values back into the same X. - | (if k == "COMMIT" or k == "AUTHOR" or k == "DURATION" then 2 elif k == "PR" or k == "FAILED AT" then -2 else 0 end) as $shave - | "*" + k + "*" - + "\u2003" * (if 12 - $L > 0 then 12 - $L else 0 end) - + "\u00a0" * ($L - $shave); - def row(k; v): padlabel(k) + v; + # Fallback path: neither PR nor commit resolved → single-line message. + if [ -z "$commit_subject" ] && [ -z "$pr_num" ]; then + payload="$(jq -n \ + --arg text "${icon} ${LABEL} ${version} | 📅 ${built_utc} UTC | <${RUN_URL}|view run>" \ + '{ text: $text }')" + else + # -- Single full-width mrkdwn section with monospace-padded labels. + # Slack's section.fields is a 2-col grid ~50/50; the value column is + # only ~30 chars, which wrapped long commit subjects and PR refs. + # Wrapping each label in inline code (`) forces monospace, so we can + # right-pad to a fixed 9-char column ("FAILED AT" is the longest at + # 9). That preserves the aligned-column look while giving the value + # column the full card width — long values no longer wrap. + # `-r` is required so the joined newlines survive raw to the outer jq. + rows_text="$(jq -n -r \ + --arg is_success "$is_success" \ + --arg failed_step "$failed_step" \ + --arg subject "$commit_subject" \ + --arg pr_label "$pr_label" --arg pr_url "$pr_url" \ + --arg author "$commit_author" \ + --arg short_sha "$short_sha" --arg commit_url "$commit_url" \ + --arg duration "$duration" \ + --arg built "$built_utc" \ + --arg url_display "$url_display" --arg url_full "$DEPLOYED_URL" ' + def esc: gsub("&";"&") | gsub("<";"<") | gsub(">";">"); + # MAINTENANCE RISK. The padlabel scheme below is empirically tuned to + # Slacks Lato font at current rendering sizes any Slack renderer + # change and column alignment drifts silently (visible only on + # screenshot review, not in any test signal). If durability matters + # more than fidelity, the safe fallback is a plain bold-then-value + # render: replace padlabel(k) + v with "*" + k + "*\n" + v. + # `label` is a jq reserved keyword (label/break), so name this `padlabel`. + # Two-space padding scheme for column alignment in Slack Lato: + # 1. Em space (\u2003 = 1 em) \u2014 pad to 12 chars total. + # 2. NBSP (\u00a0 = ~0.25 em) \u2014 add N per label = label length. + # In Slack Lato, bold letter width is ~0.75 em, em space is 1 em, NBSP + # is ~0.25 em. Total row width = L*0.75 + (12-L)*1 + L*0.25 = 12 em, + # constant across all label lengths. Pure em-space padding drifted + # because bold letter (0.75) < em (1), so longer labels came out + # narrower \u2014 visible as DURATION (8 chars) drifting left of PR (2). + # The per-label NBSP tail scales with L to make up the missing 0.25 em + # per bold letter, so DURATION gets the largest NBSP tail. + # NOTE: no apostrophes in these comments the outer shell context + # is a single-quoted jq expression and an apostrophe would close it. + def padlabel(k): + (k | length) as $L + # Empirical shave: COMMIT / AUTHOR / DURATION are all-caps single + # words dominated by wide bold letters (M, D, U, R, A, O, N), so + # under the analytical formula their rendered width overshoots + # the reference column formed by the other labels. Drop 2 NBSPs + # from those three to pull their values back into the same X. + | (if k == "COMMIT" or k == "AUTHOR" or k == "DURATION" then 2 elif k == "PR" or k == "FAILED AT" then -2 else 0 end) as $shave + | "*" + k + "*" + + "\u2003" * (if 12 - $L > 0 then 12 - $L else 0 end) + + "\u00a0" * ($L - $shave); + def row(k; v): padlabel(k) + v; - ( if $is_success == "0" and $failed_step != "" then - [ row("FAILED AT"; ($failed_step|esc)) ] - else [] end ) - + ( if $subject != "" then [ row("COMMIT"; ($subject|esc)) ] else [] end ) - + ( if $pr_label != "" then - [ row("PR"; (if $pr_url != "" then "<" + $pr_url + "|" + $pr_label + ">" else $pr_label end)) ] + ( if $is_success == "0" and $failed_step != "" then + [ row("FAILED AT"; ($failed_step|esc)) ] else [] end ) - + [ row("AUTHOR"; ($author|esc)) ] - + [ row("HASH"; (if $commit_url != "" then "<" + $commit_url + "|`" + $short_sha + "`>" else "`" + $short_sha + "`" end)) ] - + ( if $duration != "" then [ row("DURATION"; $duration) ] else [] end ) - + [ row("DATE"; ($built + " UTC")) ] - + ( if $is_success == "1" and $url_display != "" then - [ row("URL"; "<" + $url_full + "|" + $url_display + ">") ] - else [] end ) - | join("\n\n") - ')" + + ( if $subject != "" then [ row("COMMIT"; ($subject|esc)) ] else [] end ) + + ( if $pr_label != "" then + [ row("PR"; (if $pr_url != "" then "<" + $pr_url + "|" + $pr_label + ">" else $pr_label end)) ] + else [] end ) + + [ row("AUTHOR"; ($author|esc)) ] + + [ row("HASH"; (if $commit_url != "" then "<" + $commit_url + "|`" + $short_sha + "`>" else "`" + $short_sha + "`" end)) ] + + ( if $duration != "" then [ row("DURATION"; $duration) ] else [] end ) + + [ row("DATE"; ($built + " UTC")) ] + + ( if $is_success == "1" and $url_display != "" then + [ row("URL"; "<" + $url_full + "|" + $url_display + ">") ] + else [] end ) + | join("\n\n") + ')" - # Header emoji conveys status; no separate context/subtitle block. - payload="$(jq -n \ - --arg icon "$icon" --arg label "$LABEL" --arg version "$version" \ - --arg runurl "$RUN_URL" \ - --arg rows_text "$rows_text" ' - { blocks: - [ { type:"header", text:{ type:"plain_text", text:($icon + " " + $label + " " + $version), emoji:true } }, - { type:"section", text:{ type:"mrkdwn", text: $rows_text } }, - { type:"actions", elements:[ - { type:"button", - text:{ type:"plain_text", text:"View run" }, - url: $runurl } ] } ] - }')" + # Header emoji conveys status; no separate context/subtitle block. + payload="$(jq -n \ + --arg icon "$icon" --arg label "$LABEL" --arg version "$version" \ + --arg runurl "$RUN_URL" \ + --arg rows_text "$rows_text" ' + { blocks: + [ { type:"header", text:{ type:"plain_text", text:($icon + " " + $label + " " + $version), emoji:true } }, + { type:"section", text:{ type:"mrkdwn", text: $rows_text } }, + { type:"actions", elements:[ + { type:"button", + text:{ type:"plain_text", text:"View run" }, + url: $runurl } ] } ] + }')" + fi # -f makes curl fail on HTTP >=400 so Slack payload rejections surface in # the log instead of being silently reported as a successful send.