diff --git a/.env.example b/.env.example index 3de1889bf..9916679a5 100644 --- a/.env.example +++ b/.env.example @@ -55,6 +55,8 @@ COORDINATOR_SECRET=coordinator-secret # generate the actual secret with `openssl # ENABLE_REGISTRY_PROXY=true # DEPOT_TOKEN= # DEPOT_PROJECT_ID= +# DEPLOY_REGISTRY_HOST=${APP_ORIGIN} # This is the host that the deploy CLI will use to push images to the registry # CONTAINER_REGISTRY_ORIGIN= # CONTAINER_REGISTRY_USERNAME= -# CONTAINER_REGISTRY_PASSWORD= \ No newline at end of file +# CONTAINER_REGISTRY_PASSWORD= +# DEV_OTEL_EXPORTER_OTLP_ENDPOINT="http://0.0.0.0:4318" \ No newline at end of file diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 2eab2a3c5..ea6c149cd 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -3,21 +3,11 @@ on: workflow_call: jobs: publish: - strategy: - fail-fast: true # when a job fails, all remaining ones will be cancelled - matrix: - runs-on: [buildjet-4vcpu-ubuntu-2204, buildjet-4vcpu-ubuntu-2204-arm] - name: ${{matrix.runs-on}} - runs-on: ${{matrix.runs-on}} + runs-on: ubuntu-latest outputs: version: ${{ steps.get_version.outputs.version }} short_sha: ${{ steps.get_commit.outputs.sha_short }} steps: - - name: 🐳 Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - name: ⬇️ Checkout repo uses: actions/checkout@v3 with: @@ -43,19 +33,12 @@ jobs: echo "Invalid reference: ${GITHUB_REF}" exit 1 fi - if [[ ${{matrix.runs-on}} == *-arm ]]; then - IMAGE_TAG="${IMAGE_TAG}-arm" - fi echo "::set-output name=version::${IMAGE_TAG}" - name: 🔢 Get the commit hash id: get_commit run: | echo ::set-output name=sha_short::$(echo ${{ github.sha }} | cut -c1-7) - - name: 🐳 Build Docker Image - run: | - docker build -t release_build_image -f ./docker/Dockerfile . - - name: 🐙 Login to GitHub Container Registry uses: docker/login-action@v2 with: @@ -63,24 +46,11 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: 🐙 Push to GitHub Container Registry - run: | - docker tag release_build_image $REGISTRY/$REPOSITORY:$IMAGE_TAG - docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG - env: - REGISTRY: ghcr.io/triggerdotdev - REPOSITORY: trigger.dev - IMAGE_TAG: ${{ steps.get_version.outputs.version }} - - - name: 🐙 Push 'latest' to GitHub Container Registry - if: startsWith(github.ref, 'refs/tags/v.docker') - run: | - LATEST=latest - if [[ ${{matrix.runs-on}} == *-arm ]]; then - LATEST="${LATEST}-arm" - fi - docker tag release_build_image $REGISTRY/$REPOSITORY:$LATEST - docker push $REGISTRY/$REPOSITORY:$LATEST - env: - REGISTRY: ghcr.io/triggerdotdev - REPOSITORY: trigger.dev + - name: 🐳 Build image and push to GitHub Container Registry + uses: depot/build-push-action@v1 + with: + file: ./docker/Dockerfile + platforms: linux/amd64,linux/arm64 + tags: | + ghcr.io/triggerdotdev/trigger.dev:${{ steps.get_version.outputs.version }} + push: true diff --git a/apps/webapp/app/components/primitives/Table.tsx b/apps/webapp/app/components/primitives/Table.tsx index 1a300b2a1..978153ac0 100644 --- a/apps/webapp/app/components/primitives/Table.tsx +++ b/apps/webapp/app/components/primitives/Table.tsx @@ -253,7 +253,7 @@ export const TableCellMenu = forwardRef< type TableBlankRowProps = { className?: string; colSpan: number; - children: ReactNode; + children?: ReactNode; }; export const TableBlankRow = forwardRef( diff --git a/apps/webapp/app/components/runs/v3/CancelRunDialog.tsx b/apps/webapp/app/components/runs/v3/CancelRunDialog.tsx new file mode 100644 index 000000000..97f031d33 --- /dev/null +++ b/apps/webapp/app/components/runs/v3/CancelRunDialog.tsx @@ -0,0 +1,43 @@ +import { StopCircleIcon } from "@heroicons/react/20/solid"; +import { useFetcher } from "@remix-run/react"; +import { Button } from "~/components/primitives/Buttons"; +import { + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, +} from "~/components/primitives/Dialog"; + +type CancelRunDialogProps = { + runFriendlyId: string; + redirectPath: string; +}; + +export function CancelRunDialog({ runFriendlyId, redirectPath }: CancelRunDialogProps) { + const cancelFetcher = useFetcher(); + + return ( + + Cancel this run? + + Canceling a run will stop execution. If you want to run this later you will have to replay + the entire run with the original payload. + + + + + + + + ); +} diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index 684bc8b73..fb58765dc 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx @@ -1,10 +1,10 @@ import { StopIcon } from "@heroicons/react/24/outline"; -import { CheckIcon } from "@heroicons/react/24/solid"; +import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid"; import { User } from "@trigger.dev/database"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; -import { RunListItem } from "~/presenters/v3/RunListPresenter.server"; -import { v3RunPath } from "~/utils/pathBuilder"; +import { RunListAppliedFilters, RunListItem } from "~/presenters/v3/RunListPresenter.server"; +import { docsPath, v3RunPath, v3TestPath } from "~/utils/pathBuilder"; import { EnvironmentLabel } from "../../environments/EnvironmentLabel"; import { DateTime } from "../../primitives/DateTime"; import { Paragraph } from "../../primitives/Paragraph"; @@ -15,16 +15,24 @@ import { TableBody, TableCell, TableCellChevron, + TableCellMenu, TableHeader, TableHeaderCell, TableRow, } from "../../primitives/Table"; import { formatDuration } from "@trigger.dev/core/v3"; import { TaskRunStatusCombo } from "./TaskRunStatus"; +import { useEnvironments } from "~/hooks/useEnvironments"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; +import { StopCircleIcon } from "@heroicons/react/20/solid"; +import { Dialog, DialogTrigger } from "~/components/primitives/Dialog"; +import { CancelRunDialog } from "./CancelRunDialog"; +import { useLocation } from "@remix-run/react"; type RunsTableProps = { total: number; hasFilters: boolean; + filters: RunListAppliedFilters; showJob?: boolean; runs: RunListItem[]; isLoading?: boolean; @@ -34,12 +42,14 @@ type RunsTableProps = { export function TaskRunsTable({ total, hasFilters, + filters, runs, isLoading = false, currentUser, }: RunsTableProps) { const organization = useOrganization(); const project = useProject(); + const location = useLocation(); return ( @@ -65,9 +75,7 @@ export function TaskRunsTable({ {!isLoading && } ) : runs.length === 0 ? ( - - {!isLoading && } - + ) : ( runs.map((run) => { const path = v3RunPath(organization, project, run); @@ -102,7 +110,23 @@ export function TaskRunsTable({ {run.createdAt ? : "–"} - + {run.isCancellable ? ( + + + + + + + + + ) : ( + {""} + )} ); }) @@ -127,3 +151,62 @@ function NoRuns({ title }: { title: string }) { ); } + +function BlankState({ isLoading, filters }: Pick) { + const organization = useOrganization(); + const project = useProject(); + const envs = useEnvironments(); + if (isLoading) return ; + + const { environments, tasks, from, to, ...otherFilters } = filters; + + if ( + filters.environments.length === 1 && + filters.tasks.length === 1 && + filters.from === undefined && + filters.to === undefined && + Object.values(otherFilters).every((filterArray) => filterArray.length === 0) + ) { + const environment = envs?.find((env) => env.id === filters.environments[0]); + return ( + +
+ + There are no runs for {filters.tasks[0]} + {environment ? ( + <> + {" "} + in + + ) : null} + +
+ + Create a test run + + or + + Triggering a task docs + +
+
+
+ ); + } + + return ( + + + + ); +} diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 43936a232..54fcae146 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -81,6 +81,8 @@ const EnvironmentSchema = z.object({ CONTAINER_REGISTRY_ORIGIN: z.string().optional(), CONTAINER_REGISTRY_USERNAME: z.string().optional(), CONTAINER_REGISTRY_PASSWORD: z.string().optional(), + DEPLOY_REGISTRY_HOST: z.string().optional(), + DEV_OTEL_EXPORTER_OTLP_ENDPOINT: z.string().optional(), }); export type Environment = z.infer; diff --git a/apps/webapp/app/presenters/v3/RunListPresenter.server.ts b/apps/webapp/app/presenters/v3/RunListPresenter.server.ts index 37da01f31..144fe2e1c 100644 --- a/apps/webapp/app/presenters/v3/RunListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/RunListPresenter.server.ts @@ -2,6 +2,7 @@ import { Prisma, TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/databa import { Direction } from "~/components/runs/RunStatuses"; import { PrismaClient, prisma } from "~/db.server"; import { getUsername } from "~/utils/username"; +import { CANCELLABLE_STATUSES } from "~/v3/services/cancelTaskRun.server"; type RunListOptions = { userId: string; @@ -23,6 +24,7 @@ const DEFAULT_PAGE_SIZE = 20; export type RunList = Awaited>; export type RunListItem = RunList["runs"][0]; +export type RunListAppliedFilters = RunList["filters"]; export class RunListPresenter { #prismaClient: PrismaClient; @@ -220,6 +222,7 @@ export class RunListPresenter { version: run.version, taskIdentifier: run.taskIdentifier, attempts: Number(run.attempts), + isCancellable: CANCELLABLE_STATUSES.includes(run.status), environment: { type: environment.type, slug: environment.slug, @@ -233,6 +236,14 @@ export class RunListPresenter { previous, }, possibleTasks: possibleTasks.map((task) => task.slug), + filters: { + tasks: tasks || [], + versions: versions || [], + statuses: statuses || [], + environments: environments || [], + from, + to, + }, hasFilters, }; } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx index 7ababa99e..aa2c753b7 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx @@ -19,6 +19,7 @@ import { import { Header2 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import { Property, PropertyTable } from "~/components/primitives/PropertyTable"; +import { CancelRunDialog } from "~/components/runs/v3/CancelRunDialog"; import { LiveTimer } from "~/components/runs/v3/LiveTimer"; import { RunIcon } from "~/components/runs/v3/RunIcon"; import { SpanEvents } from "~/components/runs/v3/SpanEvents"; @@ -54,7 +55,6 @@ export default function Page() { const organization = useOrganization(); const project = useProject(); const { runParam } = useParams(); - const cancelFetcher = useFetcher(); return (
- - Cancel this run? - - Canceling a run will stop execution. If you want to run this later you will have - to replay the entire run with the original payload. - - - - - - - + )}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx index 733b941b2..4ec9abbdb 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx @@ -581,11 +581,7 @@ function NodeStatusIcon({ node }: { node: RunEvent }) { } function TaskLine({ isError, isSelected }: { isError: boolean; isSelected: boolean }) { - return ( -
- ); + return
; } function ShowParentLink({ runFriendlyId }: { runFriendlyId: string }) { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs._index/route.tsx index 34fcdd1be..a512648dc 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs._index/route.tsx @@ -84,6 +84,7 @@ export default function Page() { = [ +export const CANCELLABLE_STATUSES: Array = [ "PENDING", "EXECUTING", "PAUSED", diff --git a/apps/webapp/server.ts b/apps/webapp/server.ts index 3d9d335ea..02e86aec2 100644 --- a/apps/webapp/server.ts +++ b/apps/webapp/server.ts @@ -68,16 +68,25 @@ if (process.env.HTTP_SERVER_DISABLED !== "true") { next(); }); - app.all( - "*", - createRequestHandler({ - build, - mode: MODE, - }) - ); + if (process.env.DASHBOARD_AND_API_DISABLED !== "true") { + app.all( + "*", + createRequestHandler({ + build, + mode: MODE, + }) + ); + } else { + // we need to do the health check here at /healthcheck + app.get("/healthcheck", (req, res) => { + res.status(200).send("OK"); + }); + } + + const server = app.listen(port, () => { - console.log(`✅ app ready: http://localhost:${port} [NODE_ENV: ${MODE}]`); + console.log(`✅ server ready: http://localhost:${port} [NODE_ENV: ${MODE}]`); if (MODE === "development") { broadcastDevReady(build) diff --git a/depot.json b/depot.json new file mode 100644 index 000000000..60fd90c6e --- /dev/null +++ b/depot.json @@ -0,0 +1,3 @@ +{ + "id": "g2k5ln95n6" +} \ No newline at end of file diff --git a/packages/cli-v3/src/cli/common.ts b/packages/cli-v3/src/cli/common.ts index 6068fb850..9bf83c61e 100644 --- a/packages/cli-v3/src/cli/common.ts +++ b/packages/cli-v3/src/cli/common.ts @@ -10,12 +10,14 @@ 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("default"), }); export type CommonCommandOptions = z.infer; export function commonOptions(command: Command) { return command + .option("--profile ", "The login profile to use", "default") .option("-a, --api-url ", "Override the API URL", "https://api.trigger.dev") .option( "-l, --log-level ", @@ -25,9 +27,9 @@ export function commonOptions(command: Command) { .option("--skip-telemetry", "Opt-out of sending telemetry"); } -export class SkipLoggingError extends Error {} -export class SkipCommandError extends Error {} -export class OutroCommandError extends SkipCommandError {} +export class SkipLoggingError extends Error { } +export class SkipCommandError extends Error { } +export class OutroCommandError extends SkipCommandError { } export async function handleTelemetry(action: () => Promise) { try { diff --git a/packages/cli-v3/src/cli/index.ts b/packages/cli-v3/src/cli/index.ts index 11cf6406d..faa7b874c 100644 --- a/packages/cli-v3/src/cli/index.ts +++ b/packages/cli-v3/src/cli/index.ts @@ -3,12 +3,10 @@ import { configureDeployCommand } from "../commands/deploy.js"; import { configureDevCommand } from "../commands/dev.js"; import { configureInitCommand } from "../commands/init.js"; import { configureLoginCommand } from "../commands/login.js"; -import { logoutCommand } from "../commands/logout.js"; -import { updateCommand } from "../commands/update.js"; +import { configureLogoutCommand } from "../commands/logout.js"; import { configureWhoamiCommand } from "../commands/whoami.js"; import { COMMAND_NAME } from "../consts.js"; import { getVersion } from "../utilities/getVersion.js"; -import { printInitialBanner } from "../utilities/initialBanner.js"; export const program = new Command(); @@ -19,35 +17,7 @@ program configureLoginCommand(program); configureInitCommand(program); - -program - .command("logout") - .description("Logout of Trigger.dev") - .version(getVersion(), "-v, --version", "Display the version number") - .action(async (options) => { - try { - await printInitialBanner(false); - await logoutCommand(options); - //todo login command - } catch (e) { - //todo error reporting - throw e; - } - }); - configureDevCommand(program); configureDeployCommand(program); - -program - .command("update") - .description( - "Updates all @trigger.dev/* packages to their latest compatible versions or the specified version" - ) - .argument("[path]", "The path to the directory that contains the package.json file", ".") - .option("-t, --to ", "The version to update to (ex: 2.1.4)", "latest") - .action(async (path, options) => { - await printInitialBanner(false); - await updateCommand(path, options); - }); - configureWhoamiCommand(program); +configureLogoutCommand(program); diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index 2c1c53814..c06937852 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -134,7 +134,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { intro("Deploying project"); - const authorization = await login({ embedded: true, defaultApiUrl: options.apiUrl }); + const authorization = await login({ embedded: true, defaultApiUrl: options.apiUrl, profile: options.profile }); if (!authorization.ok) { if (authorization.error === "fetch failed") { @@ -228,7 +228,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { const deploymentSpinner = spinner(); deploymentSpinner.start(`Deploying version ${version}`); - const registryHost = new URL(deploymentEnv.data.apiUrl).host; + const registryHost = deploymentResponse.data.registryHost ?? options.registry ?? "registry.trigger.dev"; const buildImage = async () => { if (options.selfHosted) { @@ -345,8 +345,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { ); } else { outro( - `Version ${version} deployed with ${taskCount} detected task${ - taskCount === 1 ? "" : "s" + `Version ${version} deployed with ${taskCount} detected task${taskCount === 1 ? "" : "s" } ${deploymentLink}` ); } @@ -510,14 +509,14 @@ type BuildAndPushImageOptions = { type BuildAndPushImageResults = | { - ok: true; - image: string; - digest?: string; - } + ok: true; + image: string; + digest?: string; + } | { - ok: false; - error: string; - }; + ok: false; + error: string; + }; async function buildAndPushImage( options: BuildAndPushImageOptions @@ -579,6 +578,7 @@ async function buildAndPushImage( DEPOT_TOKEN: options.buildToken, DEPOT_PROJECT_ID: options.buildProjectId, DEPOT_NO_SUMMARY_LINK: "1", + DEPOT_NO_UPDATE_NOTIFIER: "1", DOCKER_CONFIG: dockerConfigDir, }, }); diff --git a/packages/cli-v3/src/commands/dev.tsx b/packages/cli-v3/src/commands/dev.tsx index 168791ddd..6cda6cd37 100644 --- a/packages/cli-v3/src/commands/dev.tsx +++ b/packages/cli-v3/src/commands/dev.tsx @@ -24,7 +24,7 @@ import { ClientOptions, WebSocket as wsWebSocket } from "ws"; import { z } from "zod"; import * as packageJson from "../../package.json"; import { CliApiClient } from "../apiClient"; -import { CommonCommandOptions } from "../cli/common.js"; +import { CommonCommandOptions, commonOptions, wrapCommandAction } from "../cli/common.js"; import { readConfig } from "../utilities/configFiles"; import { printStandloneInitialBanner } from "../utilities/initialBanner.js"; import { detectPackageNameFromImportPath } from "../utilities/installPackages"; @@ -47,46 +47,31 @@ const DevCommandOptions = CommonCommandOptions.extend({ type DevCommandOptions = z.infer; export function configureDevCommand(program: Command) { - program - .command("dev") - .description("Run your Trigger.dev tasks locally") - .argument("[path]", "The path to the project", ".") - .option( - "-l, --log-level ", - "The log level to use (debug, info, log, warn, error, none)", - "log" - ) - .option( - "-c, --config ", - "The name of the config file, found at [path]", - "trigger.config.mjs" - ) - .option( - "-p, --project-ref ", - "The project ref. Required if there is no config file." - ) - .option("--debugger", "Enable the debugger") - .option("--debug-otel", "Enable OpenTelemetry debugging") - .action(async (path, options) => { - try { - await devCommand(path, options); - } catch (e) { - //todo error reporting - throw e; - } + return commonOptions( + program + .command("dev") + .description("Run your Trigger.dev tasks locally") + .argument("[path]", "The path to the project", ".") + .option( + "-c, --config ", + "The name of the config file, found at [path]", + "trigger.config.mjs" + ) + .option( + "-p, --project-ref ", + "The project ref. Required if there is no config file." + ) + .option("--debugger", "Enable the debugger") + .option("--debug-otel", "Enable OpenTelemetry debugging") + ).action(async (path, options) => { + wrapCommandAction("dev", DevCommandOptions, options, async (opts) => { + await devCommand(path, opts); }); + }); } -export async function devCommand(dir: string, anyOptions: unknown) { - const options = DevCommandOptions.safeParse(anyOptions); - - if (!options.success) { - console.log(fromZodError(options.error).toString()); - - process.exit(1); - } - - const authorization = await isLoggedIn(); +export async function devCommand(dir: string, options: DevCommandOptions) { + const authorization = await isLoggedIn(options.profile); if (!authorization.ok) { if (authorization.error === "fetch failed") { @@ -101,7 +86,7 @@ export async function devCommand(dir: string, anyOptions: unknown) { let watcher; try { - const devInstance = await startDev(dir, options.data, authorization.auth); + const devInstance = await startDev(dir, options, authorization.auth); watcher = devInstance.watcher; const { waitUntilExit } = devInstance.devReactElement; await waitUntilExit(); diff --git a/packages/cli-v3/src/commands/init.ts b/packages/cli-v3/src/commands/init.ts index e8a922c12..6a8a31c05 100644 --- a/packages/cli-v3/src/commands/init.ts +++ b/packages/cli-v3/src/commands/init.ts @@ -80,7 +80,7 @@ async function _initCommand(dir: string, options: InitCommandOptions) { intro("Initializing project"); - const authorization = await login({ embedded: true, defaultApiUrl: options.apiUrl }); + const authorization = await login({ embedded: true, defaultApiUrl: options.apiUrl, profile: options.profile }); if (!authorization.ok) { if (authorization.error === "fetch failed") { @@ -96,6 +96,7 @@ async function _initCommand(dir: string, options: InitCommandOptions) { "cli.userId": authorization.userId, "cli.email": authorization.email, "cli.config.apiUrl": authorization.auth.apiUrl, + "cli.config.profile": authorization.profile, }); if (!options.overrideConfig) { diff --git a/packages/cli-v3/src/commands/login.ts b/packages/cli-v3/src/commands/login.ts index 60a707a05..9299974a7 100644 --- a/packages/cli-v3/src/commands/login.ts +++ b/packages/cli-v3/src/commands/login.ts @@ -14,11 +14,12 @@ import { wrapCommandAction, } from "../cli/common.js"; import { chalkLink } from "../utilities/colors.js"; -import { readAuthConfigFile, writeAuthConfigFile } from "../utilities/configFiles.js"; +import { readAuthConfigProfile, writeAuthConfigProfile } from "../utilities/configFiles.js"; import { getVersion } from "../utilities/getVersion.js"; import { printInitialBanner } from "../utilities/initialBanner.js"; import { LoginResult } from "../utilities/session.js"; import { whoAmI } from "./whoami.js"; +import { logger } from "../utilities/logger.js"; export const LoginCommandOptions = CommonCommandOptions.extend({ apiUrl: z.string(), @@ -48,12 +49,13 @@ export async function loginCommand(options: unknown) { } async function _loginCommand(options: LoginCommandOptions) { - return login({ defaultApiUrl: options.apiUrl, embedded: false }); + return login({ defaultApiUrl: options.apiUrl, embedded: false, profile: options.profile }); } export type LoginOptions = { defaultApiUrl?: string; embedded?: boolean; + profile?: string; }; export async function login(options?: LoginOptions): Promise { @@ -63,16 +65,17 @@ export async function login(options?: LoginOptions): Promise { span.setAttributes({ "cli.config.apiUrl": opts.defaultApiUrl, + "cli.options.profile": opts.profile, }); if (!opts.embedded) { intro("Logging in to Trigger.dev"); } - const authConfig = readAuthConfigFile(); + const authConfig = readAuthConfigProfile(options?.profile); if (authConfig && authConfig.accessToken) { - const whoAmIResult = await whoAmI(undefined, opts.embedded); + const whoAmIResult = await whoAmI({ profile: options?.profile ?? "default", skipTelemetry: !span.isRecording(), logLevel: logger.loggerLevel }, opts.embedded); if (!whoAmIResult.success) { throw new Error(whoAmIResult.error); @@ -106,6 +109,7 @@ export async function login(options?: LoginOptions): Promise { return { ok: true as const, + profile: options?.profile ?? "default", userId: whoAmIResult.data.userId, email: whoAmIResult.data.email, dashboardUrl: whoAmIResult.data.dashboardUrl, @@ -126,6 +130,7 @@ export async function login(options?: LoginOptions): Promise { return { ok: true as const, + profile: options?.profile ?? "default", userId: whoAmIResult.data.userId, email: whoAmIResult.data.email, dashboardUrl: whoAmIResult.data.dashboardUrl, @@ -170,9 +175,9 @@ export async function login(options?: LoginOptions): Promise { getPersonalAccessTokenSpinner.stop(`Logged in with token ${indexResult.obfuscatedToken}`); - writeAuthConfigFile({ accessToken: indexResult.token, apiUrl: opts.defaultApiUrl }); + writeAuthConfigProfile({ accessToken: indexResult.token, apiUrl: opts.defaultApiUrl }, options?.profile); - const whoAmIResult = await whoAmI(undefined, opts.embedded); + const whoAmIResult = await whoAmI({ profile: options?.profile ?? "default", skipTelemetry: !span.isRecording(), logLevel: logger.loggerLevel }, opts.embedded); if (!whoAmIResult.success) { throw new Error(whoAmIResult.error); @@ -188,6 +193,7 @@ export async function login(options?: LoginOptions): Promise { return { ok: true as const, + profile: options?.profile ?? "default", userId: whoAmIResult.data.userId, email: whoAmIResult.data.email, dashboardUrl: whoAmIResult.data.dashboardUrl, diff --git a/packages/cli-v3/src/commands/logout.ts b/packages/cli-v3/src/commands/logout.ts index 3ccb136d2..2e8aef653 100644 --- a/packages/cli-v3/src/commands/logout.ts +++ b/packages/cli-v3/src/commands/logout.ts @@ -1,15 +1,41 @@ -import { readAuthConfigFile, writeAuthConfigFile } from "../utilities/configFiles.js"; +import { Command } from "commander"; +import { readAuthConfigProfile, writeAuthConfigProfile } from "../utilities/configFiles.js"; import { logger } from "../utilities/logger.js"; +import { CommonCommandOptions, commonOptions, handleTelemetry, wrapCommandAction } from "../cli/common.js"; +import { printInitialBanner } from "../utilities/initialBanner.js"; +import { z } from "zod"; -export async function logoutCommand(options: any) { - const config = readAuthConfigFile(); +const LogoutCommandOptions = CommonCommandOptions; + +type LogoutCommandOptions = z.infer; + +export function configureLogoutCommand(program: Command) { + return commonOptions(program + .command("logout") + .description("Logout of Trigger.dev")) + .action(async (options) => { + await handleTelemetry(async () => { + await printInitialBanner(false); + await logoutCommand(options); + }); + }); +} + +export async function logoutCommand(options: unknown) { + return await wrapCommandAction("logoutCommand", LogoutCommandOptions, options, async (opts) => { + return await logout(opts); + }); +} + +export async function logout(options: LogoutCommandOptions) { + const config = readAuthConfigProfile(options.profile); if (!config?.accessToken) { - logger.info("You are already logged out"); + logger.info(`You are already logged out [${options.profile ?? "default"}]`); return; } - writeAuthConfigFile({ ...config, accessToken: undefined, apiUrl: undefined }); + writeAuthConfigProfile({ ...config, accessToken: undefined, apiUrl: undefined }, options.profile); - logger.info("Logged out"); + logger.info(`Logged out of Trigger.dev [${options.profile ?? "default"}]`); } diff --git a/packages/cli-v3/src/commands/whoami.ts b/packages/cli-v3/src/commands/whoami.ts index ef8b53373..f5d9e0494 100644 --- a/packages/cli-v3/src/commands/whoami.ts +++ b/packages/cli-v3/src/commands/whoami.ts @@ -1,68 +1,65 @@ -import { note, spinner } from "@clack/prompts"; +import { intro, note, spinner } from "@clack/prompts"; import { chalkLink } from "../utilities/colors.js"; import { logger } from "../utilities/logger.js"; import { isLoggedIn } from "../utilities/session.js"; import { Command } from "commander"; import { printInitialBanner } from "../utilities/initialBanner.js"; -import { CommonCommandOptions } from "../cli/common.js"; +import { CommonCommandOptions, commonOptions, handleTelemetry, wrapCommandAction } from "../cli/common.js"; import { z } from "zod"; import { CliApiClient } from "../apiClient.js"; type WhoAmIResult = | { - success: true; - data: { - userId: string; - email: string; - dashboardUrl: string; - }; - } - | { - success: false; - error: string; + success: true; + data: { + userId: string; + email: string; + dashboardUrl: string; }; + } + | { + success: false; + error: string; + }; const WhoamiCommandOptions = CommonCommandOptions; type WhoamiCommandOptions = z.infer; export function configureWhoamiCommand(program: Command) { - program + return commonOptions(program .command("whoami") - .description("display the current logged in user and project details") - .option( - "-l, --log-level ", - "The log level to use (debug, info, log, warn, error, none)", - "log" - ) + .description("display the current logged in user and project details")) .action(async (options) => { - try { - await printInitialBanner(); - await whoAmI(WhoamiCommandOptions.parse(options)); - } catch (e) { - throw e; - } + await handleTelemetry(async () => { + await printInitialBanner(false); + await whoAmICommand(options); + }); }); } +export async function whoAmICommand(options: unknown) { + return await wrapCommandAction("whoamiCommand", WhoamiCommandOptions, options, async (opts) => { + return await whoAmI(opts); + }); +} + export async function whoAmI( options?: WhoamiCommandOptions, embedded: boolean = false ): Promise { - if (options?.logLevel) { - logger.loggerLevel = options?.logLevel; - } + intro(`Displaying your account details [${options?.profile ?? "default"}]`); const loadingSpinner = spinner(); loadingSpinner.start("Checking your account details"); - const authentication = await isLoggedIn(); + const authentication = await isLoggedIn(options?.profile); if (!authentication.ok) { if (authentication.error === "fetch failed") { loadingSpinner.stop("Fetch failed. Platform down?"); } else { - loadingSpinner.stop("You must login first. Use `trigger.dev login` to login."); + loadingSpinner.stop(`You must login first. Use \`trigger.dev login --profile ${options?.profile ?? "default"}\` to login.`); } return { @@ -90,7 +87,7 @@ export async function whoAmI( Email: ${userData.data.email} URL: ${chalkLink(authentication.auth.apiUrl)} `, - "Account details" + `Account details [${authentication.profile}]` ); } else { loadingSpinner.stop(`Retrieved your account details for ${userData.data.email}`); diff --git a/packages/cli-v3/src/utilities/configFiles.ts b/packages/cli-v3/src/utilities/configFiles.ts index c3500ad57..a2f87f0bb 100644 --- a/packages/cli-v3/src/utilities/configFiles.ts +++ b/packages/cli-v3/src/utilities/configFiles.ts @@ -24,11 +24,53 @@ export const UserAuthConfigSchema = z.object({ export type UserAuthConfig = z.infer; +const UserAuthConfigFileSchema = z.record(UserAuthConfigSchema); + +type UserAuthConfigFile = z.infer; + function getAuthConfigFilePath() { return path.join(getGlobalConfigFolderPath(), "default.json"); } -export function writeAuthConfigFile(config: UserAuthConfig) { +export function writeAuthConfigProfile(config: UserAuthConfig, profile: string = "default") { + const existingConfig = readAuthConfigFile() || {}; + + existingConfig[profile] = config; + + writeAuthConfigFile(existingConfig); +} + +export function readAuthConfigProfile(profile: string = "default"): UserAuthConfig | undefined { + try { + const authConfigFilePath = getAuthConfigFilePath(); + + logger.debug(`Reading auth config file`, { authConfigFilePath }); + + const json = readJSONFileSync(authConfigFilePath); + const parsed = UserAuthConfigFileSchema.parse(json); + return parsed[profile]; + } catch (error) { + logger.debug(`Error reading auth config file: ${error}`); + return undefined; + } +} + +function readAuthConfigFile(): UserAuthConfigFile | undefined { + try { + const authConfigFilePath = getAuthConfigFilePath(); + + logger.debug(`Reading auth config file`, { authConfigFilePath }); + + const json = readJSONFileSync(authConfigFilePath); + const parsed = UserAuthConfigFileSchema.parse(json); + return parsed; + } catch (error) { + logger.debug(`Error reading auth config file: ${error}`); + return undefined; + } +} + +function writeAuthConfigFile(config: UserAuthConfigFile) { const authConfigFilePath = getAuthConfigFilePath(); mkdirSync(path.dirname(authConfigFilePath), { recursive: true, @@ -38,19 +80,6 @@ export function writeAuthConfigFile(config: UserAuthConfig) { }); } -export function readAuthConfigFile(): UserAuthConfig | undefined { - try { - const authConfigFilePath = getAuthConfigFilePath(); - - const json = readJSONFileSync(authConfigFilePath); - const parsed = UserAuthConfigSchema.parse(json); - return parsed; - } catch (error) { - logger.debug(`Error reading auth config file: ${error}`); - return undefined; - } -} - async function getConfigPath(dir: string, fileName?: string): Promise { return await findUp(fileName ? [fileName] : CONFIG_FILES, { cwd: dir }); } @@ -62,14 +91,14 @@ export type ReadConfigOptions = { export type ReadConfigResult = | { - status: "file"; - config: ResolvedConfig; - path: string; - } + status: "file"; + config: ResolvedConfig; + path: string; + } | { - status: "in-memory"; - config: ResolvedConfig; - }; + status: "in-memory"; + config: ResolvedConfig; + }; export async function readConfig( dir: string, diff --git a/packages/cli-v3/src/utilities/session.ts b/packages/cli-v3/src/utilities/session.ts index 62cc3dfb5..7f33d30a5 100644 --- a/packages/cli-v3/src/utilities/session.ts +++ b/packages/cli-v3/src/utilities/session.ts @@ -1,34 +1,35 @@ import { recordSpanException } from "@trigger.dev/core/v3"; import { CliApiClient } from "../apiClient.js"; -import { readAuthConfigFile } from "./configFiles.js"; +import { readAuthConfigProfile } from "./configFiles.js"; import { getTracer } from "../telemetry/tracing.js"; const tracer = getTracer(); export type LoginResult = | { - ok: true; - userId: string; - email: string; - dashboardUrl: string; - auth: { - apiUrl: string; - accessToken: string; - }; - } - | { - ok: false; - error: string; - auth?: { - apiUrl: string; - accessToken: string; - }; + ok: true; + profile: string, + userId: string; + email: string; + dashboardUrl: string; + auth: { + apiUrl: string; + accessToken: string; }; + } + | { + ok: false; + error: string; + auth?: { + apiUrl: string; + accessToken: string; + }; + }; -export async function isLoggedIn(): Promise { +export async function isLoggedIn(profile: string = "default"): Promise { return await tracer.startActiveSpan("isLoggedIn", async (span) => { try { - const config = readAuthConfigFile(); + const config = readAuthConfigProfile(profile); if (!config?.accessToken || !config?.apiUrl) { span.recordException(new Error("You must login first")); @@ -57,12 +58,14 @@ export async function isLoggedIn(): Promise { "login.userId": userData.data.userId, "login.email": userData.data.email, "login.dashboardUrl": userData.data.dashboardUrl, + "login.profile": profile, }); span.end(); return { ok: true as const, + profile, userId: userData.data.userId, email: userData.data.email, dashboardUrl: userData.data.dashboardUrl, diff --git a/packages/cli-v3/src/workers/dev/backgroundWorker.ts b/packages/cli-v3/src/workers/dev/backgroundWorker.ts index bb425356f..339d83fce 100644 --- a/packages/cli-v3/src/workers/dev/backgroundWorker.ts +++ b/packages/cli-v3/src/workers/dev/backgroundWorker.ts @@ -547,8 +547,8 @@ class TaskRunProcess { ...(this.worker.debugOtel ? { OTEL_LOG_LEVEL: "debug" } : {}), }, execArgv: this.worker.debuggerOn - ? ["--inspect-brk", "--trace-uncaught"] - : ["--trace-uncaught"], + ? ["--inspect-brk", "--trace-uncaught", "--no-warnings=ExperimentalWarning"] + : ["--trace-uncaught", "--no-warnings=ExperimentalWarning"], }); this._child.on("message", this.#handleMessage.bind(this)); diff --git a/packages/core/src/v3/index.ts b/packages/core/src/v3/index.ts index 14cb25e81..3f6c31abc 100644 --- a/packages/core/src/v3/index.ts +++ b/packages/core/src/v3/index.ts @@ -42,7 +42,7 @@ export { ConsoleInterceptor } from "./consoleInterceptor"; export { flattenAttributes, unflattenAttributes, - flattenAndNormalizeAttributes, + primitiveValueOrflattenedAttributes, } from "./utils/flattenAttributes"; export { defaultRetryOptions, calculateNextRetryDelay, calculateResetAt } from "./utils/retries"; export { accessoryAttributes } from "./utils/styleAttributes"; diff --git a/packages/core/src/v3/otel/tracingSDK.ts b/packages/core/src/v3/otel/tracingSDK.ts index 6de0f4f4c..b0105a91f 100644 --- a/packages/core/src/v3/otel/tracingSDK.ts +++ b/packages/core/src/v3/otel/tracingSDK.ts @@ -13,6 +13,7 @@ import { ResourceAttributes, ResourceDetectionConfig, detectResourcesSync, + processDetectorSync, } from "@opentelemetry/resources"; import { LoggerProvider, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs"; import { @@ -89,7 +90,7 @@ export class TracingSDK { : {}; const commonResources = detectResourcesSync({ - detectors: [this.asyncResourceDetector], + detectors: [this.asyncResourceDetector, processDetectorSync], }) .merge( new Resource({ diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts index 5664ef0ce..60348c82a 100644 --- a/packages/core/src/v3/schemas/api.ts +++ b/packages/core/src/v3/schemas/api.ts @@ -141,6 +141,7 @@ export const InitializeDeploymentResponseBody = z.object({ version: z.string(), imageTag: z.string(), externalBuildData: ExternalBuildData.optional().nullable(), + registryHost: z.string().optional(), }); export type InitializeDeploymentResponseBody = z.infer; diff --git a/packages/core/src/v3/utils/flattenAttributes.ts b/packages/core/src/v3/utils/flattenAttributes.ts index 621f236b1..00e6e815e 100644 --- a/packages/core/src/v3/utils/flattenAttributes.ts +++ b/packages/core/src/v3/utils/flattenAttributes.ts @@ -95,13 +95,27 @@ export function unflattenAttributes(obj: Attributes): Record { return result; } -export function flattenAndNormalizeAttributes( +export function primitiveValueOrflattenedAttributes( obj: Record | Array | string | boolean | number | undefined, - prefix: string -): Attributes { + prefix: string | undefined +): Attributes | string | number | boolean | undefined { + if ( + typeof obj === "string" || + typeof obj === "number" || + typeof obj === "boolean" || + obj === null || + obj === undefined + ) { + return obj; + } + const attributes = flattenAttributes(obj, prefix); - if (typeof attributes[prefix] !== "undefined" && attributes[prefix] !== null) { + if ( + prefix !== undefined && + typeof attributes[prefix] !== "undefined" && + attributes[prefix] !== null + ) { return attributes[prefix] as unknown as Attributes; }