Skip to content

Commit c9e4b0a

Browse files
committed
SapMachine #1985: Poll GitHub Actions results from forked repositories as PR checks (#2283)
1 parent b220de7 commit c9e4b0a

2 files changed

Lines changed: 248 additions & 6 deletions

File tree

.github/workflows/main.yml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ on:
3131
branches-ignore:
3232
# SapMachine 2020-11-04: Ignore sapmachine branch
3333
- sapmachine17
34-
# SapMachine 2020-11-04: Trigger on pull request
35-
pull_request:
36-
branches:
37-
- sapmachine17
34+
# SapMachine 2026-06-01: do not trigger github actions for docs files
35+
paths-ignore:
36+
- '**/*.md'
37+
- 'doc/**'
3838
workflow_dispatch:
3939
inputs:
4040
platforms:
@@ -98,8 +98,7 @@ jobs:
9898
function check_platform() {
9999
if [[ $GITHUB_EVENT_NAME == workflow_dispatch ]]; then
100100
input='${{ github.event.inputs.platforms }}'
101-
# SapMachine 2022-06-24: Also handle 'pull_request' event.
102-
elif [[ $GITHUB_EVENT_NAME == push ]] || [[ $GITHUB_EVENT_NAME == pull_request ]]; then
101+
elif [[ $GITHUB_EVENT_NAME == push ]]; then
103102
if [[ '${{ !secrets.JDK_SUBMIT_FILTER || startsWith(github.ref, 'refs/heads/submit/') }}' == 'false' ]]; then
104103
# If JDK_SUBMIT_FILTER is set, and this is not a "submit/" branch, don't run anything
105104
>&2 echo 'JDK_SUBMIT_FILTER is set and not a "submit/" branch'

.github/workflows/pr-check.yml

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
#
2+
# Copyright (c) 2026 SAP SE. All rights reserved.
3+
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
#
5+
# This code is free software; you can redistribute it and/or modify it
6+
# under the terms of the GNU General Public License version 2 only, as
7+
# published by the Free Software Foundation. Oracle designates this
8+
# particular file as subject to the "Classpath" exception as provided
9+
# by Oracle in the LICENSE file that accompanied this code.
10+
#
11+
# This code is distributed in the hope that it will be useful, but WITHOUT
12+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
# version 2 for more details (a copy is included in the LICENSE file that
15+
# accompanied this code).
16+
#
17+
# You should have received a copy of the GNU General Public License version
18+
# 2 along with this work; if not, write to the Free Software Foundation,
19+
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
#
21+
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
# or visit www.oracle.com if you need additional information or have any
23+
# questions.
24+
#
25+
26+
# Fires in the upstream repo context when a fork PR is opened or updated.
27+
# Polls the fork repo's Actions API for the "SapMachine GHA Sanity Checks"
28+
# run on the PR head commit, then mirrors every individual job as a native
29+
# Check Run on the upstream PR — matching the appearance of the fork's own
30+
# checks tab. No submit branches are created; no CI is re-run upstream.
31+
name: 'Pre-submit tests'
32+
33+
on:
34+
pull_request_target:
35+
types:
36+
- opened
37+
- synchronize
38+
- reopened
39+
40+
permissions:
41+
checks: write
42+
pull-requests: read
43+
44+
jobs:
45+
mirror:
46+
name: 'Mirror fork CI result'
47+
runs-on: ubuntu-24.04
48+
# Only act on fork PRs — same-repo PRs get CI directly from main.yml.
49+
if: github.event.pull_request.head.repo.full_name != github.repository
50+
steps:
51+
- name: 'Poll fork CI and mirror per-job results'
52+
env:
53+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
55+
FORK_REPO: ${{ github.event.pull_request.head.repo.full_name }}
56+
UPSTREAM_REPO: ${{ github.repository }}
57+
run: |
58+
# ── Phase 1: Wait for the fork workflow run to appear ───────────────
59+
# The fork may not have triggered its CI yet immediately after push.
60+
MAX_WAIT=20
61+
ATTEMPT=0
62+
RUN_ID=""
63+
64+
while [[ $ATTEMPT -lt $MAX_WAIT ]]; do
65+
ATTEMPT=$((ATTEMPT + 1))
66+
echo "Waiting for fork workflow run (attempt ${ATTEMPT}/${MAX_WAIT})..."
67+
68+
RUN_ID=$(gh api \
69+
"repos/${FORK_REPO}/actions/runs?head_sha=${HEAD_SHA}&per_page=10" \
70+
--jq '.workflow_runs[] | select(.name == "SapMachine GHA Sanity Checks") | .id' \
71+
2>/dev/null | head -1)
72+
73+
if [[ -n "$RUN_ID" && "$RUN_ID" != "null" ]]; then
74+
echo "Found fork workflow run: ${RUN_ID}"
75+
break
76+
fi
77+
78+
sleep 60
79+
done
80+
81+
if [[ -z "$RUN_ID" || "$RUN_ID" == "null" ]]; then
82+
echo "No fork workflow run found within ${MAX_WAIT} minutes — giving up."
83+
exit 1
84+
fi
85+
86+
# ── Phase 2: Mirror each fork job as an upstream Check Run ──────────
87+
# job name → upstream check run ID
88+
declare -A JOB_CHECK_RUN_IDS
89+
# job name → '1' once the check run has been finalized
90+
declare -A JOB_DONE
91+
92+
MAX_POLL=50
93+
POLL=0
94+
OVERALL_CONCLUSION=""
95+
96+
while [[ $POLL -lt $MAX_POLL ]]; do
97+
POLL=$((POLL + 1))
98+
echo "Poll ${POLL}/${MAX_POLL} — syncing fork job statuses..."
99+
100+
JOBS_JSON=$(gh api \
101+
"repos/${FORK_REPO}/actions/runs/${RUN_ID}/jobs?per_page=100" \
102+
2>/dev/null)
103+
104+
if [[ -z "$JOBS_JSON" ]]; then
105+
sleep 60
106+
continue
107+
fi
108+
109+
JOB_COUNT=$(echo "$JOBS_JSON" | jq '.jobs | length')
110+
111+
for i in $(seq 0 $((JOB_COUNT - 1))); do
112+
JOB_NAME=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].name")
113+
JOB_STATUS=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].status")
114+
JOB_CONCLUSION=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].conclusion")
115+
JOB_URL=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].html_url")
116+
117+
# Create a Check Run for this job the first time we see it.
118+
if [[ -z "${JOB_CHECK_RUN_IDS[$JOB_NAME]}" ]]; then
119+
echo " Creating check run: ${JOB_NAME}"
120+
CR_ID=$(gh api \
121+
-X POST \
122+
"repos/${UPSTREAM_REPO}/check-runs" \
123+
-f name="${JOB_NAME}" \
124+
-f head_sha="${HEAD_SHA}" \
125+
-f status="in_progress" \
126+
-f output[title]="Waiting for fork job: ${JOB_NAME}" \
127+
-f output[summary]="Polling fork repository for job '${JOB_NAME}' on commit ${HEAD_SHA}." \
128+
--jq '.id')
129+
JOB_CHECK_RUN_IDS[$JOB_NAME]=$CR_ID
130+
echo " → check run ID: ${CR_ID}"
131+
fi
132+
133+
# Finalize any job that has completed since the last poll.
134+
if [[ "$JOB_STATUS" == "completed" && -z "${JOB_DONE[$JOB_NAME]}" ]]; then
135+
CR_ID="${JOB_CHECK_RUN_IDS[$JOB_NAME]}"
136+
# Guard against null conclusion (e.g. cancelled mid-queue).
137+
[[ -z "$JOB_CONCLUSION" || "$JOB_CONCLUSION" == "null" ]] && JOB_CONCLUSION="cancelled"
138+
139+
if [[ "$JOB_CONCLUSION" == "success" ]]; then
140+
TITLE="Passed: ${JOB_NAME}"
141+
SUMMARY="Job **${JOB_NAME}** passed on the fork. [View job](${JOB_URL})"
142+
else
143+
TITLE="${JOB_CONCLUSION^}: ${JOB_NAME}"
144+
SUMMARY="Job **${JOB_NAME}** reported **${JOB_CONCLUSION}** on the fork. [View job](${JOB_URL})"
145+
fi
146+
147+
gh api \
148+
-X PATCH \
149+
"repos/${UPSTREAM_REPO}/check-runs/${CR_ID}" \
150+
-f status="completed" \
151+
-f conclusion="${JOB_CONCLUSION}" \
152+
-f output[title]="${TITLE}" \
153+
-f output[summary]="${SUMMARY}" \
154+
-f details_url="${JOB_URL}"
155+
156+
JOB_DONE[$JOB_NAME]=1
157+
echo " ✓ Finalized ${JOB_NAME}: ${JOB_CONCLUSION}"
158+
fi
159+
done
160+
161+
# Check whether the overall run has finished.
162+
RUN_JSON=$(gh api \
163+
"repos/${FORK_REPO}/actions/runs/${RUN_ID}" \
164+
--jq '{status: .status, conclusion: .conclusion}' \
165+
2>/dev/null)
166+
167+
RUN_STATUS=$(echo "$RUN_JSON" | jq -r '.status')
168+
OVERALL_CONCLUSION=$(echo "$RUN_JSON" | jq -r '.conclusion')
169+
170+
if [[ "$RUN_STATUS" == "completed" ]]; then
171+
echo "Fork workflow run completed with conclusion: ${OVERALL_CONCLUSION}"
172+
# Do one final job sync to catch any jobs that finished in this last window.
173+
JOBS_JSON=$(gh api \
174+
"repos/${FORK_REPO}/actions/runs/${RUN_ID}/jobs?per_page=100" \
175+
2>/dev/null)
176+
JOB_COUNT=$(echo "$JOBS_JSON" | jq '.jobs | length')
177+
for i in $(seq 0 $((JOB_COUNT - 1))); do
178+
JOB_NAME=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].name")
179+
JOB_STATUS=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].status")
180+
JOB_CONCLUSION=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].conclusion")
181+
JOB_URL=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].html_url")
182+
if [[ -z "${JOB_CHECK_RUN_IDS[$JOB_NAME]}" ]]; then
183+
CR_ID=$(gh api \
184+
-X POST \
185+
"repos/${UPSTREAM_REPO}/check-runs" \
186+
-f name="${JOB_NAME}" \
187+
-f head_sha="${HEAD_SHA}" \
188+
-f status="in_progress" \
189+
-f output[title]="Waiting for fork job: ${JOB_NAME}" \
190+
-f output[summary]="Polling fork repository for job '${JOB_NAME}' on commit ${HEAD_SHA}." \
191+
--jq '.id')
192+
JOB_CHECK_RUN_IDS[$JOB_NAME]=$CR_ID
193+
fi
194+
if [[ "$JOB_STATUS" == "completed" && -z "${JOB_DONE[$JOB_NAME]}" ]]; then
195+
CR_ID="${JOB_CHECK_RUN_IDS[$JOB_NAME]}"
196+
[[ -z "$JOB_CONCLUSION" || "$JOB_CONCLUSION" == "null" ]] && JOB_CONCLUSION="cancelled"
197+
if [[ "$JOB_CONCLUSION" == "success" ]]; then
198+
TITLE="Passed: ${JOB_NAME}"
199+
SUMMARY="Job **${JOB_NAME}** passed on the fork. [View job](${JOB_URL})"
200+
else
201+
TITLE="${JOB_CONCLUSION^}: ${JOB_NAME}"
202+
SUMMARY="Job **${JOB_NAME}** reported **${JOB_CONCLUSION}** on the fork. [View job](${JOB_URL})"
203+
fi
204+
gh api \
205+
-X PATCH \
206+
"repos/${UPSTREAM_REPO}/check-runs/${CR_ID}" \
207+
-f status="completed" \
208+
-f conclusion="${JOB_CONCLUSION}" \
209+
-f output[title]="${TITLE}" \
210+
-f output[summary]="${SUMMARY}" \
211+
-f details_url="${JOB_URL}"
212+
JOB_DONE[$JOB_NAME]=1
213+
echo " ✓ Finalized ${JOB_NAME}: ${JOB_CONCLUSION}"
214+
fi
215+
done
216+
break
217+
fi
218+
219+
sleep 60
220+
done
221+
222+
# ── Phase 3: Handle timeout ─────────────────────────────────────────
223+
if [[ -z "$OVERALL_CONCLUSION" || "$OVERALL_CONCLUSION" == "null" ]]; then
224+
OVERALL_CONCLUSION="timed_out"
225+
echo "Timed out — marking remaining open check runs."
226+
for JOB_NAME in "${!JOB_CHECK_RUN_IDS[@]}"; do
227+
if [[ -z "${JOB_DONE[$JOB_NAME]}" ]]; then
228+
CR_ID="${JOB_CHECK_RUN_IDS[$JOB_NAME]}"
229+
gh api \
230+
-X PATCH \
231+
"repos/${UPSTREAM_REPO}/check-runs/${CR_ID}" \
232+
-f status="completed" \
233+
-f conclusion="timed_out" \
234+
-f output[title]="Timed out: ${JOB_NAME}" \
235+
-f output[summary]="The polling window expired before job '${JOB_NAME}' completed."
236+
fi
237+
done
238+
fi
239+
240+
# Exit non-zero so the upstream PR shows a red check if CI did not pass.
241+
if [[ "$OVERALL_CONCLUSION" != "success" ]]; then
242+
exit 1
243+
fi

0 commit comments

Comments
 (0)