Skip to content

Commit c770874

Browse files
gh actions added
1 parent 8f2e631 commit c770874

8 files changed

Lines changed: 364 additions & 3 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env python3
2+
"""Fail if PR line coverage dropped more than TOLERANCE pp below main's baseline.
3+
4+
Inputs:
5+
build/reports/jacoco/test/jacocoTestReport.xml - PR's coverage XML
6+
coverage-baseline.txt - main's last recorded ratio,
7+
restored from GitHub Actions cache
8+
9+
Behavior:
10+
- If baseline file is missing or empty (no prior main run cached, or cache
11+
expired), the check passes with a WARNING — we don't want PRs blocked
12+
on baseline bootstrap problems.
13+
- Otherwise, compute drop = baseline - pr_ratio. Fail if drop > TOLERANCE.
14+
"""
15+
from __future__ import annotations
16+
17+
import sys
18+
import xml.etree.ElementTree as ET
19+
from pathlib import Path
20+
21+
TOLERANCE_PP = 0.05 # 5 percentage points
22+
JACOCO_XML = Path("build/reports/jacoco/test/jacocoTestReport.xml")
23+
BASELINE = Path("coverage-baseline.txt")
24+
25+
26+
def line_coverage(xml_path: Path) -> float:
27+
root = ET.parse(xml_path).getroot()
28+
for counter in root.findall("counter"):
29+
if counter.attrib.get("type") == "LINE":
30+
covered = int(counter.attrib["covered"])
31+
missed = int(counter.attrib["missed"])
32+
total = covered + missed
33+
return covered / total if total else 0.0
34+
raise ValueError(f"No LINE counter in {xml_path}")
35+
36+
37+
def main() -> int:
38+
pr_ratio = line_coverage(JACOCO_XML)
39+
40+
baseline_text = BASELINE.read_text().strip() if BASELINE.is_file() else ""
41+
if not baseline_text:
42+
print(f"::warning::No coverage baseline from main found.")
43+
print(f"PR coverage: {pr_ratio:.2%}. Skipping delta check.")
44+
print("This is normal on the first run after the baseline cache expired or never existed.")
45+
return 0
46+
47+
baseline = float(baseline_text)
48+
drop_pp = (baseline - pr_ratio) * 100
49+
50+
print(f"PR coverage: {pr_ratio:.2%}")
51+
print(f"Main baseline: {baseline:.2%}")
52+
print(f"Delta: {-drop_pp:+.2f} pp")
53+
print(f"Tolerance: {-TOLERANCE_PP * 100:.2f} pp")
54+
55+
if drop_pp > TOLERANCE_PP * 100:
56+
print(
57+
f"::error::Coverage dropped {drop_pp:.2f} pp from main "
58+
f"(tolerance is {TOLERANCE_PP * 100:.0f} pp)."
59+
)
60+
return 1
61+
62+
print("OK")
63+
return 0
64+
65+
66+
if __name__ == "__main__":
67+
sys.exit(main())
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env python3
2+
"""Extract line-coverage ratio from a JaCoCo XML report.
3+
4+
Usage:
5+
python3 extract-coverage.py [path/to/jacocoTestReport.xml]
6+
7+
Defaults to build/reports/jacoco/test/jacocoTestReport.xml (Gradle layout).
8+
Prints the ratio as a 4-decimal float on stdout, e.g. "0.8311".
9+
"""
10+
from __future__ import annotations
11+
12+
import sys
13+
import xml.etree.ElementTree as ET
14+
15+
DEFAULT_PATH = "build/reports/jacoco/test/jacocoTestReport.xml"
16+
17+
18+
def main() -> int:
19+
path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_PATH
20+
root = ET.parse(path).getroot()
21+
for counter in root.findall("counter"):
22+
if counter.attrib.get("type") == "LINE":
23+
covered = int(counter.attrib["covered"])
24+
missed = int(counter.attrib["missed"])
25+
total = covered + missed
26+
ratio = covered / total if total else 0.0
27+
print(f"{ratio:.4f}")
28+
return 0
29+
print("ERROR: no LINE counter found in JaCoCo XML", file=sys.stderr)
30+
return 1
31+
32+
33+
if __name__ == "__main__":
34+
sys.exit(main())

.github/workflows/main.yml

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
name: Main
2+
3+
# Runs only on push to main (i.e. when a PR is merged or someone pushes
4+
# directly). This is where the full forward-compat JDK matrix runs and
5+
# where we record the coverage baseline that PR runs compare against.
6+
on:
7+
push:
8+
branches: ['main']
9+
10+
permissions:
11+
contents: read
12+
13+
# Don't cancel main runs against each other — we want every merge to
14+
# produce a coverage baseline. Sequential is fine; main pushes are rare.
15+
concurrency:
16+
group: main
17+
cancel-in-progress: false
18+
19+
jobs:
20+
verify:
21+
name: Verify (JDK ${{ matrix.java }})
22+
runs-on: ubuntu-latest
23+
strategy:
24+
# Don't cancel siblings: if JDK 21 fails, we still want to know
25+
# whether 17 and 25 pass.
26+
fail-fast: false
27+
matrix:
28+
# ADR-002: tests run on JDK 17, 21, 25 to catch forward-compat
29+
# regressions. Compilation is always pinned to --release 17.
30+
java: ['17', '21', '25']
31+
32+
steps:
33+
- name: Checkout
34+
uses: actions/checkout@v4
35+
36+
# Install both JDK 17 (for compilation) and the matrix JDK (for
37+
# test execution). setup-java exports JAVA_HOME_<version>_<arch>;
38+
# Gradle's toolchain auto-detection picks them up.
39+
- name: Set up JDKs (compile=17, test=${{ matrix.java }})
40+
uses: actions/setup-java@v4
41+
with:
42+
distribution: temurin
43+
java-version: |
44+
17
45+
${{ matrix.java }}
46+
47+
- name: Set up Gradle
48+
uses: gradle/actions/setup-gradle@v4
49+
50+
- name: Build, test, lint, coverage
51+
run: ./gradlew build -PtestJdk=${{ matrix.java }} --stacktrace
52+
53+
- name: Upload test reports on failure
54+
if: failure()
55+
uses: actions/upload-artifact@v4
56+
with:
57+
name: test-reports-jdk${{ matrix.java }}
58+
path: |
59+
build/reports/tests/
60+
build/test-results/
61+
retention-days: 14
62+
63+
# The JDK 17 entry of the matrix is the canonical run for coverage
64+
# baseline purposes — its JaCoCo XML is what PR runs compare against.
65+
- name: Extract coverage baseline (JDK 17 only)
66+
if: success() && matrix.java == '17'
67+
run: |
68+
python3 .github/scripts/extract-coverage.py > coverage-baseline.txt
69+
echo "Baseline coverage: $(cat coverage-baseline.txt)"
70+
71+
- name: Save coverage baseline to cache (JDK 17 only)
72+
if: success() && matrix.java == '17'
73+
uses: actions/cache/save@v4
74+
with:
75+
path: coverage-baseline.txt
76+
# Unique per commit so each main run produces a fresh entry;
77+
# PR workflow restores via prefix-match on `coverage-baseline-main-`.
78+
key: coverage-baseline-main-${{ github.sha }}
79+
80+
- name: Upload coverage report (JDK 17 only)
81+
if: success() && matrix.java == '17'
82+
uses: actions/upload-artifact@v4
83+
with:
84+
name: jacoco-report
85+
path: build/reports/jacoco/test/
86+
retention-days: 14
87+
88+
# Integration-tests job is intentionally not wired up yet:
89+
# SDK requirements §13 says they run on PRs and release pipelines, but
90+
# they hit the live API and require a MARKETDATA_TOKEN secret. Add this
91+
# job (gated on `if: ${{ secrets.MARKETDATA_TOKEN != '' }}`) once the
92+
# token is configured in the repo's GitHub Actions secrets.

.github/workflows/pull-request.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: Pull Request
2+
3+
# Triggers only on pull request lifecycle events:
4+
# - opened (PR creation)
5+
# - synchronize (push to the PR branch while the PR is open)
6+
# - reopened
7+
# These are the default `pull_request` activity types — listed explicitly
8+
# here for clarity. Pre-PR pushes don't run CI by design (saves minutes
9+
# during early WIP commits).
10+
on:
11+
pull_request:
12+
types: [opened, synchronize, reopened]
13+
branches: ['**']
14+
15+
permissions:
16+
contents: read
17+
18+
# Cancel an in-progress run when a new commit lands on the same PR.
19+
concurrency:
20+
group: pr-${{ github.event.pull_request.number }}
21+
cancel-in-progress: true
22+
23+
jobs:
24+
verify:
25+
name: Verify (JDK 17)
26+
runs-on: ubuntu-latest
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@v4
30+
31+
# PRs only run on JDK 17 (the minimum target). Forward-compat
32+
# regressions on JDK 21/25 are caught post-merge by main.yml.
33+
- name: Set up JDK 17
34+
uses: actions/setup-java@v4
35+
with:
36+
distribution: temurin
37+
java-version: '17'
38+
39+
# Validates the wrapper jar hash and caches Gradle home + wrapper
40+
# dists between runs.
41+
- name: Set up Gradle
42+
uses: gradle/actions/setup-gradle@v4
43+
44+
- name: Build, test, lint, coverage
45+
run: ./gradlew build --stacktrace
46+
47+
- name: Upload test reports on failure
48+
if: failure()
49+
uses: actions/upload-artifact@v4
50+
with:
51+
name: test-reports
52+
path: |
53+
build/reports/tests/
54+
build/test-results/
55+
retention-days: 14
56+
57+
# Coverage ratchet: PR's line coverage cannot drop more than 5 pp
58+
# below main's last recorded value. The baseline is restored from
59+
# the cache populated by main.yml; if no baseline is available
60+
# (first ever main run hasn't completed, or cache expired), the
61+
# check passes with a warning instead of blocking the PR.
62+
- name: Restore coverage baseline from main
63+
uses: actions/cache/restore@v4
64+
with:
65+
path: coverage-baseline.txt
66+
# Unique key that won't match — forces fall-through to restore-keys.
67+
key: coverage-baseline-pr-${{ github.run_id }}
68+
restore-keys: |
69+
coverage-baseline-main-
70+
71+
- name: Check coverage delta vs main
72+
run: python3 .github/scripts/check-coverage-delta.py

CLAUDE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ The Java SDK must also satisfy the canonical, cross-language [SDK Requirements](
6868
- §12 concurrency: `Semaphore(50)` field on `MarketDataClient` (wiring of acquire/release lands with the request layer).
6969
- §15 packaging: SemVer, MIT `LICENSE`, `CHANGELOG.md` in Keep a Changelog format, version auto-detected via JAR manifest (`Implementation-Version`).
7070
- §16 security: tokens never logged verbatim (use `Tokens.redact`); TLS validated by default (`HttpClient` does not expose a skip-verify option).
71+
- ADR-002 CI: split into two workflows.
72+
- `.github/workflows/pull-request.yml` — runs on PR `opened`/`synchronize`/`reopened` (no pre-PR push trigger by design). JDK 17 only. Runs `./gradlew build` + a coverage-delta check that compares the PR's line coverage against main's last recorded value, failing if it drops more than 5 pp. Baseline is restored via `actions/cache/restore@v4` with `restore-keys: coverage-baseline-main-` (prefix match against the latest main run's cache); if no baseline exists, the check passes with a `::warning::` rather than blocking the PR.
73+
- `.github/workflows/main.yml` — runs only on `push` to `main`. Full forward-compat matrix `{17, 21, 25}` via `-PtestJdk=N` (wired into `tasks.test.javaLauncher` in `build.gradle.kts`). The JDK 17 matrix entry also extracts the coverage baseline and saves it to cache key `coverage-baseline-main-${sha}` for the next PR run.
74+
- Helper scripts live in `.github/scripts/` (`extract-coverage.py`, `check-coverage-delta.py`). Both parse `build/reports/jacoco/test/jacocoTestReport.xml` directly. Tolerance constant lives in the script (`TOLERANCE_PP = 0.05`).
7175

7276
**Deliberately deferred (require the request/endpoint layer to land first):**
7377
- §1.2 resource groupings (`client.stocks`, `client.options`, `client.funds`, `client.markets`, `client.utilities`).
@@ -78,7 +82,7 @@ The Java SDK must also satisfy the canonical, cross-language [SDK Requirements](
7882
- §9 retry/backoff policy and `/status/` cache workflow.
7983
- §12 acquire/release of the concurrency semaphore around dispatched requests.
8084
- §13 100% coverage threshold via JaCoCo `violationRules`; deferred until there is functional code worth the threshold.
81-
- CI matrix workflow (`.github/workflows/ci.yml`) on JDK 17/21/25.
85+
- §13 integration-test CI job: stubbed at the bottom of `ci.yml` with a comment. Gating on a `MARKETDATA_TOKEN` GitHub Actions secret; will be wired up once the secret exists.
8286

8387
When picking up new work, check this list before reaching for the SDK requirements doc — most foundational rules are already encoded in code; missing pieces are deferred deliberately, not by accident.
8488

build.gradle.kts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ java {
1313
toolchain {
1414
languageVersion = JavaLanguageVersion.of(17)
1515
}
16-
withJavadocJar()
17-
withSourcesJar()
16+
// Sources/Javadoc jars are produced by the Vanniktech publish plugin
17+
// (see mavenPublishing block below). Duplicating them via
18+
// withJavadocJar()/withSourcesJar() here causes "multiple artifacts
19+
// with classifier 'javadoc'" failures at publish time.
1820
}
1921

2022
tasks.withType<JavaCompile>().configureEach {
@@ -79,6 +81,19 @@ dependencies {
7981
tasks.test {
8082
useJUnitPlatform()
8183
finalizedBy(tasks.jacocoTestReport)
84+
85+
// ADR-002 CI matrix: optionally run tests on a specific JDK while
86+
// compilation stays pinned to --release 17. The CI workflow passes
87+
// -PtestJdk=17|21|25; locally you can do ./gradlew test -PtestJdk=21
88+
// (Gradle will provision the JDK via the foojay resolver if missing).
89+
val testJdk = providers.gradleProperty("testJdk").orNull
90+
if (testJdk != null) {
91+
javaLauncher.set(
92+
javaToolchains.launcherFor {
93+
languageVersion = JavaLanguageVersion.of(testJdk.toInt())
94+
}
95+
)
96+
}
8297
}
8398

8499
tasks.jacocoTestReport {
@@ -89,6 +104,11 @@ tasks.jacocoTestReport {
89104
}
90105
}
91106

107+
// Coverage ratchet (line coverage cannot drop more than 5 pp below
108+
// main's last value) is enforced in CI — see .github/workflows/pull-request.yml
109+
// and .github/scripts/check-coverage-delta.py. Not enforced locally so that
110+
// dev iteration isn't blocked while coverage is in flux.
111+
92112
spotless {
93113
java {
94114
target("src/**/*.java")
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.marketdata.sdk;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.time.Instant;
6+
import org.junit.jupiter.api.Test;
7+
8+
class RateLimitsTest {
9+
10+
@Test
11+
void recordExposesAllFields() {
12+
Instant reset = Instant.parse("2026-05-04T12:00:00Z");
13+
var rl = new RateLimits(50_000L, 49_500L, reset, 1L);
14+
15+
assertThat(rl.limit()).isEqualTo(50_000L);
16+
assertThat(rl.remaining()).isEqualTo(49_500L);
17+
assertThat(rl.reset()).isEqualTo(reset);
18+
assertThat(rl.consumed()).isEqualTo(1L);
19+
}
20+
}

0 commit comments

Comments
 (0)