Skip to content

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

Description

@amirtabatabaei69

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions