Skip to content
Open
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
20 changes: 12 additions & 8 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,11 @@ ARGUMENTS
FLAGS
-d, --body=<value> The request body (JSON string).
Use "-" to read from stdin.
--describe=<value> 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=<value> 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=<value> Additional HTTP header(s). Pass a
single "key:value" string, or a JSON object like
'{"X-Foo": "bar", "X-Baz": "qux"}' to send multiple
Expand All @@ -85,9 +86,11 @@ FLAGS
<options: GET|POST|PUT|PATCH|DELETE>
-p, --params=<value> Query parameters as a JSON object,
e.g. '{"limit": 1, "desc": true}'.
-s, --search=<value> Filter --list-endpoints by a
space-separated query. Each token must appear
(case-insensitive) in method, path, or summary.
-s, --search=<value> 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`
Expand Down Expand Up @@ -742,7 +745,8 @@ FLAGS
is taken from the '.actor/actor.json' file.
-w, --wait-for-finish=<value> 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`
Expand Down
78 changes: 61 additions & 17 deletions src/commands/actors/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,6 +30,7 @@ import {
getLocalUserInfo,
getLoggedClientOrThrow,
outputJobLog,
parseWaitForFinishMillis,
printJsonToStdout,
} from '../../lib/utils.js';

Expand Down Expand Up @@ -87,7 +93,8 @@ export class ActorsPushCommand extends ApifyCommand<typeof ActorsPushCommand> {
}),
'wait-for-finish': Flags.string({
char: 'w',
description: 'Seconds for waiting to build to finish, if no value passed, it waits forever.',
description:
'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({
Expand Down Expand Up @@ -190,9 +197,7 @@ export class ActorsPushCommand extends ApifyCommand<typeof ActorsPushCommand> {
buildTag = DEFAULT_BUILD_TAG;
}

const waitForFinishMillis = Number.isNaN(this.flags.waitForFinish)
? undefined
: Number.parseInt(this.flags.waitForFinish!, 10) * 1000;
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.
Expand Down Expand Up @@ -359,31 +364,66 @@ 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
});

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,
});

await outputJobLog({ job: build, timeoutMillis: waitForFinishMillis, apifyClient });
try {
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);
}

build = (await apifyClient.build(build.id).get())!;

// `outputJobLog` can return before the build is actually terminal (stream
// 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())!;
}

// 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. 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;
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 5s; subsequent calls referencing this tag may race.`,
});
}
}

if (this.flags.json) {
printJsonToStdout(build);
return;
Expand All @@ -408,9 +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.' });
// 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 (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!' });
Expand Down
9 changes: 8 additions & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ export const outputJobLog = async ({
}
});

if (timeoutMillis) {
if (timeoutMillis !== undefined) {
nodeTimeout = setTimeout(() => {
stream.destroy();
resolve('timeouts');
Expand Down Expand Up @@ -840,3 +840,10 @@ export function shellConfigFile(userHomeDirectory: string, shell: ReturnType<typ
}
}
}

export function parseWaitForFinishMillis(flag: string | undefined): number | undefined {
if (flag === undefined) return undefined;
const parsed = Number.parseInt(flag, 10);
if (!Number.isFinite(parsed) || parsed < 0) return undefined;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmmm, can you compare what happens right now with --waitForFinish=0 vs this PR? It's a use case we should support imo (fire-and-forget)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in actual version it is ignored, it just show you whole build log to the end. In PR it ends with

Actor build detail https://console.apify.com/actors/k3Ew4ttdaIThULhmR#/builds/0.25.6
Actor detail https://console.apify.com/actors/k3Ew4ttdaIThULhmR
Warning: Build is still running.

return parsed * 1000;
}
Loading