-
Notifications
You must be signed in to change notification settings - Fork 112
245 lines (217 loc) · 10.7 KB
/
Copy pathsync-fork-checks.yml
File metadata and controls
245 lines (217 loc) · 10.7 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
#
# Copyright (c) 2026 SAP SE. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 only, as
# published by the Free Software Foundation. Oracle designates this
# particular file as subject to the "Classpath" exception as provided
# by Oracle in the LICENSE file that accompanied this code.
#
# This code is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# version 2 for more details (a copy is included in the LICENSE file that
# accompanied this code).
#
# You should have received a copy of the GNU General Public License version
# 2 along with this work; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
# or visit www.oracle.com if you need additional information or have any
# questions.
#
# Fires in the upstream repo context when a PR from a fork is opened or updated.
# Polls the fork repo's Actions API for the "SapMachine GHA Sanity Checks"
# run on the PR head commit, then mirrors every individual job as a native
# Check Run on the upstream PR — matching the appearance of the fork's own
# checks tab. No submit branches are created; no CI is re-run upstream.
name: 'Mirror Checks from Source Fork'
on:
pull_request_target:
types:
- opened
- synchronize
- reopened
# Mirror main.yml's paths-ignore: docs-only PRs never trigger fork CI, so
# skipping this workflow avoids polling for a run that will never appear.
paths-ignore:
- '**/*.md'
- 'doc/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
statuses: write
jobs:
mirror:
name: 'Sync Actions from Fork'
runs-on: ubuntu-24.04
# Only act on fork PRs — same-repo PRs get CI directly from main.yml.
if: github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: 'Poll fork CI and mirror per-job results as commit statuses'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
FORK_REPO: ${{ github.event.pull_request.head.repo.full_name }}
UPSTREAM_REPO: ${{ github.repository }}
run: |
# Post (or overwrite) a commit status on the PR head SHA. Statuses are
# keyed purely by SHA + context, so — unlike API-created check runs —
# they reliably re-appear on the PR when it is closed and reopened
# without a new push.
post_status() {
local context="$1" state="$2" desc="$3" url="$4"
gh api -X POST "repos/${UPSTREAM_REPO}/statuses/${HEAD_SHA}" \
-f state="${state}" \
-f context="${context}" \
-f description="${desc:0:140}" \
-f target_url="${url}" >/dev/null
}
# Map a fork job conclusion to a commit-status state.
# Allowed status states: error | failure | pending | success.
map_state() {
case "$1" in
success) echo "success" ;;
failure) echo "failure" ;;
*) echo "error" ;;
esac
}
# ── Phase 1: Wait for the fork workflow run to appear ───────────────
# The fork may not have triggered its CI yet immediately after push.
MAX_WAIT=20
ATTEMPT=0
RUN_ID=""
while [[ $ATTEMPT -lt $MAX_WAIT ]]; do
ATTEMPT=$((ATTEMPT + 1))
echo "Waiting for workflow run on fork (attempt ${ATTEMPT}/${MAX_WAIT})..."
RUN_ID=$(gh api \
"repos/${FORK_REPO}/actions/runs?head_sha=${HEAD_SHA}&per_page=10" \
--jq '.workflow_runs[] | select(.name == "SapMachine GHA Sanity Checks") | .id' \
2>/dev/null | head -1)
if [[ -n "$RUN_ID" && "$RUN_ID" != "null" ]]; then
echo "Found workflow run on fork: ${RUN_ID}"
break
fi
sleep 60
done
if [[ -z "$RUN_ID" || "$RUN_ID" == "null" ]]; then
echo "No workflow run found on fork within ${MAX_WAIT} minutes — giving up."
exit 1
fi
# ── Phase 2: Mirror each fork job as a commit status ───────────────
declare -A JOB_DONE # job name → '1' once finalized
declare -A JOB_PENDING # job name → '1' once a pending status posted
declare -A JOB_URLS # job name → fork job HTML URL
MAX_POLL=180
POLL=0
OVERALL_CONCLUSION=""
# Sync the current fork job states into upstream commit statuses.
sync_jobs() {
local jobs_json job_count i job_name job_status job_conclusion job_url state
jobs_json=$(gh api \
"repos/${FORK_REPO}/actions/runs/${RUN_ID}/jobs?per_page=100" \
2>/dev/null)
[[ -z "$jobs_json" ]] && return
job_count=$(echo "$jobs_json" | jq '.jobs | length')
for i in $(seq 0 $((job_count - 1))); do
job_name=$(echo "$jobs_json" | jq -r ".jobs[$i].name")
job_status=$(echo "$jobs_json" | jq -r ".jobs[$i].status")
job_conclusion=$(echo "$jobs_json" | jq -r ".jobs[$i].conclusion")
job_url=$(echo "$jobs_json" | jq -r ".jobs[$i].html_url")
JOB_URLS[$job_name]="$job_url"
# Never mirror skipped/neutral jobs to the upstream PR. The status
# API cannot delete a status once posted, so the only way to hide
# these is to never post one. Skipped jobs are 'completed' from
# the first poll (they never enter 'in_progress'), so this fires
# before any pending status is created.
if [[ "$job_status" == "completed" ]]; then
case "$job_conclusion" in
skipped|neutral) continue ;;
esac
fi
# Post a pending status the first time we see a job.
if [[ -z "${JOB_PENDING[$job_name]}" && -z "${JOB_DONE[$job_name]}" ]]; then
echo " Pending status: ${job_name}"
post_status "${job_name}" "pending" \
"Fork job in progress…" "${job_url}"
JOB_PENDING[$job_name]=1
fi
# Finalize any job that has completed since the last poll.
if [[ "$job_status" == "completed" && -z "${JOB_DONE[$job_name]}" ]]; then
# Guard against null conclusion (e.g. cancelled mid-queue).
[[ -z "$job_conclusion" || "$job_conclusion" == "null" ]] && job_conclusion="cancelled"
state=$(map_state "$job_conclusion")
echo " ✓ Finalized ${job_name}: ${job_conclusion} → ${state}"
post_status "${job_name}" "${state}" \
"Fork job reported '${job_conclusion}'." "${job_url}"
JOB_DONE[$job_name]=1
fi
done
}
while [[ $POLL -lt $MAX_POLL ]]; do
POLL=$((POLL + 1))
echo "Poll ${POLL}/${MAX_POLL} — syncing fork job statuses..."
sync_jobs
# Check whether the overall run has finished.
RUN_JSON=$(gh api \
"repos/${FORK_REPO}/actions/runs/${RUN_ID}" \
--jq '{status: .status, conclusion: .conclusion}' \
2>/dev/null)
RUN_STATUS=$(echo "$RUN_JSON" | jq -r '.status')
OVERALL_CONCLUSION=$(echo "$RUN_JSON" | jq -r '.conclusion')
if [[ "$RUN_STATUS" == "completed" ]]; then
echo "Workflow run on fork completed with conclusion: ${OVERALL_CONCLUSION}"
# Do one final job sync to catch jobs that finished in this last window.
sync_jobs
break
fi
sleep 60
done
# ── Phase 3: Handle timeout ─────────────────────────────────────────
if [[ -z "$OVERALL_CONCLUSION" || "$OVERALL_CONCLUSION" == "null" ]]; then
OVERALL_CONCLUSION="timed_out"
echo "Timed out — marking unfinished jobs as errored."
for JOB_NAME in "${!JOB_PENDING[@]}"; do
if [[ -z "${JOB_DONE[$JOB_NAME]}" ]]; then
post_status "${JOB_NAME}" "error" \
"Polling window expired before the fork job completed." \
"${JOB_URLS[$JOB_NAME]}"
fi
done
fi
# Exit non-zero so the upstream PR shows a red check if CI did not pass.
if [[ "$OVERALL_CONCLUSION" != "success" ]]; then
exit 1
fi
# Runs only when the workflow is cancelled — either superseded by the
# concurrency group (a new push/reopen for the same PR) or cancelled
# manually by a maintainer from the Actions UI. The poll step above is
# killed abruptly and never reaches its cleanup, so any statuses it left
# as 'pending' would otherwise block the PR forever. This step re-reads
# the current statuses on the head SHA and flips any that are still
# 'pending' to 'error'.
- name: 'Mark unfinished mirrored statuses as errored on cancellation'
if: cancelled()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
UPSTREAM_REPO: ${{ github.repository }}
run: |
echo "Run cancelled — marking any pending mirrored statuses as errored."
# The combined status endpoint returns the latest status per context.
# Carry over each pending status's target_url so the "Details" link
# back to the fork job is preserved on the errored status.
gh api "repos/${UPSTREAM_REPO}/commits/${HEAD_SHA}/status" \
--jq '.statuses[] | select(.state == "pending") | [.context, .target_url] | @tsv' \
2>/dev/null | while IFS=$'\t' read -r context url; do
[[ -z "$context" ]] && continue
echo " Marking cancelled: ${context}"
gh api -X POST "repos/${UPSTREAM_REPO}/statuses/${HEAD_SHA}" \
-f state="error" \
-f context="${context}" \
-f description="Mirror run was cancelled before this job completed." \
-f target_url="${url}" >/dev/null
done