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
70 changes: 70 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Publish to Maven Central

on:
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 0.1.0, without the 'v')"
required: true
type: string
release:
description: "Publish and release. Off = upload to the Portal and stop at VALIDATED for manual review."
required: true
type: boolean
default: false

jobs:
guard:
name: Require green main.yml for this commit
runs-on: ubuntu-latest
permissions:
actions: read
steps:
- name: Check latest main.yml conclusion
env:
GH_TOKEN: ${{ github.token }}
run: |
conclusion=$(gh api \
"repos/${{ github.repository }}/actions/workflows/main.yml/runs?head_sha=${{ github.sha }}&status=completed" \
--jq '.workflow_runs[0].conclusion // "missing"')
echo "main.yml conclusion for ${{ github.sha }}: $conclusion"
if [ "$conclusion" != "success" ]; then
echo "::error::main.yml is not green for ${{ github.sha }} (got: $conclusion). Run publish from a commit on main whose CI passed."
exit 1
fi

publish:
name: Publish to Maven Central
needs: guard
runs-on: ubuntu-latest
environment:
name: maven-central
url: https://central.sonatype.com/artifact/app.marketdata/marketdata-sdk-java
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"

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

- name: Build and test
run: ./gradlew clean build -PsdkVersion=${{ inputs.version }}

- name: Upload to Portal (validate only)
if: ${{ !inputs.release }}
run: ./gradlew publishToMavenCentral -PsdkVersion=${{ inputs.version }}

- name: Publish and release
if: ${{ inputs.release }}
run: ./gradlew publishAndReleaseToMavenCentral -PsdkVersion=${{ inputs.version }}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ exception taxonomy, and Kotlin-interop foundations are in place.
```kotlin
// build.gradle.kts
dependencies {
implementation("com.marketdata:marketdata-sdk-java:0.1.0")
implementation("app.marketdata:marketdata-sdk-java:0.1.0")
}
```

Expand Down
51 changes: 46 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import com.vanniktech.maven.publish.SonatypeHost

plugins {
`java-library`
jacoco
alias(libs.plugins.spotless)
alias(libs.plugins.vanniktech.publish)
}

group = "com.marketdata"
version = "0.1.0-SNAPSHOT"
// Maven groupId = the verified Central Portal namespace (domain marketdata.app,
// reversed). Independent of the Java package, which stays com.marketdata.sdk.
group = "app.marketdata"

// Version is overridable from the command line so a manual Central Portal
// validation run can use a real release version (e.g. `-PsdkVersion=0.1.0`)
// without committing it. Default stays on the in-development SNAPSHOT.
version = (findProperty("sdkVersion") as String?) ?: "0.1.0-SNAPSHOT"

// ADR-002: minimum JDK 17, build with --release 17, single bytecode level.
java {
Expand Down Expand Up @@ -174,13 +182,46 @@ spotless {
}

// ADR-003 / requirements §15: Maven Central publishing via Vanniktech.
// Coordinates and POM metadata below are placeholders — fill in before
// the first publication.
//
// Publishes to the Sonatype Central Portal (central.sonatype.com).
// `automaticRelease = false` uploads the deployment but leaves it in the
// VALIDATED state for manual review/release (or drop) from the portal UI —
// the safe path for a first manual validation run.
//
// Upload + signing credentials are read from Gradle properties / env vars by
// the plugin (never hard-coded here):
// - ORG_GRADLE_PROJECT_mavenCentralUsername / _mavenCentralPassword
// - ORG_GRADLE_PROJECT_signingInMemoryKey / _signingInMemoryKeyPassword
// (optionally _signingInMemoryKeyId)
mavenPublishing {
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = false)
signAllPublications()

coordinates(group.toString(), "marketdata-sdk-java", version.toString())
pom {
name.set("Market Data Java SDK")
description.set("Java SDK for the Market Data API.")
// TODO: set url, scm, license, developers before publishing.
url.set("https://github.com/MarketDataApp/sdk-java")
inceptionYear.set("2026")

licenses {
license {
name.set("MIT License")
url.set("https://github.com/MarketDataApp/sdk-java/blob/main/LICENSE")
distribution.set("repo")
}
}
developers {
developer {
id.set("marketdata")
name.set("Market Data")
url.set("https://www.marketdata.app")
}
}
scm {
url.set("https://github.com/MarketDataApp/sdk-java")
connection.set("scm:git:git://github.com/MarketDataApp/sdk-java.git")
developerConnection.set("scm:git:ssh://git@github.com/MarketDataApp/sdk-java.git")
}
}
}
2 changes: 1 addition & 1 deletion examples/consumer-test/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ kotlin {
}

dependencies {
implementation("com.marketdata:marketdata-sdk-java:0.1.0-SNAPSHOT")
implementation("app.marketdata:marketdata-sdk-java:0.1.0-SNAPSHOT")
}

// Default `./gradlew run` lands on the stocks resource example (live API).
Expand Down
14 changes: 9 additions & 5 deletions src/test/java/com/marketdata/sdk/AsyncSemaphoreTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -222,14 +222,16 @@ void closeCompletesAllQueuedWaitersWithCancellation() {

sem.close();

// CompletableFuture#join unwraps CancellationException specifically: it surfaces directly
// rather than being wrapped in CompletionException. That's the same propagation downstream
// observers see, so we assert the bare exception shape here.
// join() surfaces a CancellationException, but its shape is JDK-dependent: JDK 17 rethrows the
// original directly (message "AsyncSemaphore is closed"), while JDK 21+ wraps it in a fresh
// CancellationException (message "join") carrying the original as its cause. Accept either.
for (CompletableFuture<Void> w : List.of(w1, w2, w3)) {
assertThat(w).isCompletedExceptionally();
assertThatThrownBy(w::join)
.isInstanceOf(CancellationException.class)
.hasMessageContaining("closed");
.satisfiesAnyOf(
t -> assertThat(t).hasMessageContaining("closed"),
t -> assertThat(t).hasRootCauseMessage("AsyncSemaphore is closed"));
}
assertThat(sem.queueLength()).isZero();
}
Expand All @@ -244,7 +246,9 @@ void acquireAfterCloseReturnsFailedFutureImmediately() {
assertThat(failed).isCompletedExceptionally();
assertThatThrownBy(failed::join)
.isInstanceOf(CancellationException.class)
.hasMessageContaining("closed");
.satisfiesAnyOf(
t -> assertThat(t).hasMessageContaining("closed"),
t -> assertThat(t).hasRootCauseMessage("AsyncSemaphore is closed"));
assertThat(sem.queueLength()).isZero();
}

Expand Down
Loading