ci: verify codex-rs Cargo manifests inherit workspace settings

This commit is contained in:
Michael Bolin
2026-03-31 09:57:10 -07:00
parent 03b2465591
commit 4d1c91f417
24 changed files with 221 additions and 29 deletions

View File

@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Verify that codex-rs crates inherit workspace metadata, lints, and names.
This keeps `cargo clippy` aligned with the workspace lint policy by ensuring
each crate opts into `[lints] workspace = true`, and it also checks the crate
name conventions for top-level `codex-rs/*` crates and `codex-rs/utils/*`
crates.
"""
from __future__ import annotations
import sys
import tomllib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
CARGO_RS_ROOT = ROOT / "codex-rs"
WORKSPACE_PACKAGE_FIELDS = ("version", "edition", "license")
TOP_LEVEL_NAME_EXCEPTIONS = {
"windows-sandbox-rs": "codex-windows-sandbox",
}
UTILITY_NAME_EXCEPTIONS = {
"path-utils": "codex-utils-path",
}
def main() -> int:
failures = [
(path.relative_to(ROOT), errors)
for path in cargo_manifests()
if (errors := manifest_errors(path))
]
if not failures:
return 0
print(
"Cargo manifests under codex-rs must inherit workspace package metadata and "
"opt into workspace lints."
)
print(
"Cargo only applies `codex-rs/Cargo.toml` `[workspace.lints.clippy]` "
"entries to a crate when that crate declares:"
)
print()
print("[lints]")
print("workspace = true")
print()
print(
"Without that opt-in, `cargo clippy` can miss violations that Bazel clippy "
"catches."
)
print()
print(
"Package-name checks apply to `codex-rs/<crate>/Cargo.toml` and "
"`codex-rs/utils/<crate>/Cargo.toml`."
)
print()
for path, errors in failures:
print(f"{path}:")
for error in errors:
print(f" - {error}")
return 1
def manifest_errors(path: Path) -> list[str]:
manifest = load_manifest(path)
package = manifest.get("package")
if not isinstance(package, dict):
return []
errors = []
for field in WORKSPACE_PACKAGE_FIELDS:
if not is_workspace_reference(package.get(field)):
errors.append(f"set `{field}.workspace = true` in `[package]`")
lints = manifest.get("lints")
if not (isinstance(lints, dict) and lints.get("workspace") is True):
errors.append("add `[lints]` with `workspace = true`")
expected_name = expected_package_name(path)
if expected_name is not None:
actual_name = package.get("name")
if actual_name != expected_name:
errors.append(
f"set `[package].name` to `{expected_name}` (found `{actual_name}`)"
)
return errors
def expected_package_name(path: Path) -> str | None:
parts = path.relative_to(CARGO_RS_ROOT).parts
if len(parts) == 2 and parts[1] == "Cargo.toml":
directory = parts[0]
return TOP_LEVEL_NAME_EXCEPTIONS.get(
directory,
directory if directory.startswith("codex-") else f"codex-{directory}",
)
if len(parts) == 3 and parts[0] == "utils" and parts[2] == "Cargo.toml":
directory = parts[1]
return UTILITY_NAME_EXCEPTIONS.get(directory, f"codex-utils-{directory}")
return None
def is_workspace_reference(value: object) -> bool:
return isinstance(value, dict) and value.get("workspace") is True
def load_manifest(path: Path) -> dict:
return tomllib.loads(path.read_text())
def cargo_manifests() -> list[Path]:
return sorted(
path
for path in CARGO_RS_ROOT.rglob("Cargo.toml")
if path != CARGO_RS_ROOT / "Cargo.toml"
)
if __name__ == "__main__":
sys.exit(main())

View File

@@ -14,6 +14,9 @@ jobs:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Verify codex-rs Cargo manifests inherit workspace settings
run: python3 .github/scripts/verify_cargo_workspace_manifests.py
- name: Setup pnpm
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
with:

View File

@@ -8,6 +8,9 @@ license.workspace = true
name = "codex_ansi_escape"
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
ansi-to-tui = { workspace = true }
ratatui = { workspace = true, features = [

View File

@@ -7,6 +7,9 @@ license.workspace = true
[lib]
path = "lib.rs"
[lints]
workspace = true
[dependencies]
anyhow = { workspace = true }
base64 = { workspace = true }

View File

@@ -27,7 +27,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
},
supported_in_api: preset.supported_in_api,
priority,
upgrade: preset.upgrade.as_ref().map(|u| u.into()),
upgrade: preset.upgrade.as_ref().map(Into::into),
base_instructions: "base instructions".to_string(),
model_messages: None,
supports_reasoning_summaries: false,

View File

@@ -8,6 +8,9 @@ publish = false
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
anyhow = "1"
serde = { version = "1", features = ["derive"] }

View File

@@ -8,6 +8,9 @@ license.workspace = true
name = "codex_backend_openapi_models"
path = "src/lib.rs"
[lints]
workspace = true
# Important: generated code often violates our workspace lints.
# Allow unwrap/expect in this crate so the workspace builds cleanly
# after models are regenerated.

View File

@@ -7,6 +7,9 @@ license.workspace = true
[lib]
path = "lib.rs"
[lints]
workspace = true
[dependencies]
anyhow = { workspace = true }
assert_cmd = { workspace = true }

View File

@@ -4,6 +4,9 @@ version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
anyhow.workspace = true
clap = { workspace = true, features = ["derive"] }

View File

@@ -1,3 +1,4 @@
#![allow(clippy::expect_used)]
use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;

View File

@@ -1,3 +1,4 @@
#![allow(clippy::expect_used)]
use std::io;
use std::io::IsTerminal;
use std::io::Write;

View File

@@ -1,3 +1,4 @@
#![allow(clippy::expect_used)]
use std::io::BufRead;
use std::io::BufReader;
use std::process::ChildStdout;
@@ -113,7 +114,7 @@ fn handle_server_request(
stdin: &Arc<Mutex<Option<std::process::ChildStdin>>>,
output: &Output,
) -> anyhow::Result<()> {
let server_request = match ServerRequest::try_from(request.clone()) {
let server_request = match ServerRequest::try_from(request) {
Ok(server_request) => server_request,
Err(_) => return Ok(()),
};

View File

@@ -4,6 +4,9 @@ version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
anyhow = { workspace = true }
codex-protocol = { workspace = true }

View File

@@ -7,6 +7,7 @@ use std::io::{self};
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::PoisonError;
use std::time::Duration;
use anyhow::Result;
@@ -95,11 +96,19 @@ impl CodexFeedback {
pub fn snapshot(&self, session_id: Option<ThreadId>) -> FeedbackSnapshot {
let bytes = {
let guard = self.inner.ring.lock().expect("mutex poisoned");
let guard = self
.inner
.ring
.lock()
.unwrap_or_else(PoisonError::into_inner);
guard.snapshot_bytes()
};
let tags = {
let guard = self.inner.tags.lock().expect("mutex poisoned");
let guard = self
.inner
.tags
.lock()
.unwrap_or_else(PoisonError::into_inner);
guard.clone()
};
FeedbackSnapshot {
@@ -324,7 +333,7 @@ impl FeedbackSnapshot {
use sentry::protocol::Values;
event.exception = Values::from(vec![Exception {
ty: title.clone(),
ty: title,
value: Some(r.to_string()),
..Default::default()
}]);
@@ -430,7 +439,11 @@ where
return;
}
let mut guard = self.inner.tags.lock().expect("mutex poisoned");
let mut guard = self
.inner
.tags
.lock()
.unwrap_or_else(PoisonError::into_inner);
for (key, value) in visitor.tags {
if guard.len() >= MAX_FEEDBACK_TAGS && !guard.contains_key(&key) {
continue;

View File

@@ -12,6 +12,9 @@ path = "src/main.rs"
name = "codex_file_search"
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"] }

View File

@@ -20,6 +20,7 @@ use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Condvar;
use std::sync::Mutex;
use std::sync::PoisonError;
use std::sync::RwLock;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
@@ -195,10 +196,10 @@ pub fn create_session(
threads: threads.get(),
compute_indices,
respect_gitignore,
cancelled: cancelled.clone(),
cancelled,
shutdown: Arc::new(AtomicBool::new(false)),
reporter,
work_tx: work_tx.clone(),
work_tx,
});
let matcher_inner = inner.clone();
@@ -611,14 +612,16 @@ struct RunReporter {
impl SessionReporter for RunReporter {
fn on_update(&self, snapshot: &FileSearchSnapshot) {
#[expect(clippy::unwrap_used)]
let mut guard = self.snapshot.write().unwrap();
let mut guard = self
.snapshot
.write()
.unwrap_or_else(PoisonError::into_inner);
*guard = snapshot.clone();
}
fn on_complete(&self) {
let (cv, mutex) = &self.completed;
let mut completed = mutex.lock().unwrap();
let mut completed = mutex.lock().unwrap_or_else(PoisonError::into_inner);
*completed = true;
cv.notify_all();
}
@@ -627,11 +630,14 @@ impl SessionReporter for RunReporter {
impl RunReporter {
fn wait_for_complete(&self) -> FileSearchSnapshot {
let (cv, mutex) = &self.completed;
let mut completed = mutex.lock().unwrap();
let mut completed = mutex.lock().unwrap_or_else(PoisonError::into_inner);
while !*completed {
completed = cv.wait(completed).unwrap();
completed = cv.wait(completed).unwrap_or_else(PoisonError::into_inner);
}
self.snapshot.read().unwrap().clone()
self.snapshot
.read()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
}

View File

@@ -27,12 +27,15 @@ struct StdioReporter {
impl Reporter for StdioReporter {
fn report_match(&self, file_match: &FileMatch) {
if self.write_output_as_json {
println!("{}", serde_json::to_string(&file_match).unwrap());
match serde_json::to_string(file_match) {
Ok(json) => println!("{json}"),
Err(err) => eprintln!("Failed to serialize file match as JSON: {err}"),
}
} else if self.show_indices {
let indices = file_match
.indices
.as_ref()
.expect("--compute-indices was specified");
let Some(indices) = file_match.indices.as_ref() else {
println!("{}", file_match.path.to_string_lossy());
return;
};
// `indices` is guaranteed to be sorted in ascending order. Instead
// of calling `contains` for every character (which would be O(N^2)
// in the worst-case), walk through the `indices` vector once while
@@ -61,7 +64,10 @@ impl Reporter for StdioReporter {
fn warn_matches_truncated(&self, total_match_count: usize, shown_match_count: usize) {
if self.write_output_as_json {
let value = json!({"matches_truncated": true});
println!("{}", serde_json::to_string(&value).unwrap());
match serde_json::to_string(&value) {
Ok(json) => println!("{json}"),
Err(err) => eprintln!("Failed to serialize truncation warning as JSON: {err}"),
}
} else {
eprintln!(
"Warning: showing {shown_match_count} out of {total_match_count} results. Provide a more specific pattern or increase the --limit.",

View File

@@ -7,6 +7,9 @@ license.workspace = true
[lib]
path = "lib.rs"
[lints]
workspace = true
[dependencies]
anyhow = { workspace = true }
codex-core = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "codex-network-proxy"
edition = "2024"
edition.workspace = true
version = { workspace = true }
license.workspace = true

View File

@@ -8,6 +8,9 @@ version.workspace = true
name = "codex-execve-wrapper"
path = "src/bin/main_execve_wrapper.rs"
[lints]
workspace = true
[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }

View File

@@ -601,7 +601,7 @@ mod tests {
let execve_wrapper_str = execve_wrapper.to_string_lossy().to_string();
let server = EscalateServer::new(
PathBuf::from("/bin/zsh"),
execve_wrapper.clone(),
execve_wrapper,
DeterministicEscalationPolicy {
decision: EscalationDecision::run(),
},

View File

@@ -59,15 +59,18 @@ fn extract_fds(control: &[u8]) -> Vec<OwnedFd> {
let ty = unsafe { (*cmsg).cmsg_type };
if level == libc::SOL_SOCKET && ty == libc::SCM_RIGHTS {
let data_ptr = unsafe { libc::CMSG_DATA(cmsg).cast::<RawFd>() };
let fd_count: usize = {
let Some(cmsg_data_len) = ({
// `cmsghdr::cmsg_len` is not typed consistently across targets, so normalize it
// before doing the size arithmetic.
#[allow(clippy::useless_conversion)]
let cmsg_data_len = usize::try_from(unsafe { (*cmsg).cmsg_len })
.expect("cmsghdr length fits")
- unsafe { libc::CMSG_LEN(0) as usize };
cmsg_data_len / size_of::<RawFd>()
usize::try_from(unsafe { (*cmsg).cmsg_len })
.ok()
.and_then(|len| len.checked_sub(unsafe { libc::CMSG_LEN(0) as usize }))
}) else {
cmsg = unsafe { libc::CMSG_NXTHDR(&hdr, cmsg) };
continue;
};
let fd_count = cmsg_data_len / size_of::<RawFd>();
for i in 0..fd_count {
let fd = unsafe { data_ptr.add(i).read() };
fds.push(unsafe { OwnedFd::from_raw_fd(fd) });

View File

@@ -1,5 +1,5 @@
[package]
edition = "2024"
edition.workspace = true
license.workspace = true
name = "codex-utils-pty"
version.workspace = true

View File

@@ -1,6 +1,6 @@
[package]
build = "build.rs"
edition = "2024"
edition.workspace = true
license.workspace = true
name = "codex-windows-sandbox"
version.workspace = true
@@ -17,6 +17,9 @@ path = "src/bin/setup_main.rs"
name = "codex-command-runner"
path = "src/bin/command_runner.rs"
[lints]
workspace = true
[dependencies]
anyhow = "1.0"
base64 = { workspace = true }