From 07e801abcbaa2a5d858ada12c8acddc3365b3cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Fri, 15 May 2026 00:00:31 +0200 Subject: [PATCH 01/15] fix(actors): correctly parse waitforfinish flag Resolves an issue where waitForFinishMillis could be NaN for invalid flag inputs, leading to unexpected behavior. --- src/commands/actors/push.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index 927709226..d5e648955 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -190,9 +190,8 @@ export class ActorsPushCommand extends ApifyCommand { buildTag = DEFAULT_BUILD_TAG; } - const waitForFinishMillis = Number.isNaN(this.flags.waitForFinish) - ? undefined - : Number.parseInt(this.flags.waitForFinish!, 10) * 1000; + const parsedWaitForFinish = this.flags.waitForFinish ? Number.parseInt(this.flags.waitForFinish, 10) : Number.NaN; + const waitForFinishMillis = Number.isFinite(parsedWaitForFinish) ? parsedWaitForFinish * 1000 : undefined; // User can override actorId of pushing Actor. // It causes that we push Actor to this id but attributes in localConfig will remain same. From d9bf8ed636a85fe1e24b91a7490cf88510d9ea8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Mon, 18 May 2026 12:41:31 +0200 Subject: [PATCH 02/15] extract parsing function and add regression tests --- src/commands/actors/push.ts | 4 ++-- src/lib/utils.ts | 7 ++++++ test/local/lib/parse-wait-for-finish.test.ts | 23 ++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 test/local/lib/parse-wait-for-finish.test.ts diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index d5e648955..34b3def81 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -25,6 +25,7 @@ import { getLocalUserInfo, getLoggedClientOrThrow, outputJobLog, + parseWaitForFinishMillis, printJsonToStdout, } from '../../lib/utils.js'; @@ -190,8 +191,7 @@ export class ActorsPushCommand extends ApifyCommand { buildTag = DEFAULT_BUILD_TAG; } - const parsedWaitForFinish = this.flags.waitForFinish ? Number.parseInt(this.flags.waitForFinish, 10) : Number.NaN; - const waitForFinishMillis = Number.isFinite(parsedWaitForFinish) ? parsedWaitForFinish * 1000 : undefined; + const waitForFinishMillis = parseWaitForFinishMillis(this.flags.waitForFinish); // User can override actorId of pushing Actor. // It causes that we push Actor to this id but attributes in localConfig will remain same. diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 852d1b242..c96254b0c 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -840,3 +840,10 @@ export function shellConfigFile(userHomeDirectory: string, shell: ReturnType { + it('returns undefined when flag is omitted', () => { + expect(parseWaitForFinishMillis(undefined)).toBeUndefined(); + }); + + it('returns undefined for non-numeric input', () => { + expect(parseWaitForFinishMillis('abc')).toBeUndefined(); + }); + + it('returns undefined for zero', () => { + expect(parseWaitForFinishMillis('0')).toBeUndefined(); + }); + + it('returns undefined for negative values', () => { + expect(parseWaitForFinishMillis('-5')).toBeUndefined(); + }); + + it('converts positive seconds to milliseconds', () => { + expect(parseWaitForFinishMillis('30')).toBe(30_000); + }); +}); From b1d8b3edf218de3a8a35d6d277d20afd5b7fd736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Mon, 18 May 2026 12:51:44 +0200 Subject: [PATCH 03/15] add pooling loop for push --- src/commands/actors/push.ts | 40 ++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index 34b3def81..ae4d2bd29 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -6,7 +6,12 @@ import type { Actor, ActorCollectionCreateOptions, ActorDefaultRunOptions } from import open from 'open'; import { fetchManifest } from '@apify/actor-templates'; -import { ACTOR_JOB_STATUSES, ACTOR_SOURCE_TYPES, MAX_MULTIFILE_BYTES } from '@apify/consts'; +import { + ACTOR_JOB_STATUSES, + ACTOR_JOB_TERMINAL_STATUSES, + ACTOR_SOURCE_TYPES, + MAX_MULTIFILE_BYTES, +} from '@apify/consts'; import { createHmacSignature } from '@apify/utilities'; import { ApifyCommand } from '../../lib/command-framework/apify-command.js'; @@ -358,18 +363,16 @@ Skipping push. Use --force to override.`, waitForFinish: 2, // NOTE: We need to wait some time to Apify open stream and we can create connection }); - try { - // While the log is streaming, forward interrupt signals to a - // platform-side abort so the build doesn't keep running after the - // user gives up waiting (Ctrl+C, SIGTERM from a parent process, - // SIGHUP from a closing terminal). The `using` binding guarantees - // the listener is removed before we poll for final status. - using _signalHandler = useAbortJobOnSignal({ - apifyClient, - kind: 'build', - jobId: build.id, - }); + // Forward interrupt signals (Ctrl+C, SIGTERM, SIGHUP) to a platform-side + // abort for the lifetime of log streaming AND status polling, so the + // build doesn't keep running after the user gives up waiting. + using _signalHandler = useAbortJobOnSignal({ + apifyClient, + kind: 'build', + jobId: build.id, + }); + try { await outputJobLog({ job: build, timeoutMillis: waitForFinishMillis, apifyClient }); } catch (err) { warning({ message: 'Can not get log:' }); @@ -378,6 +381,17 @@ Skipping push. Use --force to override.`, build = (await apifyClient.build(build.id).get())!; + // `outputJobLog` can return before the build is actually terminal (stream + // ended early, timeout hit). Poll so the status branches below see the + // real outcome. + if (waitForFinishMillis !== undefined) { + const deadline = Date.now() + waitForFinishMillis; + while (!ACTOR_JOB_TERMINAL_STATUSES.includes(build.status as never) && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + build = (await apifyClient.build(build.id).get())!; + } + } + if (this.flags.json) { printJsonToStdout(build); return; @@ -402,9 +416,11 @@ Skipping push. Use --force to override.`, // @ts-expect-error FIX THESE TYPES 😢 } else if (build.status === ACTOR_JOB_STATUSES.READY) { warning({ message: 'Build is waiting for allocation.' }); + process.exitCode = CommandExitCodes.BuildTimedOut; // @ts-expect-error FIX THESE TYPES 😢 } else if (build.status === ACTOR_JOB_STATUSES.RUNNING) { warning({ message: 'Build is still running.' }); + process.exitCode = CommandExitCodes.BuildTimedOut; // @ts-expect-error FIX THESE TYPES 😢 } else if (build.status === ACTOR_JOB_STATUSES.ABORTED || build.status === ACTOR_JOB_STATUSES.ABORTING) { warning({ message: 'Build was aborted!' }); From 9e26c3e2c8aefb598dae1aa673807a53b1915a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Mon, 18 May 2026 13:44:30 +0200 Subject: [PATCH 04/15] fix default pooling --- src/commands/actors/push.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index ae4d2bd29..5ad3b72f1 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -383,13 +383,12 @@ Skipping push. Use --force to override.`, // `outputJobLog` can return before the build is actually terminal (stream // ended early, timeout hit). Poll so the status branches below see the - // real outcome. - if (waitForFinishMillis !== undefined) { - const deadline = Date.now() + waitForFinishMillis; - while (!ACTOR_JOB_TERMINAL_STATUSES.includes(build.status as never) && Date.now() < deadline) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - build = (await apifyClient.build(build.id).get())!; - } + // real outcome. With no --wait-for-finish, the flag documents "waits + // forever", so poll without a deadline. + const deadline = waitForFinishMillis === undefined ? Infinity : Date.now() + waitForFinishMillis; + while (!ACTOR_JOB_TERMINAL_STATUSES.includes(build.status as never) && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + build = (await apifyClient.build(build.id).get())!; } if (this.flags.json) { From d8e957b8d5f8627c73d14afcfe4498679f71d446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Mon, 18 May 2026 13:47:11 +0200 Subject: [PATCH 05/15] fix race --- src/commands/actors/push.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index 5ad3b72f1..728b662b9 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -411,6 +411,16 @@ Skipping push. Use --force to override.`, } if (build.status === ACTOR_JOB_STATUSES.SUCCEEDED) { + // Platform updates `taggedBuilds[buildTag]` asynchronously after the + // build finishes. Wait until the tag points at this build so callers + // that immediately `actor.start({ build: buildTag })` don't race it. + if (buildTag) { + while (Date.now() < deadline) { + const a = await actorClient.get(); + if (a?.taggedBuilds?.[buildTag]?.buildId === build.id) break; + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } success({ message: 'Actor was deployed to Apify cloud and built there.' }); // @ts-expect-error FIX THESE TYPES 😢 } else if (build.status === ACTOR_JOB_STATUSES.READY) { From 1cac10f73f8186a5910f4175629774819ae358e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Mon, 18 May 2026 13:52:33 +0200 Subject: [PATCH 06/15] fix 0 hangs forever --- src/lib/utils.ts | 4 ++-- test/local/lib/parse-wait-for-finish.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c96254b0c..a664a5a4f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -607,7 +607,7 @@ export const outputJobLog = async ({ } }); - if (timeoutMillis) { + if (timeoutMillis !== undefined) { nodeTimeout = setTimeout(() => { stream.destroy(); resolve('timeouts'); @@ -844,6 +844,6 @@ export function shellConfigFile(userHomeDirectory: string, shell: ReturnType { expect(parseWaitForFinishMillis('abc')).toBeUndefined(); }); - it('returns undefined for zero', () => { - expect(parseWaitForFinishMillis('0')).toBeUndefined(); + it('returns 0 for zero (no wait)', () => { + expect(parseWaitForFinishMillis('0')).toBe(0); }); it('returns undefined for negative values', () => { From 1867aca2cbc36bd0036e378dc7747141daf68fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Mon, 18 May 2026 13:54:17 +0200 Subject: [PATCH 07/15] cap loop --- src/commands/actors/push.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index 728b662b9..22b5b6015 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -414,8 +414,10 @@ Skipping push. Use --force to override.`, // Platform updates `taggedBuilds[buildTag]` asynchronously after the // build finishes. Wait until the tag points at this build so callers // that immediately `actor.start({ build: buildTag })` don't race it. + // Capped at 30s so an unknown platform delay can't stall push forever. if (buildTag) { - while (Date.now() < deadline) { + const tagDeadline = Math.min(deadline, Date.now() + 30_000); + while (Date.now() < tagDeadline) { const a = await actorClient.get(); if (a?.taggedBuilds?.[buildTag]?.buildId === build.id) break; await new Promise((resolve) => setTimeout(resolve, 1000)); From 5a40540509fa50efb5bba8838714a05ee801d329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Mon, 18 May 2026 14:02:56 +0200 Subject: [PATCH 08/15] fix minor findings --- src/commands/actors/push.ts | 38 +++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index 22b5b6015..e9a0d9143 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -391,6 +391,32 @@ Skipping push. Use --force to override.`, build = (await apifyClient.build(build.id).get())!; } + // Platform updates `taggedBuilds[buildTag]` asynchronously after the + // build finishes. Wait until the tag points at this build so callers + // (including --json automation) that immediately + // `actor.start({ build: buildTag })` don't race it. Capped at 30s so an + // unknown platform delay can't stall push forever. + if (build.status === ACTOR_JOB_STATUSES.SUCCEEDED && buildTag) { + // 30s budget is independent of --wait-for-finish: the build is already + // done, we're only waiting on the platform to update the tag pointer. + const tagDeadline = Date.now() + 30_000; + let tagApplied = false; + while (Date.now() < tagDeadline) { + const a = await actorClient.get(); + if (a?.taggedBuilds?.[buildTag]?.buildId === build.id) { + tagApplied = true; + break; + } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + if (!tagApplied) { + warning({ + message: `Build succeeded but tag "${buildTag}" did not update within 30s; subsequent calls referencing this tag may race.`, + }); + process.exitCode = CommandExitCodes.BuildTimedOut; + } + } + if (this.flags.json) { printJsonToStdout(build); return; @@ -411,18 +437,6 @@ Skipping push. Use --force to override.`, } if (build.status === ACTOR_JOB_STATUSES.SUCCEEDED) { - // Platform updates `taggedBuilds[buildTag]` asynchronously after the - // build finishes. Wait until the tag points at this build so callers - // that immediately `actor.start({ build: buildTag })` don't race it. - // Capped at 30s so an unknown platform delay can't stall push forever. - if (buildTag) { - const tagDeadline = Math.min(deadline, Date.now() + 30_000); - while (Date.now() < tagDeadline) { - const a = await actorClient.get(); - if (a?.taggedBuilds?.[buildTag]?.buildId === build.id) break; - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - } success({ message: 'Actor was deployed to Apify cloud and built there.' }); // @ts-expect-error FIX THESE TYPES 😢 } else if (build.status === ACTOR_JOB_STATUSES.READY) { From 5d120624c49b783e1055e5c7512cdab805637937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Mon, 25 May 2026 18:51:35 +0200 Subject: [PATCH 09/15] remove test --- test/local/lib/parse-wait-for-finish.test.ts | 23 -------------------- 1 file changed, 23 deletions(-) delete mode 100644 test/local/lib/parse-wait-for-finish.test.ts diff --git a/test/local/lib/parse-wait-for-finish.test.ts b/test/local/lib/parse-wait-for-finish.test.ts deleted file mode 100644 index 158d17dee..000000000 --- a/test/local/lib/parse-wait-for-finish.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { parseWaitForFinishMillis } from '../../../src/lib/utils.js'; - -describe('parseWaitForFinishMillis()', () => { - it('returns undefined when flag is omitted', () => { - expect(parseWaitForFinishMillis(undefined)).toBeUndefined(); - }); - - it('returns undefined for non-numeric input', () => { - expect(parseWaitForFinishMillis('abc')).toBeUndefined(); - }); - - it('returns 0 for zero (no wait)', () => { - expect(parseWaitForFinishMillis('0')).toBe(0); - }); - - it('returns undefined for negative values', () => { - expect(parseWaitForFinishMillis('-5')).toBeUndefined(); - }); - - it('converts positive seconds to milliseconds', () => { - expect(parseWaitForFinishMillis('30')).toBe(30_000); - }); -}); From f4f425ae5e3b6638c7d53c1cffee4a573f929efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Wed, 27 May 2026 07:54:47 +0000 Subject: [PATCH 10/15] fix(push): don't fail exit code when user-set wait-for-finish elapses When --wait-for-finish is passed explicitly, hitting the cap with the build still in READY/RUNNING is the user's chosen bounded wait, not a failure. Only surface BuildTimedOut for these states when the flag was omitted. --- src/commands/actors/push.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index e9a0d9143..1c8cde0b3 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -441,11 +441,13 @@ Skipping push. Use --force to override.`, // @ts-expect-error FIX THESE TYPES 😢 } else if (build.status === ACTOR_JOB_STATUSES.READY) { warning({ message: 'Build is waiting for allocation.' }); - process.exitCode = CommandExitCodes.BuildTimedOut; + // User opted into a bounded wait via --wait-for-finish; the build is + // still progressing platform-side, so don't surface that as a failure. + if (this.flags.waitForFinish === undefined) process.exitCode = CommandExitCodes.BuildTimedOut; // @ts-expect-error FIX THESE TYPES 😢 } else if (build.status === ACTOR_JOB_STATUSES.RUNNING) { warning({ message: 'Build is still running.' }); - process.exitCode = CommandExitCodes.BuildTimedOut; + if (this.flags.waitForFinish === undefined) process.exitCode = CommandExitCodes.BuildTimedOut; // @ts-expect-error FIX THESE TYPES 😢 } else if (build.status === ACTOR_JOB_STATUSES.ABORTED || build.status === ACTOR_JOB_STATUSES.ABORTING) { warning({ message: 'Build was aborted!' }); From c3944f806a674371f3d536de983ec5a8277ee540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Wed, 27 May 2026 08:02:45 +0000 Subject: [PATCH 11/15] fix(push): anchor wait-for-finish deadline at build start Compute a single deadline before the build call and share it between log streaming and the status poll loop. Previously the poll loop started a fresh --wait-for-finish budget after outputJobLog returned, so a log stream that died near the cap could double the user's wait. --- src/commands/actors/push.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index 1c8cde0b3..c9e669d53 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -358,6 +358,10 @@ Skipping push. Use --force to override.`, // Build Actor on Apify and wait for build to finish run({ message: `Building Actor ${actor.name}` }); + // Anchor the deadline at build start so log streaming + status polling + // share one budget. Without this, a log stream that dies near the cap + // would let the poll loop wait another full --wait-for-finish on top. + const deadline = waitForFinishMillis === undefined ? Infinity : Date.now() + waitForFinishMillis; let build = await actorClient.build(version, { useCache: true, waitForFinish: 2, // NOTE: We need to wait some time to Apify open stream and we can create connection @@ -373,7 +377,8 @@ Skipping push. Use --force to override.`, }); try { - await outputJobLog({ job: build, timeoutMillis: waitForFinishMillis, apifyClient }); + const logBudgetMs = Number.isFinite(deadline) ? Math.max(0, deadline - Date.now()) : undefined; + await outputJobLog({ job: build, timeoutMillis: logBudgetMs, apifyClient }); } catch (err) { warning({ message: 'Can not get log:' }); console.error(err); @@ -382,10 +387,8 @@ Skipping push. Use --force to override.`, build = (await apifyClient.build(build.id).get())!; // `outputJobLog` can return before the build is actually terminal (stream - // ended early, timeout hit). Poll so the status branches below see the - // real outcome. With no --wait-for-finish, the flag documents "waits - // forever", so poll without a deadline. - const deadline = waitForFinishMillis === undefined ? Infinity : Date.now() + waitForFinishMillis; + // ended early, timeout hit). Poll the remaining budget so the status + // branches below see the real outcome. while (!ACTOR_JOB_TERMINAL_STATUSES.includes(build.status as never) && Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 1000)); build = (await apifyClient.build(build.id).get())!; From a9faf009193d26fa1cf261d423aa8e06034aa97b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Wed, 27 May 2026 08:04:21 +0000 Subject: [PATCH 12/15] fix(push): shorten tag-apply wait and downgrade miss to warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cut the post-build tag-apply budget from 30s to 5s, print a status line so users see why push is still waiting, and drop the non-zero exit code on tag-miss — the build itself succeeded, a lagging tag pointer is a warning, not a failure. --- src/commands/actors/push.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index c9e669d53..3daa053a2 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -397,12 +397,10 @@ Skipping push. Use --force to override.`, // Platform updates `taggedBuilds[buildTag]` asynchronously after the // build finishes. Wait until the tag points at this build so callers // (including --json automation) that immediately - // `actor.start({ build: buildTag })` don't race it. Capped at 30s so an - // unknown platform delay can't stall push forever. + // `actor.start({ build: buildTag })` don't race it. if (build.status === ACTOR_JOB_STATUSES.SUCCEEDED && buildTag) { - // 30s budget is independent of --wait-for-finish: the build is already - // done, we're only waiting on the platform to update the tag pointer. - const tagDeadline = Date.now() + 30_000; + run({ message: `Applying build tag "${buildTag}"...` }); + const tagDeadline = Date.now() + 5_000; let tagApplied = false; while (Date.now() < tagDeadline) { const a = await actorClient.get(); @@ -414,9 +412,8 @@ Skipping push. Use --force to override.`, } if (!tagApplied) { warning({ - message: `Build succeeded but tag "${buildTag}" did not update within 30s; subsequent calls referencing this tag may race.`, + message: `Build succeeded but tag "${buildTag}" did not update within 5s; subsequent calls referencing this tag may race.`, }); - process.exitCode = CommandExitCodes.BuildTimedOut; } } From b36c8c0b798c9c570d120e63c8d4c78840994e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Wed, 27 May 2026 08:41:56 +0000 Subject: [PATCH 13/15] fix(push): support --wait-for-finish=0 as fire-and-forget Skip the post-build tag-apply wait when --wait-for-finish=0 so the flag is truly fire-and-forget regardless of how fast the build completed. Document the 0 semantics in the flag help text. Also picks up stale doc regeneration from #1128 (--describe / --search wording). --- docs/reference.md | 20 ++++++++++++-------- src/commands/actors/push.ts | 8 +++++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/reference.md b/docs/reference.md index 80d1da98f..8b6435a15 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -69,10 +69,11 @@ ARGUMENTS FLAGS -d, --body= The request body (JSON string). Use "-" to read from stdin. - --describe= Describe an endpoint: print every HTTP - method on a path, its summary, and path parameters. - Accepts a path like "actor-runs/{runId}" or - "/v2/actor-runs/{runId}". + --describe= Print a reference for an endpoint + path: its HTTP methods, summary, and path parameters. + Leading slashes and a version prefix in the path are + optional. For example, "actor-runs/{runId}" and + "/v2/actor-runs/{runId}" are both accepted. -H, --header= Additional HTTP header(s). Pass a single "key:value" string, or a JSON object like '{"X-Foo": "bar", "X-Baz": "qux"}' to send multiple @@ -85,9 +86,11 @@ FLAGS -p, --params= Query parameters as a JSON object, e.g. '{"limit": 1, "desc": true}'. - -s, --search= Filter --list-endpoints by a - space-separated query. Each token must appear - (case-insensitive) in method, path, or summary. + -s, --search= Filter results returned by + --list-endpoints. The query is case-insensitive and split + into tokens by spaces. For an endpoint to be returned, + every token must appear in that endpoint's method, path, + or summary. ``` ##### `apify telemetry` @@ -742,7 +745,8 @@ FLAGS is taken from the '.actor/actor.json' file. -w, --wait-for-finish= Seconds for waiting to build to finish, if no value passed, it waits - forever. + forever. Pass 0 to return as soon as the build is + queued (fire-and-forget). ``` ##### `apify actors pull` / `apify pull` diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index 3daa053a2..80f5a165d 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -93,7 +93,8 @@ export class ActorsPushCommand extends ApifyCommand { }), 'wait-for-finish': Flags.string({ char: 'w', - description: 'Seconds for waiting to build to finish, if no value passed, it waits forever.', + description: + 'Seconds for waiting to build to finish, if no value passed, it waits forever. Pass 0 to return as soon as the build is queued (fire-and-forget).', required: false, }), 'open': Flags.boolean({ @@ -397,8 +398,9 @@ Skipping push. Use --force to override.`, // Platform updates `taggedBuilds[buildTag]` asynchronously after the // build finishes. Wait until the tag points at this build so callers // (including --json automation) that immediately - // `actor.start({ build: buildTag })` don't race it. - if (build.status === ACTOR_JOB_STATUSES.SUCCEEDED && buildTag) { + // `actor.start({ build: buildTag })` don't race it. Skipped when + // --wait-for-finish=0 (fire-and-forget). + if (build.status === ACTOR_JOB_STATUSES.SUCCEEDED && buildTag && waitForFinishMillis !== 0) { run({ message: `Applying build tag "${buildTag}"...` }); const tagDeadline = Date.now() + 5_000; let tagApplied = false; From 594dc03d8a38952e1c063f13aac80705c13b3ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Wed, 27 May 2026 09:13:09 +0000 Subject: [PATCH 14/15] fix(push): exit non-zero when finite --wait-for-finish elapses The previous guard was unreachable: when --wait-for-finish is unset, the poll loop only exits on terminal status, so READY/RUNNING branches were dead. Switch the guard to `waitForFinishMillis !== 0` so finite caps that hit the deadline surface a non-zero exit code (useful for scripts), while fire-and-forget (--wait-for-finish=0) still returns 0 on READY. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/commands/actors/push.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index 6ff755673..485954529 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -448,13 +448,13 @@ Skipping push. Use --force to override.`, // @ts-expect-error FIX THESE TYPES 😢 } else if (build.status === ACTOR_JOB_STATUSES.READY) { warning({ message: 'Build is waiting for allocation.' }); - // User opted into a bounded wait via --wait-for-finish; the build is - // still progressing platform-side, so don't surface that as a failure. - if (this.flags.waitForFinish === undefined) process.exitCode = CommandExitCodes.BuildTimedOut; + // Skip exit code for --wait-for-finish=0 (fire-and-forget): READY is + // the expected outcome when the user asked to return immediately. + if (waitForFinishMillis !== 0) process.exitCode = CommandExitCodes.BuildTimedOut; // @ts-expect-error FIX THESE TYPES 😢 } else if (build.status === ACTOR_JOB_STATUSES.RUNNING) { warning({ message: 'Build is still running.' }); - if (this.flags.waitForFinish === undefined) process.exitCode = CommandExitCodes.BuildTimedOut; + if (waitForFinishMillis !== 0) process.exitCode = CommandExitCodes.BuildTimedOut; // @ts-expect-error FIX THESE TYPES 😢 } else if (build.status === ACTOR_JOB_STATUSES.ABORTED || build.status === ACTOR_JOB_STATUSES.ABORTING) { warning({ message: 'Build was aborted!' }); From f00ada8870847e26f67b2aa061594ecf9c52600e Mon Sep 17 00:00:00 2001 From: Richard Solar Date: Wed, 27 May 2026 11:29:22 +0200 Subject: [PATCH 15/15] Update src/commands/actors/push.ts Co-authored-by: Edyta <142720610+szaganek@users.noreply.github.com> --- src/commands/actors/push.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index 485954529..204d3a584 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -94,7 +94,7 @@ export class ActorsPushCommand extends ApifyCommand { 'wait-for-finish': Flags.string({ char: 'w', description: - 'Seconds for waiting to build to finish, if no value passed, it waits forever. Pass 0 to return as soon as the build is queued (fire-and-forget).', + 'In seconds, how long to wait for the build to finish. If no value passed, it waits forever. To return as soon as the build is queued (fire-and-forget), pass 0.', required: false, }), 'open': Flags.boolean({