Skip to content

Commit 82dbb56

Browse files
committed
build: add retry logic and validation to prevent null commit messages in PR merge automation
--- type: pre_commit_static_analysis_report description: Results of running static analysis checks when committing changes. report: - task: lint_filenames status: passed - task: lint_editorconfig status: passed - task: lint_markdown status: na - task: lint_package_json status: na - task: lint_repl_help status: na - task: lint_javascript_src status: na - task: lint_javascript_cli status: na - task: lint_javascript_examples status: na - task: lint_javascript_tests status: na - task: lint_javascript_benchmarks status: na - task: lint_python status: na - task: lint_r status: na - task: lint_c_src status: na - task: lint_c_examples status: na - task: lint_c_benchmarks status: na - task: lint_c_tests_fixtures status: na - task: lint_shell status: na - task: lint_typescript_declarations status: passed - task: lint_typescript_tests status: na - task: lint_license_headers status: passed ---
1 parent d3f8c0e commit 82dbb56

File tree

3 files changed

+150
-50
lines changed

3 files changed

+150
-50
lines changed

.github/workflows/generate_pr_commit_message.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ jobs:
7070
COMMIT_MESSAGE=$($GITHUB_WORKSPACE/.github/workflows/scripts/generate_pr_commit_message/run $PR_NUMBER)
7171
EXIT_CODE=$?
7272
73+
if [ $EXIT_CODE -eq 1 ]; then
74+
echo "::error::Failed to generate commit message for PR #$PR_NUMBER"
75+
exit 1
76+
fi
77+
7378
echo "commit_message<<EOF" >> $GITHUB_OUTPUT
7479
echo "$COMMIT_MESSAGE" >> $GITHUB_OUTPUT
7580
echo "EOF" >> $GITHUB_OUTPUT
@@ -80,9 +85,6 @@ jobs:
8085
echo "has_tracking_issue=false" >> $GITHUB_OUTPUT
8186
fi
8287
83-
# Ensure the script itself doesn't fail the workflow:
84-
exit 0
85-
8688
# Check if the commit message already exists in the PR comments:
8789
- name: 'Check if commit message already exists in PR comments'
8890
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0

.github/workflows/scripts/generate_pr_commit_message/run

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ SUCCESS=0
4848
ERROR=1
4949
TRACKING_ISSUE_FOUND=200
5050

51+
# Configure retries for API calls:
52+
max_retries=3
53+
retry_delay=2
54+
5155

5256
# FUNCTIONS #
5357

@@ -96,7 +100,7 @@ resolve_user() {
96100
fi
97101
}
98102

99-
# Makes GitHub API requests.
103+
# Performs a GitHub API request.
100104
#
101105
# $1 - HTTP method (GET or POST)
102106
# $2 - API endpoint
@@ -105,6 +109,10 @@ github_api() {
105109
local method="$1"
106110
local endpoint="$2"
107111
local data="$3"
112+
local retry_count=0
113+
local response=""
114+
local status_code
115+
local success=false
108116

109117
# Initialize an array to hold curl headers:
110118
local headers=()
@@ -114,40 +122,84 @@ github_api() {
114122
headers+=("-H" "Authorization: token $GITHUB_TOKEN")
115123
fi
116124

117-
# Determine the HTTP method and construct the curl command accordingly...
118-
case "$method" in
119-
GET)
120-
curl -s "${headers[@]}" "$GITHUB_API_URL$endpoint"
121-
;;
122-
POST)
123-
# For POST requests, always set the Content-Type header:
124-
headers+=("-H" "Content-Type: application/json")
125-
126-
# If data is provided, include it in the request:
127-
if [ -n "$data" ]; then
128-
curl -s -X POST "${headers[@]}" -d "$data" "$GITHUB_API_URL$endpoint"
129-
else
130-
# Handle cases where POST data is required but not provided:
131-
echo "POST request requires data."
132-
on_error $ERROR
133-
fi
134-
;;
135-
*)
136-
echo "Invalid HTTP method: $method"
137-
on_error $ERROR
138-
;;
139-
esac
125+
# For POST requests, always set the Content-Type header:
126+
if [ "$method" != "GET" ]; then
127+
headers+=("-H" "Content-Type: application/json")
128+
fi
129+
130+
# Add retry logic...
131+
while [ $retry_count -lt $max_retries ] && [ "$success" = false ]; do
132+
if [ $retry_count -gt 0 ]; then
133+
echo "Retrying request (attempt $((retry_count+1))/${max_retries})..." >&2
134+
sleep $retry_delay
135+
fi
136+
137+
# Make the API request:
138+
if [ -n "${data}" ]; then
139+
response=$(curl -s -w "%{http_code}" -X "${method}" "${headers[@]}" -d "${data}" "${GITHUB_API_URL}${endpoint}")
140+
else
141+
response=$(curl -s -w "%{http_code}" -X "${method}" "${headers[@]}" "${GITHUB_API_URL}${endpoint}")
142+
fi
143+
144+
# Extract status code (last 3 digits) and actual response (everything before):
145+
status_code="${response: -3}"
146+
response="${response:0:${#response}-3}"
147+
148+
# Check if we got a successful response:
149+
if [[ $status_code -ge 200 && $status_code -lt 300 ]]; then
150+
success=true
151+
else
152+
echo "API request failed with status $status_code: $response" >&2
153+
retry_count=$((retry_count+1))
154+
fi
155+
done
156+
157+
if [ "$success" = false ]; then
158+
echo "Failed to complete API request after $max_retries attempts" >&2
159+
return 1
160+
fi
161+
162+
# Validate that response is valid JSON if expected:
163+
if ! echo "$response" | jq -e '.' > /dev/null 2>&1; then
164+
echo "Warning: Response is not valid JSON: ${response}" >&2
165+
# Return empty JSON object as fallback:
166+
echo "{}"
167+
return 0
168+
fi
169+
170+
# Return the actual response data (without status code):
171+
echo "$response"
172+
return 0
140173
}
141174

142175
# Main execution sequence.
143176
main() {
144177
# Fetch pull request details:
145178
pr_details=$(github_api "GET" "/repos/$REPO_OWNER/$REPO_NAME/pulls/$pr_number")
179+
if [ $? -ne 0 ]; then
180+
echo "ERROR: Failed to fetch PR details from GitHub API." >&2
181+
on_error $ERROR
182+
fi
146183
pr_title=$(echo "$pr_details" | jq -r '.title')
147184
pr_body=$(echo "$pr_details" | jq -r '.body // ""')
148185
pr_url=$(echo "$pr_details" | jq -r '.html_url')
149186
pr_author_login=$(echo "$pr_details" | jq -r '.user.login')
150187

188+
# Validate that critical PR fields were successfully retrieved:
189+
if [ -z "$pr_title" ] || [ "$pr_title" = "null" ]; then
190+
echo "ERROR: Failed to retrieve PR title from GitHub API." >&2
191+
echo "API response: $pr_details" >&2
192+
on_error $ERROR
193+
fi
194+
if [ -z "$pr_url" ] || [ "$pr_url" = "null" ]; then
195+
echo "ERROR: Failed to retrieve PR URL from GitHub API." >&2
196+
on_error $ERROR
197+
fi
198+
if [ -z "$pr_author_login" ] || [ "$pr_author_login" = "null" ]; then
199+
echo "ERROR: Failed to retrieve PR author login from GitHub API." >&2
200+
on_error $ERROR
201+
fi
202+
151203
# Resolve the PR author's name and email using .mailmap:
152204
pr_author_resolved=$(resolve_user "$pr_author_login")
153205

.github/workflows/scripts/merge_ready_prs/merge_pr

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ ERROR=1
5252
# Identifier for PR commit message comments:
5353
COMMENT_IDENTIFIER="<!-- PR_COMMIT_MESSAGE -->"
5454

55+
# Configure retries for API calls:
56+
max_retries=3
57+
retry_delay=2
58+
5559

5660
# FUNCTIONS #
5761

@@ -63,7 +67,7 @@ on_error() {
6367
exit ${ERROR}
6468
}
6569

66-
# Makes GitHub API requests.
70+
# Performs a GitHub API request.
6771
#
6872
# $1 - HTTP method
6973
# $2 - API endpoint
@@ -72,6 +76,10 @@ github_api() {
7276
local method="${1}"
7377
local endpoint="${2}"
7478
local data="${3}"
79+
local retry_count=0
80+
local response=""
81+
local status_code
82+
local success=false
7583

7684
# Initialize an array to hold curl headers:
7785
local headers=()
@@ -81,28 +89,54 @@ github_api() {
8189
headers+=("-H" "Authorization: token ${GITHUB_TOKEN}")
8290
fi
8391

84-
# Determine the HTTP method and construct the curl command accordingly:
85-
case "${method}" in
86-
GET)
87-
curl -s "${headers[@]}" "${GITHUB_API_URL}${endpoint}"
88-
;;
89-
POST)
90-
# For POST requests, always set the Content-Type header:
91-
headers+=("-H" "Content-Type: application/json")
92-
93-
# If data is provided, include it in the request:
94-
if [ -n "${data}" ]; then
95-
curl -s -X POST "${headers[@]}" -d "${data}" "${GITHUB_API_URL}${endpoint}"
96-
else
97-
# Handle cases where POST data is required but not provided:
98-
echo "POST request requires data."
99-
on_error "POST request requires data"
100-
fi
101-
;;
102-
*)
103-
on_error "Invalid HTTP method: ${method}"
104-
;;
105-
esac
92+
# For POST requests, always set the Content-Type header:
93+
if [ "${method}" != "GET" ]; then
94+
headers+=("-H" "Content-Type: application/json")
95+
fi
96+
97+
# Add retry logic...
98+
while [ $retry_count -lt $max_retries ] && [ "$success" = false ]; do
99+
if [ $retry_count -gt 0 ]; then
100+
echo "Retrying request (attempt $((retry_count+1))/${max_retries})..." >&2
101+
sleep $retry_delay
102+
fi
103+
104+
# Make the API request:
105+
if [ -n "${data}" ]; then
106+
response=$(curl -s -w "%{http_code}" -X "${method}" "${headers[@]}" -d "${data}" "${GITHUB_API_URL}${endpoint}")
107+
else
108+
response=$(curl -s -w "%{http_code}" -X "${method}" "${headers[@]}" "${GITHUB_API_URL}${endpoint}")
109+
fi
110+
111+
# Extract status code (last 3 digits) and actual response (everything before):
112+
status_code="${response: -3}"
113+
response="${response:0:${#response}-3}"
114+
115+
# Check if we got a successful response:
116+
if [[ $status_code -ge 200 && $status_code -lt 300 ]]; then
117+
success=true
118+
else
119+
echo "API request failed with status $status_code: $response" >&2
120+
retry_count=$((retry_count+1))
121+
fi
122+
done
123+
124+
if [ "$success" = false ]; then
125+
echo "Failed to complete API request after $max_retries attempts" >&2
126+
return 1
127+
fi
128+
129+
# Validate that response is valid JSON if expected:
130+
if ! echo "$response" | jq -e '.' > /dev/null 2>&1; then
131+
echo "Warning: Response is not valid JSON: ${response}" >&2
132+
# Return empty JSON object as fallback:
133+
echo "{}"
134+
return 0
135+
fi
136+
137+
# Return the actual response data (without status code):
138+
echo "$response"
139+
return 0
106140
}
107141

108142
# Main execution sequence.
@@ -120,6 +154,9 @@ main() {
120154

121155
# Fetch PR comments to look for commit message comment:
122156
comments=$(github_api "GET" "/repos/${REPO_OWNER}/${REPO_NAME}/issues/${pr_number}/comments")
157+
if [ $? -ne 0 ]; then
158+
on_error "Failed to fetch comments for PR #${pr_number}"
159+
fi
123160

124161
# Look for the PR commit message comment with the identifier:
125162
commit_message_comment=$(echo "${comments}" | jq -r --arg id "${COMMENT_IDENTIFIER}" '.[] | select(.body | contains($id)) | .body')
@@ -147,6 +184,15 @@ main() {
147184
on_error "Couldn't extract commit body from PR commit message"
148185
fi
149186

187+
# Validate that the commit message does not contain placeholder values
188+
# (e.g., from a failed API response during commit message generation):
189+
if [ "${commit_subject}" = "null" ]; then
190+
on_error "Commit subject is the literal string 'null', indicating a failed commit message generation"
191+
fi
192+
if echo "${commit_body}" | grep -q "^PR-URL: null$"; then
193+
on_error "Commit body contains 'PR-URL: null', indicating a failed commit message generation"
194+
fi
195+
150196
# Squash and merge the PR with the extracted subject and body:
151197
if ! gh pr merge "${pr_number}" --admin --squash --subject "${commit_subject}" --body "${commit_body}"; then
152198
on_error "Failed to merge PR #${pr_number}"

0 commit comments

Comments
 (0)