From ca33b174549a4cfc84f63c2cec3e3e5e445f8ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Proch=C3=A1zka?= Date: Tue, 14 Jul 2026 19:57:56 +0000 Subject: [PATCH 1/3] chore: sync Java client with Apify OpenAPI spec v2-2026-07-13T092445Z Spec bump only adds already-handled error responses (402/408) and relaxes required stats counters; no interface or behavior change. Bumps client to 0.3.1 and updates the spec-version constant, README, and CHANGELOG. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VEDd2d9MHc8azfZ6J86Fn3 --- CHANGELOG.md | 11 +++++++++++ README.md | 4 ++-- pom.xml | 2 +- src/main/java/com/apify/client/Version.java | 4 ++-- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 624363d..62eabc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to the Apify Java client are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.1] - 2026-07-14 + +### Changed + +- Verified the client against OpenAPI specification version `v2-2026-07-13T092445Z` and bumped + `Version.API_SPEC_VERSION`. The spec change only added already-handled error responses (`402` + Payment Required on actor and run-resurrect run-sync endpoints, `408` Request Timeout on the + task run-sync endpoint) and relaxed `required` constraints on run/build/key-value-store/webhook + stats counters; both are already covered by the client's generic status-code error handling and + its forward-compatible model deserialization, so no interface or behavior change was needed. + ## [0.3.0] - 2026-07-11 ### Added diff --git a/README.md b/README.md index 12aabfc..b8e3985 100644 --- a/README.md +++ b/README.md @@ -189,9 +189,9 @@ try { The public `com.apify.client.Version` class (`import com.apify.client.Version;`) exposes two constants: -- `Version.CLIENT_VERSION` — the semantic version of this client (`0.3.0`). +- `Version.CLIENT_VERSION` — the semantic version of this client (`0.3.1`). - `Version.API_SPEC_VERSION` — the Apify OpenAPI specification version this client was verified - against (`v2-2026-07-10T105921Z`). + against (`v2-2026-07-13T092445Z`). Changes to the public interface other than additive ones are considered breaking changes and follow [Semantic Versioning](https://semver.org/). diff --git a/pom.xml b/pom.xml index 91e1476..de02dbf 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.apify apify-client - 0.3.0 + 0.3.1 jar Apify Java Client diff --git a/src/main/java/com/apify/client/Version.java b/src/main/java/com/apify/client/Version.java index c3333a0..e4d344f 100644 --- a/src/main/java/com/apify/client/Version.java +++ b/src/main/java/com/apify/client/Version.java @@ -13,13 +13,13 @@ public final class Version { * The semantic version of this client library (see SemVer). * Changes to the public interface other than additive ones are considered breaking changes. */ - public static final String CLIENT_VERSION = "0.3.0"; + public static final String CLIENT_VERSION = "0.3.1"; /** * The version of the Apify OpenAPI specification this client was generated and verified against. * Corresponds to the {@code info.version} field of the Apify OpenAPI document. */ - public static final String API_SPEC_VERSION = "v2-2026-07-10T105921Z"; + public static final String API_SPEC_VERSION = "v2-2026-07-13T092445Z"; private Version() {} } From e9b0958858c5e794c83720050b0ab1a0eccb0bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Proch=C3=A1zka?= Date: Tue, 14 Jul 2026 19:57:56 +0000 Subject: [PATCH 2/3] docs: bump README install snippets to 0.3.1 Maven, Gradle, and quick-start pom examples still pinned 0.3.0 after the version bump; align them with the released 0.3.1 coordinate. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VEDd2d9MHc8azfZ6J86Fn3 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b8e3985..4a08490 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Maven (Maven Central is a default repository, so no extra configuration is neede com.apify apify-client - 0.3.0 + 0.3.1 ``` @@ -35,7 +35,7 @@ repositories { } dependencies { - implementation 'com.apify:apify-client:0.3.0' + implementation 'com.apify:apify-client:0.3.1' } ``` @@ -57,7 +57,7 @@ A complete, copy-pasteable first program (save as `HelloApify.java`). First scaf com.apify apify-client - 0.3.0 + 0.3.1 From 742d7b11f661f4c7f9e5e0056947918d80c57452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Proch=C3=A1zka?= Date: Tue, 14 Jul 2026 20:25:15 +0000 Subject: [PATCH 3/3] fix: guard version drift, align example fragments, make iteration tests eventual-consistency tolerant - Add hermetic test asserting pom == Version.CLIENT_VERSION via a filtered build resource - Align docs/examples.md fragments with their standalone example files - Make create-then-iterate integration assertions tolerant of collection LIST eventual consistency via a bounded retry, fixing intermittent IterationIntegrationTest.iterateRequestQueues failures --- CHANGELOG.md | 13 +++ docs/examples.md | 24 +++-- pom.xml | 14 +++ .../java/com/apify/client/ClientMetaTest.java | 31 +++++++ .../integration/IterationIntegrationTest.java | 88 +++++++++++++++---- .../apify-client-build.properties | 4 + 6 files changed, 149 insertions(+), 25 deletions(-) create mode 100644 src/test/resources-filtered/apify-client-build.properties diff --git a/CHANGELOG.md b/CHANGELOG.md index 62eabc7..09d3ff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.3.1] - 2026-07-14 +### Added + +- Build-time version-drift guard test asserting the Maven project `` (read from a filtered + build resource) equals `Version.CLIENT_VERSION`, so the two can no longer diverge on a release bump. + ### Changed - Verified the client against OpenAPI specification version `v2-2026-07-13T092445Z` and bumped @@ -16,6 +21,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). stats counters; both are already covered by the client's generic status-code error handling and its forward-compatible model deserialization, so no interface or behavior change was needed. +### Fixed + +- Integration tests: create-then-iterate collection assertions now tolerate collection LIST-endpoint + eventual consistency via a bounded retry that rebuilds the iterator between attempts, fixing an + intermittent `IterationIntegrationTest.iterateRequestQueues` failure. +- Documentation: `docs/examples.md` fragments now match their standalone example files (Store + iteration output, the named-storage suffix scheme, and the account example's email line). + ## [0.3.0] - 2026-07-11 ### Added diff --git a/docs/examples.md b/docs/examples.md index a84a6ab..4743610 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -26,10 +26,10 @@ System.out.println("Items in this page: " + items.getCount()); ```java // Named storages persist on your account; each block deletes its storage in a finally so the // example does not leak them. A unique suffix keeps the names from colliding across parallel runs. -String suffix = "-" + System.currentTimeMillis(); +String suffix = Long.toString(System.currentTimeMillis()); // Dataset: create, push items, read them back. -Dataset dataset = client.datasets().getOrCreate("example-ds" + suffix); +Dataset dataset = client.datasets().getOrCreate("java-example-ds-" + suffix); try { client.dataset(dataset.getId()).pushItems(List.of(Map.of("hello", "world"))); PaginationList items = client.dataset(dataset.getId()).listItems(new DatasetListItemsOptions()); @@ -39,7 +39,7 @@ try { } // Key-value store: create, set a record, read it back. -KeyValueStore store = client.keyValueStores().getOrCreate("example-kvs" + suffix); +KeyValueStore store = client.keyValueStores().getOrCreate("java-example-kvs-" + suffix); try { client.keyValueStore(store.getId()).setRecordJson("OUTPUT", Map.of("answer", 42)); Optional record = client.keyValueStore(store.getId()).getRecord("OUTPUT"); @@ -49,7 +49,7 @@ try { } // Request queue: create, add a request, read the head. -RequestQueue queue = client.requestQueues().getOrCreate("example-rq" + suffix); +RequestQueue queue = client.requestQueues().getOrCreate("java-example-rq-" + suffix); try { client.requestQueue(queue.getId()).addRequest(new RequestQueueRequest("https://example.com", "example"), false); RequestQueueHead head = client.requestQueue(queue.getId()).listHead(10L); @@ -63,7 +63,12 @@ try { ```java Optional user = client.me().get(); -user.ifPresent(u -> System.out.println("Account " + u.getId() + " / " + u.getUsername())); +user.ifPresent(u -> { + System.out.println("Account ID: " + u.getId()); + System.out.println("Username: " + u.getUsername()); + // Fields not modelled on User (email, plan, ...) live in the untyped extras map. + System.out.println("Email: " + u.getExtra().get("email")); +}); ``` ## Create a new Actor, build it, run it, wait, and print the finished run log @@ -108,10 +113,11 @@ if (last.isPresent()) { ```java Iterator it = client.store().iterate(new StoreListOptions().limit(10L), 10L); -int shown = 0; -while (shown < 5 && it.hasNext()) { - System.out.println(it.next().getName()); - shown++; +int count = 0; +while (count < 5 && it.hasNext()) { + ActorStoreListItem item = it.next(); + System.out.println((count + 1) + ". " + item.getUsername() + "/" + item.getName()); + count++; } ``` diff --git a/pom.xml b/pom.xml index de02dbf..7d62df7 100644 --- a/pom.xml +++ b/pom.xml @@ -105,6 +105,20 @@ + + + + src/test/resources + + + src/test/resources-filtered + true + + + org.apache.maven.plugins diff --git a/src/test/java/com/apify/client/ClientMetaTest.java b/src/test/java/com/apify/client/ClientMetaTest.java index d225e21..d99c03b 100644 --- a/src/test/java/com/apify/client/ClientMetaTest.java +++ b/src/test/java/com/apify/client/ClientMetaTest.java @@ -2,8 +2,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; import org.junit.jupiter.api.Test; /** Offline tests for version constants, User-Agent formatting, and base-URL resolution. */ @@ -16,6 +20,33 @@ void versionConstants() { assertTrue(Version.API_SPEC_VERSION.endsWith("Z"), Version.API_SPEC_VERSION); } + @Test + void clientVersionMatchesBuildVersion() throws IOException { + // The Maven in pom.xml and Version.CLIENT_VERSION must be bumped in lockstep on every + // release. Nothing else fails the build if they diverge, and version-bump PRs are exactly where + // that slips through, so this test compares Version.CLIENT_VERSION against the built + // project.version (substituted into a filtered resource during the build). Hermetic: reads a + // classpath resource, no network. + Properties props = new Properties(); + try (InputStream in = + ClientMetaTest.class.getResourceAsStream("/apify-client-build.properties")) { + assertNotNull( + in, + "apify-client-build.properties missing from the test classpath; check the pom's filtered" + + " testResource"); + props.load(in); + } + String buildVersion = props.getProperty("project.version"); + assertNotNull(buildVersion, "project.version not present in apify-client-build.properties"); + assertFalse( + buildVersion.contains("${"), + "project.version was not filtered (still a Maven placeholder): " + buildVersion); + assertEquals( + buildVersion, + Version.CLIENT_VERSION, + "pom.xml and Version.CLIENT_VERSION must match; bump them together"); + } + @Test void userAgentFormat() { String ua = ApifyClient.create("token").getUserAgent(); diff --git a/src/test/java/com/apify/client/integration/IterationIntegrationTest.java b/src/test/java/com/apify/client/integration/IterationIntegrationTest.java index d84e349..1772e2d 100644 --- a/src/test/java/com/apify/client/integration/IterationIntegrationTest.java +++ b/src/test/java/com/apify/client/integration/IterationIntegrationTest.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; +import java.util.function.Supplier; import org.junit.jupiter.api.Test; /** @@ -42,6 +43,14 @@ */ class IterationIntegrationTest extends IntegrationBase { + /** + * Attempts and backoff bounding how long {@link #findsAllEventually} waits for a freshly created + * resource to surface in its collection listing. Budget: {@code (ATTEMPTS - 1) * BACKOFF} = ~15s. + */ + private static final int ITER_FIND_ATTEMPTS = 16; + + private static final long ITER_FIND_BACKOFF_MILLIS = 1000L; + /** Iterates until every target id is seen (lazily; stops early) or the iterator is exhausted. */ private static boolean findsAll( Iterator it, Function idOf, Set targets) { @@ -53,6 +62,35 @@ private static boolean findsAll( return remaining.isEmpty(); } + /** + * Like {@link #findsAll}, but tolerant of collection LIST-endpoint eventual consistency. A + * resource created through a write endpoint is not always immediately reflected in its + * collection's LIST response — the write and the list index converge asynchronously on the + * server, so a create-then-iterate assertion that scans exactly once races that convergence and + * flakes when the just-created entity has not yet propagated. This helper rebuilds a fresh + * iterator via {@code newIterator} and re-scans up to {@link #ITER_FIND_ATTEMPTS} times, sleeping + * {@link #ITER_FIND_BACKOFF_MILLIS} between attempts, returning as soon as every target id is + * seen. An already-consistent account matches on the first pass with no sleeping. Mirrors the + * sibling Go and Rust clients' tolerance for this exact intermittent failure. + */ + private static boolean findsAllEventually( + Supplier> newIterator, Function idOf, Set targets) { + for (int attempt = 0; attempt < ITER_FIND_ATTEMPTS; attempt++) { + if (findsAll(newIterator.get(), idOf, targets)) { + return true; + } + if (attempt + 1 < ITER_FIND_ATTEMPTS) { + try { + Thread.sleep(ITER_FIND_BACKOFF_MILLIS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + return false; + } + @Test void iterateDatasetItems() { ApifyClient client = requireClient(); @@ -128,8 +166,12 @@ void iterateDatasets() { } // Page size 1 forces the offset iterator across multiple pages; desc puts the fresh ones // first. - Iterator it = client.datasets().iterate(new StorageListOptions().desc(true), 1L); - assertTrue(findsAll(it, Dataset::getId, ids), "iterate should find every created dataset"); + assertTrue( + findsAllEventually( + () -> client.datasets().iterate(new StorageListOptions().desc(true), 1L), + Dataset::getId, + ids), + "iterate should find every created dataset"); } finally { for (String id : ids) { client.dataset(id).delete(); @@ -145,9 +187,11 @@ void iterateKeyValueStores() { for (int i = 0; i < 2; i++) { ids.add(client.keyValueStores().getOrCreate(uniqueName("it-kvs-" + i)).getId()); } - Iterator it = - client.keyValueStores().iterate(new StorageListOptions().desc(true), 1L); - assertTrue(findsAll(it, KeyValueStore::getId, ids)); + assertTrue( + findsAllEventually( + () -> client.keyValueStores().iterate(new StorageListOptions().desc(true), 1L), + KeyValueStore::getId, + ids)); } finally { for (String id : ids) { client.keyValueStore(id).delete(); @@ -163,9 +207,11 @@ void iterateRequestQueues() { for (int i = 0; i < 2; i++) { ids.add(client.requestQueues().getOrCreate(uniqueName("it-rq-" + i)).getId()); } - Iterator it = - client.requestQueues().iterate(new StorageListOptions().desc(true), 1L); - assertTrue(findsAll(it, RequestQueue::getId, ids)); + assertTrue( + findsAllEventually( + () -> client.requestQueues().iterate(new StorageListOptions().desc(true), 1L), + RequestQueue::getId, + ids)); } finally { for (String id : ids) { client.requestQueue(id).delete(); @@ -182,8 +228,9 @@ void iterateTasks() { ids.add( client.tasks().create(TaskIntegrationTest.taskDef(uniqueName("it-task-" + i))).getId()); } - Iterator it = client.tasks().iterate(new ListOptions().desc(true), 1L); - assertTrue(findsAll(it, Task::getId, ids)); + assertTrue( + findsAllEventually( + () -> client.tasks().iterate(new ListOptions().desc(true), 1L), Task::getId, ids)); } finally { for (String id : ids) { client.task(id).delete(); @@ -203,8 +250,11 @@ void iterateSchedules() { .create(ScheduleIntegrationTest.scheduleDef(uniqueName("it-sch-" + i))) .getId()); } - Iterator it = client.schedules().iterate(new ListOptions().desc(true), 1L); - assertTrue(findsAll(it, Schedule::getId, ids)); + assertTrue( + findsAllEventually( + () -> client.schedules().iterate(new ListOptions().desc(true), 1L), + Schedule::getId, + ids)); } finally { for (String id : ids) { client.schedule(id).delete(); @@ -224,8 +274,11 @@ void iterateWebhooks() { .create(WebhookIntegrationTest.webhookDef("https://example.com/it-wh-" + i)) .getId()); } - Iterator it = client.webhooks().iterate(new ListOptions().desc(true), 1L); - assertTrue(findsAll(it, Webhook::getId, ids)); + assertTrue( + findsAllEventually( + () -> client.webhooks().iterate(new ListOptions().desc(true), 1L), + Webhook::getId, + ids)); } finally { for (String id : ids) { client.webhook(id).delete(); @@ -246,8 +299,11 @@ void iterateActors() { .getId()); } // Restrict to the current user's Actors so iteration finds the freshly-created ones quickly. - Iterator it = client.actors().iterate(new ActorListOptions().my(true).desc(true), 1L); - assertTrue(findsAll(it, Actor::getId, ids)); + assertTrue( + findsAllEventually( + () -> client.actors().iterate(new ActorListOptions().my(true).desc(true), 1L), + Actor::getId, + ids)); } finally { for (String id : ids) { client.actor(id).delete(); diff --git a/src/test/resources-filtered/apify-client-build.properties b/src/test/resources-filtered/apify-client-build.properties new file mode 100644 index 0000000..fbeda9e --- /dev/null +++ b/src/test/resources-filtered/apify-client-build.properties @@ -0,0 +1,4 @@ +# Generated by Maven resource filtering during the build. The value is substituted from the +# project's in pom.xml, so the version-drift test can compare it against +# Version.CLIENT_VERSION without hardcoding either side. Do not edit by hand. +project.version=${project.version}