apify-client 0.4.1

An official, but experimental, AI-generated and AI-maintained Rust client for the Apify API (https://apify.com).
Documentation
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.
on:
  workflow_dispatch:
    inputs:
      dry_run:
        description: 'Run all checks but do not publish, create/push the tag, or create the release.'
        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.
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.
permissions:
  contents: write
  id-token: write

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          # Full history so the tag points at the real commit and tag lookups work.
          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.
      - name: Require master branch
        run: |
          if [ "${GITHUB_REF}" != "refs/heads/master" ]; then
            echo "::error::Publishing is only allowed from master, but this run is on '${GITHUB_REF}'."
            exit 1
          fi

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy

      - name: Cache cargo registry and build
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            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.
      - name: Check formatting
        run: cargo fmt --all -- --check

      - name: Clippy (deny warnings)
        run: cargo clippy --all-targets -- -D warnings

      - 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.
      - name: Resolve version from Cargo.toml
        id: version
        run: |
          version=$(cargo metadata --no-deps --format-version 1 \
            | python3 -c 'import json,sys; print(json.load(sys.stdin)["packages"][0]["version"])')
          if [ -z "${version}" ]; then
            echo "::error::Could not read the crate version from Cargo.toml."
            exit 1
          fi
          if ! echo "${version}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
            echo "::error::Crate version '${version}' is not a bare semver (X.Y.Z)."
            exit 1
          fi
          echo "version=${version}" >> "$GITHUB_OUTPUT"
          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.
      - name: Ensure tag does not already exist
        env:
          TAG: ${{ steps.version.outputs.tag }}
        run: |
          if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
            echo "::error::Tag ${TAG} already exists locally."
            exit 1
          fi
          if git ls-remote --exit-code --tags origin "${TAG}" >/dev/null 2>&1; then
            echo "::error::Tag ${TAG} already exists on origin; bump the version in Cargo.toml first."
            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.
      - 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.
      - name: Create and push release tag
        if: ${{ github.event.inputs.dry_run != 'true' }}
        env:
          TAG: ${{ steps.version.outputs.tag }}
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          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.
      - 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.
          notes=$(awk -v ver="## [${VERSION}]" '
            index($0, ver)==1 {capture=1; next}
            capture && /^## / {exit}
            capture {print}
          ' CHANGELOG.md)
          if [ -z "$(echo "${notes}" | tr -d '[:space:]')" ]; then
            notes="Apify Rust client v${VERSION}. See CHANGELOG.md for details."
          fi
          {
            echo "RELEASE_NOTES<<__EOF__"
            echo "${notes}"
            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.
      - name: Create GitHub release
        if: ${{ github.event.inputs.dry_run != 'true' }}
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG: ${{ steps.version.outputs.tag }}
        run: |
          if gh release view "${TAG}" >/dev/null 2>&1; then
            gh release edit "${TAG}" \
              --title "${TAG}" \
              --notes "${RELEASE_NOTES}"
          else
            gh release create "${TAG}" \
              --title "${TAG}" \
              --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).
      - 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