Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 55 additions & 10 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,19 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

# Install both JDK 17 (for compilation) and the matrix JDK (for
# test execution). setup-java exports JAVA_HOME_<version>_<arch>;
# Gradle's toolchain auto-detection picks them up.
- name: Set up JDKs (compile=17, test=${{ matrix.java }})
# Install both the matrix JDK (test execution) and JDK 17 (compile +
# Gradle daemon runtime). Order matters: setup-java sets JAVA_HOME
# to the LAST entry, and Gradle 8.12 only supports JDKs up to 23 as
# its own runtime — so JDK 17 must be last for matrix.java = 25.
# setup-java still exports JAVA_HOME_<version>_<arch> for both, and
# setup-gradle registers them as toolchain candidates.
- name: Set up JDKs (test=${{ matrix.java }}, compile/daemon=17)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: |
17
${{ matrix.java }}
17

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
Expand Down Expand Up @@ -72,8 +75,50 @@ jobs:
files: build/reports/jacoco/test/jacocoTestReport.xml
fail_ci_if_error: true

# Integration-tests job is intentionally not wired up yet:
# SDK requirements §13 says they run on PRs and release pipelines, but
# they hit the live API and require a MARKETDATA_TOKEN secret. Add this
# job (gated on `if: ${{ secrets.MARKETDATA_TOKEN != '' }}`) once the
# token is configured in the repo's GitHub Actions secrets.
# SDK requirements §13: integration tests run mandatorily on every push
# to main (release-pipeline gate) on the full forward-compat matrix.
# On PRs they're triggered on demand instead — see
# pr-integration-on-demand.yml.
integration-tests:
name: Integration tests (live API, JDK ${{ matrix.java }})
runs-on: ubuntu-latest
needs: verify
strategy:
fail-fast: false
matrix:
java: ['17', '21', '25']
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDKs (test=${{ matrix.java }}, compile/daemon=17)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: |
${{ matrix.java }}
17

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4

- 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 required on main; integration tests must run on every merge."
exit 1
fi
./gradlew integrationTest -PtestJdk=${{ matrix.java }} --stacktrace

- name: Upload integration-test reports on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: integration-test-reports-jdk${{ matrix.java }}
path: |
build/reports/tests/integrationTest/
build/test-results/integrationTest/
retention-days: 14
226 changes: 226 additions & 0 deletions .github/workflows/pr-integration-on-demand.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
name: Integration tests on demand

# Manually triggered by commenting on an open PR with a slash-command on
# the FIRST line of the comment body (everything after the first line is
# ignored):
# /integrationtest → JDK 17 only
# /integrationtestfull → full matrix {17, 21, 25}
#
# The slash + first-line constraint prevents accidental triggers from
# review comments that mention the workflow by name, quoted replies
# (`> /integrationtest`), pasted documentation, or stack traces. The
# Guard job below filters on `startsWith(... '/integrationtest')` and
# then a strict bash `case` validates the exact command — anything that
# slips through is rejected before any live-API request is made.
#
# Integration tests hit the live Market Data API, so we don't run them
# automatically on every PR open/sync (saves API quota + CI minutes).
# They ARE required for merge — branch protection on `main` should list
# "Integration tests pass" as a required status check, which is the
# aggregator job below. PRs cannot merge until a reviewer comments one
# of the two slash-commands AND the resulting run is green.
#
# Important security note: workflows triggered by `issue_comment` always
# run from the *default branch's* version of the workflow file, not from
# the PR. Adding/changing this file on a feature branch has no effect
# until it lands on main.
on:
issue_comment:
types: [created]

permissions:
contents: read
pull-requests: write # to react and post the result comment

# Multiple trigger comments on the same PR cancel earlier runs.
concurrency:
group: pr-integration-on-demand-${{ github.event.issue.number }}
cancel-in-progress: true

jobs:
guard:
name: Guard
runs-on: ubuntu-latest
# Only fire on PR comments whose body starts with `/integrationtest`.
# `startsWith` rejects comments that merely mention the command in
# passing (quoted replies start with `>`, prose with anything else,
# so they don't match). The strict `case` in the matrix step below
# rejects anything that slips through (e.g. `/integrationtest-foo`)
# before any live-API request fires.
if: |
github.event.issue.pull_request != null &&
startsWith(github.event.comment.body, '/integrationtest')
outputs:
head_sha: ${{ steps.pr.outputs.head_sha }}
jdks: ${{ steps.matrix.outputs.jdks }}
mode: ${{ steps.matrix.outputs.mode }}
steps:
- name: Verify commenter has write permission
uses: actions/github-script@v7
with:
script: |
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: context.payload.comment.user.login,
});
const allowed = ['write', 'maintain', 'admin'].includes(perm.permission);
if (!allowed) {
core.setFailed(
`@${context.payload.comment.user.login} (${perm.permission}) ` +
`cannot trigger integration tests; write access required.`
);
}

- name: React 👀 to the trigger comment
uses: actions/github-script@v7
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'eyes',
});

- name: Resolve PR head SHA
id: pr
uses: actions/github-script@v7
with:
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.issue.number,
});
if (pr.state !== 'open') {
core.setFailed(`PR #${pr.number} is ${pr.state}; refusing to run.`);
return;
}
core.setOutput('head_sha', pr.head.sha);

- name: Decide JDK matrix from comment body
id: matrix
env:
BODY: ${{ github.event.comment.body }}
run: |
# The if: filter above only guarantees the body starts with
# '/integrationtest'. We still need to disambiguate single vs
# full and reject anything that just shares the prefix
# (e.g. '/integrationtest-foo' or '/integrationtestlong').
# Match on the first line only — trailing context in the
# comment body is ignored.
first_line=$(printf '%s' "$BODY" | head -n 1 | tr -d '[:space:]')
case "$first_line" in
/integrationtest)
echo 'jdks=["17"]' >> "$GITHUB_OUTPUT"
echo 'mode=single' >> "$GITHUB_OUTPUT"
echo "Trigger: /integrationtest → JDK 17"
;;
/integrationtestfull)
echo 'jdks=["17","21","25"]' >> "$GITHUB_OUTPUT"
echo 'mode=full' >> "$GITHUB_OUTPUT"
echo "Trigger: /integrationtestfull → matrix {17, 21, 25}"
;;
*)
echo "::error::Unrecognized command on first line: '$first_line' (expected '/integrationtest' or '/integrationtestfull')"
exit 1
;;
esac

integration-tests:
name: Integration tests (JDK ${{ matrix.java }})
needs: guard
runs-on: ubuntu-latest
strategy:
# Don't cancel siblings: if JDK 21 fails, we still want 17 and 25
# results to surface.
fail-fast: false
matrix:
java: ${{ fromJSON(needs.guard.outputs.jdks) }}

steps:
# Check out exactly the PR's HEAD commit so we test the proposed
# change, not the merge ref.
- name: Checkout PR head
uses: actions/checkout@v4
with:
ref: ${{ needs.guard.outputs.head_sha }}

# Order matters: JDK 17 must be last so JAVA_HOME=17 (Gradle 8.12
# only supports JDKs up to 23 as its daemon runtime). The matrix
# JDK is still installed and registered as a toolchain target.
- name: Set up JDKs (test=${{ matrix.java }}, compile/daemon=17)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: |
${{ matrix.java }}
17

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4

- 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 missing — cannot run integration tests."
exit 1
fi
./gradlew integrationTest -PtestJdk=${{ matrix.java }} --stacktrace

- name: Upload integration-test reports on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: integration-test-reports-jdk${{ matrix.java }}
path: |
build/reports/tests/integrationTest/
build/test-results/integrationTest/
retention-days: 14

# Aggregator job. Branch protection on `main` should require this
# check name ("Integration tests pass") so a single required check
# covers both `integrationtest` (matrix=[17]) and `integrationtestfull`
# (matrix=[17,21,25]) modes uniformly. Without this, branch protection
# would have to list the per-matrix-entry check names which only exist
# in the `full` mode.
required-check:
name: Integration tests pass
needs: [guard, integration-tests]
if: always() && needs.guard.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Aggregate matrix outcome
env:
MATRIX_RESULT: ${{ needs.integration-tests.result }}
MODE: ${{ needs.guard.outputs.mode }}
run: |
echo "Mode: $MODE"
echo "Matrix outcome: $MATRIX_RESULT"
if [[ "$MATRIX_RESULT" != "success" ]]; then
echo "::error::One or more integration-test JDK entries failed."
exit 1
fi
echo "All integration tests passed."

- name: Comment outcome on the PR
if: always()
uses: actions/github-script@v7
with:
script: |
const ok = '${{ needs.integration-tests.result }}' === 'success';
const mode = '${{ needs.guard.outputs.mode }}';
const emoji = ok ? '✅' : '❌';
const status = ok ? 'passed' : 'failed';
const matrix = mode === 'full' ? '`{17, 21, 25}`' : '`17`';
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
body: `${emoji} On-demand integration tests on JDK ${matrix} ${status}. [View run](${runUrl}).`,
});
8 changes: 5 additions & 3 deletions .github/workflows/pr-matrix-on-demand.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,16 @@ jobs:
with:
ref: ${{ needs.guard.outputs.head_sha }}

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

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,10 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
files: build/reports/jacoco/test/jacocoTestReport.xml
fail_ci_if_error: true

# NOTE: integration tests are NOT run automatically on PR open/sync.
# They are required for merge but only fire when a reviewer comments
# `integrationtest` (JDK 17) or `integrationtestfull` (matrix 17/21/25)
# — see .github/workflows/pr-integration-on-demand.yml. Branch-protection
# rules should require the "Integration tests pass" check name produced
# by that workflow before allowing merge to main.
Loading
Loading