Repository navigation
270 lines (246 loc) · 11.2 KB
/
Copy pathpull-request.yml
File metadata and controls
270 lines (246 loc) · 11.2 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
name: Tests
# Triggers only on pull request lifecycle events:
# - opened (PR creation)
# - synchronize (push to the PR branch while the PR is open)
# - reopened
# These are the default `pull_request` activity types — listed explicitly
# here for clarity. Pre-PR pushes don't run CI by design (saves minutes
# during early WIP commits).
on:
pull_request:
types: [opened, synchronize, reopened]
branches: ['**']
permissions:
contents: read
# Cancel an in-progress run when a new commit lands on the same PR.
concurrency:
group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
changes:
# A documentation-only pull request does not need the suite below. But a
# required status check can never be conditional -- the list is static -- and
# a matrix job that GitHub SKIPS does not expand its name, so it reports no
# per-combination context at all and the pull request waits forever for
# checks that never arrive. Measured on MarketDataApp/sdk-php#95.
#
# So `Tests passed` at the bottom is the single context branch protection
# requires, and this job decides whether the suite does any work.
name: Decide whether the suite applies
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Detect whether anything but documentation changed
id: filter
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: |
# Only a pull request can be documentation-only here. A push, a release
# and a workflow_call all run the suite.
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "code=true" >> "$GITHUB_OUTPUT"
echo "Not a pull request, so running the suite."
exit 0
fi
# Every path out of here that is not a confident "documentation only"
# says true. A gate that cannot see both ends of the diff must never
# conclude that no code changed.
for sha in "$BASE" "$HEAD"; do
if ! git cat-file -e "$sha^{commit}" 2>/dev/null; then
echo "code=true" >> "$GITHUB_OUTPUT"
echo "Cannot reach $sha, so running the suite rather than guessing."
exit 0
fi
done
# Three dots: the diff from the merge base, not from wherever the base
# branch has moved to since.
files=$(git diff --name-only "$BASE...$HEAD")
echo "changed files:"
printf '%s\n' "$files" | sed 's/^/ /'
# Inverted deliberately. This lists what DOCUMENTATION looks like and
# runs the suite for everything else. Enumerating code paths instead
# would skip the suite the first time somebody adds a source directory
# nobody remembered to list.
if [ -n "$files" ] && ! printf '%s\n' "$files" | grep -qvE '(^docs/|\.md$)'; then
echo "code=false" >> "$GITHUB_OUTPUT"
echo "Documentation only: the suite skips, and Tests passed says so."
else
echo "code=true" >> "$GITHUB_OUTPUT"
echo "Something other than documentation changed: running the suite."
fi
verify:
needs: changes
# `!= 'false'`, not `== 'true'`. A failed gate leaves this empty,
# and the safe reading of empty is RUN THE SUITE.
if: needs.changes.outputs.code != 'false'
name: Verify (JDK 17)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
# PRs only run on JDK 17 (the minimum target). Forward-compat
# regressions on JDK 21/25 are caught post-merge by main.yml.
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '17'
# Validates the wrapper jar hash and caches Gradle home + wrapper
# dists between runs.
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build, test, lint, coverage
run: ./gradlew build --stacktrace
- name: Upload test reports on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-reports
path: |
build/reports/tests/
build/test-results/
retention-days: 14
# Coverage ratchet lives in Codecov: codecov.yml at the repo root
# configures `threshold: 5%` so a PR fails the Codecov status check
# if line coverage drops more than 5 pp vs the base branch.
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: build/reports/jacoco/test/jacocoTestReport.xml
fail_ci_if_error: true
integration:
name: Integration tests (live API, JDK 17)
# SDK requirements §13: the live suite runs on every pull request. JDK 17
# only, like `/integrationtest`; the full {17, 21, 25} matrix stays in
# main.yml and behind `/integrationtestfull`.
#
# `needs: changes` and NOT `needs: verify`. The code review starts when
# this workflow completes and reads the result of this job, so running the
# two side by side is what keeps the wait at the longer of them rather
# than their sum. The live suite takes about two minutes and a build that
# does not compile fails here before a single request is sent, so the
# quota a red `verify` would have saved is small. sdk-py, sdk-csharp,
# sdk-go and sdk-php gate their live job on `changes` alone as well.
needs: changes
# Three conditions, and `Tests passed` below accepts a skip for exactly
# these three and no other:
# 1. the gate did not say "documentation only", so a markdown change
# spends no live API quota. `!= 'false'` so a failed gate still runs;
# 2. the pull request was not opened by dependabot, and
# 3. it does not come from a fork: neither receives repository secrets
# on `pull_request`. They skip this JOB, which shows as "skipped" in
# the checks, not as a green run that tested nothing.
# Clause 2 reads the AUTHOR of the pull request, not `github.actor`, which
# is whoever triggered this run. A maintainer who reopens a dependabot
# pull request becomes the actor, and the run then does receive the
# token, so a test on the actor would send a freshly bumped dependency's
# code to the live API without anybody having decided to. A dependabot
# pull request never runs the suite on its own, whoever triggers the run;
# a maintainer runs it for one, or for a fork, with `/integrationtest`.
# A head repository that has since been deleted reads as empty, which is
# not this repository, so it skips too.
if: >-
needs.changes.outputs.code != 'false' &&
github.event.pull_request.user.login != 'dependabot[bot]' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
# Every test talks to the live API; without a cap one hung request would
# hold the runner until the 6-hour default.
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '17'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
# SDK requirements §17.3: a missing token fails the job, never skips it.
- name: Run integration tests against live API
env:
MARKETDATA_TOKEN: ${{ secrets.MARKETDATA_TOKEN }}
MARKETDATA_RUN_INTEGRATION_TESTS: 'true'
run: |
if [ -z "$MARKETDATA_TOKEN" ]; then
echo "::error::MARKETDATA_TOKEN secret is missing; the live integration suite fails rather than skips (SDK requirements 17.3)."
exit 1
fi
./gradlew integrationTest -PtestJdk=17 --stacktrace
- name: Upload integration-test reports on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: integration-test-reports-jdk17
path: |
build/reports/tests/integrationTest/
build/test-results/integrationTest/
retention-days: 14
tests-passed:
# The single context branch protection requires. It always reports, so it is
# always a decision rather than a silence.
name: Tests passed
needs: [changes, verify, integration]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Require the suite to have passed, or to have been skipped for cause
env:
CODE: ${{ needs.changes.outputs.code }}
R0: ${{ needs.verify.result }}
R1: ${{ needs.integration.result }}
# The negation of the last two clauses of the `integration` job's
# `if:`, evaluated by the same expression engine so the two cannot
# disagree: `true` for a pull request opened by dependabot, whoever
# triggered this run, and for a fork.
NO_SECRETS: ${{ github.event.pull_request.user.login == 'dependabot[bot]' || github.event.pull_request.head.repo.full_name != github.repository }}
run: |
echo "gate said code=${CODE:-<nothing: the gate itself failed>}"
echo "verify: ${R0}"
echo "integration: ${R1} (pull request without secrets: ${NO_SECRETS})"
# Two classes of job, because they skip for different reasons.
#
# STRICT jobs apply to every change, so the only acceptable skip is the
# one this workflow's gate asked for. If the gate failed, CODE is empty,
# the job skipped for some other reason, and a green here would cover
# untested code.
for result in "${R0}"; do
case "$result" in
success) ;;
skipped)
if [ "$CODE" != "false" ]; then
echo "A required job was skipped and the gate did not ask for it."
exit 1
fi
;;
*) echo "A required job reported: $result"; exit 1 ;;
esac
done
# LIVE jobs need the API token, so they have one more acceptable skip:
# a pull request opened by dependabot or coming from a fork, which
# never runs the suite on its own. Nothing else is accepted, and a
# FAILURE always fails: that includes the job refusing to run without
# its token on a pull request that should have had one.
for result in "${R1}"; do
case "$result" in
success) ;;
skipped)
if [ "$CODE" != "false" ] && [ "$NO_SECRETS" != "true" ]; then
echo "The live suite was skipped, and neither the gate nor a pull request without secrets explains it."
exit 1
fi
;;
*) echo "The live suite reported: $result"; exit 1 ;;
esac
done
echo "Every job passed, or skipped for a reason this gate accepts."