payloadcms / payloadcms/payload

payload run reports success in four distinct failure modes, one of which dev-pushes to a remote database

Open
#18,086 2 comments 0 reactions 1 assignee View on GitHub

@r1tsuu is already working on this.

Since Sep 11, 2026.

Bug
Dominant language
TypeScript
Stars
44.8k
Forks
4.2k
Avg merge
2d 21h
Merged PRs (30d)
53

Description

payload run reports success in four distinct failure modes, one of which writes to a remote database.

Verified against payload@3.87.1 and @payloadcms/db-postgres@3.87.1 by reading the published dist. Reporting these together because they share a root cause — the CLI wrapper does not preserve the semantics of the script it runs — and because together they make payload run unsafe as a CI or build gate. We used it as a pre-deploy migration gate, and the gate could never have failed.

1. The exit code is discarded

dist/bin/index.js ends the non-cron branch with an unconditional process.exit(0) after runBinScript returns:

const { payload } = await runBinScript({ args, script })
if (payload) { await payload.destroy() }
process.exit(0)

A script that sets process.exitCode = 1 — the documented, non-throwing way to fail — therefore reports success to the shell. A thrown error is the only failure signal that propagates (via the catch inside runBinScript, which calls process.exit(1)).

Repro: payload run ./fail.ts where fail.ts is process.exitCode = 1; echo $? prints 0.

Suggested fix: propagate a non-zero process.exitCode, or document prominently that only throw fails.

2. A floating promise is abandoned

payload run script.ts awaits module evaluation and then exits, so a trailing void main() never completes. The script prints nothing and exits 0 — indistinguishable from success. Top-level await is the workaround; a docs line would save the discovery.

3. CLI flags never reach the script

process.argv is rebuilt from minimist's positional arguments only:

process.argv = [process.argv[0], process.argv[1], ...args._.slice(2)]

So --pre is consumed by the CLI and process.argv.includes('--pre') inside the script is always false, silently.

Suggested fix: forward unrecognised flags, or document that arguments must be passed via the environment.

4. It performs a dev schema push against whatever DATABASE_URL names

db-postgres/dist/connect.js:110 guards dev push with three conditions:

if (process.env.NODE_ENV !== 'production' &&
    process.env.PAYLOAD_MIGRATING !== 'true' &&
    this.push !== false) {
  await pushDevSchema(this)
}

Two of those are set for you where it matters — bin/migrate.js sets PAYLOAD_MIGRATING='true', and next build sets NODE_ENV=production. payload run sets neither: bin/index.js sets only DISABLE_PAYLOAD_HMR, and bin/loadEnv.js merely reads NODE_ENV. So for an ad-hoc payload run ./seed.ts with NODE_ENV unset — the default in a plain shell — all three conditions pass and the adapter dev-pushes against whatever DATABASE_URL resolves to, including a production database. It then stamps payload_migrations = (dev, batch -1), and the next deploy's migration gate refuses to run.

Two things make this expensive rather than merely surprising:

  • migrate:status does not show the (dev, -1) row, so the tool you would reach for to diagnose a refused deploy does not reveal the cause.
  • Combined with (1), a guard script written to prevent this cannot fail the build, because payload run discards its exit code.

Ask: NODE_ENV is a weak proxy for "is this database disposable". Either default push to false for payload run (a one-off script is not a dev server), or refuse a dev push against a non-local host unless explicitly opted in, or at minimum log a warning naming the host before pushing. Today the safe configuration is something each user discovers only after being bitten.


Happy to open a PR for any of these if the direction is agreed — (1) and (3) look mechanical; (4) is a behavioural default you may want to choose differently than we would.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.