Skip to content

Commit 0050824

Browse files
Merge pull request #2 from MarketDataApp/01_endpoint_v1_markets_status
implement /v1/markets/status/ endpoint + integration testing pipeline
2 parents 557cf8e + 63fc0a6 commit 0050824

39 files changed

Lines changed: 3209 additions & 92 deletions

.github/workflows/main.yml

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,19 @@ jobs:
3434
- name: Checkout
3535
uses: actions/checkout@v4
3636

37-
# Install both JDK 17 (for compilation) and the matrix JDK (for
38-
# test execution). setup-java exports JAVA_HOME_<version>_<arch>;
39-
# Gradle's toolchain auto-detection picks them up.
40-
- name: Set up JDKs (compile=17, test=${{ matrix.java }})
37+
# Install both the matrix JDK (test execution) and JDK 17 (compile +
38+
# Gradle daemon runtime). Order matters: setup-java sets JAVA_HOME
39+
# to the LAST entry, and Gradle 8.12 only supports JDKs up to 23 as
40+
# its own runtime — so JDK 17 must be last for matrix.java = 25.
41+
# setup-java still exports JAVA_HOME_<version>_<arch> for both, and
42+
# setup-gradle registers them as toolchain candidates.
43+
- name: Set up JDKs (test=${{ matrix.java }}, compile/daemon=17)
4144
uses: actions/setup-java@v4
4245
with:
4346
distribution: temurin
4447
java-version: |
45-
17
4648
${{ matrix.java }}
49+
17
4750
4851
- name: Set up Gradle
4952
uses: gradle/actions/setup-gradle@v4
@@ -72,8 +75,50 @@ jobs:
7275
files: build/reports/jacoco/test/jacocoTestReport.xml
7376
fail_ci_if_error: true
7477

75-
# Integration-tests job is intentionally not wired up yet:
76-
# SDK requirements §13 says they run on PRs and release pipelines, but
77-
# they hit the live API and require a MARKETDATA_TOKEN secret. Add this
78-
# job (gated on `if: ${{ secrets.MARKETDATA_TOKEN != '' }}`) once the
79-
# token is configured in the repo's GitHub Actions secrets.
78+
# SDK requirements §13: integration tests run mandatorily on every push
79+
# to main (release-pipeline gate) on the full forward-compat matrix.
80+
# On PRs they're triggered on demand instead — see
81+
# pr-integration-on-demand.yml.
82+
integration-tests:
83+
name: Integration tests (live API, JDK ${{ matrix.java }})
84+
runs-on: ubuntu-latest
85+
needs: verify
86+
strategy:
87+
fail-fast: false
88+
matrix:
89+
java: ['17', '21', '25']
90+
steps:
91+
- name: Checkout
92+
uses: actions/checkout@v4
93+
94+
- name: Set up JDKs (test=${{ matrix.java }}, compile/daemon=17)
95+
uses: actions/setup-java@v4
96+
with:
97+
distribution: temurin
98+
java-version: |
99+
${{ matrix.java }}
100+
17
101+
102+
- name: Set up Gradle
103+
uses: gradle/actions/setup-gradle@v4
104+
105+
- name: Run integration tests against live API
106+
env:
107+
MARKETDATA_TOKEN: ${{ secrets.MARKETDATA_TOKEN }}
108+
MARKETDATA_RUN_INTEGRATION_TESTS: 'true'
109+
run: |
110+
if [ -z "$MARKETDATA_TOKEN" ]; then
111+
echo "::error::MARKETDATA_TOKEN secret is required on main; integration tests must run on every merge."
112+
exit 1
113+
fi
114+
./gradlew integrationTest -PtestJdk=${{ matrix.java }} --stacktrace
115+
116+
- name: Upload integration-test reports on failure
117+
if: failure()
118+
uses: actions/upload-artifact@v4
119+
with:
120+
name: integration-test-reports-jdk${{ matrix.java }}
121+
path: |
122+
build/reports/tests/integrationTest/
123+
build/test-results/integrationTest/
124+
retention-days: 14
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
name: Integration tests on demand
2+
3+
# Manually triggered by commenting on an open PR with a slash-command on
4+
# the FIRST line of the comment body (everything after the first line is
5+
# ignored):
6+
# /integrationtest → JDK 17 only
7+
# /integrationtestfull → full matrix {17, 21, 25}
8+
#
9+
# The slash + first-line constraint prevents accidental triggers from
10+
# review comments that mention the workflow by name, quoted replies
11+
# (`> /integrationtest`), pasted documentation, or stack traces. The
12+
# Guard job below filters on `startsWith(... '/integrationtest')` and
13+
# then a strict bash `case` validates the exact command — anything that
14+
# slips through is rejected before any live-API request is made.
15+
#
16+
# Integration tests hit the live Market Data API, so we don't run them
17+
# automatically on every PR open/sync (saves API quota + CI minutes).
18+
# They ARE required for merge — branch protection on `main` should list
19+
# "Integration tests pass" as a required status check, which is the
20+
# aggregator job below. PRs cannot merge until a reviewer comments one
21+
# of the two slash-commands AND the resulting run is green.
22+
#
23+
# Important security note: workflows triggered by `issue_comment` always
24+
# run from the *default branch's* version of the workflow file, not from
25+
# the PR. Adding/changing this file on a feature branch has no effect
26+
# until it lands on main.
27+
on:
28+
issue_comment:
29+
types: [created]
30+
31+
permissions:
32+
contents: read
33+
pull-requests: write # to react and post the result comment
34+
35+
# Multiple trigger comments on the same PR cancel earlier runs.
36+
concurrency:
37+
group: pr-integration-on-demand-${{ github.event.issue.number }}
38+
cancel-in-progress: true
39+
40+
jobs:
41+
guard:
42+
name: Guard
43+
runs-on: ubuntu-latest
44+
# Only fire on PR comments whose body starts with `/integrationtest`.
45+
# `startsWith` rejects comments that merely mention the command in
46+
# passing (quoted replies start with `>`, prose with anything else,
47+
# so they don't match). The strict `case` in the matrix step below
48+
# rejects anything that slips through (e.g. `/integrationtest-foo`)
49+
# before any live-API request fires.
50+
if: |
51+
github.event.issue.pull_request != null &&
52+
startsWith(github.event.comment.body, '/integrationtest')
53+
outputs:
54+
head_sha: ${{ steps.pr.outputs.head_sha }}
55+
jdks: ${{ steps.matrix.outputs.jdks }}
56+
mode: ${{ steps.matrix.outputs.mode }}
57+
steps:
58+
- name: Verify commenter has write permission
59+
uses: actions/github-script@v7
60+
with:
61+
script: |
62+
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
63+
owner: context.repo.owner,
64+
repo: context.repo.repo,
65+
username: context.payload.comment.user.login,
66+
});
67+
const allowed = ['write', 'maintain', 'admin'].includes(perm.permission);
68+
if (!allowed) {
69+
core.setFailed(
70+
`@${context.payload.comment.user.login} (${perm.permission}) ` +
71+
`cannot trigger integration tests; write access required.`
72+
);
73+
}
74+
75+
- name: React 👀 to the trigger comment
76+
uses: actions/github-script@v7
77+
with:
78+
script: |
79+
await github.rest.reactions.createForIssueComment({
80+
owner: context.repo.owner,
81+
repo: context.repo.repo,
82+
comment_id: context.payload.comment.id,
83+
content: 'eyes',
84+
});
85+
86+
- name: Resolve PR head SHA
87+
id: pr
88+
uses: actions/github-script@v7
89+
with:
90+
script: |
91+
const { data: pr } = await github.rest.pulls.get({
92+
owner: context.repo.owner,
93+
repo: context.repo.repo,
94+
pull_number: context.payload.issue.number,
95+
});
96+
if (pr.state !== 'open') {
97+
core.setFailed(`PR #${pr.number} is ${pr.state}; refusing to run.`);
98+
return;
99+
}
100+
core.setOutput('head_sha', pr.head.sha);
101+
102+
- name: Decide JDK matrix from comment body
103+
id: matrix
104+
env:
105+
BODY: ${{ github.event.comment.body }}
106+
run: |
107+
# The if: filter above only guarantees the body starts with
108+
# '/integrationtest'. We still need to disambiguate single vs
109+
# full and reject anything that just shares the prefix
110+
# (e.g. '/integrationtest-foo' or '/integrationtestlong').
111+
# Match on the first line only — trailing context in the
112+
# comment body is ignored.
113+
first_line=$(printf '%s' "$BODY" | head -n 1 | tr -d '[:space:]')
114+
case "$first_line" in
115+
/integrationtest)
116+
echo 'jdks=["17"]' >> "$GITHUB_OUTPUT"
117+
echo 'mode=single' >> "$GITHUB_OUTPUT"
118+
echo "Trigger: /integrationtest → JDK 17"
119+
;;
120+
/integrationtestfull)
121+
echo 'jdks=["17","21","25"]' >> "$GITHUB_OUTPUT"
122+
echo 'mode=full' >> "$GITHUB_OUTPUT"
123+
echo "Trigger: /integrationtestfull → matrix {17, 21, 25}"
124+
;;
125+
*)
126+
echo "::error::Unrecognized command on first line: '$first_line' (expected '/integrationtest' or '/integrationtestfull')"
127+
exit 1
128+
;;
129+
esac
130+
131+
integration-tests:
132+
name: Integration tests (JDK ${{ matrix.java }})
133+
needs: guard
134+
runs-on: ubuntu-latest
135+
strategy:
136+
# Don't cancel siblings: if JDK 21 fails, we still want 17 and 25
137+
# results to surface.
138+
fail-fast: false
139+
matrix:
140+
java: ${{ fromJSON(needs.guard.outputs.jdks) }}
141+
142+
steps:
143+
# Check out exactly the PR's HEAD commit so we test the proposed
144+
# change, not the merge ref.
145+
- name: Checkout PR head
146+
uses: actions/checkout@v4
147+
with:
148+
ref: ${{ needs.guard.outputs.head_sha }}
149+
150+
# Order matters: JDK 17 must be last so JAVA_HOME=17 (Gradle 8.12
151+
# only supports JDKs up to 23 as its daemon runtime). The matrix
152+
# JDK is still installed and registered as a toolchain target.
153+
- name: Set up JDKs (test=${{ matrix.java }}, compile/daemon=17)
154+
uses: actions/setup-java@v4
155+
with:
156+
distribution: temurin
157+
java-version: |
158+
${{ matrix.java }}
159+
17
160+
161+
- name: Set up Gradle
162+
uses: gradle/actions/setup-gradle@v4
163+
164+
- name: Run integration tests against live API
165+
env:
166+
MARKETDATA_TOKEN: ${{ secrets.MARKETDATA_TOKEN }}
167+
MARKETDATA_RUN_INTEGRATION_TESTS: 'true'
168+
run: |
169+
if [ -z "$MARKETDATA_TOKEN" ]; then
170+
echo "::error::MARKETDATA_TOKEN secret missing — cannot run integration tests."
171+
exit 1
172+
fi
173+
./gradlew integrationTest -PtestJdk=${{ matrix.java }} --stacktrace
174+
175+
- name: Upload integration-test reports on failure
176+
if: failure()
177+
uses: actions/upload-artifact@v4
178+
with:
179+
name: integration-test-reports-jdk${{ matrix.java }}
180+
path: |
181+
build/reports/tests/integrationTest/
182+
build/test-results/integrationTest/
183+
retention-days: 14
184+
185+
# Aggregator job. Branch protection on `main` should require this
186+
# check name ("Integration tests pass") so a single required check
187+
# covers both `integrationtest` (matrix=[17]) and `integrationtestfull`
188+
# (matrix=[17,21,25]) modes uniformly. Without this, branch protection
189+
# would have to list the per-matrix-entry check names which only exist
190+
# in the `full` mode.
191+
required-check:
192+
name: Integration tests pass
193+
needs: [guard, integration-tests]
194+
if: always() && needs.guard.result == 'success'
195+
runs-on: ubuntu-latest
196+
steps:
197+
- name: Aggregate matrix outcome
198+
env:
199+
MATRIX_RESULT: ${{ needs.integration-tests.result }}
200+
MODE: ${{ needs.guard.outputs.mode }}
201+
run: |
202+
echo "Mode: $MODE"
203+
echo "Matrix outcome: $MATRIX_RESULT"
204+
if [[ "$MATRIX_RESULT" != "success" ]]; then
205+
echo "::error::One or more integration-test JDK entries failed."
206+
exit 1
207+
fi
208+
echo "All integration tests passed."
209+
210+
- name: Comment outcome on the PR
211+
if: always()
212+
uses: actions/github-script@v7
213+
with:
214+
script: |
215+
const ok = '${{ needs.integration-tests.result }}' === 'success';
216+
const mode = '${{ needs.guard.outputs.mode }}';
217+
const emoji = ok ? '✅' : '❌';
218+
const status = ok ? 'passed' : 'failed';
219+
const matrix = mode === 'full' ? '`{17, 21, 25}`' : '`17`';
220+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
221+
await github.rest.issues.createComment({
222+
owner: context.repo.owner,
223+
repo: context.repo.repo,
224+
issue_number: context.payload.issue.number,
225+
body: `${emoji} On-demand integration tests on JDK ${matrix} ${status}. [View run](${runUrl}).`,
226+
});

.github/workflows/pr-matrix-on-demand.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,16 @@ jobs:
110110
with:
111111
ref: ${{ needs.guard.outputs.head_sha }}
112112

113-
# Compile=17, test=matrix JDK; same shape as main.yml.
114-
- name: Set up JDKs (compile=17, test=${{ matrix.java }})
113+
# Order matters: JDK 17 must be last so JAVA_HOME=17 (Gradle 8.12
114+
# doesn't support JDK 24+ as its daemon runtime). The matrix JDK
115+
# is still installed and registered as a toolchain target.
116+
- name: Set up JDKs (test=${{ matrix.java }}, compile/daemon=17)
115117
uses: actions/setup-java@v4
116118
with:
117119
distribution: temurin
118120
java-version: |
119-
17
120121
${{ matrix.java }}
122+
17
121123
122124
- name: Set up Gradle
123125
uses: gradle/actions/setup-gradle@v4

.github/workflows/pull-request.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,10 @@ jobs:
6363
token: ${{ secrets.CODECOV_TOKEN }}
6464
files: build/reports/jacoco/test/jacocoTestReport.xml
6565
fail_ci_if_error: true
66+
67+
# NOTE: integration tests are NOT run automatically on PR open/sync.
68+
# They are required for merge but only fire when a reviewer comments
69+
# `integrationtest` (JDK 17) or `integrationtestfull` (matrix 17/21/25)
70+
# — see .github/workflows/pr-integration-on-demand.yml. Branch-protection
71+
# rules should require the "Integration tests pass" check name produced
72+
# by that workflow before allowing merge to main.

0 commit comments

Comments
 (0)