Skip to content

feat(actors): add apify actors exists, ls --name-only, and improve push name-collision UX#1260

Draft
DaveHanns wants to merge 1 commit into
masterfrom
feat/f61-actors-ls-exists-collision-ux
Draft

feat(actors): add apify actors exists, ls --name-only, and improve push name-collision UX#1260
DaveHanns wants to merge 1 commit into
masterfrom
feat/f61-actors-ls-exists-collision-ux

Conversation

@DaveHanns

Copy link
Copy Markdown
Contributor

Summary

Adds two small, scripting-friendly primitives for asking "is this Actor name available?" and improves the CLI transcript when apify push reuses an existing Actor or fails on a name conflict.

  • New: apify actors exists <name> — exits 0 if the Actor exists, 1 if not. Accepts a short name (resolved in the logged-in user's namespace), a fully qualified <username>/<name>, or an Actor ID. --json emits { exists, query, resolvedId, actor }.
  • New flag: apify actors ls --name-only — prints one <username>/<name> per line and short-circuits before the per-row hydration Promise.all (which normally fetches an extra .get() + runs list per row for the table). Composes with --my, --limit, --offset, --desc.
  • apify push transcript when the Actor already exists in your namespace: previously silent — now emits Info: Updating existing Actor <username>/<name> (id=…).
  • apify push error when POST /v2/acts fails on a duplicate/reserved name: the raw API message (e.g. Actor name is already used) is rewritten with actionable next steps.

Motivation

apify actors ls --my and apify push are the entry points people reach for when scripting Actor management (CI pipelines, template scaffolders, agent-driven tooling). Two workflows are awkward today:

1. "Is this name free?" has no first-class answer

apify actors ls --my prints a decorated table (Runs / Last run / Build columns) that is hard to grep and, per row, does an extra apifyClient.actor(id).get() + runs list fetch just for the display. Callers scripting a collision check end up reaching for the raw REST API instead — e.g.

curl -sH "Authorization: Bearer $APIFY_TOKEN" \
  'https://api.apify.com/v2/acts?my=1&limit=100' \
  | jq -r '.data.items[].name'

With this PR:

apify actors ls --my --name-only
# apify-user/my-crawler
# apify-user/hello-world

apify actors exists my-crawler && echo taken || echo free

And the canonical scripted "create only if free" pattern becomes:

apify actors exists my-crawler || apify push

2. apify push is silent when it reuses an existing Actor

The relevant branch in src/commands/actors/push.ts today (before this PR) is:

actor = (await apifyClient.actor(`${usernameOrId}/${actorConfig!.name}`).get())!;
if (actor) {
    actorId = actor.id;
    // <-- no log emitted; identical-looking to the "create" branch below
} else {
    ...
    actor = await apifyClient.actors().create(newActor);
    info({ message: `Created Actor with name ${actorConfig!.name} on Apify.` });
}

If a user (or an automated pipeline) forgets they have already used a given name in their namespace, the push silently overwrites the previously deployed Actor with no visible signal. After this PR the transcript shows explicitly:

Info: Updating existing Actor apify-user/my-crawler (id=E2jjCZBezvAZnX8Rb).
Info: Deploying Actor 'my-crawler' to Apify.

3. POST /v2/acts duplicate-name errors are opaque

The pre-flight apify.actor("<user>/<name>").get() doesn't catch (a) reserved / conflicting names, or (b) a race where a parallel process claimed the name between the .get() and the .create(). When that happens the surfaced error is the raw API message with no next step:

Error: Actor name is already used

After this PR:

Error: Cannot create Actor "my-crawler": the name is unavailable on the Apify platform (Actor name is already used).
Next steps:
  - Rename the Actor: edit the "name" field in .actor/actor.json and run 'apify push' again.
  - Push to an existing Actor: 'apify push <actor-id-or-username/name>'.
  - List names you already own: 'apify actors ls --my --name-only'.
  - Check whether a name is free: 'apify actors exists <name>'.

Design notes

  • Exit-code convention for exists: 0 = exists (name is taken), 1 = not found. Chosen so a bare apify actors exists foo && echo taken idiom works without extra flags, matching test, grep, gh repo view, kubectl get.
  • Short-name resolution in exists: if the argument has no / and doesn't look like an Actor ID (/^[A-Za-z0-9]{17}$/), it's resolved as <logged-in-user>/<name>, so apify actors exists my-crawler targets the user's own namespace by default.
  • --name-only short-circuits before hydration: the existing ls code path awaits a per-row apifyClient.actor(id).get() + runs list for the table. When the caller only needs identifiers, that work is wasted (and can be slow for --limit 100), so --name-only returns straight from apifyClient.actors().list().
  • Error rewrite in push is defensive: the name-conflict detection matches on already used, not available, or duplicate in the error message. If the pattern doesn't match, the original error is re-thrown unchanged.

Test plan

  • apify actors ls --my --name-only prints <username>/<name> lines and exits 0.
  • apify actors ls --my --name-only with zero Actors prints nothing and exits 0.
  • apify actors ls --my --name-only --json still returns the JSON payload (the flag is a no-op under --json).
  • apify actors exists <name-you-own> → exit 0, prints Actor "<user>/<name>" exists (id=…)..
  • apify actors exists <name-that-does-not-exist> → exit 1, prints Actor "<user>/<name>" does not exist..
  • apify actors exists <name> --json → exit 0/1 and JSON with exists: true/false.
  • apify actors exists apify/hello-world resolves the fully-qualified name (doesn't prepend the logged-in username).
  • apify push in a directory whose .actor/actor.json names an Actor the user already owns prints Info: Updating existing Actor ….
  • apify push where POST /v2/acts fails with a duplicate-name response prints the rewritten error with the four suggested next steps.
  • pnpm tsc --noEmit passes.

Files changed

  • src/commands/actors/exists.ts — new command.
  • src/commands/actors/ls.ts — add --name-only flag; short-circuit before hydration.
  • src/commands/actors/push.ts — emit Info: Updating existing Actor …; rewrite name-conflict errors with actionable next steps.
  • src/commands/actors/_index.ts — register ActorsExistsCommand.

Surfaced during an evaluation of Apify surfaces for agent-driven Actor development.

…actors ls`, and improve name-collision UX on push

Scripting-friendly name checks
------------------------------

`apify actors ls` and `apify push` are used in automation (CI, agent
scripts, template scaffolding) that needs to answer "is this Actor name
free?" before creating one. Today there is no built-in way to do that:

- `apify actors ls --my` prints a decorated table (Runs / Last run /
  Build columns) intended for a terminal, and does an extra `.get()` +
  runs list per row for hydration. Callers scripting a collision check
  have to reach for `curl` against `/v2/acts?my=1` or grep the table.
- There is no cheap "does this name exist?" primitive at all, so
  scripts fall back to grepping `apify actors info <name>`'s error
  output.

This adds:

- `apify actors ls --name-only` — prints one `<username>/<name>` per
  line and short-circuits before the per-row hydration `Promise.all`.
  Composes with `--my`, `--limit`, `--offset`, `--desc`.
- `apify actors exists <name>` — new subcommand. Exits 0 if the Actor
  exists, 1 if not. Accepts a short name (resolved in the logged-in
  user's namespace), a fully qualified `<username>/<name>`, or an
  Actor ID. `--json` emits `{ exists, query, resolvedId, actor }`.

Combined, `apify actors exists my-crawler || apify push` is now the
canonical scripted collision check, replacing the raw-API workaround.

`apify push` collision UX
-------------------------

Two changes to make destination + failure modes obvious in the CLI
transcript:

1. When `apify push` finds an existing Actor under the current user's
   namespace and reuses it, it now emits an `Info:` line
   ("Updating existing Actor <username>/<name> (id=…)."). Previously
   this branch was silent and looked identical to creating a new
   Actor, so a user who forgot they had already claimed a name would
   silently overwrite their prior version.

2. When `apifyClient.actors().create()` fails with a name-conflict
   error (reserved name, race with a parallel process, etc.), the raw
   API message ("Actor name is already used") is now rewritten with
   actionable next steps: rename in `.actor/actor.json`, push to an
   existing Actor by id, list your own names with the new
   `--name-only` flag, or query with `apify actors exists`.

Surfaced during an evaluation of Apify surfaces for agent-driven Actor development.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@DaveHanns DaveHanns requested a review from l2ysho as a code owner July 4, 2026 23:00
@DaveHanns DaveHanns marked this pull request as draft July 5, 2026 11:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants