From 4621b9c2f789e49e43963012e13ed5e29882e5d8 Mon Sep 17 00:00:00 2001 From: Deploy Bot Date: Mon, 2 Feb 2026 16:05:03 +0530 Subject: [PATCH] fix(cli-v3): detect package manager (yarn/pnpm/npm) and lockfile for correct deploy builds (#2914) --- .changeset/fix-cli-deploy-yarn-workspaces.md | 5 ++ packages/cli-v3/src/build/buildWorker.ts | 57 ++++++++++++++----- packages/cli-v3/src/deploy/buildImage.test.ts | 50 ++++++++++++++++ packages/cli-v3/src/deploy/buildImage.ts | 40 ++++++++++--- 4 files changed, 131 insertions(+), 21 deletions(-) create mode 100644 .changeset/fix-cli-deploy-yarn-workspaces.md create mode 100644 packages/cli-v3/src/deploy/buildImage.test.ts diff --git a/.changeset/fix-cli-deploy-yarn-workspaces.md b/.changeset/fix-cli-deploy-yarn-workspaces.md new file mode 100644 index 0000000000..99b6e4b710 --- /dev/null +++ b/.changeset/fix-cli-deploy-yarn-workspaces.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/cli-v3": patch +--- + +Fix `trigger deploy` to detect and use the correct package manager (Yarn, pnpm, npm) and lockfile for builds. This fixes issues with Yarn Workspaces and ensures reproducible builds. (#2914) diff --git a/packages/cli-v3/src/build/buildWorker.ts b/packages/cli-v3/src/build/buildWorker.ts index b23b802f50..35eadb32f2 100644 --- a/packages/cli-v3/src/build/buildWorker.ts +++ b/packages/cli-v3/src/build/buildWorker.ts @@ -1,5 +1,6 @@ import { ResolvedConfig } from "@trigger.dev/core/v3/build"; import { BuildManifest, BuildTarget } from "@trigger.dev/core/v3/schemas"; +import { detectPackageManager, PackageManager } from "nypm"; import { BundleResult, bundleWorker, createBuildManifestFromBundle } from "./bundle.js"; import { createBuildContext, @@ -10,7 +11,7 @@ import { import { createExternalsBuildExtension } from "./externals.js"; import { join, relative, sep } from "node:path"; import { generateContainerfile } from "../deploy/buildImage.js"; -import { writeFile } from "node:fs/promises"; +import { writeFile, copyFile } from "node:fs/promises"; import { buildManifestToJSON } from "../utilities/buildManifest.js"; import { readPackageJSON } from "pkg-types"; import { writeJSONFile } from "../utilities/fileSystem.js"; @@ -53,16 +54,16 @@ export async function buildWorker(options: BuildWorkerOptions) { const buildContext = createBuildContext(options.target, resolvedConfig, { logger: options.plain ? { - debug: (...args) => console.log(...args), - log: (...args) => console.log(...args), - warn: (...args) => console.log(...args), - progress: (message) => console.log(message), - spinner: (message) => { - const $spinner = spinner({ plain: true }); - $spinner.start(message); - return $spinner; - }, - } + debug: (...args) => console.log(...args), + log: (...args) => console.log(...args), + warn: (...args) => console.log(...args), + progress: (message) => console.log(message), + spinner: (message) => { + const $spinner = spinner({ plain: true }); + $spinner.start(message); + return $spinner; + }, + } : undefined, }); buildContext.prependExtension(externalsExtension); @@ -208,8 +209,31 @@ async function writeDeployFiles({ true ); + + + const packageManager = await detectPackageManager(resolvedConfig.workingDir); + + // lockFile can be a string or an array of strings + const lockFile = Array.isArray(packageManager?.lockFile) + ? packageManager?.lockFile[0] + : packageManager?.lockFile; + + if (lockFile) { + try { + await copyFile( + join(resolvedConfig.workingDir, lockFile), + join(outputPath, lockFile) + ); + } catch (e) { + logger.debug("Failed to copy lockfile", { + lockFile, + error: e instanceof Error ? e.message : e, + }); + } + } + await writeJSONFile(join(outputPath, "build.json"), buildManifestToJSON(buildManifest)); - await writeContainerfile(outputPath, buildManifest); + await writeContainerfile(outputPath, buildManifest, packageManager, lockFile); } async function readProjectPackageJson(packageJsonPath: string) { @@ -218,7 +242,12 @@ async function readProjectPackageJson(packageJsonPath: string) { return packageJson; } -async function writeContainerfile(outputPath: string, buildManifest: BuildManifest) { +async function writeContainerfile( + outputPath: string, + buildManifest: BuildManifest, + packageManager?: PackageManager | null, + lockfilePath?: string +) { if (!buildManifest.runControllerEntryPoint || !buildManifest.indexControllerEntryPoint) { throw new Error("Something went wrong with the build. Aborting deployment. [code 7789]"); } @@ -229,6 +258,8 @@ async function writeContainerfile(outputPath: string, buildManifest: BuildManife build: buildManifest.build, image: buildManifest.image, indexScript: buildManifest.indexControllerEntryPoint, + packageManager, + lockfilePath, }); const containerfilePath = join(outputPath, "Containerfile"); diff --git a/packages/cli-v3/src/deploy/buildImage.test.ts b/packages/cli-v3/src/deploy/buildImage.test.ts new file mode 100644 index 0000000000..55b12ded51 --- /dev/null +++ b/packages/cli-v3/src/deploy/buildImage.test.ts @@ -0,0 +1,50 @@ + +import { describe, it, expect } from "vitest"; +import { generateContainerfile, GenerateContainerfileOptions } from "./buildImage.js"; + +describe("generateContainerfile", () => { + const defaultOptions: GenerateContainerfileOptions = { + runtime: "node", + build: { + env: {}, + }, + image: {}, + indexScript: "index.js", + entrypoint: "entrypoint.js", + }; + + it("should generate npm install command by default", async () => { + const dockerfile = await generateContainerfile(defaultOptions); + expect(dockerfile).toContain("COPY --chown=node:node package.json ./"); + expect(dockerfile).toContain("RUN npm i --no-audit --no-fund --no-save --no-package-lock"); + }); + + it("should generate yarn install command when packageManager is yarn", async () => { + const options: GenerateContainerfileOptions = { + ...defaultOptions, + packageManager: { name: "yarn", command: "yarn", version: "1.22.19" }, + }; + const dockerfile = await generateContainerfile(options); + expect(dockerfile).toContain("RUN yarn install"); + }); + + it("should generate pnpm install command when packageManager is pnpm", async () => { + const options: GenerateContainerfileOptions = { + ...defaultOptions, + packageManager: { name: "pnpm", command: "pnpm", version: "8.6.0" }, + }; + const dockerfile = await generateContainerfile(options); + expect(dockerfile).toContain("RUN corepack enable"); + expect(dockerfile).toContain("RUN pnpm install"); + }); + + it("should copy lockfile if provided", async () => { + const options: GenerateContainerfileOptions = { + ...defaultOptions, + packageManager: { name: "yarn", command: "yarn", version: "1.22.19" }, + lockfilePath: "yarn.lock", + }; + const dockerfile = await generateContainerfile(options); + expect(dockerfile).toContain("COPY --chown=node:node yarn.lock ./"); + }); +}); diff --git a/packages/cli-v3/src/deploy/buildImage.ts b/packages/cli-v3/src/deploy/buildImage.ts index 2225d7db05..7f88188823 100644 --- a/packages/cli-v3/src/deploy/buildImage.ts +++ b/packages/cli-v3/src/deploy/buildImage.ts @@ -1,4 +1,5 @@ import { logger } from "../utilities/logger.js"; +import { PackageManager } from "nypm"; import { depot } from "@depot/cli"; import { x } from "tinyexec"; import { BuildManifest, BuildRuntime } from "@trigger.dev/core/v3/schemas"; @@ -550,13 +551,12 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise = { @@ -869,7 +871,11 @@ ENV NODE_ENV=production ENV NPM_CONFIG_UPDATE_NOTIFIER=false COPY --chown=node:node package.json ./ -RUN npm i --no-audit --no-fund --no-save --no-package-lock +${options.lockfilePath + ? `COPY --chown=node:node ${options.lockfilePath} ./` + : "# No lockfile path provided" + } +${getInstallCommand(options.packageManager)} # Now copy all the files # IMPORTANT: Do this after running npm install because npm i will wipe out the node_modules directory @@ -1161,3 +1167,21 @@ function getOutputOptions({ return outputOptions; } + +function getInstallCommand(packageManager?: PackageManager | null) { + switch (packageManager?.name) { + case "yarn": { + return "RUN yarn install"; + } + case "pnpm": { + return ` + RUN corepack enable + RUN pnpm install + `; + } + case "npm": + default: { + return "RUN npm i --no-audit --no-fund --no-save --no-package-lock"; + } + } +}