Skip to content

Commit fdd39b0

Browse files
authored
Merge branch 'main' into shew/deprecate-scan-command
2 parents 160a8de + 281e89b commit fdd39b0

File tree

41 files changed

+1462
-169
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1462
-169
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/docs/content/docs/reference/run.mdx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ related:
1111
- /docs/reference/options-overview
1212
---
1313

14+
import { ExperimentalBadge } from "@/components/geistdocs/experimental-badge";
15+
1416
Run tasks specified in `turbo.json`.
1517

1618
```bash title="Terminal"
@@ -379,6 +381,49 @@ turbo run build test lint --graph=my-graph.svg
379381
and tasks involved.
380382
</Callout>
381383
384+
### `--json` <ExperimentalBadge />
385+
386+
Output machine-readable [NDJSON](https://github.com/ndjson/ndjson-spec) to stdout instead of human-readable text. Disables the TUI and forces stream mode.
387+
388+
```bash title="Terminal"
389+
turbo run build --json
390+
```
391+
392+
Each line is a JSON object with the following shape:
393+
394+
```json
395+
{"timestamp":1710000000000,"source":"web#build","level":"stdout","text":"Compiled successfully"}
396+
```
397+
398+
| Field | Description |
399+
| --- | --- |
400+
| `timestamp` | Unix epoch milliseconds |
401+
| `source` | `"turbo"` for Turborepo messages, or `"<package>#<task>"` for task output |
402+
| `level` | `"info"`, `"warn"`, `"error"` for Turborepo messages; `"stdout"`, `"stderr"` for task output |
403+
| `text` | The message content, with ANSI escape sequences stripped |
404+
405+
Can be combined with `--log-file` to write to both stdout and a file simultaneously.
406+
407+
<Callout type="warn">
408+
Structured output captures **all** task output verbatim. If your build scripts echo secrets to stdout, those values will appear in the output.
409+
</Callout>
410+
411+
### `--log-file [path]` <ExperimentalBadge />
412+
413+
Write structured JSON logs to a file. If no path is given, writes to `.turbo/logs/<timestamp>.json`.
414+
415+
```bash title="Terminal"
416+
# Default location
417+
turbo run build --log-file
418+
419+
# Custom path
420+
turbo run build --log-file=build-log.json
421+
```
422+
423+
The file is a valid JSON array at all times, even if the process is interrupted. Log files are created with owner-only permissions (`0600`) on Unix, and the path is restricted to the repository root.
424+
425+
Can also be set with the [`TURBO_LOG_FILE`](/docs/reference/system-environment-variables#turbo_log_file) environment variable. The CLI flag takes precedence.
426+
382427
### `--log-order <option>`
383428
384429
Default: `auto`

apps/docs/content/docs/reference/system-environment-variables.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,18 @@ System environment variables are always overridden by flag values provided direc
128128
executed.
129129
</td>
130130
</tr>
131+
<tr id="turbo_log_file">
132+
<td>
133+
<code>TURBO_LOG_FILE</code>
134+
</td>
135+
<td>
136+
Write structured JSON logs to a file. Set to <code>1</code> or{" "}
137+
<code>true</code> for the default location (
138+
<code>.turbo/logs/&lt;timestamp&gt;.json</code>), or provide a custom
139+
file path. Equivalent to the{" "}
140+
<a href="/docs/reference/run#--log-file-path">--log-file</a> flag.
141+
</td>
142+
</tr>
131143
<tr id="turbo_log_order">
132144
<td>
133145
<code>TURBO_LOG_ORDER</code>

apps/docs/next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3-
import "./.next/types/routes.d.ts";
3+
import "./.next/dev/types/routes.d.ts";
44

55
// NOTE: This file should not be edited
66
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

apps/docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"lucide-react": "^0.555.0",
5050
"motion": "^12.23.25",
5151
"nanoid": "^5.1.6",
52-
"next": "16.1.7",
52+
"next": "16.1.5",
5353
"next-themes": "^0.4.6",
5454
"radix-ui": "latest",
5555
"react": "^19.2.3",

crates/turborepo-config/src/env.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use turborepo_types::{EnvMode, LogOrder, UIMode};
1212

1313
use crate::{
1414
ConfigurationOptions, Error, ExperimentalObservabilityOptions, ExperimentalOtelOptions,
15-
ResolvedConfigurationOptions,
15+
LogFileConfig, ResolvedConfigurationOptions,
1616
};
1717

1818
const TURBO_MAPPING: &[(&str, &str)] = [
@@ -46,6 +46,7 @@ const TURBO_MAPPING: &[(&str, &str)] = [
4646
("turbo_concurrency", "concurrency"),
4747
("turbo_no_update_notifier", "no_update_notifier"),
4848
("turbo_sso_login_callback_port", "sso_login_callback_port"),
49+
("turbo_log_file", "structured_log_file"),
4950
(
5051
"turbo_experimental_otel_enabled",
5152
"experimental_otel_enabled",
@@ -278,6 +279,20 @@ impl ResolvedConfigurationOptions for EnvVars {
278279
.map_err(Error::InvalidSsoLoginCallbackPort)?;
279280

280281
let experimental_otel = ExperimentalOtelOptions::from_env_map(&self.output_map)?;
282+
283+
// TURBO_LOG_FILE: "1"/"true" → default location, path string → custom path
284+
let log_file = self
285+
.output_map
286+
.get("structured_log_file")
287+
.filter(|s| !s.is_empty())
288+
.map(|s| {
289+
if s == "1" || s == "true" {
290+
LogFileConfig::Enabled
291+
} else {
292+
LogFileConfig::Path(s.clone())
293+
}
294+
});
295+
281296
let experimental_observability =
282297
experimental_otel.map(|otel| ExperimentalObservabilityOptions { otel: Some(otel) });
283298

@@ -318,6 +333,7 @@ impl ResolvedConfigurationOptions for EnvVars {
318333
// Do not allow future flags to be set by env var
319334
future_flags: None,
320335
experimental_observability,
336+
log_file,
321337
};
322338

323339
Ok(output)

crates/turborepo-config/src/lib.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,39 @@ pub struct ExperimentalObservabilityOptions {
6161
pub otel: Option<ExperimentalOtelOptions>,
6262
}
6363

64+
/// Configuration for structured log file output.
65+
///
66+
/// - `LogFileConfig::Enabled` — write to default location
67+
/// (`.turbo/logs/<epoch_millis>.json`)
68+
/// - `LogFileConfig::Path(p)` — write to a custom file path
69+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
70+
pub enum LogFileConfig {
71+
Enabled,
72+
Path(String),
73+
}
74+
75+
impl<'de> Deserialize<'de> for LogFileConfig {
76+
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
77+
let value = serde_json::Value::deserialize(d)?;
78+
match value {
79+
serde_json::Value::Bool(true) => Ok(LogFileConfig::Enabled),
80+
serde_json::Value::Bool(false) => Err(serde::de::Error::custom(
81+
"use null/absent instead of false to disable log file",
82+
)),
83+
serde_json::Value::String(path) => Ok(LogFileConfig::Path(path)),
84+
_ => Err(serde::de::Error::custom(
85+
"expected true or a file path string",
86+
)),
87+
}
88+
}
89+
}
90+
91+
impl Merge for LogFileConfig {
92+
fn merge(&mut self, _other: Self) {
93+
// CLI/env takes precedence, no merging needed
94+
}
95+
}
96+
6497
// Re-export default constants for tests and external use
6598
pub const DEFAULT_API_URL: &str = "https://vercel.com/api";
6699
pub const DEFAULT_LOGIN_URL: &str = "https://vercel.com";
@@ -276,6 +309,10 @@ pub struct ConfigurationOptions {
276309
pub future_flags: Option<FutureFlags>,
277310
#[serde(rename = "experimentalObservability")]
278311
pub experimental_observability: Option<ExperimentalObservabilityOptions>,
312+
/// Structured log file destination, configured via `logFile` in
313+
/// turbo.json or `TURBO_LOG_FILE` env var.
314+
#[serde(rename = "logFile")]
315+
pub log_file: Option<LogFileConfig>,
279316
}
280317

281318
#[derive(Default)]
@@ -538,6 +575,10 @@ impl ConfigurationOptions {
538575
pub fn experimental_observability(&self) -> Option<&ExperimentalObservabilityOptions> {
539576
self.experimental_observability.as_ref()
540577
}
578+
579+
pub fn log_file(&self) -> Option<&LogFileConfig> {
580+
self.log_file.as_ref()
581+
}
541582
}
542583

543584
// Maps Some("") to None to emulate how Go handles empty strings

crates/turborepo-config/src/turbo_json.rs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,23 @@ impl<'a> TurboJsonReader<'a> {
9595

9696
opts.future_flags = turbo_json.future_flags.map(|f| *f.as_inner());
9797

98-
// Only read observability config if futureFlags.experimentalObservability is
99-
// enabled
100-
if opts
101-
.future_flags
102-
.map(|f| f.experimental_observability)
103-
.unwrap_or(false)
104-
&& let Some(raw_observability) = turbo_json.experimental_observability
105-
{
106-
opts.experimental_observability = Some(convert_raw_observability(raw_observability)?);
98+
if let Some(raw_observability) = turbo_json.experimental_observability {
99+
let otel_enabled = opts
100+
.future_flags
101+
.map(|f| f.experimental_observability)
102+
.unwrap_or(false);
103+
104+
let converted = convert_raw_observability(raw_observability)?;
105+
106+
let effective = ExperimentalObservabilityOptions {
107+
otel: if otel_enabled { converted.otel } else { None },
108+
};
109+
110+
if effective.otel.is_some() {
111+
opts.experimental_observability = Some(effective);
112+
}
107113
}
114+
108115
Ok(opts)
109116
}
110117
}

crates/turborepo-lib/src/cli/mod.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,14 @@ pub struct ExecutionArgs {
998998
/// turbo decide based on its own heuristics. (default auto)
999999
#[clap(long, value_enum)]
10001000
pub log_order: Option<LogOrder>,
1001+
/// Output machine-readable NDJSON to stdout instead of human-readable
1002+
/// text. Disables the TUI and forces stream mode.
1003+
#[clap(long)]
1004+
pub json: bool,
1005+
/// Write structured JSON logs to a file. If no path is given, writes to
1006+
/// `.turbo/logs/<epoch_millis>.json`.
1007+
#[clap(long)]
1008+
pub log_file: Option<Option<String>>,
10011009
/// Only executes the tasks specified, does not execute parent tasks.
10021010
#[clap(long)]
10031011
pub only: bool,
@@ -1453,7 +1461,14 @@ async fn run_main(
14531461

14541462
let mut command = get_command(&mut cli_args)?;
14551463

1456-
if should_print_version() {
1464+
// Suppress the version banner in --json mode — all output on stdout
1465+
// must be machine-readable NDJSON.
1466+
let is_json_mode = matches!(
1467+
&command,
1468+
Command::Run { execution_args, .. } | Command::Watch { execution_args, .. }
1469+
if execution_args.json
1470+
);
1471+
if should_print_version() && !is_json_mode {
14571472
eprintln!("{}", GREY.apply_to(format!("• turbo {}", get_version())));
14581473
}
14591474

crates/turborepo-lib/src/cli/snapshots/turborepo_lib__cli__test__turbo-watch-build---no-daemon.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ error: unexpected argument '--no-daemon' found
77
tip: a similar argument exists: '--no-update-notifier'
88
tip: to pass '--no-daemon' as a value, use '-- --no-daemon'
99

10-
Usage: turbo watch --no-update-notifier <--cache-dir <CACHE_DIR>|--concurrency <CONCURRENCY>|--continue[=<CONTINUE>]|--single-package|--framework-inference [<BOOL>]|--global-deps <GLOBAL_DEPS>|--env-mode [<ENV_MODE>]|--filter <FILTER>|--affected|--output-logs <OUTPUT_LOGS>|--log-order <LOG_ORDER>|--only|--pkg-inference-root <PKG_INFERENCE_ROOT>|--log-prefix <LOG_PREFIX>|TASKS|PASS_THROUGH_ARGS>
10+
Usage: turbo watch --no-update-notifier <--cache-dir <CACHE_DIR>|--concurrency <CONCURRENCY>|--continue[=<CONTINUE>]|--single-package|--framework-inference [<BOOL>]|--global-deps <GLOBAL_DEPS>|--env-mode [<ENV_MODE>]|--filter <FILTER>|--affected|--output-logs <OUTPUT_LOGS>|--log-order <LOG_ORDER>|--json|--log-file [<LOG_FILE>]|--only|--pkg-inference-root <PKG_INFERENCE_ROOT>|--log-prefix <LOG_PREFIX>|TASKS|PASS_THROUGH_ARGS>
1111

1212
For more information, try '--help'.

0 commit comments

Comments
 (0)