Problem
apify-cli's CommandExitCodes enum at src/lib/consts.ts:74-91 collapses three semantically-distinct failure categories to exit code 1, with two more pairs colliding on codes 3 and 5:
// src/lib/consts.ts:74-91
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
export enum CommandExitCodes {
BuildFailed = 1,
RunFailed = 1,
MissingAuth = 1,
BuildTimedOut = 2,
BuildAborted = 3,
RunAborted = 3,
NoFilesToPush = 4,
InvalidInput = 5,
InvalidActorJson = 5,
NotFound = 250,
NotImplemented = 255,
}
The eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values directive is the canary — the collisions are deliberate, but they force every calling script or agent into brittle stderr string-matching to answer the two most common operational questions:
- "Did this fail because of auth, or because of code?" — auth is non-transient (needs re-auth); code failures may be transient (retry).
- "Should I retry, or fail-fast?" —
MissingAuth is non-transient, BuildFailed may be a network blip, RunFailed is usually deterministic.
What the standard says
POSIX only mandates 0 = success, non-zero = failure. Codes 1-125 are per-tool convention. The de-facto modern pattern (gh, AWS CLI, curl, git, npm) is distinct exit codes per error category. BSD sysexits.h reserves 64-78; EX_NOPERM = 77 is the canonical "missing auth" mapping.
- GitHub CLI:
0 success, 1 generic, 2 cancelled, 4 requires authentication — the most widely-copied modern pattern.
- AWS CLI: distinct codes for user-error, service-error, syntax-error.
- curl: 90+ distinct codes (
6 DNS, 7 connect, 28 timeout, 67 bad user/password, etc.).
Proposal
Adopt a gh-CLI-style banded scheme with distinct codes per category. Sketch:
| Code |
Constant |
Meaning |
| 0 |
Success |
— |
| 1 |
GenericError |
fallback / unclassified |
| 2 |
Cancelled |
user Ctrl+C |
| 4 |
MissingAuth |
apify login needed |
| 5 |
InvalidInput |
bad CLI usage / bad actor.json |
| 10-19 |
Build band |
BuildFailed = 10, BuildTimedOut = 11, BuildAborted = 12, NoFilesToPush = 13 |
| 20-29 |
Run band |
RunFailed = 20, RunAborted = 21, RunTimedOut = 22 |
| 250 |
NotFound |
Actor/run/dataset not found (already used) |
| 255 |
NotImplemented |
(already used) |
oclif supports arbitrary exit codes via this.error({exit: N}), so no upstream framework changes needed.
Migration
This is a semver-major behavior change for any caller that pattern-matches on $? == 1. Options:
- Ship behind a
APIFY_CLI_STRUCTURED_EXIT_CODES=1 opt-in env var for a release or two, then flip the default and remove the flag.
- Emit the classification on stderr as a machine-readable line (
[apify] error-category: missing-auth) alongside the new exit code, so callers can pick either signal.
Companion improvement: on MissingAuth (or any non-zero exit) with --json, emit a structured {error, code, category} JSON envelope on stdout (currently prose-only — tracked separately as F14).
Reproducer
$ apify push # in a project with no token set
# ... prose error about needing to login ...
$ echo $?
1
$ apify push # in a project with intentional TypeScript error
# ... build error ...
$ echo $?
1
Two categorically different failures, indistinguishable to the caller.
Impact
Medium (P2). Every CI job or agent harness using apify-cli hits this. Cross-references F14 (same root cause on a different signal channel — the JSON error envelope).
Surfaced during an evaluation of Apify surfaces for agent-driven Actor development.
Problem
apify-cli'sCommandExitCodesenum atsrc/lib/consts.ts:74-91collapses three semantically-distinct failure categories to exit code1, with two more pairs colliding on codes3and5:The
eslint-disable-next-line @typescript-eslint/no-duplicate-enum-valuesdirective is the canary — the collisions are deliberate, but they force every calling script or agent into brittle stderr string-matching to answer the two most common operational questions:MissingAuthis non-transient,BuildFailedmay be a network blip,RunFailedis usually deterministic.What the standard says
POSIX only mandates 0 = success, non-zero = failure. Codes 1-125 are per-tool convention. The de-facto modern pattern (gh, AWS CLI, curl, git, npm) is distinct exit codes per error category. BSD
sysexits.hreserves64-78;EX_NOPERM = 77is the canonical "missing auth" mapping.0success,1generic,2cancelled,4requires authentication — the most widely-copied modern pattern.6DNS,7connect,28timeout,67bad user/password, etc.).Proposal
Adopt a gh-CLI-style banded scheme with distinct codes per category. Sketch:
apify loginneededactor.jsonBuildFailed = 10,BuildTimedOut = 11,BuildAborted = 12,NoFilesToPush = 13RunFailed = 20,RunAborted = 21,RunTimedOut = 22oclif supports arbitrary exit codes via
this.error({exit: N}), so no upstream framework changes needed.Migration
This is a semver-major behavior change for any caller that pattern-matches on
$? == 1. Options:APIFY_CLI_STRUCTURED_EXIT_CODES=1opt-in env var for a release or two, then flip the default and remove the flag.[apify] error-category: missing-auth) alongside the new exit code, so callers can pick either signal.Companion improvement: on
MissingAuth(or any non-zero exit) with--json, emit a structured{error, code, category}JSON envelope on stdout (currently prose-only — tracked separately as F14).Reproducer
Two categorically different failures, indistinguishable to the caller.
Impact
Medium (P2). Every CI job or agent harness using
apify-clihits this. Cross-references F14 (same root cause on a different signal channel — the JSON error envelope).Surfaced during an evaluation of Apify surfaces for agent-driven Actor development.