Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-github-install-node-version-2913.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/cli-v3": patch
---

Fix: Ignore engine checks during deployment install phase to prevent failure on build server when Node version mismatch exists. (#2913)
86 changes: 36 additions & 50 deletions packages/cli-v3/src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
}

if (!options.skipUpdateCheck) {
await updateTriggerPackages(dir, { ...options }, true, true);
await updateTriggerPackages(dir, { ...options, ignoreEngines: true }, true, true);
}

const cwd = process.cwd();
Expand Down Expand Up @@ -489,9 +489,8 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
const version = deployment.version;

const rawDeploymentLink = `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.project}/deployments/${deployment.shortCode}`;
const rawTestLink = `${authorization.dashboardUrl}/projects/v3/${
resolvedConfig.project
}/test?environment=${options.env === "prod" ? "prod" : "stg"}`;
const rawTestLink = `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.project
}/test?environment=${options.env === "prod" ? "prod" : "stg"}`;

const deploymentLink = cliLink("View deployment", rawDeploymentLink);
const testLink = cliLink("Test tasks", rawTestLink);
Expand Down Expand Up @@ -708,8 +707,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
}
} else {
outro(
`Version ${version} deployed with ${taskCount} detected task${taskCount === 1 ? "" : "s"} ${
isLinksSupported ? `| ${deploymentLink} | ${testLink}` : ""
`Version ${version} deployed with ${taskCount} detected task${taskCount === 1 ? "" : "s"} ${isLinksSupported ? `| ${deploymentLink} | ${testLink}` : ""
}`
);

Expand All @@ -733,18 +731,16 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
TRIGGER_VERSION: version,
TRIGGER_DEPLOYMENT_SHORT_CODE: deployment.shortCode,
TRIGGER_DEPLOYMENT_URL: `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.project}/deployments/${deployment.shortCode}`,
TRIGGER_TEST_URL: `${authorization.dashboardUrl}/projects/v3/${
resolvedConfig.project
}/test?environment=${options.env === "prod" ? "prod" : "stg"}`,
TRIGGER_TEST_URL: `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.project
}/test?environment=${options.env === "prod" ? "prod" : "stg"}`,
},
outputs: {
deploymentVersion: version,
workerVersion: version,
deploymentShortCode: deployment.shortCode,
deploymentUrl: `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.project}/deployments/${deployment.shortCode}`,
testUrl: `${authorization.dashboardUrl}/projects/v3/${
resolvedConfig.project
}/test?environment=${options.env === "prod" ? "prod" : "stg"}`,
testUrl: `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.project
}/test?environment=${options.env === "prod" ? "prod" : "stg"}`,
needsPromotion: options.skipPromotion ? "true" : "false",
},
});
Expand Down Expand Up @@ -787,8 +783,7 @@ async function failDeploy(
checkLogsForErrors(logs);

outro(
`${chalkError(`${prefix}:`)} ${
error.message
`${chalkError(`${prefix}:`)} ${error.message
}. Full build logs have been saved to ${logPath}`
);

Expand Down Expand Up @@ -1088,9 +1083,8 @@ async function handleNativeBuildServerDeploy({
const deployment = initializeDeploymentResult.data;

const rawDeploymentLink = `${dashboardUrl}/projects/v3/${config.project}/deployments/${deployment.shortCode}`;
const rawTestLink = `${dashboardUrl}/projects/v3/${config.project}/test?environment=${
options.env === "prod" ? "prod" : "stg"
}`;
const rawTestLink = `${dashboardUrl}/projects/v3/${config.project}/test?environment=${options.env === "prod" ? "prod" : "stg"
}`;

const exposedDeploymentLink = isLinksSupported
? cliLink(chalk.bold(rawDeploymentLink), rawDeploymentLink)
Expand Down Expand Up @@ -1156,8 +1150,7 @@ async function handleNativeBuildServerDeploy({
log.warn(`Failed streaming build logs, open the deployment in the dashboard to view the logs`);

outro(
`Version ${deployment.version} is being deployed ${
isLinksSupported ? `| ${cliLink("View deployment", rawDeploymentLink)}` : ""
`Version ${deployment.version} is being deployed ${isLinksSupported ? `| ${cliLink("View deployment", rawDeploymentLink)}` : ""
}`
);

Expand Down Expand Up @@ -1204,10 +1197,10 @@ async function handleNativeBuildServerDeploy({
level === "error"
? chalk.bold(chalkError(message))
: level === "warn"
? chalkWarning(message)
: level === "debug"
? chalkGrey(message)
: message;
? chalkWarning(message)
: level === "debug"
? chalkGrey(message)
: message;

// We use console.log here instead of clack's logger as the current version does not support changing the line spacing.
// And the logs look verbose with the default spacing.
Expand Down Expand Up @@ -1240,8 +1233,7 @@ async function handleNativeBuildServerDeploy({
log.error("Failed dequeueing build, please try again shortly");

throw new OutroCommandError(
`Version ${deployment.version} ${
isLinksSupported ? `| ${cliLink("View deployment", rawDeploymentLink)}` : ""
`Version ${deployment.version} ${isLinksSupported ? `| ${cliLink("View deployment", rawDeploymentLink)}` : ""
}`
);
}
Expand All @@ -1256,8 +1248,7 @@ async function handleNativeBuildServerDeploy({
}

throw new OutroCommandError(
`Version ${deployment.version} ${
isLinksSupported ? `| ${cliLink("View deployment", rawDeploymentLink)}` : ""
`Version ${deployment.version} ${isLinksSupported ? `| ${cliLink("View deployment", rawDeploymentLink)}` : ""
}`
);
}
Expand All @@ -1283,13 +1274,12 @@ async function handleNativeBuildServerDeploy({
}

outro(
`Version ${deployment.version} was deployed ${
isLinksSupported
? `| ${cliLink("Test tasks", rawTestLink)} | ${cliLink(
"View deployment",
rawDeploymentLink
)}`
: ""
`Version ${deployment.version} was deployed ${isLinksSupported
? `| ${cliLink("Test tasks", rawTestLink)} | ${cliLink(
"View deployment",
rawDeploymentLink
)}`
: ""
}`
);
return process.exit(0);
Expand All @@ -1303,14 +1293,13 @@ async function handleNativeBuildServerDeploy({
chalk.bold(
chalkError(
"Deployment failed" +
(finalDeploymentEvent.message ? `: ${finalDeploymentEvent.message}` : "")
(finalDeploymentEvent.message ? `: ${finalDeploymentEvent.message}` : "")
)
)
);

throw new OutroCommandError(
`Version ${deployment.version} deployment failed ${
isLinksSupported ? `| ${cliLink("View deployment", rawDeploymentLink)}` : ""
`Version ${deployment.version} deployment failed ${isLinksSupported ? `| ${cliLink("View deployment", rawDeploymentLink)}` : ""
}`
);
}
Expand All @@ -1323,14 +1312,13 @@ async function handleNativeBuildServerDeploy({
chalk.bold(
chalkError(
"Deployment timed out" +
(finalDeploymentEvent.message ? `: ${finalDeploymentEvent.message}` : "")
(finalDeploymentEvent.message ? `: ${finalDeploymentEvent.message}` : "")
)
)
);

throw new OutroCommandError(
`Version ${deployment.version} deployment timed out ${
isLinksSupported ? `| ${cliLink("View deployment", rawDeploymentLink)}` : ""
`Version ${deployment.version} deployment timed out ${isLinksSupported ? `| ${cliLink("View deployment", rawDeploymentLink)}` : ""
}`
);
}
Expand All @@ -1343,14 +1331,13 @@ async function handleNativeBuildServerDeploy({
chalk.bold(
chalkError(
"Deployment was canceled" +
(finalDeploymentEvent.message ? `: ${finalDeploymentEvent.message}` : "")
(finalDeploymentEvent.message ? `: ${finalDeploymentEvent.message}` : "")
)
)
);

throw new OutroCommandError(
`Version ${deployment.version} deployment canceled ${
isLinksSupported ? `| ${cliLink("View deployment", rawDeploymentLink)}` : ""
`Version ${deployment.version} deployment canceled ${isLinksSupported ? `| ${cliLink("View deployment", rawDeploymentLink)}` : ""
}`
);
}
Expand All @@ -1369,13 +1356,12 @@ async function handleNativeBuildServerDeploy({
}

outro(
`Version ${deployment.version} ${
isLinksSupported
? `| ${cliLink("Test tasks", rawTestLink)} | ${cliLink(
"View deployment",
rawDeploymentLink
)}`
: ""
`Version ${deployment.version} ${isLinksSupported
? `| ${cliLink("Test tasks", rawTestLink)} | ${cliLink(
"View deployment",
rawDeploymentLink
)}`
: ""
}`
);
return process.exit(0);
Expand Down
113 changes: 113 additions & 0 deletions packages/cli-v3/src/commands/update.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { updateTriggerPackages } from "./update.js";
import * as nypm from "nypm";
import * as pkgTypes from "pkg-types";
import * as fs from "node:fs/promises";
import * as clack from "@clack/prompts";
import path from "node:path";

// Mock dependencies
vi.mock("nypm");
vi.mock("pkg-types");
vi.mock("node:fs/promises");
vi.mock("@clack/prompts");
vi.mock("std-env", () => ({
hasTTY: true,
isCI: false,
}));
vi.mock("../utilities/initialBanner.js", () => ({
updateCheck: vi.fn().mockResolvedValue(undefined),
printStandloneInitialBanner: vi.fn(),
}));
vi.mock("../version.js", () => ({
VERSION: "3.0.0",
}));
vi.mock("../cli/common.js", () => ({
CommonCommandOptions: { pick: () => ({}) },
}));
vi.mock("../utilities/cliOutput.js", () => ({
chalkError: vi.fn(),
prettyError: vi.fn(),
prettyWarning: vi.fn(),
}));
vi.mock("../utilities/fileSystem.js", () => ({
removeFile: vi.fn(),
writeJSONFilePreserveOrder: vi.fn(),
}));
vi.mock("../utilities/logger.js", () => ({
logger: {
debug: vi.fn(),
log: vi.fn(),
table: vi.fn(),
},
}));
vi.mock("../utilities/windows.js", () => ({
spinner: () => ({
start: vi.fn(),
message: vi.fn(),
stop: vi.fn(),
}),
}));

describe("updateTriggerPackages", () => {
beforeEach(() => {
vi.resetAllMocks();

// Default mocks
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(fs.rm).mockResolvedValue(undefined);
vi.mocked(pkgTypes.readPackageJSON).mockResolvedValue({
dependencies: {
"@trigger.dev/sdk": "2.0.0", // Mismatch
},
});
vi.mocked(pkgTypes.resolvePackageJSON).mockResolvedValue("/path/to/package.json");
vi.mocked(clack.confirm).mockResolvedValue(true); // User confirms update
vi.mocked(nypm.installDependencies).mockResolvedValue(undefined);
});

afterEach(() => {
vi.clearAllMocks();
});
Comment on lines +10 to +72
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Mocks are prohibited by repo testing guidelines.

This test suite relies heavily on vi.mock / vi.mocked, which the repository rules disallow. Please refactor to avoid mocks (e.g., integration-style tests with temp dirs/real dependencies, or testcontainers where applicable).
As per coding guidelines: **/*.test.{ts,tsx,js,jsx}: Use vitest exclusively for testing and never mock anything - use testcontainers instead; Tests should avoid mocks or stubs and use the helpers from @internal/testcontainers when Redis or Postgres are needed.

🤖 Prompt for AI Agents
In `@packages/cli-v3/src/commands/update.test.ts` around lines 10 - 72, The test
file uses forbidden vi.mock/vi.mocked calls (mocking nypm, pkg-types, fs,
`@clack/prompts`, etc.) inside the describe("updateTriggerPackages") suite; remove
all vi.mock/vi.mocked usage and refactor to an integration-style test: create a
temporary project dir (use fs real methods) and a real package.json with the
desired dependencies instead of mocking
pkgTypes.readPackageJSON/resolvePackageJSON, run the actual
updateTriggerPackages flow (the function under test) and invoke real
nypm.installDependencies (or call the CLI path) while using
`@internal/testcontainers` helpers if Redis/Postgres are required, and replace
clack.confirm mocks by programmatically simulating user input or calling the
underlying API in non-interactive mode; ensure you restore any test-only cleanup
in beforeEach/afterEach but do not use vi.mock/vi.mocked anywhere.


it("should pass --no-engine-strict for npm when ignoreEngines is true", async () => {
vi.mocked(nypm.detectPackageManager).mockResolvedValue({ name: "npm", command: "npm", version: "1.0.0" } as any);

await updateTriggerPackages(".", { ignoreEngines: true } as any, true, true);

expect(nypm.installDependencies).toHaveBeenCalledWith(expect.objectContaining({
args: ["--no-engine-strict"],
}));
});

it("should pass --config.engine-strict=false for pnpm when ignoreEngines is true", async () => {
vi.mocked(nypm.detectPackageManager).mockResolvedValue({ name: "pnpm", command: "pnpm", version: "1.0.0" } as any);

await updateTriggerPackages(".", { ignoreEngines: true } as any, true, true);

expect(nypm.installDependencies).toHaveBeenCalledWith(expect.objectContaining({
args: ["--config.engine-strict=false"],
}));
});

it("should pass --ignore-engines for yarn when ignoreEngines is true", async () => {
vi.mocked(nypm.detectPackageManager).mockResolvedValue({ name: "yarn", command: "yarn", version: "1.0.0" } as any);

await updateTriggerPackages(".", { ignoreEngines: true } as any, true, true);

expect(nypm.installDependencies).toHaveBeenCalledWith(expect.objectContaining({
args: ["--ignore-engines"],
}));
});

it("should NOT pass engine flags if ignoreEngines is false (default)", async () => {
vi.mocked(nypm.detectPackageManager).mockResolvedValue({ name: "npm", command: "npm", version: "1.0.0" } as any);

await updateTriggerPackages(".", { ignoreEngines: false } as any, true, true);

expect(nypm.installDependencies).toHaveBeenCalledWith(expect.objectContaining({
args: [],
}));
});
});
22 changes: 19 additions & 3 deletions packages/cli-v3/src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as semver from "semver";
export const UpdateCommandOptions = CommonCommandOptions.pick({
logLevel: true,
skipTelemetry: true,
ignoreEngines: true,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 ignoreEngines property picked from schema that doesn't contain it

The UpdateCommandOptions schema is created by picking ignoreEngines from CommonCommandOptions, but CommonCommandOptions does not define an ignoreEngines property.

Click to expand

Issue

In update.ts:18-22, UpdateCommandOptions is defined as:

export const UpdateCommandOptions = CommonCommandOptions.pick({
  logLevel: true,
  skipTelemetry: true,
  ignoreEngines: true,  // This doesn't exist in CommonCommandOptions!
});

But looking at common.ts:12-17, CommonCommandOptions only has:

export const CommonCommandOptions = z.object({
  apiUrl: z.string().optional(),
  logLevel: z.enum(["debug", "info", "log", "warn", "error", "none"]).default("log"),
  skipTelemetry: z.boolean().default(false),
  profile: z.string().default(readAuthConfigCurrentProfileName()),
});

Zod's .pick() silently ignores non-existent keys, so UpdateCommandOptions won't include ignoreEngines in its schema or type definition.

Impact

When updateTriggerPackages is called from deploy.ts with { ...options, ignoreEngines: true }, the property is passed directly and works. However, the UpdateCommandOptions type doesn't include ignoreEngines, creating a type mismatch. If the standalone update command ever needs to support ignoreEngines, it won't work because the schema won't validate it.

Recommendation

Add ignoreEngines to CommonCommandOptions with a default of false, or use .extend() instead of .pick() to add the property to UpdateCommandOptions.

Recommendation: Either add ignoreEngines: z.boolean().default(false) to CommonCommandOptions in common.ts, or change UpdateCommandOptions to use .extend() to add the property: CommonCommandOptions.pick({ logLevel: true, skipTelemetry: true }).extend({ ignoreEngines: z.boolean().default(false) })

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

});

export type UpdateCommandOptions = z.infer<typeof UpdateCommandOptions>;
Expand Down Expand Up @@ -257,11 +258,26 @@ export async function updateTriggerPackages(
`Installing new package versions${packageManager ? ` with ${packageManager.name}` : ""}`
);

await installDependencies({ cwd: projectPath, silent: true });
const installArgs: string[] = [];

if (options.ignoreEngines && packageManager) {
switch (packageManager.name) {
case "npm":
installArgs.push("--no-engine-strict");
break;
case "pnpm":
installArgs.push("--config.engine-strict=false");
break;
case "yarn":
installArgs.push("--ignore-engines");
break;
}
}

await installDependencies({ cwd: projectPath, silent: true, args: installArgs });
Comment on lines +261 to +277
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find the installDependencies function and understand the context
fd -t f "installDependencies" --exec rg -l "installDependencies" {} \;

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Let's check the update.ts file to see imports and context
cat -n packages/cli-v3/src/commands/update.ts | head -50

Repository: triggerdotdev/trigger.dev

Length of output: 2521


🏁 Script executed:

# Search for nypm usage in the codebase
rg "nypm|detectPackageManager" -t ts -t js --max-count=20

Repository: triggerdotdev/trigger.dev

Length of output: 2226


🏁 Script executed:

# Let's see the full context around lines 261-277 in update.ts
sed -n '240,290p' packages/cli-v3/src/commands/update.ts | cat -n

Repository: triggerdotdev/trigger.dev

Length of output: 1998


🏁 Script executed:

# Let's check the test file to see what flags are being verified
cat -n packages/cli-v3/src/commands/update.test.ts | grep -A 10 -B 5 "ignoreEngines\|installArgs\|--no-engine-strict\|engine-strict\|ignore-engines"

Repository: triggerdotdev/trigger.dev

Length of output: 2256


🏁 Script executed:

# Let's find if there are any inline comments or documentation about these flags
rg "no-engine-strict|engine-strict|ignore-engines" packages/cli-v3 -B 2 -A 2

Repository: triggerdotdev/trigger.dev

Length of output: 3185


🌐 Web query:

npm install --no-engine-strict flag documentation

💡 Result:

--no-engine-strict is the negated form of npm’s boolean engine-strict configuration, so it disables strict engine checks for that command invocation (equivalent to --engine-strict=false). The engine-strict setting controls whether npm refuses to install packages whose declared engines don’t match your current Node.js version. [1]

You can also set it persistently via npm config (e.g., in .npmrc) using engine-strict=false (or enable it with engine-strict=true). [1]

Docs: npm config option engine-strict (applies to npm install, and can be toggled with --engine-strict / --no-engine-strict). [1]

Sources
[1] npm Docs – CLI config (engine-strict): https://docs.npmjs.com/cli/v11/using-npm/config


🌐 Web query:

pnpm install --config.engine-strict=false flag documentation

💡 Result:

--config.<key>=<value> is pnpm’s documented escape hatch for passing “unknown” CLI options through as npm_config_* environment variables (some packages read these during install). Example from the pnpm CLI docs: pnpm install --config.target_arch=x64. [1]

engine-strict itself is a pnpm setting (from .npmrc settings): when engine-strict=true, pnpm refuses to install packages whose engines claim incompatibility with the current Node version (default is false). [2]

So pnpm install --config.engine-strict=false is using that --config.<key> mechanism [1] with the engine-strict setting name [2]. If you want to set it persistently (instead of per-command), use pnpm config management: pnpm config set engine-strict false (optionally with --location=project / --global). [3]

Sources: pnpm CLI “Differences vs npm” (--config.<key>), pnpm .npmrc settings (engine-strict), pnpm config command. [1] [2] [3]


🌐 Web query:

yarn install --ignore-engines flag support classic berry

💡 Result:

  • Yarn Classic (v1): yarn install --ignore-engines is supported (it “Ignore[s] engines check”). [1]
  • Yarn Berry (v2+): yarn install does not support --ignore-engines (Berry’s documented install options don’t include it). [2] Tools that pass old v1 --ignore-* flags to Berry installs can fail with “Unsupported option name …” errors. [3]

Sources:
[1] Yarn Classic yarn install --ignore-engines docs: (classic.yarnpkg.com)
[2] Yarn (Berry) yarn install options list: (yarnpkg.com)
[3] Example of Berry rejecting v1-style --ignore-* flags during yarn install: (github.com)

Citations:


🌐 Web query:

nypm detectPackageManager package manager name values

💡 Result:

In nypm, detectPackageManager() returns a PackageManager object whose name is typed as:

  • "npm"
  • "yarn"
  • "pnpm"
  • "bun"
  • "deno" [1]

Source: PackageManagerName = "npm" | "yarn" | "pnpm" | "bun" | "deno" in nypm’s published TypeScript definitions. [1]

References

[1] https://app.unpkg.com/nypm@0.5.2/files/dist/index.d.mts


Yarn Berry does not support --ignore-engines flag.

The flags for npm and pnpm are correct, but yarn --ignore-engines only works with Yarn Classic (v1). Yarn Berry (v2+) does not recognize this flag and will fail with "Unsupported option name" errors. Since nypm's detectPackageManager returns "yarn" for both versions without distinguishing between them, users with Yarn Berry will encounter silent failures or errors when --ignoreEngines is enabled. Consider either detecting the Yarn version explicitly or documenting this limitation.

🤖 Prompt for AI Agents
In `@packages/cli-v3/src/commands/update.ts` around lines 261 - 277, The code adds
a yarn-specific --ignore-engines flag unconditionally which breaks on Yarn Berry
(v2+); update the logic around installArgs/packageManager.name so that for
"yarn" you first detect the Yarn major version (e.g., run a quick yarn --version
check or use existing packageManager.version if available) and only push
"--ignore-engines" when major === 1 (Yarn Classic); for Yarn v2+ skip adding the
flag and proceed to call installDependencies({ cwd: projectPath, silent: true,
args: installArgs }) as before.

} catch (error) {
installSpinner.stop(
`Failed to install new package versions${
packageManager ? ` with ${packageManager.name}` : ""
`Failed to install new package versions${packageManager ? ` with ${packageManager.name}` : ""
}`
);

Expand Down