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
15 changes: 2 additions & 13 deletions .github/workflows/rust-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,24 +77,13 @@ jobs:
env:
# The integration-test token is stored as a repository secret.
APIFY_TOKEN: ${{ secrets.APIFY_TOKEN }}
# Limit parallelism to be gentle on the shared test account. The documentation example
# programs in `tests/examples.rs` (test names prefixed `example_`) are exercised by the
# standalone `Test examples` step below, so they are skipped here to keep the two
# concerns separate.
# `example_*` tests run in the `Test examples` step below.
run: cargo test --lib --tests --verbose -- --skip example_ --test-threads=4

# Standalone CI step that verifies the documentation examples actually work. It runs the
# example programs from `examples/` end-to-end against the live API (via the `example_*`
# smoke tests in `tests/examples.rs`, each of which executes `cargo run --example <name>`)
# and runs the in-documentation code snippets as doctests (`cargo test --doc`, which
# compiles every fenced `rust` block in the README and the `docs/` pages and runs the
# runnable ones). Both are required by the documentation requirements: each documentation
# example has a CI test that actually runs the code.
# Runs the example programs and the documentation doctests, see "Testing" in README.md.
- name: Test examples
env:
APIFY_TOKEN: ${{ secrets.APIFY_TOKEN }}
# Match the integration-test thread cap so the example programs and any doctests that hit
# the live API stay gentle on the shared account.
run: |
cargo test --test examples --verbose -- --test-threads=4
cargo test --doc --verbose -- --test-threads=4
87 changes: 16 additions & 71 deletions .github/workflows/rust-publish.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
name: Publish Rust client to crates.io

# Language-specific publish workflow for the Rust client. It is triggered manually only
# (workflow_dispatch) so a maintainer deliberately decides when a release is cut. Publishing
# to the crates.io registry is the language-specific distribution standard for Rust.
#
# In addition to publishing the crate, this workflow tags the released commit and creates a
# matching GitHub release. The release tag is derived from the single source of truth (the
# `version` field in Cargo.toml, which is also what `CLIENT_VERSION` reads via CARGO_PKG_VERSION)
# so the published crate version, the git tag, and the in-code client version can never disagree.
#
# Authentication to crates.io uses Trusted Publishing (OIDC) via `rust-lang/crates-io-auth-action`,
# the registry's recommended mechanism — no long-lived crates.io API token is stored as a secret.
# A Trusted Publisher must be configured for the crate on crates.io (this repository + this
# workflow file). The only repository secret still used is the built-in GITHUB_TOKEN, for pushing
# the tag and creating the GitHub release.
# Manual release from master; see "Releasing" in README.md.
on:
workflow_dispatch:
inputs:
Expand All @@ -22,16 +9,12 @@ on:
type: boolean
default: false

# Never allow two publish runs to race; a half-finished publish to a registry or a duplicate
# release tag is hard to undo.
# Two publish runs must never race.
concurrency:
group: rust-publish
cancel-in-progress: false

# The default GITHUB_TOKEN needs write access to push the release tag and create the GitHub
# release. `id-token: write` lets the job mint a short-lived OpenID Connect token that
# `rust-lang/crates-io-auth-action` exchanges for an ephemeral crates.io publish token
# (Trusted Publishing) — so no long-lived crates.io registry secret is stored in the repo.
# `contents: write` for the tag and release, `id-token: write` for crates.io Trusted Publishing.
permissions:
contents: write
id-token: write
Expand All @@ -43,12 +26,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
with:
# Full history so the tag points at the real commit and tag lookups work.
# Full history for the tag lookups.
fetch-depth: 0

# A published crate version and its release tag are immutable, so a release must only ever
# be cut from master. Refuse to run from any other ref, even when dispatched manually, so a
# maintainer cannot accidentally publish an unmerged feature-branch commit as a version.
# A released version is immutable, so only master may be released.
- name: Require master branch
run: |
if [ "${GITHUB_REF}" != "refs/heads/master" ]; then
Expand All @@ -70,7 +51,7 @@ jobs:
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}

# Gate the release on the same quality bar as CI so a broken build can never be published.
# Same quality gate as CI.
- name: Check formatting
run: cargo fmt --all -- --check

Expand All @@ -80,10 +61,7 @@ jobs:
- name: Build
run: cargo build --verbose

# Derive the release tag from the single source of truth (Cargo.toml `version`, which is also
# what CLIENT_VERSION exposes via CARGO_PKG_VERSION). Keeping the tag in lock step with the
# crate version means consumers who read the constant and consumers who pull a tag always see
# the same version.
# The tag comes from the `version` in Cargo.toml, which `CLIENT_VERSION` reads as well.
- name: Resolve version from Cargo.toml
id: version
run: |
Expand All @@ -101,8 +79,7 @@ jobs:
echo "tag=v${version}" >> "$GITHUB_OUTPUT"
echo "Resolved release tag: v${version}"

# Fail early (before any publish or tag creation) if this version was already released, so a
# publish run can never silently no-op or clobber an existing release.
# Fail before publishing when this version was already released.
- name: Ensure tag does not already exist
env:
TAG: ${{ steps.version.outputs.tag }}
Expand All @@ -116,31 +93,13 @@ jobs:
exit 1
fi

# Always verify packaging works (this also runs as part of `cargo publish`, but doing it
# explicitly surfaces packaging problems before any registry interaction or tag creation).
# No registry credentials are needed for `--dry-run`: it only packages and verifies the
# crate locally without contacting the registry for authentication.
# Surfaces packaging problems before any tag or registry interaction.
- name: Verify package (dry run)
run: cargo publish --dry-run --verbose

# Ordering rationale (deliberate): the git tag and GitHub release are created and pushed
# BEFORE `cargo publish`. Unlike Go — where pushing the tag *is* the publish, so there is one
# atomic step — Rust has two irreversible-ish actions (push a tag, publish to crates.io) that
# cannot be made a single atomic transaction. We tag first because:
# * The git tag is the canonical, immutable "this commit is version X" marker; deriving it
# and the GitHub release from the source-of-truth version up front guarantees they always
# agree with each other and with the crate version.
# * A failure AFTER tagging but BEFORE publish is cleanly recoverable: delete the just-pushed
# tag and release, then re-run. The "Ensure tag does not already exist" guard makes that
# re-run safe and refuses to clobber.
# * A failure AFTER publish (the genuinely unrecoverable step, since a crates.io version can
# never be re-published) then leaves the tag and release already in place and consistent,
# rather than a published crate with no tag/release. Recovery is just re-running the two
# git/release steps, never the publish.
# If `cargo publish` fails here, recover by deleting the tag/release and bumping the version,
# or by manually completing the publish for the existing tag.

# Tag the released commit so the crate version, git tag, and GitHub release all line up.
# The tag and release come before `cargo publish`, see "Releasing" in README.md.

# Tag the released commit.
- name: Create and push release tag
if: ${{ github.event.inputs.dry_run != 'true' }}
env:
Expand All @@ -151,16 +110,13 @@ jobs:
git tag -a "${TAG}" -m "Release ${TAG}"
git push origin "${TAG}"

# Extract the matching CHANGELOG section so the GitHub release page shows the actual changes
# for this version rather than a static pointer. Falls back to a one-liner if the section is
# missing, so a forgotten CHANGELOG entry never blocks the release.
# The release notes are the CHANGELOG section of this version.
- name: Build release notes from CHANGELOG
if: ${{ github.event.inputs.dry_run != 'true' }}
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
# Match the heading as a literal string (index) so the version's dots and brackets are
# not interpreted as a regex/character class.
# `index` matches the heading literally, not as a regex.
notes=$(awk -v ver="## [${VERSION}]" '
index($0, ver)==1 {capture=1; next}
capture && /^## / {exit}
Expand All @@ -175,12 +131,7 @@ jobs:
echo "__EOF__"
} >> "$GITHUB_ENV"

# Create a GitHub release for the tag. Uses the default GITHUB_TOKEN from repository secrets;
# no personal access token is needed.
# Idempotent: create the release, or update it if one already exists for the tag. This keeps a
# re-run safe in the post-tag/pre-publish recovery path (see the ordering rationale above): if
# a maintainer re-pushes the tag after a partial failure, the release step will not hard-fail
# on an already-existing release.
# Creates the release, or updates it when a re-run finds one.
- name: Create GitHub release
if: ${{ github.event.inputs.dry_run != 'true' }}
env:
Expand All @@ -197,20 +148,14 @@ jobs:
--notes "${RELEASE_NOTES}"
fi

# Trusted Publishing: exchange the GitHub Actions OIDC identity for a short-lived crates.io
# token instead of storing a long-lived registry secret. The action sets a `token` output
# and automatically revokes it in its post step when the job finishes. This requires a
# matching Trusted Publisher to be configured for the crate on crates.io (organization,
# repository and workflow file name).
# Trusted Publishing: a short-lived crates.io token from the OIDC identity.
- name: Authenticate to crates.io (Trusted Publishing)
id: auth
if: ${{ github.event.inputs.dry_run != 'true' }}
uses: rust-lang/crates-io-auth-action@v1

- name: Publish to crates.io
# Skip the actual publish when the run was dispatched as a dry run.
if: ${{ github.event.inputs.dry_run != 'true' }}
env:
# Short-lived token minted by Trusted Publishing (no stored registry secret).
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
run: cargo publish --verbose
82 changes: 82 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,88 @@ All notable changes to the Rust Apify API client are documented here. The format
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres
to [Semantic Versioning](https://semver.org/).

## [0.15.0] - 2026-10-29

Ports the `apify-client-js` v3 changes.

### Added
- Timeout tiers: `Timeout` (`Short`, `Medium`, `Long`, `NoTimeout`, `Custom`), the builder options
`timeout_short`, `timeout_medium`, `timeout_long` and `timeout_max`, `with_timeout` on every
resource client and `RequestQueueClient::with_timeout_cap`.
- `HttpCompressor` with `BrotliHttpCompressor` and `GzipHttpCompressor` (configurable quality), set
with `ApifyClientBuilder::http_compressor`.
- `ApifyClientBuilder::default_header`.
- `ApifyClient::stats`: the `Statistics` of the calls, requests and `429` responses.
- `KeyValueStoreClient::set_record_raw_with_options`, `set_record_json_with_options` and
`SetRecordOptions`, with `do_not_retry_timeouts`.
- `RunListOptions::deployment`, `StoreListOptions::include_input_schema` and `LogOptions::download`.
- `RunClient::iterate_dataset_items`, `RunDatasetItemsIterator`: iterate the items of a run's
default dataset while the run is still producing them, like the reference client's
`iterateDatasetItems`.
- Typed, normalized URL fields: `ActorRun::console_url` and `output`, `Task::standby_url`,
`Dataset::console_url` and `items_public_url`, `KeyValueStore::console_url`, `keys_public_url` and
`records_public_url`, `KeyValueStoreKey::record_public_url`, `RequestQueue::console_url`,
`ActorStoreListItem::url` and `user_picture_url`, `User::profile`, `WebhookDispatch::webhook`.

- Log redirection like the reference client's `StreamedLog`: `StreamedLog` with `start` and `stop`,
`StreamedLogOptions`, and the `LogSink` trait (`StdoutLogSink`, or any `Fn(&str)` closure) behind
`LogRedirect`. `ActorClient::call` and `call_raw` redirect the log of the run to the standard
output by default, with the `ActorCallOptions::log` option to change or disable it. A redirection
that ends early on an error does not fail the call: a `Log redirection stopped due to error: ..`
message goes to the sink.
- `ActorCallOptions` and `TaskStartOptions`.

### Changed
- Synced with Apify OpenAPI spec `v2-2026-10-29T100357Z` (`API_SPEC_VERSION`).
- **Breaking:** every method has a timeout tier (5s, 30s or 360s by default) instead of one 360s
timeout; `timeout` is now an alias of `timeout_max`. `wait_for_finish(None)` has no timeout, and
a `waitForFinish` request gets the server-side wait added to its tier.
- **Breaking:** `HttpRequest::timeout` may be `timeouts::NO_TIMEOUT`, which a custom `HttpBackend`
must not abort.
- **Breaking:** any `404` resolves to `None`, not only `record-not-found`, but only for a client that
names its resource by id. `run.dataset()`, `run.log()` and the other chained clients return it as
an error.
- **Breaking:** `get_statistics`, `get_input`, `schedule.get_log` and `get_openapi_definition`
return the `404` as an error and no longer wrap the result in `Option`.
- **Breaking:** `LogClient::stream` and `stream_with_options` return an `Option`: `None` for a
missing log.
- **Breaking:** `RunClient::get_streamed_log` takes `StreamedLogOptions` and returns a
`StreamedLog` (`None` for a missing run when redirecting by default), and
`get_streamed_log_with_options` is removed. For the raw chunks of the log, use
`run.log().stream_with_options(..)`.
- **Breaking:** `ActorClient::call` and `call_raw` take `ActorCallOptions` (the start options, the
wait budget `wait_secs` and the `log` option) instead of `ActorStartOptions` and `wait_secs`.
`TaskClient::start` and `call` take `TaskStartOptions`, which has no `content_type` and no
`force_permission_level`, because `POST /actor-tasks/{id}/runs` does not document them.
- **Breaking:** `call` starts the run, and `TaskClient::call` too, without a client-side timeout
unless the client has `with_timeout`, which then applies to the polls too. A start that outlives
a tier is no longer aborted and retried into a second run. `start`, `start_raw`, `build` and
`TaskClient::start` do not retry a transport timeout either.
- **Breaking:** the internals `QueryParams`, `common::{encode_path_segment, build_user_agent,
to_safe_id, create_hmac_signature, sign_storage_content}`, `http_client::HttpClient` and
`http_client::RetryConfig` (which also lost its `timeout` field) are no longer public.
- `wait_for_finish(Some(n))` gives each poll the `Medium` tier plus the hold it asks for, so a
stalled connection cannot hang it.
- `WaitForResources::ForSecs` with an infinite or huge value waits without a limit, and `NaN`
fails with `InvalidArgument`, instead of panicking.
- `RequestQueueClient::get_request` no longer sends `clientKey`.
- **Breaking:** `ActorStartOptions::timeout_secs` and `RunResurrectOptions::timeout_secs` are now
`run_timeout_secs`.
- **Breaking:** `LogOptions`, `RunListOptions` and `StoreListOptions` gained a public field.
- **Breaking:** an id, record key or request id that is empty or a dot segment fails with
`InvalidArgument`; ids are percent-encoded and every `/` of an id becomes `~`.
- **Breaking:** `dataset.push_items` rejects anything but an object or an array of objects, and does
not retry a transport timeout.
- **Breaking:** the URL fields listed above are normalized, and no longer in `extra`.
- **Breaking:** `RequestCompression` moved to `http_compressors` (still re-exported at the crate
root).
- `ListRequestsOptions::exclusive_start_id` is deprecated in favor of `cursor`.
- The storages and the log chained from a `last_run()` with a `status` or `origin` filter now
belong to the filtered run.
- A zero `timeout_*` option or `with_timeout` fails the requests with `InvalidArgument`.
- `UserClient::limits`, `monthly_usage` and `update_limits` work for any user, not only `me`.
- `batch_add_requests` serializes each request once and rejects an oversized one before sending.

## [0.14.1] - 2026-10-26

### Changed
Expand Down
9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[package]
name = "apify-client"
version = "0.14.1"
version = "0.15.0"
authors = ["Apify Technologies <[email protected]>"]
description = "An official, but experimental, AI-generated and AI-maintained Rust client for the Apify API (https://apify.com)."
description = "Rust client for the Apify API (https://apify.com)."
license = "Apache-2.0"
edition = "2021"
repository = "https://github.com/apify/apify-client-rust"
Expand All @@ -20,7 +20,8 @@ chrono = { version = "0.4", features = ["serde"] }
thiserror = "2.0"
async-trait = "0.1"
futures-util = "0.3"
tokio = { version = "1", features = ["time"] }
tokio = { version = "1", features = ["time", "rt", "sync", "macros"] }
url = "2"
base64 = "0.22"
hmac = "0.12"
sha2 = "0.10"
Expand All @@ -33,7 +34,7 @@ rustls-webpki = { version = "0.103.16", optional = true }
webpki-roots = { version = "1.0.10", optional = true }

[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "test-util"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "test-util", "net", "io-util"] }
uuid = { version = "1", features = ["v4"] }

[features]
Expand Down
Loading
Loading