trigger.dev
Version:
A Command-Line Interface for Trigger.dev projects
347 lines • 17 kB
JavaScript
import { intro, outro, log } from "@clack/prompts";
import { getBranch, prepareDeploymentError } from "@trigger.dev/core/v3";
import { Option as CommandOption } from "commander";
import { resolve } from "node:path";
import { z } from "zod";
import { buildWorker } from "../../build/buildWorker.js";
import { CommonCommandOptions, commonOptions, handleTelemetry, SkipLoggingError, wrapCommandAction, } from "../../cli/common.js";
import { loadConfig } from "../../config.js";
import { buildImage } from "../../deploy/buildImage.js";
import { checkLogsForErrors, checkLogsForWarnings, printErrors, printWarnings, saveLogs, } from "../../deploy/logs.js";
import { chalkError, cliLink, isLinksSupported, prettyError } from "../../utilities/cliOutput.js";
import { loadDotEnvVars } from "../../utilities/dotEnv.js";
import { printStandloneInitialBanner } from "../../utilities/initialBanner.js";
import { logger } from "../../utilities/logger.js";
import { getProjectClient } from "../../utilities/session.js";
import { getTmpDir } from "../../utilities/tempDirectories.js";
import { spinner } from "../../utilities/windows.js";
import { login } from "../login.js";
import { updateTriggerPackages } from "../update.js";
import { resolveAlwaysExternal } from "../../build/externals.js";
import { createGitMeta } from "../../utilities/gitMeta.js";
const WorkersBuildCommandOptions = CommonCommandOptions.extend({
// docker build options
load: z.boolean().default(false),
network: z.enum(["default", "none", "host"]).optional(),
tag: z.string().optional(),
push: z.boolean().default(false),
noCache: z.boolean().default(false),
// trigger options
local: z.boolean().default(false), // TODO: default to true when webapp has no remote build support
dryRun: z.boolean().default(false),
skipSyncEnvVars: z.boolean().default(false),
env: z.enum(["prod", "staging", "preview"]),
branch: z.string().optional(),
config: z.string().optional(),
projectRef: z.string().optional(),
apiUrl: z.string().optional(),
saveLogs: z.boolean().default(false),
skipUpdateCheck: z.boolean().default(false),
envFile: z.string().optional(),
});
export function configureWorkersBuildCommand(program) {
return commonOptions(program
.command("build")
.description("Build a self-hosted worker image")
.argument("[path]", "The path to the project", ".")
.option("-e, --env <env>", "Deploy to a specific environment (currently only prod and staging are supported)", "prod")
.option("-b, --branch <branch>", "The branch to deploy to. If not provided, the branch will be detected from the current git branch.")
.option("--skip-update-check", "Skip checking for @trigger.dev package updates")
.option("-c, --config <config file>", "The name of the config file, found at [path]")
.option("-p, --project-ref <project ref>", "The project ref. Required if there is no config file. This will override the project specified in the config file.")
.option("--skip-sync-env-vars", "Skip syncing environment variables when using the syncEnvVars extension.")
.option("--env-file <env file>", "Path to the .env file to load into the CLI process. Defaults to .env in the project directory."))
.addOption(new CommandOption("--dry-run", "This will only create the build context without actually building the image. This can be useful for debugging.").hideHelp())
.addOption(new CommandOption("--no-cache", "Do not use any build cache. This will significantly slow down the build process but can be useful to fix caching issues.").hideHelp())
.option("--local", "Force building the image locally.")
.option("--push", "Push the image to the configured registry.")
.option("-t, --tag <tag>", "Specify the full name of the resulting image with an optional tag. The tag will always be overridden for remote builds.")
.option("--load", "Load the built image into your local docker")
.option("--network <mode>", "The networking mode for RUN instructions when using --local", "host")
.option("--platform <platform>", "The platform to build the deployment image for", "linux/amd64")
.option("--save-logs", "If provided, will save logs even for successful builds")
.action(async (path, options) => {
await handleTelemetry(async () => {
await printStandloneInitialBanner(true, options.profile);
await workersBuildCommand(path, options);
});
});
}
async function workersBuildCommand(dir, options) {
return await wrapCommandAction("workerBuildCommand", WorkersBuildCommandOptions, options, async (opts) => {
return await _workerBuildCommand(dir, opts);
});
}
async function _workerBuildCommand(dir, options) {
intro("Building worker image");
if (!options.skipUpdateCheck) {
await updateTriggerPackages(dir, { ...options }, true, true);
}
const projectPath = resolve(process.cwd(), dir);
const authorization = await login({
embedded: true,
defaultApiUrl: options.apiUrl,
profile: options.profile,
});
if (!authorization.ok) {
if (authorization.error === "fetch failed") {
throw new Error(`Failed to connect to ${authorization.auth?.apiUrl}. Are you sure it's the correct URL?`);
}
else {
throw new Error(`You must login first. Use the \`login\` CLI command.\n\n${authorization.error}`);
}
}
const resolvedConfig = await loadConfig({
cwd: projectPath,
overrides: { project: options.projectRef },
configFile: options.config,
});
logger.debug("Resolved config", resolvedConfig);
const gitMeta = await createGitMeta(resolvedConfig.workspaceDir);
logger.debug("gitMeta", gitMeta);
const branch = options.env === "preview" ? getBranch({ specified: options.branch, gitMeta }) : undefined;
if (options.env === "preview" && !branch) {
throw new Error("You need to specify a preview branch when deploying to preview, pass --branch <branch>.");
}
const projectClient = await getProjectClient({
accessToken: authorization.auth.accessToken,
apiUrl: authorization.auth.apiUrl,
projectRef: resolvedConfig.project,
env: options.env,
branch,
profile: options.profile,
});
if (!projectClient) {
throw new Error("Failed to get project client");
}
const serverEnvVars = await projectClient.client.getEnvironmentVariables(resolvedConfig.project);
loadDotEnvVars(resolvedConfig.workingDir, options.envFile);
const destination = getTmpDir(resolvedConfig.workingDir, "build", options.dryRun);
const $buildSpinner = spinner();
const forcedExternals = await resolveAlwaysExternal(projectClient.client);
const buildManifest = await buildWorker({
target: "unmanaged",
environment: options.env,
branch,
destination: destination.path,
resolvedConfig,
rewritePaths: true,
envVars: serverEnvVars.success ? serverEnvVars.data.variables : {},
forcedExternals,
listener: {
onBundleStart() {
$buildSpinner.start("Building project");
},
onBundleComplete(result) {
$buildSpinner.stop("Successfully built project");
logger.debug("Bundle result", result);
},
},
});
logger.debug("Successfully built project to", destination.path);
if (options.dryRun) {
logger.info(`Dry run complete. View the built project at ${destination.path}`);
return;
}
const deploymentResponse = await projectClient.client.initializeDeployment({
contentHash: buildManifest.contentHash,
userId: authorization.userId,
selfHosted: options.local,
type: "UNMANAGED",
isNativeBuild: false,
});
if (!deploymentResponse.success) {
throw new Error(`Failed to start deployment: ${deploymentResponse.error}`);
}
const deployment = deploymentResponse.data;
let local = options.local;
// If the deployment doesn't have any externalBuildData, then we can't use the remote image builder
if (!deployment.externalBuildData && !options.local) {
log.warn("This webapp instance does not support remote builds, falling back to local build. Please use the `--local` flag to skip this warning.");
local = true;
}
if (buildManifest.deploy.sync &&
buildManifest.deploy.sync.env &&
Object.keys(buildManifest.deploy.sync.env).length > 0) {
const numberOfEnvVars = Object.keys(buildManifest.deploy.sync.env).length;
const vars = numberOfEnvVars === 1 ? "var" : "vars";
if (!options.skipSyncEnvVars) {
const $spinner = spinner();
$spinner.start(`Syncing ${numberOfEnvVars} env ${vars} with the server`);
const success = await syncEnvVarsWithServer(projectClient.client, resolvedConfig.project, options.env, buildManifest.deploy.sync.env, buildManifest.deploy.sync.parentEnv);
if (!success) {
await failDeploy(projectClient.client, deployment, {
name: "SyncEnvVarsError",
message: `Failed to sync ${numberOfEnvVars} env ${vars} with the server`,
}, "", $spinner);
}
else {
$spinner.stop(`Successfully synced ${numberOfEnvVars} env ${vars} with the server`);
}
}
else {
logger.log("Skipping syncing env vars. The environment variables in your project have changed, but the --skip-sync-env-vars flag was provided.");
}
}
const version = deployment.version;
const deploymentLink = cliLink("View deployment", `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.project}/deployments/${deployment.shortCode}`);
const testLink = cliLink("Test tasks", `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.project}/test?environment=${options.env === "prod" ? "prod" : "stg"}`);
const $spinner = spinner();
if (isLinksSupported) {
$spinner.start(`Building worker version ${version} ${deploymentLink}`);
}
else {
$spinner.start(`Building worker version ${version}`);
}
const buildResult = await buildImage({
isLocalBuild: local,
imagePlatform: deployment.imagePlatform,
noCache: options.noCache,
push: options.push,
deploymentId: deployment.id,
deploymentVersion: deployment.version,
imageTag: deployment.imageTag,
load: options.load,
contentHash: deployment.contentHash,
externalBuildId: deployment.externalBuildData?.buildId,
externalBuildToken: deployment.externalBuildData?.buildToken,
externalBuildProjectId: deployment.externalBuildData?.projectId,
projectId: projectClient.id,
projectRef: resolvedConfig.project,
apiUrl: projectClient.client.apiURL,
apiKey: projectClient.client.accessToken,
apiClient: projectClient.client,
branchName: branch,
authAccessToken: authorization.auth.accessToken,
compilationPath: destination.path,
buildEnvVars: buildManifest.build.env,
network: options.network,
builder: "trigger",
});
logger.debug("Build result", buildResult);
const warnings = checkLogsForWarnings(buildResult.logs);
if (!warnings.ok) {
await failDeploy(projectClient.client, deployment, { name: "BuildError", message: warnings.summary }, buildResult.logs, $spinner, warnings.warnings, warnings.errors);
throw new SkipLoggingError("Failed to build image");
}
if (!buildResult.ok) {
await failDeploy(projectClient.client, deployment, { name: "BuildError", message: buildResult.error }, buildResult.logs, $spinner, warnings.warnings);
throw new SkipLoggingError("Failed to build image");
}
// Index the deployment
// const runtime = new UnmanagedWorkerRuntime({
// name: projectClient.name,
// config: resolvedConfig,
// args: {
// ...options,
// debugOtel: false,
// },
// client: projectClient.client,
// dashboardUrl: authorization.dashboardUrl,
// });
// await runtime.init();
// console.log("buildManifest", buildManifest);
// await runtime.initializeWorker(buildManifest);
const getDeploymentResponse = await projectClient.client.getDeployment(deployment.id);
if (!getDeploymentResponse.success) {
await failDeploy(projectClient.client, deployment, { name: "DeploymentError", message: getDeploymentResponse.error }, buildResult.logs, $spinner);
throw new SkipLoggingError("Failed to get deployment with worker");
}
const deploymentWithWorker = getDeploymentResponse.data;
if (!deploymentWithWorker.worker) {
await failDeploy(projectClient.client, deployment, { name: "DeploymentError", message: "Failed to get deployment with worker" }, buildResult.logs, $spinner);
throw new SkipLoggingError("Failed to get deployment with worker");
}
$spinner.stop(`Successfully built worker version ${version}`);
const taskCount = deploymentWithWorker.worker?.tasks.length ?? 0;
log.message(`Detected ${taskCount} task${taskCount === 1 ? "" : "s"}`);
if (taskCount > 0) {
logger.table(deploymentWithWorker.worker.tasks.map((task) => ({
id: task.slug,
export: task.exportName ?? "@deprecated",
path: task.filePath,
})));
}
outro(`Version ${version} built and ready to deploy: ${deployment.imageTag} ${isLinksSupported ? `| ${deploymentLink} | ${testLink}` : ""}`);
}
export async function syncEnvVarsWithServer(apiClient, projectRef, environmentSlug, envVars, parentEnvVars) {
const uploadResult = await apiClient.importEnvVars(projectRef, environmentSlug, {
variables: envVars,
parentVariables: parentEnvVars,
override: true,
});
return uploadResult.success;
}
async function failDeploy(client, deployment, error, logs, $spinner, warnings, errors) {
$spinner.stop(`Failed to deploy project`);
const doOutputLogs = async (prefix = "Error") => {
if (logs.trim() !== "") {
const logPath = await saveLogs(deployment.shortCode, logs);
printWarnings(warnings);
printErrors(errors);
checkLogsForErrors(logs);
outro(`${chalkError(`${prefix}:`)} ${error.message}. Full build logs have been saved to ${logPath}`);
}
else {
outro(`${chalkError(`${prefix}:`)} ${error.message}.`);
}
};
const exitCommand = (message) => {
throw new SkipLoggingError(message);
};
const deploymentResponse = await client.getDeployment(deployment.id);
if (!deploymentResponse.success) {
logger.debug(`Failed to get deployment with worker: ${deploymentResponse.error}`);
}
else {
const serverDeployment = deploymentResponse.data;
switch (serverDeployment.status) {
case "PENDING":
case "DEPLOYING":
case "BUILDING": {
await doOutputLogs();
await client.failDeployment(deployment.id, {
error,
});
exitCommand("Failed to deploy project");
break;
}
case "CANCELED": {
await doOutputLogs("Canceled");
exitCommand("Failed to deploy project");
break;
}
case "FAILED": {
const errorData = serverDeployment.errorData
? prepareDeploymentError(serverDeployment.errorData)
: undefined;
if (errorData) {
prettyError(errorData.name, errorData.stack, errorData.stderr);
if (logs.trim() !== "") {
const logPath = await saveLogs(deployment.shortCode, logs);
outro(`Aborting deployment. Full build logs have been saved to ${logPath}`);
}
else {
outro(`Aborting deployment`);
}
}
else {
await doOutputLogs("Failed");
}
exitCommand("Failed to deploy project");
break;
}
case "DEPLOYED": {
await doOutputLogs("Deployed with errors");
exitCommand("Deployed with errors");
break;
}
case "TIMED_OUT": {
await doOutputLogs("TimedOut");
exitCommand("Timed out");
break;
}
}
}
}
//# sourceMappingURL=build.js.map