-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathupsert-ci-status.sh
More file actions
executable file
·153 lines (134 loc) · 6.54 KB
/
Copy pathupsert-ci-status.sh
File metadata and controls
executable file
·153 lines (134 loc) · 6.54 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
#!/usr/bin/env bash
# Create or update the single unified CI status comment on a PR, replacing only
# one section (build | lint | tests). All three CI comment workflows call this
# through the ci-status-comment composite action, so the three separate bot
# comments collapse into one.
#
# The comment is keyed by the hidden <!-- ci-status --> marker and holds three
# sections, each fenced by its own start/end markers:
#
# <!-- ci-status -->
# ### 🚦 CI Status
# <!-- ci:build:start --> …build… <!-- ci:build:end -->
# <!-- ci:lint:start --> …lint… <!-- ci:lint:end -->
# <!-- ci:tests:start --> …tests… <!-- ci:tests:end -->
#
# Build and Unity Test run as independent workflows whose comment writers can
# fire at the same time, so a plain read-modify-write would drop a section or
# create a duplicate comment. Each attempt collapses any duplicates (keeping the
# oldest), rewrites only its own section on that comment, then re-reads to
# confirm the section landed and no duplicate slipped in — retrying otherwise.
set -euo pipefail
MARKER="<!-- ci-status -->"
HEADER="### 🚦 CI Status"
BOT="github-actions[bot]"
START="<!-- ci:${SECTION}:start -->"
END="<!-- ci:${SECTION}:end -->"
# Neutral "waiting" placeholder for a section that has not reported yet. Used
# only when seeding a brand-new comment; a real run always overwrites its own.
section_default() {
case "$1" in
build) printf '\n\n_Waiting for the build to start…_' ;;
lint) printf '\n\n_Waiting for lint to start…_' ;;
tests) printf '\n\n_Waiting for tests to start…_' ;;
esac
}
# One section, fenced by its start/end markers.
wrap_section() { printf '<!-- ci:%s:start -->\n%s\n<!-- ci:%s:end -->' "$1" "$2" "$1"; }
# A fresh comment with every section defaulted to "waiting".
skeleton() {
printf '%s\n%s\n\n%s\n\n%s\n\n%s\n' \
"$MARKER" "$HEADER" \
"$(wrap_section build "$(section_default build)")" \
"$(wrap_section lint "$(section_default lint)")" \
"$(wrap_section tests "$(section_default tests)")"
}
# Emit the section body for this run to a file so awk can splice it verbatim,
# free of shell quoting concerns. Parts of the body (lint findings, failed test
# names) originate in the untrusted pull_request job, so drop any line shaped
# like a section marker before writing it — a body line must never open or close
# a section fence, or it would scramble the comment structure / wedge the survive
# check below.
printf '%s\n' "$SECTION_BODY" \
| grep -vE '^[[:space:]]*<!-- ci[-:][^>]*-->[[:space:]]*$' > section_body.md || true
WANT="$(cat section_body.md)"
# Replace the content between START and END in $1 with section_body.md.
replace_section() {
awk -v s="$START" -v e="$END" -v f="section_body.md" '
$0==s { print; while ((getline line < f) > 0) print line; close(f); skip=1; next }
$0==e { print; skip=0; next }
skip { next }
{ print }
' <<< "$1"
}
# Trimmed content currently between START and END in $1 (for the survive check).
extract_section() {
awk -v s="$START" -v e="$END" '
$0==s { grab=1; next }
$0==e { grab=0; next }
grab { print }
' <<< "$1"
}
# Normalise the comment list to a flat array, whether `--paginate --slurp` hands
# back a flat array of comments or an array of per-page arrays.
flatten_pages() { jq -c '[.[] | if type=="array" then .[] else . end]' <<< "$1"; }
# IDs of every marker-bearing bot comment on the PR, oldest first. Flattened
# first so sort_by orders globally rather than only within a page.
marker_ids() {
jq -r --arg m "$MARKER" --arg bot "$BOT" \
'[.[] | select(.user.login==$bot and (.body|contains($m)))] | sort_by(.id) | .[].id' <<< "$(flatten_pages "$1")"
}
for attempt in 1 2 3 4 5; do
COMMENTS=$(gh api "/repos/$REPO/issues/$PR_NUMBER/comments" --paginate --slurp)
IDS=()
while IFS= read -r line; do [ -n "$line" ] && IDS+=("$line"); done <<< "$(marker_ids "$COMMENTS")"
COMMENT_ID="${IDS[0]:-}"
# Collapse accidental duplicates from a create race: keep the oldest, drop the rest.
if [ "${#IDS[@]}" -gt 1 ]; then
for extra in "${IDS[@]:1}"; do
echo "Deleting duplicate CI status comment $extra."
gh api -X DELETE "/repos/$REPO/issues/comments/$extra" >/dev/null || true
done
fi
if [ -n "$COMMENT_ID" ]; then
CURRENT_BODY=$(jq -r --arg id "$COMMENT_ID" '.[] | select(.id==($id|tonumber)) | .body' <<< "$(flatten_pages "$COMMENTS")")
else
CURRENT_BODY=""
fi
# No unified comment yet, or one missing our section markers: start clean so
# all three sections are always present.
if [ -z "$CURRENT_BODY" ] || ! grep -qF "$START" <<< "$CURRENT_BODY"; then
CURRENT_BODY="$(skeleton)"
fi
NEW_BODY="$(replace_section "$CURRENT_BODY")"
if [ -z "$COMMENT_ID" ]; then
RESULT=$(jq -n --arg b "$NEW_BODY" '{body:$b}' \
| gh api -X POST "/repos/$REPO/issues/$PR_NUMBER/comments" --input -)
COMMENT_ID=$(jq -r '.id' <<< "$RESULT")
else
jq -n --arg b "$NEW_BODY" '{body:$b}' \
| gh api -X PATCH "/repos/$REPO/issues/comments/$COMMENT_ID" --input - >/dev/null
fi
# Re-read and confirm our section landed on the surviving comment, and that no
# concurrent writer left a duplicate behind.
sleep 1
RECHECK=$(gh api "/repos/$REPO/issues/$PR_NUMBER/comments" --paginate --slurp)
RIDS=()
while IFS= read -r line; do [ -n "$line" ] && RIDS+=("$line"); done <<< "$(marker_ids "$RECHECK")"
LIVE_BODY=$(jq -r --arg id "$COMMENT_ID" '.[] | select(.id==($id|tonumber)) | .body' <<< "$(flatten_pages "$RECHECK")")
# Success means our section landed on the comment we wrote — nothing more.
# Duplicate collapsing is best-effort cleanup (the DELETE above may lack
# permission); a duplicate we could not remove must not block reporting that
# already succeeded, or every run would burn all 5 attempts and warn forever.
if [ "$(extract_section "$LIVE_BODY")" = "$WANT" ]; then
echo "CI status '$SECTION' section updated (attempt $attempt)."
if [ "${#RIDS[@]}" -gt 1 ]; then
echo "A duplicate CI status comment remains (could not be deleted); it will be retried next run."
fi
exit 0
fi
echo "Section '$SECTION' not settled (attempt $attempt); retrying."
sleep $((attempt * 2))
done
echo "::warning::Could not confirm the '$SECTION' CI status section after 5 attempts."
exit 0