vercel/turborepo

notifyUpdate() exits with code 0 on error, masking failures in @turbo/gen CLI

Summary

  • Context: The @turbo/gen CLI is a command-line tool for extending Turborepo projects by running generators and creating workspaces.

  • Bug: When an error occurs during CLI execution, the notifyUpdate() function calls process.exit() without an exit code argument, causing the process to exit with code 0 (success) instead of the intended code 1 (error).

  • Actual vs. expected: The CLI exits with code 0 (success) after encountering an error, when it should exit with code 1 (error).

  • Impact: CI/CD pipelines, shell scripts, and automation tools that rely on exit codes will incorrectly interpret failed CLI executions as successful, potentially masking errors and causing downstream issues.

Code with bug

Location: packages/turbo-gen/src/utils/notifyUpdate.ts

export async function notifyUpdate(): Promise<void> {
  try {
    const res = await update;

    if (res?.latest) {
      logger.log();
      logger.log(
        picocolors.yellow(
          picocolors.bold(
            `A new version of \`${cliPkgJson.name}\` is available!`,
          ),
        ),
      );
      logger.log();
    }

    process.exit(); 
    // <-- BUG 🔴 exits with code 0, preventing error exit code from being set
  } catch (_) {
    // ignore error
  }
}

Error handler in: packages/turbo-gen/src/cli.ts

turboGenCli
  .parseAsync()
  .then(notifyUpdate)
  .catch(async (error) => {
    logger.log();

    if (error instanceof GeneratorError) {
      logger.error(error.message);
    } else {
      logger.error("Unexpected error. Please report it as a bug:");
      logger.log(error);
    }

    logger.log();

    await notifyUpdate(); // <-- calls notifyUpdate which exits with code 0
    process.exit(1);      // <-- this line is never reached
  });

Example

Repro steps and resulting behavior:

  1. Run: turbo-gen run --config /nonexistent/config.js

  2. CLI throws GeneratorError: “No config at ‘/nonexistent/config.js’” and enters the catch handler, which logs the error and awaits notifyUpdate().

  3. notifyUpdate() calls process.exit() without an argument; no update is available, so it exits immediately with code 0.

  4. The intended process.exit(1) in the catch handler is never reached.

Expected exit code: 1 Actual exit code: 0

Recommended fix

Copy directly from the original exploration:

packages/turbo-gen/src/utils/notifyUpdate.ts:

export async function notifyUpdate(
  exitCode: number = 0, // <-- FIX 🟢 accept exit code
): Promise<void> {
  try {
    const res = await update;

    if (res?.latest) {
      logger.log();
      logger.log(
        picocolors.yellow(
          picocolors.bold(
            `A new version of \`${cliPkgJson.name}\` is available!`,
          ),
        ),
      );
      logger.log();
    }

    process.exit(exitCode); // <-- FIX 🟢 use provided exit code
  } catch (_) {
    // ignore error
  }
}

packages/turbo-gen/src/cli.ts:

turboGenCli
  .parseAsync()
  .then(notifyUpdate) // uses default exitCode = 0
  .catch(async (error) => {
    logger.log();

    if (error instanceof GeneratorError) {
      logger.error(error.message);
    } else {
      logger.error("Unexpected error. Please report it as a bug:");
      logger.log(error);
    }

    logger.log();

    await notifyUpdate(1); 
    // pass exitCode = 1
    // process.exit(1) line can now be removed as it's redundant
  });