Skip to content

Commit 85afe2b

Browse files
committed
chore(core): keep exec helper - still imported by external consumers
The Exec/ExecResult/redactArgsForLogging/Output helpers in v3/apps/exec.ts are still imported by external consumers of @trigger.dev/core, so keep them exported. Restore exec.ts, its test, and the barrel re-export. The other v3 provider/checkpoint helpers stay removed - they have no consumers.
1 parent 3b421b1 commit 85afe2b

3 files changed

Lines changed: 174 additions & 0 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, expect, it } from "vitest";
2+
import { redactArgsForLogging } from "./exec.js";
3+
4+
describe("redactArgsForLogging", () => {
5+
it("masks the value following a credential flag", () => {
6+
expect(
7+
redactArgsForLogging(["login", "--username", "robot", "--password", "s3cr3t", "host:80"])
8+
).toEqual(["login", "--username", "robot", "--password", "[redacted]", "host:80"]);
9+
});
10+
11+
it("masks inline --flag=value form", () => {
12+
expect(redactArgsForLogging(["--token=abc123"])).toEqual(["--token=[redacted]"]);
13+
});
14+
15+
it("leaves non-credential args untouched", () => {
16+
expect(redactArgsForLogging(["push", "--tls-verify=false", "host:80/img"])).toEqual([
17+
"push",
18+
"--tls-verify=false",
19+
"host:80/img",
20+
]);
21+
});
22+
23+
it("passes undefined through", () => {
24+
expect(redactArgsForLogging(undefined)).toBeUndefined();
25+
});
26+
});

packages/core/src/v3/apps/exec.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { SimpleStructuredLogger } from "../utils/structuredLogger.js";
2+
import { type Output, type Result, x } from "tinyexec";
3+
4+
export class ExecResult {
5+
pid?: number;
6+
exitCode?: number;
7+
aborted: boolean;
8+
killed: boolean;
9+
10+
constructor(result: Result) {
11+
this.pid = result.pid;
12+
this.exitCode = result.exitCode;
13+
this.aborted = result.aborted;
14+
this.killed = result.killed;
15+
}
16+
}
17+
18+
export interface ExecOptions {
19+
logger?: SimpleStructuredLogger;
20+
abortSignal?: AbortSignal;
21+
logOutput?: boolean;
22+
trimArgs?: boolean;
23+
neverThrow?: boolean;
24+
}
25+
26+
// Long-form flags whose value carries a credential - the following arg (or inline
27+
// `--flag=value`) is replaced before args are logged so it never reaches log sinks.
28+
const REDACTED_FLAGS = new Set([
29+
"--password",
30+
"--token",
31+
"--secret",
32+
"--access-token",
33+
"--registry-token",
34+
"--registry-password",
35+
"--api-key",
36+
]);
37+
38+
export function redactArgsForLogging(args?: string[]): string[] | undefined {
39+
if (!args) {
40+
return args;
41+
}
42+
43+
return args.map((arg, index) => {
44+
const previous = index > 0 ? args[index - 1]?.trim() : undefined;
45+
if (previous && REDACTED_FLAGS.has(previous)) {
46+
return "[redacted]";
47+
}
48+
49+
const equalsIndex = arg.indexOf("=");
50+
if (equalsIndex > 0 && REDACTED_FLAGS.has(arg.slice(0, equalsIndex).trim())) {
51+
return `${arg.slice(0, equalsIndex)}=[redacted]`;
52+
}
53+
54+
return arg;
55+
});
56+
}
57+
58+
export class Exec {
59+
private logger: SimpleStructuredLogger;
60+
private abortSignal: AbortSignal | undefined;
61+
62+
private logOutput: boolean;
63+
private trimArgs: boolean;
64+
private neverThrow: boolean;
65+
66+
constructor(opts: ExecOptions) {
67+
this.logger = opts.logger ?? new SimpleStructuredLogger("exec");
68+
this.abortSignal = opts.abortSignal;
69+
70+
this.logOutput = opts.logOutput ?? true;
71+
this.trimArgs = opts.trimArgs ?? true;
72+
this.neverThrow = opts.neverThrow ?? false;
73+
}
74+
75+
async x(
76+
command: string,
77+
args?: string[],
78+
opts?: { neverThrow?: boolean; ignoreAbort?: boolean }
79+
): Promise<Output> {
80+
const argsTrimmed = this.trimArgs ? args?.map((arg) => arg.trim()) : args;
81+
82+
const argsForLogging = redactArgsForLogging(args);
83+
const argsTrimmedForLogging = redactArgsForLogging(argsTrimmed);
84+
85+
const commandWithFirstArg = `${command}${argsTrimmedForLogging?.length ? ` ${argsTrimmedForLogging[0]}` : ""}`;
86+
this.logger.debug(`exec: ${commandWithFirstArg}`, {
87+
command,
88+
args: argsForLogging,
89+
argsTrimmed: argsTrimmedForLogging,
90+
});
91+
92+
const result = x(command, argsTrimmed, {
93+
signal: opts?.ignoreAbort ? undefined : this.abortSignal,
94+
// We don't use this as it doesn't cover killed and aborted processes
95+
// throwOnError: true,
96+
});
97+
98+
const output = await result;
99+
100+
const metadata = {
101+
command,
102+
argsRaw: argsForLogging,
103+
argsTrimmed: argsTrimmedForLogging,
104+
globalOpts: {
105+
trimArgs: this.trimArgs,
106+
neverThrow: this.neverThrow,
107+
hasAbortSignal: !!this.abortSignal,
108+
},
109+
localOpts: opts,
110+
stdout: output.stdout,
111+
stderr: output.stderr,
112+
pid: result.pid,
113+
exitCode: result.exitCode,
114+
aborted: result.aborted,
115+
killed: result.killed,
116+
};
117+
118+
if (this.logOutput) {
119+
this.logger.debug(`output: ${commandWithFirstArg}`, metadata);
120+
}
121+
122+
if (this.neverThrow || opts?.neverThrow) {
123+
return output;
124+
}
125+
126+
if (result.aborted) {
127+
this.logger.error(`aborted: ${commandWithFirstArg}`, metadata);
128+
throw new ExecResult(result);
129+
}
130+
131+
if (result.killed) {
132+
this.logger.error(`killed: ${commandWithFirstArg}`, metadata);
133+
throw new ExecResult(result);
134+
}
135+
136+
if (result.exitCode !== 0) {
137+
this.logger.error(`non-zero exit: ${commandWithFirstArg}`, metadata);
138+
throw new ExecResult(result);
139+
}
140+
141+
return output;
142+
}
143+
144+
static Result = ExecResult;
145+
}
146+
147+
export { type Output };

packages/core/src/v3/apps/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./backoff.js";
22
export * from "./http.js";
3+
export * from "./exec.js";

0 commit comments

Comments
 (0)