Repository navigation
563 lines (506 loc) · 25.4 KB
/
Copy pathci-status-report.yml
File metadata and controls
563 lines (506 loc) · 25.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
name: CI Status Report
# Reads config/projects.json and reports the latest ci.yml/ci.yaml/ci-release.yml
# workflow run status for every OSS and commercial branch of every Spring Cloud
# project, so a branch whose CI has quietly started failing is visible in
# one place instead of being discovered branch-by-branch.
#
# See README-ci-status-report.md for details.
on:
workflow_dispatch:
inputs:
projects:
description: 'Comma-separated list of Spring Cloud project names to check (e.g. spring-cloud-build,spring-cloud-config). When empty, all projects in projects.json are checked.'
required: false
type: string
default: ''
repo_type:
description: 'Check commercial, oss, or both?'
required: false
type: choice
default: 'both'
options:
- both
- oss
- commercial
token:
description: 'GitHub token with read access to all target repos. Falls back to GH_ACTIONS_REPO_TOKEN.'
required: false
type: string
default: ''
# Runs twice on weekdays, at ~4:00am and ~6:07am US Eastern time - see
# "Determine phase" in the setup job below for what distinguishes them.
# GitHub Actions cron always runs in UTC and has no notion of DST, so each
# of the two times needs its own pair of entries - one at the UTC offset
# for EDT (UTC-4), one for EST (UTC-5) - selected by month. The actual US
# DST boundary (2nd Sunday in March / 1st Sunday in November) falls in the
# middle of a month, so for a few days each side of that boundary these
# fire an hour early or late local time; that's an accepted tradeoff for a
# status report rather than something worth a date-computing workaround.
# There is no existing DST convention elsewhere in this repo's scheduled
# workflows to follow (examples/deploy.yml's schedule doesn't account for
# DST at all).
#
# The 6:07 entries use :07, not :00 - GitHub's own docs flag the top of the
# hour as the highest-congestion time for scheduled workflows and recommend
# an off-the-hour minute to reduce the chance of delay or a dropped run.
# The 4:00 entries are left on the hour since nothing downstream depends on
# split-second timing for them - see the phase note above.
schedule:
- cron: '0 8 * 3-10 1-5' # ~4:00am EDT, March-October
- cron: '0 9 * 11,12,1,2 1-5' # ~4:00am EST, November-February
- cron: '7 10 * 3-10 1-5' # ~6:07am EDT, March-October
- cron: '7 11 * 11,12,1,2 1-5' # ~6:07am EST, November-February
permissions:
contents: read
jobs:
setup:
name: Build Matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.build-matrix.outputs.matrix }}
count: ${{ steps.build-matrix.outputs.count }}
phase: ${{ steps.phase.outputs.phase }}
steps:
- name: Checkout
uses: actions/checkout@v4
# Two-phase design: the ~4am run finds failures and triggers a rerun of
# just the failed jobs, then exits quietly (no Chat post). The ~6:07am
# run scans again - by then most flaky reruns have had ~2 hours to
# finish - and is the one that posts to Chat, distinguishing "recovered
# after rerun" from "still failing after rerun" using each run's own
# run_attempt (GitHub increments it in place when failed jobs are
# rerun, so this needs no state passed between the two runs). A manual
# workflow_dispatch has no github.event.schedule, so it defaults to
# "report" - the rerun side-effect should never happen from a manual
# click; it only ever fires from the ~4am schedule.
- name: Determine phase
id: phase
env:
EVENT_SCHEDULE: ${{ github.event.schedule }}
run: |
case "$EVENT_SCHEDULE" in
'0 8 * 3-10 1-5'|'0 9 * 11,12,1,2 1-5')
PHASE="detect-and-rerun"
;;
*)
PHASE="report"
;;
esac
echo "phase=${PHASE}" >> "$GITHUB_OUTPUT"
echo "Determined phase: ${PHASE} (event.schedule='${EVENT_SCHEDULE}')"
- name: Build matrix
id: build-matrix
env:
PROJECTS_FILTER: ${{ inputs.projects }}
REPO_TYPE: ${{ inputs.repo_type }}
MATRIX_LIB: ${{ github.workspace }}/.github/scripts/project-branch-matrix.js
run: |
node - << 'JSEOF'
const fs = require('fs');
const { buildBranchMatrix } = require(process.env.MATRIX_LIB);
const projects = JSON.parse(fs.readFileSync('config/projects.json', 'utf8'));
// Expand every project's oss/commercial section into one matrix entry per branch
// it lists as "scheduled" - the full set of maintained branches, as opposed to
// "default" which is just the primary one. `-internal` branches are kept
// (excludeInternal: false): they run ci-release.yml just like some OSS release/*
// branches do, and this workflow checks whichever of ci-release.yml/ci.yml/ci.yaml
// actually exists on the branch.
const { entries } = buildBranchMatrix(projects, {
filter: process.env.PROJECTS_FILTER,
repoType: process.env.REPO_TYPE,
excludeInternal: false,
});
console.log(`Branches to check: ${entries.length}`);
for (const e of entries) console.log(` ${e.repo}@${e.branch} (${e.type})`);
fs.appendFileSync(process.env.GITHUB_OUTPUT,
`matrix=${JSON.stringify({ include: entries })}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `count=${entries.length}\n`);
JSEOF
check:
name: "CI Status — ${{ matrix.repo }}@${{ matrix.branch }}"
needs: setup
if: needs.setup.outputs.count != '0'
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 8
matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
steps:
- name: Check CI workflow status
id: check
env:
GH_TOKEN: ${{ inputs.token || secrets.GH_ACTIONS_REPO_TOKEN }}
REPO: ${{ matrix.repo }}
BRANCH: ${{ matrix.branch }}
PROJECT: ${{ matrix.project }}
TYPE: ${{ matrix.type }}
PHASE: ${{ needs.setup.outputs.phase }}
run: |
set -euo pipefail
# A project's CI workflow file is usually ci.yml or ci.yaml, but
# -internal branches and some OSS release/* branches run ci-release.yml
# instead (same convention already used by the trigger-branch-ci
# action) - try all three and use whichever one actually exists on
# this branch. Trying a name that doesn't exist is just a cheap
# 404, so there's no need to gate this by branch name pattern.
RUN_JSON=""
WORKFLOW_FILE=""
for wf in ci-release.yml ci.yml ci.yaml; do
RESP=$(gh api "repos/${REPO}/actions/workflows/${wf}/runs?branch=${BRANCH}&per_page=1" 2>/dev/null || echo "")
if [[ -n "$RESP" ]] && [[ "$(echo "$RESP" | jq -r '.total_count // 0')" != "0" ]]; then
RUN_JSON="$RESP"
WORKFLOW_FILE="$wf"
break
fi
done
if [[ -z "$RUN_JSON" ]]; then
echo "No ci-release.yml/ci.yml/ci.yaml runs found for ${REPO}@${BRANCH}."
STATUS="not-found"
CONCLUSION="none"
URL=""
RUN_NUMBER=""
CREATED_AT=""
RUN_ID=""
RUN_ATTEMPT="1"
else
RUN=$(echo "$RUN_JSON" | jq -c '.workflow_runs[0]')
STATUS=$(echo "$RUN" | jq -r '.status')
CONCLUSION=$(echo "$RUN" | jq -r '.conclusion // "none"')
URL=$(echo "$RUN" | jq -r '.html_url')
RUN_NUMBER=$(echo "$RUN" | jq -r '.run_number')
CREATED_AT=$(echo "$RUN" | jq -r '.created_at')
RUN_ID=$(echo "$RUN" | jq -r '.id')
RUN_ATTEMPT=$(echo "$RUN" | jq -r '.run_attempt // 1')
echo "Found ${WORKFLOW_FILE} run #${RUN_NUMBER} (attempt ${RUN_ATTEMPT}): status=${STATUS} conclusion=${CONCLUSION}"
fi
# Detect-and-rerun phase only: kick off a rerun of just the failed
# jobs in this run, then move on without waiting for it - the
# report phase, ~2 hours later, scans again and reads the outcome
# off this same run's (by then likely incremented) run_attempt.
# This is what actually automates today's "someone notices and
# clicks rerun" step; the report phase never triggers a rerun
# itself, it only classifies what it observes.
RERUN_TRIGGERED="false"
if [[ "$PHASE" == "detect-and-rerun" && "$CONCLUSION" == "failure" && -n "$RUN_ID" ]]; then
echo "Triggering a rerun of failed jobs for ${REPO}@${BRANCH} (run ${RUN_ID})..."
if gh api --method POST "repos/${REPO}/actions/runs/${RUN_ID}/rerun-failed-jobs" >/dev/null 2>&1; then
RERUN_TRIGGERED="true"
echo "Rerun triggered."
else
echo "Could not trigger a rerun for ${REPO}@${BRANCH} (run ${RUN_ID}) - it may have no rerunnable failed jobs, or be outside GitHub's rerun window."
fi
fi
# Only the report phase, and only branches that are actually
# failing, get the extra "who broke it" lookback below. The
# detect-and-rerun phase skips it entirely since its findings are
# never shown anywhere and are about to be re-attempted anyway; a
# "not-found" branch has no CI runs to walk through in the first
# place, and a passing/pending branch doesn't need a culprit - so
# the extra API calls scale with the number of red branches actually
# being reported, not the size of the whole matrix.
echo '{}' > blame.json
if [[ "$PHASE" == "report" && "$CONCLUSION" == "failure" ]]; then
echo "Walking back through ${WORKFLOW_FILE} history on ${BRANCH} to find when it broke..."
gh api "repos/${REPO}/actions/workflows/${WORKFLOW_FILE}/runs?branch=${BRANCH}&per_page=100" \
2>/dev/null > history.json || echo '{"workflow_runs":[]}' > history.json
node - << 'JSEOF' > blame.json
const fs = require('fs');
const data = JSON.parse(fs.readFileSync('history.json', 'utf8'));
// GitHub returns workflow runs newest-first. Only completed runs
// have a meaningful conclusion, so in-progress/queued entries are
// dropped before walking the pass/fail streak.
const runs = (data.workflow_runs || []).filter(r => r.status === 'completed');
let passIndex = -1;
for (let i = 0; i < runs.length; i++) {
if (runs[i].conclusion === 'success') { passIndex = i; break; }
}
// No passing run inside the fetched window (the most recent 100
// completed runs) - the branch has been red for at least that
// long. Report the oldest run seen as a lower bound rather than
// paginating further back (the case where paginating would cost
// the most is exactly the chronically-red branch we're trying to
// bound the cost for).
const approximate = passIndex === -1;
const breakRun = approximate ? runs[runs.length - 1] : runs[passIndex - 1];
const failingRuns = approximate ? runs.length : passIndex;
const out = breakRun ? {
breakSha: breakRun.head_sha,
breakRunUrl: breakRun.html_url,
breakRunCreatedAt: breakRun.created_at,
failingRuns,
approximate,
} : {};
process.stdout.write(JSON.stringify(out));
JSEOF
BREAK_SHA=$(jq -r '.breakSha // ""' blame.json)
if [[ -n "$BREAK_SHA" ]]; then
COMMIT=$(gh api "repos/${REPO}/commits/${BREAK_SHA}" 2>/dev/null || echo "")
if [[ -n "$COMMIT" ]]; then
echo "$COMMIT" | jq \
--slurpfile blame blame.json \
'$blame[0] + {
breakAuthor: (if .author.login then "@" + .author.login else .commit.author.name end),
breakMessage: (.commit.message | split("\n")[0]),
breakCommitDate: .commit.author.date,
breakCommitUrl: .html_url
}' > blame-merged.json
mv blame-merged.json blame.json
echo "Broke at ${BREAK_SHA:0:7} ($(jq -r '.breakAuthor' blame.json)): $(jq -r '.breakMessage' blame.json)"
else
echo "Could not look up commit ${BREAK_SHA} for author/message details."
fi
fi
fi
SAFE=$(echo "${REPO}-${BRANCH}" | tr '/' '-')
echo "safe-name=${SAFE}" >> "$GITHUB_OUTPUT"
jq -n \
--arg project "$PROJECT" \
--arg repo "$REPO" \
--arg type "$TYPE" \
--arg branch "$BRANCH" \
--arg workflowFile "$WORKFLOW_FILE" \
--arg status "$STATUS" \
--arg conclusion "$CONCLUSION" \
--arg url "$URL" \
--arg runNumber "$RUN_NUMBER" \
--arg createdAt "$CREATED_AT" \
--arg runId "$RUN_ID" \
--arg runAttempt "$RUN_ATTEMPT" \
--argjson rerunTriggered "$RERUN_TRIGGERED" \
'{project: $project, repo: $repo, type: $type, branch: $branch, workflowFile: $workflowFile, status: $status, conclusion: $conclusion, url: $url, runNumber: $runNumber, createdAt: $createdAt, runId: $runId, runAttempt: $runAttempt, rerunTriggered: $rerunTriggered}' \
> base.json
jq -s '.[0] * .[1]' base.json blame.json > "result-${SAFE}.json"
- name: Upload result
if: always()
uses: actions/upload-artifact@v4
with:
name: result-${{ steps.check.outputs.safe-name }}
path: result-${{ steps.check.outputs.safe-name }}.json
summary:
name: Summary
needs: [setup, check]
runs-on: ubuntu-latest
if: always()
steps:
- name: Download results
uses: actions/download-artifact@v4
with:
pattern: result-*
merge-multiple: true
path: results
- name: Write summary
id: write-summary
env:
PHASE: ${{ needs.setup.outputs.phase }}
run: |
node - << 'JSEOF'
const fs = require('fs');
const phase = process.env.PHASE;
let results = [];
try {
results = fs.readdirSync('results')
.filter(f => f.endsWith('.json'))
.map(f => JSON.parse(fs.readFileSync(`results/${f}`, 'utf8')))
.sort((a, b) =>
a.project.localeCompare(b.project) ||
a.type.localeCompare(b.type) ||
a.branch.localeCompare(b.branch));
} catch (err) {
console.log('No results to summarize.');
}
// GitHub increments run_attempt in place when a run's failed jobs
// are rerun (rerun-failed-jobs doesn't create a new run) - so a
// report-phase scan can tell "this was retried" straight from the
// same run object it already fetches, with no state carried over
// from the earlier detect-and-rerun run.
const wasRerun = r => Number(r.runAttempt || 1) > 1;
const icon = r => {
if (r.status === 'not-found') return '❔';
if (r.status !== 'completed') return wasRerun(r) ? '⏳' : '🔄';
if (r.conclusion === 'success') return wasRerun(r) ? '🔁✅' : '✅';
if (r.conclusion === 'failure') return '❌';
if (r.conclusion === 'cancelled') return '⚠️';
return '❔';
};
const label = r => {
if (r.status === 'not-found') return 'no runs found';
if (r.status !== 'completed') return wasRerun(r) ? 'rerun still running' : r.status;
if (r.conclusion === 'success') return wasRerun(r) ? 'recovered after rerun' : 'success';
if (r.conclusion === 'failure') return wasRerun(r) ? 'still failing after rerun' : 'failure';
return r.conclusion;
};
// Only failing branches carry breakSha etc. (the check job only
// spends the extra API calls on those - see ci-status-report.yml).
const daysSince = iso => {
if (!iso) return null;
return Math.max(0, Math.floor((Date.now() - new Date(iso).getTime()) / 86400000));
};
const blameFacts = r => {
if (!r.breakSha) return null;
const days = daysSince(r.breakRunCreatedAt);
return {
since: r.breakRunCreatedAt ? r.breakRunCreatedAt.slice(0, 10) : 'unknown',
daysLabel: days === null ? '' : `${days} day${days === 1 ? '' : 's'}`,
runsLabel: `${r.failingRuns}${r.approximate ? '+' : ''} run${r.failingRuns === 1 && !r.approximate ? '' : 's'}`,
sha: r.breakSha.slice(0, 7),
author: r.breakAuthor || 'unknown author',
message: r.breakMessage || '(no commit message)',
commitUrl: r.breakCommitUrl || '',
};
};
// GitHub-flavored markdown, for the job summary.
const blameLineMd = r => {
const f = blameFacts(r);
if (!f) return null;
const commitLink = f.commitUrl ? `[\`${f.sha}\`](${f.commitUrl})` : `\`${f.sha}\``;
return `Failing since **${f.since}** (${f.daysLabel}, ${f.runsLabel}) — broke at ${commitLink} by **${f.author}**: "${f.message}"`;
};
// Google Chat uses its own lightweight formatting (single-asterisk
// bold, <url|text> links) rather than GitHub markdown, so it gets
// a separate, plainer renderer.
const blameLineChat = r => {
const f = blameFacts(r);
if (!f) return null;
const shaLink = f.commitUrl ? `<${f.commitUrl}|${f.sha}>` : f.sha;
return `since ${f.since} (${f.daysLabel}, ${f.runsLabel}) - broke at ${shaLink} by ${f.author}: "${f.message}"`;
};
const lines = [];
lines.push('## CI Status Report');
if (phase === 'detect-and-rerun') {
lines.push('');
lines.push('_Detect-and-rerun pass - reruns were triggered for any failures below;_ ' +
'_the follow-up report run in ~2 hours posts to Chat with the outcome._');
}
lines.push('');
lines.push('| | Project | Type | Branch | Workflow | Status | Run |');
lines.push('|---|---|---|---|---|---|---|');
for (const r of results) {
const run = r.url ? `[#${r.runNumber}](${r.url})` : '-';
lines.push(`| ${icon(r)} | \`${r.project}\` | ${r.type} | \`${r.branch}\` | ` +
`${r.workflowFile || '-'} | ${label(r)} | ${run} |`);
}
lines.push('');
const passing = results.filter(r => r.conclusion === 'success' && !wasRerun(r));
const recovered = results.filter(r => r.conclusion === 'success' && wasRerun(r));
const failing = results.filter(r => r.conclusion === 'failure');
const stillFailing = failing.filter(wasRerun);
const newFailing = failing.filter(r => !wasRerun(r));
const notFound = results.filter(r => r.status === 'not-found');
lines.push(`**${results.length}** branches checked — ` +
`**${passing.length}** passing, ` +
`**${recovered.length}** recovered after rerun, ` +
`**${failing.length}** failing, ` +
`**${notFound.length}** with no CI runs found.`);
if (stillFailing.length) {
lines.push('');
lines.push('### Still failing after rerun');
lines.push('');
for (const r of stillFailing) {
lines.push(`- \`${r.repo}\`@\`${r.branch}\` — [run #${r.runNumber}](${r.url})`);
const blame = blameLineMd(r);
if (blame) lines.push(` - ${blame}`);
}
}
if (newFailing.length) {
lines.push('');
lines.push('### New failures (no rerun attempted yet)');
lines.push('');
for (const r of newFailing) {
lines.push(`- \`${r.repo}\`@\`${r.branch}\` — [run #${r.runNumber}](${r.url})`);
const blame = blameLineMd(r);
if (blame) lines.push(` - ${blame}`);
}
}
if (recovered.length) {
lines.push('');
lines.push('### Recovered after rerun');
lines.push('');
for (const r of recovered) {
lines.push(`- \`${r.repo}\`@\`${r.branch}\` — [run #${r.runNumber}](${r.url})`);
}
}
if (notFound.length) {
lines.push('');
lines.push('### No CI runs found');
lines.push('');
for (const r of notFound) {
lines.push(`- \`${r.repo}\`@\`${r.branch}\``);
}
}
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, lines.join('\n') + '\n');
console.log(lines.join('\n'));
// Surface counts (and the per-branch details, for the chat
// notification step below) without ever failing this job - the
// report itself succeeding is independent of what it reports.
// Nothing here is phase-gated: on a detect-and-rerun run these
// outputs are simply unused, since that phase never posts to Chat.
fs.appendFileSync(process.env.GITHUB_OUTPUT, `total=${results.length}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `passing=${passing.length}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `recovered=${recovered.length}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `failing=${failing.length}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `not-found=${notFound.length}\n`);
const failingDetails = failing.map(r => {
const repo = `*${r.repo}*`;
const runLink = r.url ? ` (<${r.url}|run #${r.runNumber}>)` : '';
const retried = wasRerun(r) ? ' [still failing after rerun]' : ' [new, no rerun attempted]';
const blame = blameLineChat(r);
return blame
? `${repo}@${r.branch}${runLink}${retried} — ${blame}`
: `${repo}@${r.branch}${runLink}${retried}`;
}).join('\n');
const recoveredDetails = recovered.map(r => {
const repo = `*${r.repo}*`;
const runLink = r.url ? ` (<${r.url}|run #${r.runNumber}>)` : '';
return `${repo}@${r.branch}${runLink}`;
}).join('\n');
// Multiline GITHUB_OUTPUT values need the <<delimiter heredoc form
// rather than a plain key=value line.
const failingDelimiter = `ghadelim_${Date.now()}_failing`;
fs.appendFileSync(process.env.GITHUB_OUTPUT,
`failing-details<<${failingDelimiter}\n${failingDetails}\n${failingDelimiter}\n`);
const recoveredDelimiter = `ghadelim_${Date.now()}_recovered`;
fs.appendFileSync(process.env.GITHUB_OUTPUT,
`recovered-details<<${recoveredDelimiter}\n${recoveredDetails}\n${recoveredDelimiter}\n`);
JSEOF
- name: Send Google Chat notification
# Only the report phase (~6:07am) posts. The detect-and-rerun phase
# (~4am) has already triggered reruns for anything failing above and
# deliberately stays quiet - this run, ~2 hours later, is the one
# that reflects their outcome.
if: needs.setup.outputs.phase == 'report'
env:
WEBHOOK_URL: ${{ secrets.SPRING_CLOUD_CORE_CI_GCHAT_WEBHOOK_URL }}
TOTAL: ${{ steps.write-summary.outputs.total }}
PASSING: ${{ steps.write-summary.outputs.passing }}
RECOVERED: ${{ steps.write-summary.outputs.recovered }}
FAILING: ${{ steps.write-summary.outputs.failing }}
NOT_FOUND: ${{ steps.write-summary.outputs.not-found }}
FAILING_DETAILS: ${{ steps.write-summary.outputs.failing-details }}
RECOVERED_DETAILS: ${{ steps.write-summary.outputs.recovered-details }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [[ -z "${WEBHOOK_URL}" ]]; then
echo "SPRING_CLOUD_CORE_CI_GCHAT_WEBHOOK_URL is not set - skipping Google Chat notification."
exit 0
fi
ICON="✅"
if [[ "${FAILING:-0}" != "0" ]]; then
ICON="❌"
elif [[ "${NOT_FOUND:-0}" != "0" ]]; then
ICON="❔"
fi
TEXT="${ICON} *CI Status Report* — ${PASSING:-0} passing, ${RECOVERED:-0} recovered after rerun, ${FAILING:-0} failing, ${NOT_FOUND:-0} not found (of ${TOTAL:-0} branches checked)"
if [[ -n "${RECOVERED_DETAILS:-}" ]]; then
TEXT=$(printf '%s\n\nRecovered after rerun (was flaky, passed on retry):\n%s' "$TEXT" "$RECOVERED_DETAILS")
fi
if [[ -n "${FAILING_DETAILS:-}" ]]; then
TEXT=$(printf '%s\n\nFailing:\n%s' "$TEXT" "$FAILING_DETAILS")
fi
TEXT=$(printf '%s\n\n<%s|View full report>' "$TEXT" "$RUN_URL")
jq -n --arg text "$TEXT" '{text: $text}' > chat-message.json
curl --fail --silent --show-error \
-X POST \
-H 'Content-Type: application/json; charset=UTF-8' \
-d @chat-message.json \
"${WEBHOOK_URL}"