Bug description
When two apify push operations run against the same actor concurrently (e.g. two CI/CD pipeline runs, two team members, or two terminals), the last write wins with no error and no warning. The actor version source is silently overwritten, and both operations queue independent builds — potentially deploying code that neither developer intended as the final state.
Steps to reproduce
- Have an actor already pushed to the platform.
- From two terminals (or two CI jobs) simultaneously run
apify push with the same credentials and the same actor.json name.
- Observe: both commands exit 0, both report success.
Expected behavior
The second push should detect the conflict and fail with a clear message, e.g.: "Actor was modified by another process since you started this push. Re-pull and retry, or use --force to override."
Actual behavior
Both pushes succeed silently. The version source is overwritten by the last PUT /actor/{id}/versions/{v} call. Both builds are queued; the one that finishes last and claims the latest tag wins. The other build ran against overwritten source code.
Root cause
The staleness check in src/commands/actors/push.ts (lines 369–393) is:
-
Client-side only — it compares local file mtime against actor.modifiedAt read at the start of the command. There is no server-side conditional update (no ETag / If-Match / If-Unmodified-Since).
-
Raceable (TOCTOU) — the window between GET actor (read modifiedAt) and PUT version (write source) is unbounded. A concurrent push that lands in that window is undetected.
-
Skipped for large files — the check is guarded by filesSize < MAX_MULTIFILE_BYTES. Actors larger than ~5 MB (ZIP upload path) have zero conflict detection.
-
Bypassed by --force — common in CI pipelines, which removes the only soft guard.
Impact
- CI/CD pipelines: two pipeline runs triggered in parallel (e.g. two PRs merged in quick succession) will race. Wrong source code deploys silently — no alert, no failed step.
- Teams: two developers pushing the same actor at the same time silently lose one person's changes.
- Severity is amplified because the failure mode is silent — the command exits 0 and reports success.
Proposed fix
Short-term (CLI-only, partial): Remove the filesSize < MAX_MULTIFILE_BYTES guard so the staleness check also runs for ZIP uploads. Add a warning on the ZIP path noting it cannot fully prevent races.
Proper fix (requires API change): Add conditional update support to the platform API — PUT /actor/{id}/versions/{v} should accept an If-Unmodified-Since or ETag header. The CLI passes the modifiedAt value it read at step 1; the server rejects with HTTP 412 if the actor was modified since. The CLI surfaces a clear error to the user.
Environment
- Reproducible on any platform version (race is architectural, not version-specific)
- Affects both
apify push and actors push (they share the same ActorsPushCommand)
Bug description
When two
apify pushoperations run against the same actor concurrently (e.g. two CI/CD pipeline runs, two team members, or two terminals), the last write wins with no error and no warning. The actor version source is silently overwritten, and both operations queue independent builds — potentially deploying code that neither developer intended as the final state.Steps to reproduce
apify pushwith the same credentials and the sameactor.jsonname.Expected behavior
The second push should detect the conflict and fail with a clear message, e.g.: "Actor was modified by another process since you started this push. Re-pull and retry, or use --force to override."
Actual behavior
Both pushes succeed silently. The version source is overwritten by the last
PUT /actor/{id}/versions/{v}call. Both builds are queued; the one that finishes last and claims thelatesttag wins. The other build ran against overwritten source code.Root cause
The staleness check in
src/commands/actors/push.ts(lines 369–393) is:Client-side only — it compares local file
mtimeagainstactor.modifiedAtread at the start of the command. There is no server-side conditional update (noETag/If-Match/If-Unmodified-Since).Raceable (TOCTOU) — the window between
GET actor(readmodifiedAt) andPUT version(write source) is unbounded. A concurrent push that lands in that window is undetected.Skipped for large files — the check is guarded by
filesSize < MAX_MULTIFILE_BYTES. Actors larger than ~5 MB (ZIP upload path) have zero conflict detection.Bypassed by
--force— common in CI pipelines, which removes the only soft guard.Impact
Proposed fix
Short-term (CLI-only, partial): Remove the
filesSize < MAX_MULTIFILE_BYTESguard so the staleness check also runs for ZIP uploads. Add a warning on the ZIP path noting it cannot fully prevent races.Proper fix (requires API change): Add conditional update support to the platform API —
PUT /actor/{id}/versions/{v}should accept anIf-Unmodified-SinceorETagheader. The CLI passes themodifiedAtvalue it read at step 1; the server rejects with HTTP 412 if the actor was modified since. The CLI surfaces a clear error to the user.Environment
apify pushandactors push(they share the sameActorsPushCommand)