Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9e94891
feat: camelCase-only TestPlan Node API with includeGlob
jonathanong Aug 22, 2026
25762a4
feat: add --profile ci and memoize ciTopology in-process
jonathanong Aug 22, 2026
35a377f
fix: keep Node plan documents round-trippable after camelCase
jonathanong Aug 22, 2026
14d02db
merge: pick up TestPlan camelCase round-trip fixes
jonathanong Aug 22, 2026
ea5f6db
docs: resolve node-api merge conflict markers
jonathanong Aug 22, 2026
965b137
fix: decamelize string planJson and keep testsWhy path keys
jonathanong Aug 22, 2026
7321e5f
merge: TestPlan document round-trip from #764
jonathanong Aug 22, 2026
09c847c
fix: key ciTopology memo by workflows and honor profile ci timeouts
jonathanong Aug 22, 2026
97f12e5
style: oxfmt ciTopology memo helper
jonathanong Aug 22, 2026
433265b
lint: avoid copying Map keys just to iterate them
jonathanong Aug 22, 2026
099a138
fix: round-trip camelCase TestPlan files and batched reports
jonathanong Aug 22, 2026
0975ed2
style: oxfmt analyzeProject camelize dispatch
jonathanong Aug 22, 2026
862eb1b
merge: camelCase plan files and batched reports from #764
jonathanong Aug 22, 2026
dfab19c
fix: keep testsWhy on plan paths instead of planJson
jonathanong Aug 22, 2026
4ad41e9
merge: testsWhy plan-path materialization from #764
jonathanong Aug 22, 2026
ddd7c9e
fix: give each materialized testsWhy plan its own temp directory
jonathanong Aug 23, 2026
02b58c4
merge: unique testsWhy temp dirs from #764
jonathanong Aug 23, 2026
8ea9180
fix: remove generated testsWhy plan directories after native calls
jonathanong Aug 23, 2026
4b707bd
merge: testsWhy generated-dir cleanup from #764
jonathanong Aug 23, 2026
094ca87
fix: declare planJson on TestsWhyOptions
jonathanong Aug 23, 2026
494875d
merge: TestsWhyOptions planJson from #764
jonathanong Aug 23, 2026
4439515
merge: origin/main after #764
jonathanong Aug 23, 2026
2f62730
fix: keep node-api runtime export table parseable
jonathanong Aug 23, 2026
22f1d28
fix: pin native ciTopology calls to the resolved absolute root
jonathanong Aug 23, 2026
f3b3e1a
merge: origin/main after #766
jonathanong Aug 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions crates/no-mistakes/src/invocation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ fn deadline_test_lock() -> &'static std::sync::Mutex<()> {
LOCK.get_or_init(|| std::sync::Mutex::new(()))
}

#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
enum InvocationProfile {
/// Unbounded command and lock wait (`timeout: 0`, `lock-timeout: 0`).
Ci,
}

#[derive(clap::Args, Debug, Clone, Copy)]
pub struct InvocationArgs {
/// Maximum command execution time in seconds; 0 disables the deadline.
Expand All @@ -67,6 +73,9 @@ pub struct InvocationArgs {
/// Fail immediately when another no-mistakes invocation holds the lock.
#[arg(long, global = true)]
fail_on_lock: bool,
/// Named timeout defaults. `ci` sets `--timeout 0 --lock-timeout 0`.
#[arg(long, value_enum, global = true)]
profile: Option<InvocationProfile>,
}

impl Default for InvocationArgs {
Expand All @@ -75,15 +84,20 @@ impl Default for InvocationArgs {
timeout: DEFAULT_TIMEOUT_SECONDS,
lock_timeout: DEFAULT_TIMEOUT_SECONDS,
fail_on_lock: false,
profile: None,
}
}
}

impl InvocationArgs {
pub fn options(self) -> InvocationOptions {
let (timeout, lock_timeout) = match self.profile {
Some(InvocationProfile::Ci) => (0, 0),
None => (self.timeout, self.lock_timeout),
};
InvocationOptions {
timeout: nonzero_seconds(self.timeout),
lock_timeout: nonzero_seconds(self.lock_timeout),
timeout: nonzero_seconds(timeout),
lock_timeout: nonzero_seconds(lock_timeout),
fail_on_lock: self.fail_on_lock,
jobs: None,
}
Expand Down
13 changes: 11 additions & 2 deletions crates/no-mistakes/src/invocation/napi_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,20 @@ pub fn extract_napi_options_value(
}
};
let jobs = take_jobs(object)?;
let profile_ci = match object.remove("profile") {
None | Some(Value::Null) => false,
Some(Value::String(value)) if value == "ci" => true,
Some(_) => {
return Err(anyhow!(
"invalid options JSON: profile must be \"ci\" when set"
))
}
};
Ok((
value,
InvocationOptions {
timeout,
lock_timeout,
timeout: if profile_ci { None } else { timeout },
lock_timeout: if profile_ci { None } else { lock_timeout },
fail_on_lock,
jobs,
},
Expand Down
20 changes: 20 additions & 0 deletions crates/no-mistakes/src/invocation/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ fn cli_defaults_and_zero_values_have_napi_parity() {
timeout: 0,
lock_timeout: 0,
fail_on_lock: true,
profile: None,
}
.options(),
InvocationOptions {
Expand All @@ -58,6 +59,25 @@ fn cli_defaults_and_zero_values_have_napi_parity() {
);
}

#[test]
fn ci_profile_disables_timeouts() {
assert_eq!(
InvocationArgs {
timeout: DEFAULT_TIMEOUT_SECONDS,
lock_timeout: DEFAULT_TIMEOUT_SECONDS,
fail_on_lock: false,
profile: Some(InvocationProfile::Ci),
}
.options(),
InvocationOptions {
timeout: None,
lock_timeout: None,
fail_on_lock: false,
jobs: None,
}
);
}

#[test]
fn disabled_deadline_allows_timeout_check() {
let _serial = deadline_test_lock()
Expand Down
14 changes: 14 additions & 0 deletions crates/no-mistakes/src/invocation/tests/napi_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ fn napi_missing_controls_disable_timeouts() {
assert_eq!(options.jobs, None);
}

#[test]
fn napi_profile_ci_is_stripped() {
let (json, options) =
extract_napi_options(r#"{"profile":"ci","timeout":10,"lockTimeout":5,"root":"."}"#)
.unwrap();
assert_eq!(options.timeout, None);
assert_eq!(options.lock_timeout, None);
assert_eq!(
serde_json::from_str::<Value>(&json).unwrap(),
serde_json::json!({"root":"."})
);
}

#[test]
fn napi_jobs_parses_non_negative_integer_or_null() {
let (_, options) = extract_napi_options(r#"{"jobs":4}"#).unwrap();
Expand All @@ -59,6 +72,7 @@ fn napi_controls_validate_types() {
r#"{"failOnLock":1}"#,
r#"{"jobs":-1}"#,
r#"{"jobs":"4"}"#,
r#"{"profile":"local"}"#,
"[]",
"not-json",
] {
Expand Down
1 change: 1 addition & 0 deletions crates/no-mistakes/tests/cli_invocation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ fn invocation_help_documents_independent_timeouts_and_lock_failure() {
assert!(help.contains("--timeout <SECONDS>"));
assert!(help.contains("--lock-timeout <SECONDS>"));
assert!(help.contains("--fail-on-lock"));
assert!(help.contains("--profile"));
assert!(help.contains("[default: 30]"));
}

Expand Down
5 changes: 5 additions & 0 deletions docs/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ or after the command name:
is `30`; `0` waits indefinitely.
- `--fail-on-lock` fails immediately when another invocation holds the lock,
overriding `--lock-timeout`.
- `--profile ci` sets `--timeout 0 --lock-timeout 0` for CI jobs that should
wait for the machine-wide lock and run without a command deadline. Node
`profile: "ci"` clears any supplied `timeout` / `lockTimeout` the same way.
`ciTopology()` memoizes in-process by resolved root, config mtime, and
workflows filter so repeated calls in one process do not re-parse workflows.

Command and lock-wait timeouts exit with status `124`. Immediate lock
contention and lock setup errors exit with status `2`. Errors are written to
Expand Down
Loading
Loading