=
- CompletableFuture.completedFuture(null)
+ assertThat(response.statusCode()).isEqualTo(503)
+ verify(7, postRequestedFor(urlPathEqualTo("/something")))
+ assertThat(sleeper.durations).hasSize(6)
+ // retries=5: backoff hits the 8s cap * [0.75, 1.0]
+ assertThat(sleeper.durations[4]).isBetween(Duration.ofMillis(6000), Duration.ofMillis(8000))
+ // retries=6: still capped at 8s * [0.75, 1.0]
+ assertThat(sleeper.durations[5]).isBetween(Duration.ofMillis(6000), Duration.ofMillis(8000))
+ assertNoResponseLeaks()
+ }
- override fun close() {}
- }
+ @ParameterizedTest
+ @ValueSource(booleans = [false, true])
+ fun execute_withRetryAfterMsPriorityOverRetryAfter(async: Boolean) {
+ stubFor(
+ post(urlPathEqualTo("/something"))
+ .inScenario("foo")
+ .whenScenarioStateIs(Scenario.STARTED)
+ .willReturn(
+ serviceUnavailable()
+ .withHeader("Retry-After-Ms", "50")
+ .withHeader("Retry-After", "2")
+ )
+ .willSetStateTo("RETRY")
+ )
+ stubFor(
+ post(urlPathEqualTo("/something"))
+ .inScenario("foo")
+ .whenScenarioStateIs("RETRY")
+ .willReturn(ok())
+ .willSetStateTo("COMPLETED")
+ )
+ val sleeper = RecordingSleeper()
+ val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(1).build()
+
+ val response =
+ retryingClient.execute(
+ HttpRequest.builder()
+ .method(HttpMethod.POST)
+ .baseUrl(baseUrl)
+ .addPathSegment("something")
+ .build(),
+ async,
+ )
+
+ assertThat(response.statusCode()).isEqualTo(200)
+ // Retry-After-Ms (50ms) takes priority over Retry-After (2s).
+ assertThat(sleeper.durations).containsExactly(Duration.ofMillis(50))
+ assertNoResponseLeaks()
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = [false, true])
+ fun execute_withRetryAfterUnparseable(async: Boolean) {
+ stubFor(
+ post(urlPathEqualTo("/something"))
+ .inScenario("foo")
+ .whenScenarioStateIs(Scenario.STARTED)
+ .willReturn(serviceUnavailable().withHeader("Retry-After", "not-a-date-or-number"))
+ .willSetStateTo("RETRY")
+ )
+ stubFor(
+ post(urlPathEqualTo("/something"))
+ .inScenario("foo")
+ .whenScenarioStateIs("RETRY")
+ .willReturn(ok())
+ .willSetStateTo("COMPLETED")
+ )
+ val sleeper = RecordingSleeper()
+ val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(1).build()
+
+ val response =
+ retryingClient.execute(
+ HttpRequest.builder()
+ .method(HttpMethod.POST)
+ .baseUrl(baseUrl)
+ .addPathSegment("something")
+ .build(),
+ async,
)
+ assertThat(response.statusCode()).isEqualTo(200)
+ // Unparseable Retry-After falls through to exponential backoff.
+ assertThat(sleeper.durations).hasSize(1)
+ assertThat(sleeper.durations[0]).isBetween(Duration.ofMillis(375), Duration.ofMillis(500))
+ assertNoResponseLeaks()
+ }
+
+ private fun retryingHttpClientBuilder(
+ sleeper: RecordingSleeper,
+ clock: Clock = Clock.systemUTC(),
+ ) = RetryingHttpClient.builder().httpClient(httpClient).sleeper(sleeper).clock(clock)
+
private fun HttpClient.execute(request: HttpRequest, async: Boolean): HttpResponse =
if (async) executeAsync(request).get() else execute(request)
diff --git a/phoebe-java-proguard-test/build.gradle.kts b/phoebe-java-proguard-test/build.gradle.kts
index 932d96a..248bccd 100644
--- a/phoebe-java-proguard-test/build.gradle.kts
+++ b/phoebe-java-proguard-test/build.gradle.kts
@@ -18,8 +18,8 @@ dependencies {
testImplementation(project(":phoebe-java"))
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
- testImplementation("org.assertj:assertj-core:3.25.3")
- testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
+ testImplementation("org.assertj:assertj-core:3.27.7")
+ testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0")
}
tasks.shadowJar {
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
diff --git a/scripts/build b/scripts/build
index f406348..16a2b00 100755
--- a/scripts/build
+++ b/scripts/build
@@ -5,4 +5,4 @@ set -e
cd "$(dirname "$0")/.."
echo "==> Building classes"
-./gradlew build testClasses -x test
+./gradlew build testClasses "$@" -x test
diff --git a/scripts/fast-format b/scripts/fast-format
index 1b3bc47..35a1dee 100755
--- a/scripts/fast-format
+++ b/scripts/fast-format
@@ -24,8 +24,8 @@ if [ ! -f "$FILE_LIST" ]; then
exit 1
fi
-if ! command -v ktfmt-fast-format &> /dev/null; then
- echo "Error: ktfmt-fast-format not found"
+if ! command -v ktfmt &> /dev/null; then
+ echo "Error: ktfmt not found"
exit 1
fi
@@ -36,7 +36,7 @@ echo "==> Done looking for Kotlin files"
if [[ -n "$kt_files" ]]; then
echo "==> will format Kotlin files"
- echo "$kt_files" | tr '\n' '\0' | xargs -0 ktfmt-fast-format --kotlinlang-style "$@"
+ echo "$kt_files" | tr '\n' '\0' | xargs -0 ktfmt --kotlinlang-style "$@"
else
echo "No Kotlin files to format -- expected outcome during incremental formatting"
fi
diff --git a/scripts/mock b/scripts/mock
index 0b28f6e..5cd7c15 100755
--- a/scripts/mock
+++ b/scripts/mock
@@ -19,23 +19,34 @@ fi
echo "==> Starting mock server with URL ${URL}"
-# Run prism mock on the given spec
+# Run steady mock on the given spec
if [ "$1" == "--daemon" ]; then
- npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log &
+ # Pre-install the package so the download doesn't eat into the startup timeout
+ npm exec --package=@stdy/cli@0.20.2 -- steady --version
- # Wait for server to come online
+ npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &
+
+ # Wait for server to come online via health endpoint (max 30s)
echo -n "Waiting for server"
- while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do
+ attempts=0
+ while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do
+ if ! kill -0 $! 2>/dev/null; then
+ echo
+ cat .stdy.log
+ exit 1
+ fi
+ attempts=$((attempts + 1))
+ if [ "$attempts" -ge 300 ]; then
+ echo
+ echo "Timed out waiting for Steady server to start"
+ cat .stdy.log
+ exit 1
+ fi
echo -n "."
sleep 0.1
done
- if grep -q "✖ fatal" ".prism.log"; then
- cat .prism.log
- exit 1
- fi
-
echo
else
- npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL"
+ npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
fi
diff --git a/scripts/test b/scripts/test
index 047bc1d..61c1163 100755
--- a/scripts/test
+++ b/scripts/test
@@ -9,8 +9,8 @@ GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
-function prism_is_running() {
- curl --silent "http://localhost:4010" >/dev/null 2>&1
+function steady_is_running() {
+ curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1
}
kill_server_on_port() {
@@ -25,7 +25,7 @@ function is_overriding_api_base_url() {
[ -n "$TEST_API_BASE_URL" ]
}
-if ! is_overriding_api_base_url && ! prism_is_running ; then
+if ! is_overriding_api_base_url && ! steady_is_running ; then
# When we exit this script, make sure to kill the background mock server process
trap 'kill_server_on_port 4010' EXIT
@@ -36,19 +36,19 @@ fi
if is_overriding_api_base_url ; then
echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}"
echo
-elif ! prism_is_running ; then
- echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server"
+elif ! steady_is_running ; then
+ echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady server"
echo -e "running against your OpenAPI spec."
echo
echo -e "To run the server, pass in the path or url of your OpenAPI"
- echo -e "spec to the prism command:"
+ echo -e "spec to the steady command:"
echo
- echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}"
+ echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}"
echo
exit 1
else
- echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}"
+ echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}"
echo
fi
diff --git a/scripts/upload-artifacts b/scripts/upload-artifacts
new file mode 100755
index 0000000..10f3c70
--- /dev/null
+++ b/scripts/upload-artifacts
@@ -0,0 +1,193 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+# ANSI Color Codes
+GREEN='\033[32m'
+RED='\033[31m'
+NC='\033[0m' # No Color
+
+MAVEN_REPO_PATH="./build/local-maven-repo"
+
+log_error() {
+ local msg="$1"
+ local headers="$2"
+ local body="$3"
+ echo -e "${RED}${msg}${NC}"
+ [[ -f "$headers" ]] && echo -e "${RED}Headers:$(cat "$headers")${NC}"
+ echo -e "${RED}Body: ${body}${NC}"
+ exit 1
+}
+
+upload_file() {
+ local file_name="$1"
+ local tmp_headers
+ tmp_headers=$(mktemp)
+
+ if [ -f "$file_name" ]; then
+ echo -e "${GREEN}Processing file: $file_name${NC}"
+ pkg_file_name="mvn${file_name#"${MAVEN_REPO_PATH}"}"
+
+ # Get signed URL for uploading artifact file
+ signed_url_response=$(curl -X POST -G "$URL" \
+ -sS --retry 5 \
+ -D "$tmp_headers" \
+ --data-urlencode "filename=$pkg_file_name" \
+ -H "Authorization: Bearer $AUTH" \
+ -H "Content-Type: application/json")
+
+ # Validate JSON and extract URL
+ if ! signed_url=$(echo "$signed_url_response" | jq -e -r '.url' 2>/dev/null) || [[ "$signed_url" == "null" ]]; then
+ log_error "Failed to get valid signed URL" "$tmp_headers" "$signed_url_response"
+ fi
+
+ # Set content-type based on file extension
+ local extension="${file_name##*.}"
+ local content_type
+ case "$extension" in
+ jar) content_type="application/java-archive" ;;
+ md5|sha1|sha256|sha512) content_type="text/plain" ;;
+ module) content_type="application/json" ;;
+ pom|xml) content_type="application/xml" ;;
+ html) content_type="text/html" ;;
+ *) content_type="application/octet-stream" ;;
+ esac
+
+ # Upload file
+ upload_response=$(curl -v -X PUT \
+ --retry 5 \
+ --retry-all-errors \
+ -D "$tmp_headers" \
+ -H "Content-Type: $content_type" \
+ --data-binary "@${file_name}" "$signed_url" 2>&1)
+
+ if ! echo "$upload_response" | grep -q "HTTP/[0-9.]* 200"; then
+ log_error "Failed to upload artifact file" "$tmp_headers" "$upload_response"
+ fi
+
+ # Insert small throttle to reduce rate limiting risk
+ sleep 0.1
+ fi
+}
+
+walk_tree() {
+ local current_dir="$1"
+
+ for entry in "$current_dir"/*; do
+ # Check that entry is valid
+ [ -e "$entry" ] || [ -h "$entry" ] || continue
+
+ if [ -d "$entry" ]; then
+ walk_tree "$entry"
+ else
+ upload_file "$entry"
+ fi
+ done
+}
+
+generate_instructions() {
+ cat << EOF > "$MAVEN_REPO_PATH/index.html"
+
+
+
+ Maven Repo
+
+
+ Stainless SDK Maven Repository
+ This is the Maven repository for your Stainless Java SDK build.
+
+ Project configuration
+
+ The details depend on whether you're using Maven or Gradle as your build tool.
+
+ Maven
+
+ Add the following to your project's pom.xml:
+ <repositories>
+ <repository>
+ <id>stainless-sdk-repo</id>
+ <url>https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn</url>
+ </repository>
+</repositories>
+
+ Gradle
+ Add the following to your build.gradle file:
+ repositories {
+ maven {
+ url "https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn"
+ }
+}
+
+
+ Configuring authentication (if required)
+
+ Some accounts may require authentication to access the repository. If so, use the
+ following instructions, replacing YOUR_STAINLESS_API_TOKEN with your actual token.
+
+ Maven with authentication
+
+ First, ensure you have the following in your Maven settings.xml for repo authentication:
+ <servers>
+ <server>
+ <id>stainless-sdk-repo</id>
+ <configuration>
+ <httpHeaders>
+ <property>
+ <name>Authorization</name>
+ <value>Bearer YOUR_STAINLESS_API_TOKEN</value>
+ </property>
+ </httpHeaders>
+ </configuration>
+ </server>
+</servers>
+
+ Then, add the following to your project's pom.xml:
+ <repositories>
+ <repository>
+ <id>stainless-sdk-repo</id>
+ <url>https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn</url>
+ </repository>
+</repositories>
+
+ Gradle with authentication
+ Add the following to your build.gradle file:
+ repositories {
+ maven {
+ url "https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn"
+ credentials(HttpHeaderCredentials) {
+ name = "Authorization"
+ value = "Bearer YOUR_STAINLESS_API_TOKEN"
+ }
+ authentication {
+ header(HttpHeaderAuthentication)
+ }
+ }
+}
+
+
+ Using the repository
+ Once you've configured the repository, you can include dependencies from it as usual. See your
+ project README
+ for more details.
+
+
+EOF
+ upload_file "${MAVEN_REPO_PATH}/index.html"
+
+ echo "Configure maven or gradle to use the repo located at 'https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn'"
+ echo "For more details, see the directions in https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn/index.html"
+}
+
+cd "$(dirname "$0")/.."
+
+echo "::group::Creating local Maven content"
+./gradlew publishMavenPublicationToLocalFileSystemRepository -PpublishLocal
+echo "::endgroup::"
+
+echo "::group::Uploading to pkg.stainless.com"
+walk_tree "$MAVEN_REPO_PATH"
+echo "::endgroup::"
+
+echo "::group::Generating instructions"
+generate_instructions
+echo "::endgroup::"