diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml new file mode 100644 index 0000000..e1a6dde --- /dev/null +++ b/.github/workflows/publish-sonatype.yml @@ -0,0 +1,39 @@ +# This workflow is triggered when a GitHub release is created. +# It can also be run manually to re-publish to Sonatype in case it failed for some reason. +# You can run this workflow by navigating to https://www.github.com/OneBusAway/java-sdk/actions/workflows/publish-sonatype.yml +name: Publish Sonatype +on: + workflow_dispatch: + + release: + types: [published] + +jobs: + publish: + name: publish + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: | + 8 + 17 + cache: gradle + + - name: Set up Gradle + uses: gradle/gradle-build-action@v2 + + - name: Publish to Sonatype + run: | + ./gradlew --parallel --no-daemon publish + env: + SONATYPE_USERNAME: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }} + GPG_SIGNING_KEY_ID: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_GPG_SIGNING_KEY_ID || secrets.GPG_SIGNING_KEY_ID }} + GPG_SIGNING_KEY: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }} + GPG_SIGNING_PASSWORD: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 0000000..f328321 --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,25 @@ +name: Release Doctor +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + if: github.repository == 'OneBusAway/java-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + + steps: + - uses: actions/checkout@v4 + + - name: Check release environment + run: | + bash ./bin/check-release-environment + env: + SONATYPE_USERNAME: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }} + GPG_SIGNING_KEY_ID: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_GPG_SIGNING_KEY_ID || secrets.GPG_SIGNING_KEY_ID }} + GPG_SIGNING_KEY: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }} + GPG_SIGNING_PASSWORD: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..c476280 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.1-alpha.0" +} \ No newline at end of file diff --git a/README.md b/README.md index b6cc009..7d5a7e3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # Onebusaway SDK Java API Library + + [![Maven Central](https://img.shields.io/maven-central/v/com.open_transit.api/onebusaway-sdk-java)](https://central.sonatype.com/artifact/com.open_transit.api/onebusaway-sdk-java/0.0.1-alpha.0) + + The Onebusaway SDK Java SDK provides convenient access to the Onebusaway SDK REST API from applications written in Java. It includes helper classes with helpful types and documentation for every request and response property. The Onebusaway SDK Java SDK is similar to the Onebusaway SDK Kotlin SDK but with minor differences that make it more ergonomic for use in Java, such as `Optional` instead of nullable values, `Stream` instead of `Sequence`, and `CompletableFuture` instead of suspend functions. @@ -20,6 +24,8 @@ The REST API documentation can be foundĀ on [developer.onebusaway.org](https://d #### Gradle + + ```kotlin implementation("com.open_transit.api:onebusaway-sdk-java:0.0.1-alpha.0") ``` @@ -34,6 +40,8 @@ implementation("com.open_transit.api:onebusaway-sdk-java:0.0.1-alpha.0") ``` + + ### Configure the client Use `OnebusawaySdkOkHttpClient.builder()` to configure the client. At a minimum you need to set `.apiKey()`: @@ -241,7 +249,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/open-transit-java/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/OneBusAway/java-sdk/issues) with questions, bugs, or suggestions. ## Requirements diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 0000000..7dd019c --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +errors=() + +if [ -z "${SONATYPE_USERNAME}" ]; then + errors+=("The ONEBUSAWAY_SDK_SONATYPE_USERNAME secret has not been set. Please set it in either this repository's secrets or your organization secrets") +fi + +if [ -z "${SONATYPE_PASSWORD}" ]; then + errors+=("The ONEBUSAWAY_SDK_SONATYPE_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets") +fi + +if [ -z "${GPG_SIGNING_KEY}" ]; then + errors+=("The ONEBUSAWAY_SDK_SONATYPE_GPG_SIGNING_KEY secret has not been set. Please set it in either this repository's secrets or your organization secrets") +fi + +if [ -z "${GPG_SIGNING_PASSWORD}" ]; then + errors+=("The ONEBUSAWAY_SDK_SONATYPE_GPG_SIGNING_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets") +fi + +lenErrors=${#errors[@]} + +if [[ lenErrors -gt 0 ]]; then + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" diff --git a/build.gradle.kts b/build.gradle.kts index afac859..42a1886 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { allprojects { group = "com.open_transit.api" - version = "0.0.1-alpha.0" + version = "0.0.1-alpha.0" // x-release-please-version } nexusPublishing { diff --git a/buildSrc/src/main/kotlin/onebusaway-sdk.publish.gradle.kts b/buildSrc/src/main/kotlin/onebusaway-sdk.publish.gradle.kts index 8c75ada..f224d9b 100644 --- a/buildSrc/src/main/kotlin/onebusaway-sdk.publish.gradle.kts +++ b/buildSrc/src/main/kotlin/onebusaway-sdk.publish.gradle.kts @@ -33,9 +33,9 @@ configure { } scm { - connection.set("scm:git:git://github.com/stainless-sdks/open-transit-java.git") - developerConnection.set("scm:git:git://github.com/stainless-sdks/open-transit-java.git") - url.set("https://github.com/stainless-sdks/open-transit-java") + connection.set("scm:git:git://github.com/OneBusAway/java-sdk.git") + developerConnection.set("scm:git:git://github.com/OneBusAway/java-sdk.git") + url.set("https://github.com/OneBusAway/java-sdk") } versionMapping { diff --git a/onebusaway-sdk-java-core/src/main/kotlin/com/open_transit/api/core/http/RetryingHttpClient.kt b/onebusaway-sdk-java-core/src/main/kotlin/com/open_transit/api/core/http/RetryingHttpClient.kt index ae65b4d..63e93f8 100644 --- a/onebusaway-sdk-java-core/src/main/kotlin/com/open_transit/api/core/http/RetryingHttpClient.kt +++ b/onebusaway-sdk-java-core/src/main/kotlin/com/open_transit/api/core/http/RetryingHttpClient.kt @@ -40,9 +40,16 @@ private constructor( maybeAddIdempotencyHeader(request) + // Don't send the current retry count in the headers if the caller set their own value. + val shouldSendRetryCount = !request.headers.containsKey("x-stainless-retry-count") + var retries = 0 while (true) { + if (shouldSendRetryCount) { + setRetryCountHeader(request, retries) + } + val response = try { val response = httpClient.execute(request, requestOptions) @@ -74,10 +81,21 @@ private constructor( maybeAddIdempotencyHeader(request) + // Don't send the current retry count in the headers if the caller set their own value. + val shouldSendRetryCount = !request.headers.containsKey("x-stainless-retry-count") + var retries = 0 - fun wrap(future: CompletableFuture): CompletableFuture { - return future + fun executeWithRetries( + request: HttpRequest, + requestOptions: RequestOptions, + ): CompletableFuture { + if (shouldSendRetryCount) { + setRetryCountHeader(request, retries) + } + + return httpClient + .executeAsync(request, requestOptions) .handleAsync( fun( response: HttpResponse?, @@ -97,7 +115,7 @@ private constructor( val backoffMillis = getRetryBackoffMillis(retries, response) return sleepAsync(backoffMillis.toMillis()).thenCompose { - wrap(httpClient.executeAsync(request, requestOptions)) + executeWithRetries(request, requestOptions) } }, MoreExecutors.directExecutor() @@ -105,7 +123,7 @@ private constructor( .thenCompose(Function.identity()) } - return wrap(httpClient.executeAsync(request, requestOptions)) + return executeWithRetries(request, requestOptions) } override fun close() { @@ -118,6 +136,11 @@ private constructor( return request.body?.repeatable() ?: true } + private fun setRetryCountHeader(request: HttpRequest, retries: Int) { + request.headers.removeAll("x-stainless-retry-count") + request.headers.put("x-stainless-retry-count", retries.toString()) + } + private fun idempotencyKey(): String = "stainless-java-retry-${UUID.randomUUID()}" private fun maybeAddIdempotencyHeader(request: HttpRequest) { diff --git a/onebusaway-sdk-java-core/src/test/kotlin/com/open_transit/api/core/http/RetryingHttpClientTest.kt b/onebusaway-sdk-java-core/src/test/kotlin/com/open_transit/api/core/http/RetryingHttpClientTest.kt index 682519f..6d46e26 100644 --- a/onebusaway-sdk-java-core/src/test/kotlin/com/open_transit/api/core/http/RetryingHttpClientTest.kt +++ b/onebusaway-sdk-java-core/src/test/kotlin/com/open_transit/api/core/http/RetryingHttpClientTest.kt @@ -8,6 +8,8 @@ import com.open_transit.api.client.okhttp.OkHttpClient import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource @WireMockTest internal class RetryingHttpClientTest { @@ -50,8 +52,9 @@ internal class RetryingHttpClientTest { verify(1, postRequestedFor(urlPathEqualTo("/something"))) } - @Test - fun retryAfterHeader() { + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun retryAfterHeader(async: Boolean) { val request = HttpRequest.builder().method(HttpMethod.POST).addPathSegment("something").build() stubFor( @@ -79,9 +82,67 @@ internal class RetryingHttpClientTest { ) val retryingClient = RetryingHttpClient.builder().httpClient(httpClient).maxRetries(2).build() - val response = retryingClient.execute(request) + + val response = + if (async) retryingClient.executeAsync(request).get() + else retryingClient.execute(request) + assertThat(response.statusCode()).isEqualTo(200) - verify(3, postRequestedFor(urlPathEqualTo("/something"))) + verify( + 1, + postRequestedFor(urlPathEqualTo("/something")) + .withHeader("x-stainless-retry-count", equalTo("0")) + ) + verify( + 1, + postRequestedFor(urlPathEqualTo("/something")) + .withHeader("x-stainless-retry-count", equalTo("1")) + ) + verify( + 1, + postRequestedFor(urlPathEqualTo("/something")) + .withHeader("x-stainless-retry-count", equalTo("2")) + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun overwriteRetryCountHeader(async: Boolean) { + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .addPathSegment("something") + .putHeader("x-stainless-retry-count", "42") + .build() + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") // first we fail with a retry after header given as a date + .whenScenarioStateIs(Scenario.STARTED) + .willReturn( + serviceUnavailable().withHeader("Retry-After", "Wed, 21 Oct 2015 07:28:00 GMT") + ) + .willSetStateTo("RETRY_AFTER_DATE") + ) + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") // then we return a success + .whenScenarioStateIs("RETRY_AFTER_DATE") + .willReturn(ok()) + .willSetStateTo("COMPLETED") + ) + val retryingClient = + RetryingHttpClient.builder().httpClient(httpClient).maxRetries(2).build() + + val response = + if (async) retryingClient.executeAsync(request).get() + else retryingClient.execute(request) + + assertThat(response.statusCode()).isEqualTo(200) + verify( + 2, + postRequestedFor(urlPathEqualTo("/something")) + .withHeader("x-stainless-retry-count", equalTo("42")) + ) } @Test diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..8f98719 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,67 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "simple", + "extra-files": [ + "README.md", + "build.gradle.kts" + ] +} \ No newline at end of file