Skip to content
Closed
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/cli-catalog-protocol-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---

Prevent CLI crash when @trigger.dev dependencies use bun/pnpm catalog: protocol.
120 changes: 120 additions & 0 deletions packages/cli-v3/src/commands/update.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { describe, expect, it } from "vitest";
import { getTriggerDependencies, getVersionMismatches, type Dependency } from "./update.js";

describe("getTriggerDependencies", () => {
it("skips dependencies using catalog: and workspace: protocols", async () => {
const packageJson = {
dependencies: {
"@trigger.dev/sdk": "catalog:",
"@trigger.dev/core": "catalog:default",
"@trigger.dev/react-hooks": "workspace:*",
lodash: "^4.17.21",
},
devDependencies: {
"@trigger.dev/build": "catalog:tools",
"@trigger.dev/schema-to-json": "workspace:^3.0.0",
"@trigger.dev/companyicons": "^1.0.0",
},
};

const deps = await getTriggerDependencies(packageJson, "/fake/project/package.json");

expect(deps).toEqual([]);
});

it("includes normal @trigger.dev dependencies", async () => {
const packageJson = {
dependencies: {
"@trigger.dev/sdk": "^3.0.0",
},
devDependencies: {
"@trigger.dev/core": "~3.0.0",
},
};

const deps = await getTriggerDependencies(packageJson, "/fake/project/package.json");

expect(deps).toHaveLength(2);
expect(deps).toContainEqual({
type: "dependencies",
name: "@trigger.dev/sdk",
version: "^3.0.0",
});
expect(deps).toContainEqual({
type: "devDependencies",
name: "@trigger.dev/core",
version: "~3.0.0",
});
});
});

describe("getVersionMismatches", () => {
it("does not throw when encountering non-semver strings like catalog: or workspace:", () => {
const deps: Dependency[] = [
{
type: "dependencies",
name: "@trigger.dev/sdk",
version: "catalog:",
},
{
type: "dependencies",
name: "@trigger.dev/core",
version: "catalog:named",
},
{
type: "devDependencies",
name: "@trigger.dev/build",
version: "workspace:*",
},
{
type: "devDependencies",
name: "@trigger.dev/react-hooks",
version: "invalid-semver-string",
},
];

expect(() => getVersionMismatches(deps, "3.0.0")).not.toThrow();

const { mismatches, isDowngrade } = getVersionMismatches(deps, "3.0.0");
expect(mismatches).toHaveLength(4);
expect(isDowngrade).toBe(false);
});

it("correctly identifies downgrades when valid semver is newer than target CLI version", () => {
const deps: Dependency[] = [
{
type: "dependencies",
name: "@trigger.dev/sdk",
version: "^4.0.0",
},
];

const { mismatches, isDowngrade } = getVersionMismatches(deps, "3.0.0");
expect(mismatches).toHaveLength(1);
expect(isDowngrade).toBe(true);
});

it("ignores packages matching targetVersion, 0.0.0, or pkg.pr.new", () => {
const deps: Dependency[] = [
{
type: "dependencies",
name: "@trigger.dev/sdk",
version: "3.0.0",
},
{
type: "dependencies",
name: "@trigger.dev/core",
version: "0.0.0-prerelease",
},
{
type: "devDependencies",
name: "@trigger.dev/build",
version: "https://pkg.pr.new/@trigger.dev/build@123",
},
];

const { mismatches, isDowngrade } = getVersionMismatches(deps, "3.0.0");
expect(mismatches).toHaveLength(0);
expect(isDowngrade).toBe(false);
});
});
92 changes: 50 additions & 42 deletions packages/cli-v3/src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,45 +116,6 @@ export async function updateTriggerPackages(

logger.debug("Resolved trigger deps", { triggerDependencies });

function getVersionMismatches(
deps: Dependency[],
targetVersion: string
): {
mismatches: Dependency[];
isDowngrade: boolean;
} {
logger.debug("Checking for version mismatches", { deps, targetVersion });

const mismatches: Dependency[] = [];

for (const dep of deps) {
if (
dep.version === targetVersion ||
dep.version.startsWith("https://pkg.pr.new") ||
dep.version.startsWith("0.0.0")
) {
continue;
}

mismatches.push(dep);
}

const isDowngrade = mismatches.some((dep) => {
const depMinVersion = semver.minVersion(dep.version);

if (!depMinVersion) {
return false;
}

return semver.gt(depMinVersion, targetVersion);
});

return {
mismatches,
isDowngrade,
};
}

const { mismatches, isDowngrade } = getVersionMismatches(triggerDependencies, cliVersion);

logger.debug("Version mismatches", { mismatches, isDowngrade });
Expand Down Expand Up @@ -314,13 +275,60 @@ export async function updateTriggerPackages(
return hasOutput;
}

type Dependency = {
export type Dependency = {
type: "dependencies" | "devDependencies";
name: string;
version: string;
};

async function getTriggerDependencies(
export function getVersionMismatches(
deps: Dependency[],
targetVersion: string
): {
mismatches: Dependency[];
isDowngrade: boolean;
} {
logger.debug("Checking for version mismatches", { deps, targetVersion });

const mismatches: Dependency[] = [];

for (const dep of deps) {
if (
dep.version === targetVersion ||
dep.version.startsWith("https://pkg.pr.new") ||
dep.version.startsWith("0.0.0")
) {
continue;
}

mismatches.push(dep);
}

const isDowngrade = mismatches.some((dep) => {
if (!semver.validRange(dep.version)) {
return false;
}

try {
const depMinVersion = semver.minVersion(dep.version);

if (!depMinVersion) {
return false;
}

return semver.gt(depMinVersion, targetVersion);
} catch {
return false;
}
Comment on lines +307 to +322

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 Unknown protocols remain mismatches

getVersionMismatches avoids the crash but retains every unknown protocol in mismatches. Required checks still abort, while interactive updates replace those specifiers with the CLI version.

Devin Review


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

});

return {
mismatches,
isDowngrade,
};
}

export async function getTriggerDependencies(
packageJson: PackageJson,
packageJsonPath: string
): Promise<Dependency[]> {
Expand All @@ -332,7 +340,7 @@ async function getTriggerDependencies(
continue;
}

if (version.startsWith("workspace")) {
if (version.startsWith("workspace") || version.startsWith("catalog:")) {
continue;
}
Comment on lines +343 to 345

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Catalog dependencies bypass version enforcement

With installed catalog dependencies, getTriggerDependencies drops them before resolving their concrete versions. dev, deploy, and update then accept incompatible Trigger.dev package versions without warning or updating them.

Learn more

Catalog specifiers such as catalog: name a version stored in workspace configuration. They are not evidence that the installed package matches the CLI. The existing resolver can recover that package's concrete installed version, and previously did so whenever resolution succeeded. Returning early now removes the dependency from both mismatch detection and the required update gate.

Example: A workspace catalog pins @trigger.dev/sdk to 4.5.0 while the CLI is 4.6.3. Even with 4.5.0 installed and resolvable, the dependency list is empty, so trigger deploy passes the required update check.

Recommended fix: Resolve catalog dependencies and retain their protocol metadata separately from the concrete installed version. Use the concrete version for mismatch enforcement. When applying an update, modify the relevant catalog definition or emit actionable guidance instead of replacing the package manifest's catalog reference.

Devin Review


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


Expand Down