Merge branch 'main' into v3/infra-updates

This commit is contained in:
nicktrn
2024-03-19 09:20:54 +00:00
31 changed files with 426 additions and 289 deletions
+3 -1
View File
@@ -55,6 +55,8 @@ COORDINATOR_SECRET=coordinator-secret # generate the actual secret with `openssl
# ENABLE_REGISTRY_PROXY=true
# DEPOT_TOKEN=<Depot org token>
# DEPOT_PROJECT_ID=<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 origin e.g. https://registry.digitalocean.com>
# CONTAINER_REGISTRY_USERNAME=<Container registry username e.g. Digital ocean email address>
# CONTAINER_REGISTRY_PASSWORD=<Container registry password e.g. Digital ocean PAT>
# CONTAINER_REGISTRY_PASSWORD=<Container registry password e.g. Digital ocean PAT>
# DEV_OTEL_EXPORTER_OTLP_ENDPOINT="http://0.0.0.0:4318"
+9 -39
View File
@@ -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
@@ -253,7 +253,7 @@ export const TableCellMenu = forwardRef<
type TableBlankRowProps = {
className?: string;
colSpan: number;
children: ReactNode;
children?: ReactNode;
};
export const TableBlankRow = forwardRef<HTMLTableRowElement, TableBlankRowProps>(
@@ -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 (
<DialogContent>
<DialogHeader>Cancel this run?</DialogHeader>
<DialogDescription>
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.
</DialogDescription>
<DialogFooter>
<cancelFetcher.Form action={`/resources/taskruns/${runFriendlyId}/cancel`} method="post">
<Button
type="submit"
name="redirectUrl"
value={redirectPath}
variant="danger/small"
LeadingIcon={cancelFetcher.state === "idle" ? StopCircleIcon : "spinner-white"}
disabled={cancelFetcher.state !== "idle"}
shortcut={{ modifiers: ["meta"], key: "enter" }}
>
{cancelFetcher.state === "idle" ? "Cancel run" : "Canceling..."}
</Button>
</cancelFetcher.Form>
</DialogFooter>
</DialogContent>
);
}
@@ -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 (
<Table>
@@ -65,9 +75,7 @@ export function TaskRunsTable({
{!isLoading && <NoRuns title="No runs found" />}
</TableBlankRow>
) : runs.length === 0 ? (
<TableBlankRow colSpan={9}>
{!isLoading && <NoRuns title="No runs match your filters" />}
</TableBlankRow>
<BlankState isLoading={isLoading} filters={filters} />
) : (
runs.map((run) => {
const path = v3RunPath(organization, project, run);
@@ -102,7 +110,23 @@ export function TaskRunsTable({
<TableCell to={path}>
{run.createdAt ? <DateTime date={run.createdAt} /> : ""}
</TableCell>
<TableCellChevron to={path} isSticky />
{run.isCancellable ? (
<TableCellMenu isSticky>
<Dialog>
<DialogTrigger asChild>
<Button variant="small-menu-item" LeadingIcon={StopCircleIcon}>
Cancel run
</Button>
</DialogTrigger>
<CancelRunDialog
runFriendlyId={run.friendlyId}
redirectPath={`${location.pathname}${location.search}`}
/>
</Dialog>
</TableCellMenu>
) : (
<TableCell to={path}>{""}</TableCell>
)}
</TableRow>
);
})
@@ -127,3 +151,62 @@ function NoRuns({ title }: { title: string }) {
</div>
);
}
function BlankState({ isLoading, filters }: Pick<RunsTableProps, "isLoading" | "filters">) {
const organization = useOrganization();
const project = useProject();
const envs = useEnvironments();
if (isLoading) return <TableBlankRow colSpan={9}></TableBlankRow>;
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 (
<TableBlankRow colSpan={9}>
<div className="py-14">
<Paragraph className="w-auto" variant="base/bright" spacing>
There are no runs for {filters.tasks[0]}
{environment ? (
<>
{" "}
in <EnvironmentLabel environment={environment} size="large" />
</>
) : null}
</Paragraph>
<div className="flex items-center justify-center gap-2">
<LinkButton
to={v3TestPath(organization, project)}
variant="primary/small"
LeadingIcon={BeakerIcon}
className="inline-flex"
>
Create a test run
</LinkButton>
<Paragraph variant="small">or</Paragraph>
<LinkButton
to={docsPath("v3/triggering")}
variant="primary/small"
LeadingIcon={BookOpenIcon}
className="inline-flex"
>
Triggering a task docs
</LinkButton>
</div>
</div>
</TableBlankRow>
);
}
return (
<TableBlankRow colSpan={9}>
<NoRuns title="No runs match your filters" />
</TableBlankRow>
);
}
+2
View File
@@ -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<typeof EnvironmentSchema>;
@@ -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<ReturnType<RunListPresenter["call"]>>;
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,
};
}
@@ -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 (
<div
@@ -176,38 +176,15 @@ export default function Page() {
Cancel run
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>Cancel this run?</DialogHeader>
<DialogDescription>
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.
</DialogDescription>
<DialogFooter>
<cancelFetcher.Form
action={`/resources/taskruns/${event.runId}/cancel`}
method="post"
>
<Button
type="submit"
name="redirectUrl"
value={v3RunSpanPath(
organization,
project,
{ friendlyId: runParam },
{ spanId: event.spanId }
)}
variant="danger/small"
LeadingIcon={
cancelFetcher.state === "idle" ? StopCircleIcon : "spinner-white"
}
disabled={cancelFetcher.state !== "idle"}
shortcut={{ modifiers: ["meta"], key: "enter" }}
>
{cancelFetcher.state === "idle" ? "Cancel run" : "Canceling..."}
</Button>
</cancelFetcher.Form>
</DialogFooter>
</DialogContent>
<CancelRunDialog
runFriendlyId={event.runId}
redirectPath={v3RunSpanPath(
organization,
project,
{ friendlyId: runParam },
{ spanId: event.spanId }
)}
/>
</Dialog>
)}
</div>
@@ -581,11 +581,7 @@ function NodeStatusIcon({ node }: { node: RunEvent }) {
}
function TaskLine({ isError, isSelected }: { isError: boolean; isSelected: boolean }) {
return (
<div
className={cn("h-8 w-2 border-r", isError ? "border-rose-500/10" : "border-charcoal-800")}
/>
);
return <div className={cn("h-8 w-2 border-r border-grid-bright")} />;
}
function ShowParentLink({ runFriendlyId }: { runFriendlyId: string }) {
@@ -84,6 +84,7 @@ export default function Page() {
<TaskRunsTable
total={list.runs.length}
hasFilters={list.hasFilters}
filters={list.filters}
runs={list.runs}
isLoading={isLoading}
currentUser={user}
+13 -9
View File
@@ -1,5 +1,6 @@
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
import { InitializeDeploymentRequestBody } from "@trigger.dev/core/v3";
import { InitializeDeploymentRequestBody, InitializeDeploymentResponseBody } from "@trigger.dev/core/v3";
import { env } from "~/env.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { InitializeDeploymentService } from "~/v3/services/initializeDeployment.server";
@@ -31,15 +32,18 @@ export async function action({ request, params }: ActionFunctionArgs) {
const { deployment, imageTag } = await service.call(authenticatedEnv, body.data);
const responseBody: InitializeDeploymentResponseBody = {
id: deployment.friendlyId,
contentHash: deployment.contentHash,
shortCode: deployment.shortCode,
version: deployment.version,
externalBuildData: deployment.externalBuildData as InitializeDeploymentResponseBody["externalBuildData"],
imageTag,
registryHost: env.DEPLOY_REGISTRY_HOST
}
return json(
{
id: deployment.friendlyId,
contentHash: deployment.contentHash,
shortCode: deployment.shortCode,
version: deployment.version,
externalBuildData: deployment.externalBuildData,
imageTag,
},
responseBody,
{ status: 200 }
);
}
@@ -30,7 +30,7 @@ function parseSecretKey(key: string) {
const SecretValue = z.object({ secret: z.string() });
export class EnvironmentVariablesRepository implements Repository {
constructor(private prismaClient: PrismaClient = prisma) {}
constructor(private prismaClient: PrismaClient = prisma) { }
async create(
projectId: string,
@@ -415,7 +415,12 @@ export class EnvironmentVariablesRepository implements Repository {
}
if (environment.type === "DEVELOPMENT") {
return [];
return [
{
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
value: env.DEV_OTEL_EXPORTER_OTLP_ENDPOINT ?? env.APP_ORIGIN,
}
];
}
return [
+2 -6
View File
@@ -9,10 +9,10 @@ import {
SpanEvents,
TaskEventStyle,
correctErrorStackTrace,
flattenAndNormalizeAttributes,
flattenAttributes,
isExceptionSpanEvent,
omit,
primitiveValueOrflattenedAttributes,
unflattenAttributes,
} from "@trigger.dev/core/v3";
import { Prisma, TaskEvent, TaskEventStatus, type TaskEventKind } from "@trigger.dev/database";
@@ -178,10 +178,7 @@ export class EventRepository {
metadata: event.metadata as Attributes,
style: event.style as Attributes,
output: options?.attributes.output
? flattenAndNormalizeAttributes(
options.attributes.output,
SemanticInternalAttributes.OUTPUT
)
? primitiveValueOrflattenedAttributes(options.attributes.output, undefined)
: undefined,
});
}
@@ -564,7 +561,6 @@ export class EventRepository {
queueName: options.attributes.queueName,
batchId: options.attributes.batchId ?? undefined,
properties: {
...style,
...(flattenAttributes(metadata, SemanticInternalAttributes.METADATA) as Record<
string,
string
@@ -8,7 +8,7 @@ import { assertUnreachable } from "../utils/asserts.server";
import { CancelAttemptService } from "./cancelAttempt.server";
import { logger } from "~/services/logger.server";
const CANCELLABLE_STATUSES: Array<TaskRunStatus> = [
export const CANCELLABLE_STATUSES: Array<TaskRunStatus> = [
"PENDING",
"EXECUTING",
"PAUSED",
+17 -8
View File
@@ -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)
+3
View File
@@ -0,0 +1,3 @@
{
"id": "g2k5ln95n6"
}
+5 -3
View File
@@ -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<typeof CommonCommandOptions>;
export function commonOptions(command: Command) {
return command
.option("--profile <profile>", "The login profile to use", "default")
.option("-a, --api-url <value>", "Override the API URL", "https://api.trigger.dev")
.option(
"-l, --log-level <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<void>) {
try {
+2 -32
View File
@@ -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 <version tag>", "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);
+11 -11
View File
@@ -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,
},
});
+24 -39
View File
@@ -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<typeof DevCommandOptions>;
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 <level>",
"The log level to use (debug, info, log, warn, error, none)",
"log"
)
.option(
"-c, --config <config file>",
"The name of the config file, found at [path]",
"trigger.config.mjs"
)
.option(
"-p, --project-ref <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 <config file>",
"The name of the config file, found at [path]",
"trigger.config.mjs"
)
.option(
"-p, --project-ref <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();
+2 -1
View File
@@ -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) {
+12 -6
View File
@@ -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<LoginResult> {
@@ -63,16 +65,17 @@ export async function login(options?: LoginOptions): Promise<LoginResult> {
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<LoginResult> {
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<LoginResult> {
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<LoginResult> {
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<LoginResult> {
return {
ok: true as const,
profile: options?.profile ?? "default",
userId: whoAmIResult.data.userId,
email: whoAmIResult.data.email,
dashboardUrl: whoAmIResult.data.dashboardUrl,
+32 -6
View File
@@ -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<typeof LogoutCommandOptions>;
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"}]`);
}
+28 -31
View File
@@ -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<typeof WhoamiCommandOptions>;
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 <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<WhoAmIResult> {
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}`);
+50 -21
View File
@@ -24,11 +24,53 @@ export const UserAuthConfigSchema = z.object({
export type UserAuthConfig = z.infer<typeof UserAuthConfigSchema>;
const UserAuthConfigFileSchema = z.record(UserAuthConfigSchema);
type UserAuthConfigFile = z.infer<typeof UserAuthConfigFileSchema>;
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<string | undefined> {
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,
+22 -19
View File
@@ -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<LoginResult> {
export async function isLoggedIn(profile: string = "default"): Promise<LoginResult> {
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<LoginResult> {
"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,
@@ -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));
+1 -1
View File
@@ -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";
+2 -1
View File
@@ -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({
+1
View File
@@ -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<typeof InitializeDeploymentResponseBody>;
@@ -95,13 +95,27 @@ export function unflattenAttributes(obj: Attributes): Record<string, unknown> {
return result;
}
export function flattenAndNormalizeAttributes(
export function primitiveValueOrflattenedAttributes(
obj: Record<string, unknown> | Array<unknown> | 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;
}