From 74e0161c21701fad2dab450cedecfedff8f9b8c3 Mon Sep 17 00:00:00 2001 From: Shijie Rao Date: Mon, 18 May 2026 12:21:56 -0700 Subject: [PATCH 01/25] Release 0.132.0-alpha.1 --- codex-rs/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index efc64577dd70..ff15aef2ca9e 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -117,7 +117,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.0.0" +version = "0.132.0-alpha.1" # Track the edition for all workspace crates in one place. Individual # crates can still override this value, but keeping it here means new # crates created with `cargo new -w ...` automatically inherit the 2024 From 13595c36e218fcbd13df118eeadf00d4eb0e6d31 Mon Sep 17 00:00:00 2001 From: Shijie Rao Date: Tue, 19 May 2026 16:22:37 -0700 Subject: [PATCH 02/25] ## New Features - The Python SDK now supports first-class authentication, including API key login, ChatGPT browser and device-code flows, account inspection, and logout APIs. (#23093) - Python turn APIs are easier to use for text-only workflows: you can pass a plain string as input, and handle-based runs now return a richer `TurnResult` with collected items, timing, and usage data. (#23151, #23162) - `codex exec resume` now accepts `--output-schema`, so resumed automations can keep session context while still enforcing structured JSON output. (#23123) - TUI startup is faster because terminal capability probes are now batched instead of waiting on several serial checks before the first interactive frame. (#23175) - Remote executor registration can now use standard Codex auth instead of a separate registry credential flow. (#22769) - App-server turns can preserve requested image fidelity, including original-resolution local images, across user inputs and image-producing tools. (#20693) ## Bug Fixes - Goal continuations now stop when they hit usage limits or a repeated blocker instead of looping and burning more tokens, and completion responses phrase usage more naturally. (#23094, #22907) - The session picker is easier to trust: renamed threads now show `name (thread-id)` in resume hints, and pasted text works in the picker search box. (#23234, #23338) - Multi-session TUI flows are more reliable: in-progress MCP calls stay marked as active during replay, and elicitation replies are sent back to the thread that requested them. (#23236, #23241) - Remote sessions now keep websocket connections alive and show repo-relative diff paths again instead of `/tmp/...`-prefixed paths. (#23226, #23261) - Windows installs are more robust: `codex doctor` now detects npm-managed installs correctly, and MSVC release binaries no longer depend on separately installed VC++ runtime DLLs. (#22967, #22905) - TUI polish fixes include immediate shutdown feedback on exit, hiding the ChatGPT usage link for non-OpenAI providers, and keeping a cleared Fast tier from reappearing after side-thread resume. (#23323, #23127, #23121) ## Documentation - The Python SDK docs, FAQ, and examples were refreshed around the new auth flow and turn APIs, with clearer setup guidance and simpler text-only examples. (#22941, #23093, #23151, #23162) ## Chores - Memory summaries are now versioned and rebuilt when the stored format is stale, which should keep long-lived memory context leaner and more predictable. (#23148) ## Changelog Full Changelog: https://github.com/openai/codex/compare/rust-v0.131.0...rust-v0.132.0 - #20693 Preserve image detail in app-server inputs @fjord-oai - #22891 tui: pass active permission profiles through app commands @bolinfest - #22924 app-server-protocol: remove PermissionProfile from API @bolinfest - #22941 [codex] Refine Python SDK user-facing docs @aibrahim-oai - #22967 Fix Windows doctor npm root probe @etraut-openai - #22920 core: set permission profiles from snapshots @bolinfest - #22939 [codex] Split Python SDK helper logic @aibrahim-oai - #22907 Improve goal completion usage reporting @etraut-openai - #23030 test: construct permission profiles directly @bolinfest - #22769 exec-server: support auth-backed remote executor registration @miz-openai - #22946 [codex] preserve MCP result meta in McpToolCallItemResult @miaolin-oai - #23069 multiagent: trim model-visible description, cap to 5 models @sayan-oai - #22913 [1 of 4] tui: route primary settings writes through app server @etraut-openai - #23093 sdk/python: add first-class login support @aibrahim-oai - #23151 [codex] Return TurnResult from Python turn handles @aibrahim-oai - #23147 Make multi-agent v2 tool namespace configurable @jif-oai - #23036 test: reduce core sandbox policy test setup @bolinfest - #23162 [codex] Accept string input for Python turns @aibrahim-oai - #23226 Add exec-server websocket keepalive @starr-openai - #23148 Densify and version memory summaries @jif-oai - #22448 [codex] Add installed-plugin mention API @xli-oai - #23288 chore: goal ext skeleton @jif-oai - #23291 Make extension lifecycle hooks async @jif-oai - #23293 feat: add extension event sink capability @jif-oai - #23295 chore: isolate thread goal storage behind GoalStore @jif-oai - #23301 chore: goal resumed metrics @jif-oai - #23305 chore: make token usage async @jif-oai - #23306 Emit goal update events from goal extension tools @jif-oai - #23121 tui: keep cleared Fast tier from reappearing after side-thread resume @etraut-openai - #23123 Support --output-schema for exec resume @etraut-openai - #23128 Fix TUI stream cleanup after turn errors @etraut-openai - #23127 Hide ChatGPT usage link for non-OpenAI status @etraut-openai - #23175 [1 of 2] Optimize TUI startup terminal probes @etraut-openai - #22706 [codex] Remove legacy shell output formatting paths @pakrym-oai - #23332 nit: read prompt @jif-oai - #22905 windows: link MSVC release binaries with static CRT @iceweasel-oai - #23323 fix(tui): show shutdown feedback on exit @fcoury-oai - #23261 Fix remote turn diff display roots @starr-openai - #22569 Simplify legacy Windows sandbox ACL persistence @iceweasel-oai - #23273 Upload rust full CI JUnit reports @starr-openai - #22893 fix: harden plugin creator sharing validation @efrazer-oai - #23094 goal: pause continuation loops on usage limits and blockers @etraut-openai - #23234 Clarify resume hints for renamed threads @etraut-openai - #23241 TUI: route elicitation responses to request thread @etraut-openai - #23236 TUI: replay in-progress MCP calls as started @etraut-openai - #23088 goals: keep pause transitions explicit @etraut-openai - #23338 feat(tui): handle paste in session picker @fcoury-oai - #23335 feat(app-server): add optional thread_id to experimentalFeature/list @owenlin0 --- codex-rs/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index efc64577dd70..5e0b16c4cb11 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -117,7 +117,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.0.0" +version = "0.132.0" # Track the edition for all workspace crates in one place. Individual # crates can still override this value, but keeping it here means new # crates created with `cargo new -w ...` automatically inherit the 2024 From 5fde5431ed3d7e7fbec58c65563c9e4f91f0f6b6 Mon Sep 17 00:00:00 2001 From: wallentx Date: Tue, 19 May 2026 21:51:31 -0500 Subject: [PATCH 03/25] Apply Termux compatibility patch --- codex-rs/Cargo.lock | 7 +- codex-rs/Cargo.toml | 4 +- codex-rs/arg0/Cargo.toml | 1 + codex-rs/arg0/src/lib.rs | 29 +++-- codex-rs/core/Cargo.toml | 5 + codex-rs/core/src/installation_id.rs | 8 +- codex-rs/execpolicy/Cargo.toml | 1 + codex-rs/execpolicy/src/amend.rs | 23 +++- codex-rs/utils/file-lock/BUILD.bazel | 6 + codex-rs/utils/file-lock/Cargo.toml | 10 ++ codex-rs/utils/file-lock/src/lib.rs | 168 +++++++++++++++++++++++++++ justfile | 5 + scripts/termux-lock-audit.sh | 126 ++++++++++++++++++++ 13 files changed, 377 insertions(+), 16 deletions(-) create mode 100644 codex-rs/utils/file-lock/BUILD.bazel create mode 100644 codex-rs/utils/file-lock/Cargo.toml create mode 100644 codex-rs/utils/file-lock/src/lib.rs create mode 100755 scripts/termux-lock-audit.sh diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 5ced1803fa4c..e6b21cb95465 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2123,6 +2123,7 @@ dependencies = [ "codex-sandboxing", "codex-shell-escalation", "codex-utils-absolute-path", + "codex-utils-file-lock", "codex-utils-home-dir", "dotenvy", "tempfile", @@ -2530,6 +2531,7 @@ dependencies = [ "codex-utils-absolute-path", "codex-utils-cache", "codex-utils-cargo-bin", + "codex-utils-file-lock", "codex-utils-home-dir", "codex-utils-image", "codex-utils-output-truncation", @@ -2786,6 +2788,7 @@ dependencies = [ "anyhow", "clap", "codex-utils-absolute-path", + "codex-utils-file-lock", "multimap", "pretty_assertions", "serde", @@ -13713,9 +13716,9 @@ dependencies = [ [[package]] name = "v8" -version = "146.4.0" +version = "146.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97bcac5cdc5a195a4813f1855a6bc658f240452aac36caa12fd6c6f16026ab1" +checksum = "59b21777080203cb33828681f177e53402fa2927a1e2d17adead072b0c26b401" dependencies = [ "bindgen", "bitflags 2.10.0", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index ff15aef2ca9e..319f61b72be6 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -81,6 +81,7 @@ members = [ "v8-poc", "utils/absolute-path", "utils/cargo-bin", + "utils/file-lock", "git-utils", "utils/cache", "utils/image", @@ -219,6 +220,7 @@ codex-utils-cache = { path = "utils/cache" } codex-utils-cargo-bin = { path = "utils/cargo-bin" } codex-utils-cli = { path = "utils/cli" } codex-utils-elapsed = { path = "utils/elapsed" } +codex-utils-file-lock = { path = "utils/file-lock" } codex-utils-fuzzy-match = { path = "utils/fuzzy-match" } codex-utils-home-dir = { path = "utils/home-dir" } codex-utils-image = { path = "utils/image" } @@ -413,7 +415,7 @@ unicode-width = "0.2" url = "2" urlencoding = "2.1" uuid = "1" -v8 = "=146.4.0" +v8 = "=146.9.0" vt100 = "0.16.2" walkdir = "2.5.0" webbrowser = "1.0" diff --git a/codex-rs/arg0/Cargo.toml b/codex-rs/arg0/Cargo.toml index 7ee21a770e49..7c2c17692020 100644 --- a/codex-rs/arg0/Cargo.toml +++ b/codex-rs/arg0/Cargo.toml @@ -20,6 +20,7 @@ codex-linux-sandbox = { workspace = true } codex-sandboxing = { workspace = true } codex-shell-escalation = { workspace = true } codex-utils-absolute-path = { workspace = true } +codex-utils-file-lock = { workspace = true } codex-utils-home-dir = { workspace = true } dotenvy = { workspace = true } tempfile = { workspace = true } diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index 2f6ae4653c65..5a558769a7e5 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -6,6 +6,8 @@ use std::path::PathBuf; use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1; use codex_exec_server::CODEX_FS_HELPER_ARG1; use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; +use codex_utils_file_lock::TryFileLockOutcome; +use codex_utils_file_lock::try_lock_exclusive_optional; use codex_utils_home_dir::find_codex_home; #[cfg(unix)] use std::os::unix::fs::symlink; @@ -33,12 +35,12 @@ pub struct Arg0DispatchPaths { /// Keeps the per-session PATH entry alive and locked for the process lifetime. pub struct Arg0PathEntryGuard { _temp_dir: TempDir, - _lock_file: File, + _lock_file: Option, paths: Arg0DispatchPaths, } impl Arg0PathEntryGuard { - fn new(temp_dir: TempDir, lock_file: File, paths: Arg0DispatchPaths) -> Self { + fn new(temp_dir: TempDir, lock_file: Option, paths: Arg0DispatchPaths) -> Self { Self { _temp_dir: temp_dir, _lock_file: lock_file, @@ -326,7 +328,13 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result Some(lock_file), + TryFileLockOutcome::Unsupported => None, + TryFileLockOutcome::WouldBlock => { + return Err(std::io::Error::from(std::io::ErrorKind::WouldBlock).into()); + } + }; for filename in &[ APPLY_PATCH_ARG0, @@ -445,10 +453,9 @@ fn try_lock_dir(dir: &Path) -> std::io::Result> { Err(err) => return Err(err), }; - match lock_file.try_lock() { - Ok(()) => Ok(Some(lock_file)), - Err(std::fs::TryLockError::WouldBlock) => Ok(None), - Err(err) => Err(err.into()), + match try_lock_exclusive_optional(&lock_file)? { + TryFileLockOutcome::Acquired => Ok(Some(lock_file)), + TryFileLockOutcome::WouldBlock | TryFileLockOutcome::Unsupported => Ok(None), } } @@ -562,7 +569,13 @@ mod tests { let dir = root.path().join("locked"); fs::create_dir(&dir)?; let lock_file = create_lock(&dir)?; - lock_file.try_lock()?; + match try_lock_exclusive_optional(&lock_file)? { + TryFileLockOutcome::Acquired => {} + TryFileLockOutcome::Unsupported => return Ok(()), + TryFileLockOutcome::WouldBlock => { + panic!("newly created lock file should not be locked"); + } + } janitor_cleanup(root.path())?; diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 8472918dcae9..fdf1d4b18a9e 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -63,6 +63,7 @@ codex-thread-store = { workspace = true } codex-tools = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-cache = { workspace = true } +codex-utils-file-lock = { workspace = true } codex-utils-image = { workspace = true } codex-utils-home-dir = { workspace = true } codex-utils-output-truncation = { workspace = true } @@ -125,6 +126,10 @@ openssl-sys = { workspace = true, features = ["vendored"] } [target.aarch64-unknown-linux-musl.dependencies] openssl-sys = { workspace = true, features = ["vendored"] } +# Build OpenSSL from source for Android builds. +[target.aarch64-linux-android.dependencies] +openssl-sys = { workspace = true, features = ["vendored"] } + [target.'cfg(unix)'.dependencies] codex-shell-escalation = { workspace = true } diff --git a/codex-rs/core/src/installation_id.rs b/codex-rs/core/src/installation_id.rs index a42e6b6d8353..a87c660f6132 100644 --- a/codex-rs/core/src/installation_id.rs +++ b/codex-rs/core/src/installation_id.rs @@ -11,6 +11,9 @@ use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::PermissionsExt; use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_file_lock::FileLockOutcome; +use codex_utils_file_lock::acquire_sibling_lock_dir; +use codex_utils_file_lock::lock_exclusive_optional; use tokio::fs; use uuid::Uuid; @@ -29,7 +32,10 @@ pub async fn resolve_installation_id(codex_home: &AbsolutePathBuf) -> Result None, + FileLockOutcome::Unsupported => Some(acquire_sibling_lock_dir(&path)?), + }; #[cfg(unix)] { diff --git a/codex-rs/execpolicy/Cargo.toml b/codex-rs/execpolicy/Cargo.toml index b22226a79e4b..b87af084d7d1 100644 --- a/codex-rs/execpolicy/Cargo.toml +++ b/codex-rs/execpolicy/Cargo.toml @@ -21,6 +21,7 @@ workspace = true anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] } codex-utils-absolute-path = { workspace = true } +codex-utils-file-lock = { workspace = true } multimap = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/codex-rs/execpolicy/src/amend.rs b/codex-rs/execpolicy/src/amend.rs index e25fd1bd9144..1ef36b1ff5d8 100644 --- a/codex-rs/execpolicy/src/amend.rs +++ b/codex-rs/execpolicy/src/amend.rs @@ -6,6 +6,10 @@ use std::io::Write; use std::path::Path; use std::path::PathBuf; +use codex_utils_file_lock::FileLockOutcome; +use codex_utils_file_lock::acquire_sibling_lock_dir; +use codex_utils_file_lock::lock_exclusive_optional; + use crate::decision::Decision; use crate::rule::NetworkRuleProtocol; use crate::rule::normalize_network_rule_host; @@ -154,10 +158,21 @@ fn append_locked_line(policy_path: &Path, line: &str) -> Result<(), AmendError> path: policy_path.to_path_buf(), source, })?; - file.lock().map_err(|source| AmendError::LockPolicyFile { - path: policy_path.to_path_buf(), - source, - })?; + let _lock_dir_guard = + match lock_exclusive_optional(&file).map_err(|source| AmendError::LockPolicyFile { + path: policy_path.to_path_buf(), + source, + })? { + FileLockOutcome::Acquired => None, + FileLockOutcome::Unsupported => { + Some(acquire_sibling_lock_dir(policy_path).map_err(|source| { + AmendError::LockPolicyFile { + path: policy_path.to_path_buf(), + source, + } + })?) + } + }; file.seek(SeekFrom::Start(0)) .map_err(|source| AmendError::SeekPolicyFile { diff --git a/codex-rs/utils/file-lock/BUILD.bazel b/codex-rs/utils/file-lock/BUILD.bazel new file mode 100644 index 000000000000..face70c53518 --- /dev/null +++ b/codex-rs/utils/file-lock/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "file-lock", + crate_name = "codex_utils_file_lock", +) diff --git a/codex-rs/utils/file-lock/Cargo.toml b/codex-rs/utils/file-lock/Cargo.toml new file mode 100644 index 000000000000..5e3877fc1630 --- /dev/null +++ b/codex-rs/utils/file-lock/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "codex-utils-file-lock" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] diff --git a/codex-rs/utils/file-lock/src/lib.rs b/codex-rs/utils/file-lock/src/lib.rs new file mode 100644 index 000000000000..fdcfbdb81e75 --- /dev/null +++ b/codex-rs/utils/file-lock/src/lib.rs @@ -0,0 +1,168 @@ +use std::fs::File; +use std::fs::create_dir; +use std::fs::remove_dir; +use std::io; +use std::path::Path; +use std::path::PathBuf; +use std::thread; +use std::time::Duration; + +const LOCK_DIR_RETRY_SLEEP: Duration = Duration::from_millis(100); + +/// Result of acquiring a blocking advisory file lock. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileLockOutcome { + Acquired, + Unsupported, +} + +/// Result of attempting to acquire a non-blocking advisory file lock. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryFileLockOutcome { + Acquired, + WouldBlock, + Unsupported, +} + +/// Result of attempting to acquire a non-blocking lock directory. +#[derive(Debug, PartialEq, Eq)] +pub enum TryLockDirOutcome { + Acquired(LockDirGuard), + WouldBlock, +} + +/// Guard for a sibling lock directory created with an atomic `mkdir`. +#[derive(Debug, PartialEq, Eq)] +pub struct LockDirGuard { + path: PathBuf, +} + +impl Drop for LockDirGuard { + fn drop(&mut self) { + let _ = remove_dir(&self.path); + } +} + +/// Acquires an exclusive advisory file lock, treating unsupported file locking +/// as a distinct outcome for platforms such as Termux. +pub fn lock_exclusive_optional(file: &File) -> io::Result { + match file.lock() { + Ok(()) => Ok(FileLockOutcome::Acquired), + Err(err) if err.kind() == io::ErrorKind::Unsupported => Ok(FileLockOutcome::Unsupported), + Err(err) => Err(err), + } +} + +/// Attempts to acquire an exclusive advisory file lock without blocking, +/// preserving `WouldBlock` and unsupported file locking as distinct outcomes. +pub fn try_lock_exclusive_optional(file: &File) -> io::Result { + match file.try_lock() { + Ok(()) => Ok(TryFileLockOutcome::Acquired), + Err(std::fs::TryLockError::WouldBlock) => Ok(TryFileLockOutcome::WouldBlock), + Err(std::fs::TryLockError::Error(err)) if err.kind() == io::ErrorKind::Unsupported => { + Ok(TryFileLockOutcome::Unsupported) + } + Err(std::fs::TryLockError::Error(err)) => Err(err), + } +} + +/// Returns the sibling directory path used as a fallback lock for `path`. +pub fn sibling_lock_dir(path: &Path) -> PathBuf { + let Some(file_name) = path.file_name() else { + return path.with_file_name(".lock"); + }; + + let mut lock_name = file_name.to_os_string(); + lock_name.push(".lock"); + path.with_file_name(lock_name) +} + +/// Acquires a sibling lock directory, blocking until it is available. +pub fn acquire_sibling_lock_dir(path: &Path) -> io::Result { + loop { + match try_acquire_sibling_lock_dir(path)? { + TryLockDirOutcome::Acquired(guard) => return Ok(guard), + TryLockDirOutcome::WouldBlock => thread::sleep(LOCK_DIR_RETRY_SLEEP), + } + } +} + +/// Attempts to acquire a sibling lock directory without blocking. +pub fn try_acquire_sibling_lock_dir(path: &Path) -> io::Result { + let lock_dir = sibling_lock_dir(path); + match create_dir(&lock_dir) { + Ok(()) => Ok(TryLockDirOutcome::Acquired(LockDirGuard { path: lock_dir })), + Err(err) if err.kind() == io::ErrorKind::AlreadyExists => Ok(TryLockDirOutcome::WouldBlock), + Err(err) => Err(err), + } +} + +/// Attempts to acquire a shared advisory file lock without blocking, +/// preserving `WouldBlock` and unsupported file locking as distinct outcomes. +pub fn try_lock_shared_optional(file: &File) -> io::Result { + match file.try_lock_shared() { + Ok(()) => Ok(TryFileLockOutcome::Acquired), + Err(std::fs::TryLockError::WouldBlock) => Ok(TryFileLockOutcome::WouldBlock), + Err(std::fs::TryLockError::Error(err)) if err.kind() == io::ErrorKind::Unsupported => { + Ok(TryFileLockOutcome::Unsupported) + } + Err(std::fs::TryLockError::Error(err)) => Err(err), + } +} + +#[cfg(test)] +mod tests { + use super::TryLockDirOutcome; + use super::sibling_lock_dir; + use super::try_acquire_sibling_lock_dir; + use std::fs; + use std::path::PathBuf; + use std::time::SystemTime; + + fn unique_temp_file_path(name: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("system clock should be after Unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "codex-file-lock-{name}-{}-{nanos}", + std::process::id() + )) + } + + #[test] + fn sibling_lock_dir_appends_lock_suffix() { + let path = PathBuf::from("/tmp/history.jsonl"); + + assert_eq!( + sibling_lock_dir(&path), + PathBuf::from("/tmp/history.jsonl.lock") + ); + } + + #[test] + fn try_acquire_sibling_lock_dir_is_exclusive_until_drop() { + let path = unique_temp_file_path("exclusive"); + let lock_dir = sibling_lock_dir(&path); + let _ = fs::remove_dir_all(&lock_dir); + + let guard = match try_acquire_sibling_lock_dir(&path).expect("acquire lock dir") { + TryLockDirOutcome::Acquired(guard) => guard, + TryLockDirOutcome::WouldBlock => panic!("first lock attempt should acquire"), + }; + assert!(lock_dir.is_dir()); + + assert!(matches!( + try_acquire_sibling_lock_dir(&path).expect("try acquire held lock dir"), + TryLockDirOutcome::WouldBlock + )); + + drop(guard); + assert!(!lock_dir.exists()); + + let reacquired = try_acquire_sibling_lock_dir(&path).expect("reacquire lock dir"); + assert!(matches!(reacquired, TryLockDirOutcome::Acquired(_))); + + let _ = fs::remove_dir_all(lock_dir); + } +} diff --git a/justfile b/justfile index ab2fbc63629a..569a04695216 100644 --- a/justfile +++ b/justfile @@ -116,6 +116,11 @@ argument-comment-lint *args: argument-comment-lint-from-source *args: {{ justfile_directory() }}/tools/argument-comment-lint/run.py "$@" +# Audit advisory file locks that may need Termux Unsupported handling. +[no-cd] +termux-lock-audit *args: + {{ justfile_directory() }}/scripts/termux-lock-audit.sh "$@" + # Tail logs from the state SQLite database log *args: if [ "${1:-}" = "--" ]; then shift; fi; cargo run -p codex-state --bin logs_client -- "$@" diff --git a/scripts/termux-lock-audit.sh b/scripts/termux-lock-audit.sh new file mode 100755 index 000000000000..ac40f8e57246 --- /dev/null +++ b/scripts/termux-lock-audit.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: scripts/termux-lock-audit.sh [--strict] + +Audits Rust advisory file-lock usage that may need Termux compatibility handling. + +By default this script prints findings and exits successfully. With --strict, it +exits non-zero when candidate file-lock calls are found in files that do not also +mention Unsupported/TryLockError handling. +USAGE +} + +strict=false +for arg in "$@"; do + case "$arg" in + --strict) + strict=true + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "unknown argument: $arg" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if ! command -v rg >/dev/null 2>&1; then + echo "error: rg is required for this audit" >&2 + exit 1 +fi + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +unsupported_pattern='std::fs::TryLockError|fs::TryLockError|TryLockError::|ErrorKind::Unsupported|std::io::ErrorKind::Unsupported' +lock_context_pattern='(\.lock\(\)|\.try_lock\(|\.try_lock_shared\(|TryLockError|ErrorKind::Unsupported)' +candidate_file_lock_pattern='\b([A-Za-z_][A-Za-z0-9_]*_file|file)\.(lock|try_lock|try_lock_shared)\(' +candidate_false_positive_pattern='poisoned' +helper_pattern='codex_utils_file_lock|FileLockOutcome|TryFileLockOutcome|lock_exclusive_optional|try_lock_exclusive_optional|try_lock_shared_optional' + +print_section() { + printf '\n== %s ==\n' "$1" +} + +print_section "Unsupported-aware lock handling" +echo "These files already mention Unsupported/TryLockError and are likely patched or intentionally reviewed:" + +unsupported_count=0 +while IFS= read -r file; do + if rg -q "$lock_context_pattern" "$file"; then + rg -n -H "$lock_context_pattern" "$file" + unsupported_count=$((unsupported_count + 1)) + fi +done < <(rg -l "$unsupported_pattern" codex-rs -g '*.rs' || true) + +if [ "$unsupported_count" -eq 0 ]; then + echo "No unsupported-aware lock handling found." +fi + +print_section "Optional file-lock helper usage" +echo "These files use the shared optional advisory file-lock helper:" + +helper_count=0 +while IFS= read -r file; do + rg -n -H "$helper_pattern" "$file" + helper_count=$((helper_count + 1)) +done < <(rg -l "$helper_pattern" codex-rs -g '*.rs' || true) + +if [ "$helper_count" -eq 0 ]; then + echo "No optional file-lock helper usage found." +fi + +print_section "Candidate file-lock calls for manual review" +echo "These receiver names may be std::fs::File advisory locks, but the file does not mention Unsupported/TryLockError handling." +echo "This section can include false positives, such as mutexes wrapping a file." + +review_count=0 +while IFS= read -r file; do + if ! rg -q "$unsupported_pattern" "$file"; then + matches="$(rg -n -H "$candidate_file_lock_pattern" "$file" | rg -v "$candidate_false_positive_pattern" || true)" + if [ -n "$matches" ]; then + echo "$matches" + review_count=$((review_count + 1)) + fi + fi +done < <(rg -l "$candidate_file_lock_pattern" codex-rs -g '*.rs' || true) + +if [ "$review_count" -eq 0 ]; then + echo "No unpatched candidate file-lock calls found." +fi + +print_section "Candidate file-lock calls already in unsupported-aware files" +echo "These are candidate file-lock calls in files that already mention Unsupported/TryLockError handling:" + +aware_candidate_count=0 +while IFS= read -r file; do + if rg -q "$unsupported_pattern" "$file"; then + matches="$(rg -n -H "$candidate_file_lock_pattern" "$file" | rg -v "$candidate_false_positive_pattern" || true)" + if [ -n "$matches" ]; then + echo "$matches" + aware_candidate_count=$((aware_candidate_count + 1)) + fi + fi +done < <(rg -l "$candidate_file_lock_pattern" codex-rs -g '*.rs' || true) + +if [ "$aware_candidate_count" -eq 0 ]; then + echo "No unsupported-aware candidate file-lock calls found." +fi + +print_section "Summary" +echo "unsupported-aware files: $unsupported_count" +echo "optional helper files: $helper_count" +echo "review candidate files: $review_count" +echo "unsupported-aware candidate files: $aware_candidate_count" + +if [ "$strict" = true ] && [ "$review_count" -gt 0 ]; then + echo "strict mode: manual-review candidate files were found" >&2 + exit 1 +fi From 285aa5aa1540f1fab74731c02b36636c36c3f48c Mon Sep 17 00:00:00 2001 From: wallentx Date: Tue, 12 May 2026 02:29:30 -0500 Subject: [PATCH 04/25] Disable realtime audio on Android builds (cherry picked from commit 337303c72c5c624386937c5f2aa9dc3a8dcfa2b4) --- codex-rs/tui/Cargo.toml | 2 +- codex-rs/tui/src/lib.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index c213e92bae41..40624baba0d0 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -126,7 +126,7 @@ uuid = { workspace = true } codex-windows-sandbox = { workspace = true } tokio-util = { workspace = true, features = ["time"] } -[target.'cfg(not(target_os = "linux"))'.dependencies] +[target.'cfg(all(not(target_os = "linux"), not(target_os = "android")))'.dependencies] cpal = "0.15" [target.'cfg(unix)'.dependencies] diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 9ece5e3776f5..fbd1d7eb79bc 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -88,9 +88,9 @@ mod app_server_approval_conversions; mod app_server_session; mod approval_events; mod ascii_animation; -#[cfg(not(target_os = "linux"))] +#[cfg(not(any(target_os = "linux", target_os = "android")))] mod audio_device; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] #[allow(dead_code)] mod audio_device { use crate::app_event::RealtimeAudioDeviceKind; @@ -194,11 +194,11 @@ mod update_prompt; mod update_versions; mod updates; mod version; -#[cfg(not(target_os = "linux"))] +#[cfg(not(any(target_os = "linux", target_os = "android")))] mod voice; mod width; mod workspace_command; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] #[allow(dead_code)] mod voice { use crate::app_event_sender::AppEventSender; From 7ead295bb5d9e9f176a4cc50a983079ece10b132 Mon Sep 17 00:00:00 2001 From: wallentx Date: Tue, 19 May 2026 21:53:56 -0500 Subject: [PATCH 05/25] Update Termux v8 dependency --- codex-rs/Cargo.lock | 392 ++++++++++++++++++++++++-------------------- codex-rs/Cargo.toml | 2 +- 2 files changed, 211 insertions(+), 183 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index e6b21cb95465..a3b39903eefe 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -402,7 +402,7 @@ checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "app_test_support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -1518,9 +1518,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" [[package]] name = "calendrical_calculations" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7" +checksum = "5abbd6eeda6885048d357edc66748eea6e0268e3dd11f326fff5bd248d779c26" dependencies = [ "core_maths", "displaydoc", @@ -1778,7 +1778,7 @@ dependencies = [ [[package]] name = "codex-agent-graph-store" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-protocol", @@ -1793,7 +1793,7 @@ dependencies = [ [[package]] name = "codex-agent-identity" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -1812,7 +1812,7 @@ dependencies = [ [[package]] name = "codex-analytics" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-app-server-protocol", "codex-git-utils", @@ -1832,7 +1832,7 @@ dependencies = [ [[package]] name = "codex-ansi-escape" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "ansi-to-tui", "ratatui", @@ -1841,7 +1841,7 @@ dependencies = [ [[package]] name = "codex-api" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_matches", @@ -1875,7 +1875,7 @@ dependencies = [ [[package]] name = "codex-app-server" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "app_test_support", @@ -1960,7 +1960,7 @@ dependencies = [ [[package]] name = "codex-app-server-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-app-server", "codex-app-server-protocol", @@ -1987,7 +1987,7 @@ dependencies = [ [[package]] name = "codex-app-server-daemon" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-app-server-protocol", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "codex-app-server-protocol" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2035,7 +2035,7 @@ dependencies = [ [[package]] name = "codex-app-server-test-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2056,7 +2056,7 @@ dependencies = [ [[package]] name = "codex-app-server-transport" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "axum", @@ -2095,7 +2095,7 @@ dependencies = [ [[package]] name = "codex-apply-patch" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -2114,7 +2114,7 @@ dependencies = [ [[package]] name = "codex-arg0" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-apply-patch", @@ -2132,7 +2132,7 @@ dependencies = [ [[package]] name = "codex-async-utils" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "pretty_assertions", @@ -2142,7 +2142,7 @@ dependencies = [ [[package]] name = "codex-aws-auth" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "aws-config", "aws-credential-types", @@ -2157,7 +2157,7 @@ dependencies = [ [[package]] name = "codex-backend-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-api", @@ -2174,7 +2174,7 @@ dependencies = [ [[package]] name = "codex-backend-openapi-models" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "serde", "serde_json", @@ -2183,7 +2183,7 @@ dependencies = [ [[package]] name = "codex-bwrap" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "cc", "libc", @@ -2192,7 +2192,7 @@ dependencies = [ [[package]] name = "codex-chatgpt" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2215,7 +2215,7 @@ dependencies = [ [[package]] name = "codex-cli" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -2280,7 +2280,7 @@ dependencies = [ [[package]] name = "codex-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "bytes", @@ -2311,7 +2311,7 @@ dependencies = [ [[package]] name = "codex-cloud-requirements" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "base64 0.22.1", @@ -2336,7 +2336,7 @@ dependencies = [ [[package]] name = "codex-cloud-tasks" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -2368,7 +2368,7 @@ dependencies = [ [[package]] name = "codex-cloud-tasks-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -2383,7 +2383,7 @@ dependencies = [ [[package]] name = "codex-cloud-tasks-mock-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "chrono", @@ -2393,7 +2393,7 @@ dependencies = [ [[package]] name = "codex-code-mode" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-channel", "async-trait", @@ -2410,11 +2410,11 @@ dependencies = [ [[package]] name = "codex-collaboration-mode-templates" -version = "0.0.0" +version = "0.132.0-alpha.1" [[package]] name = "codex-config" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -2461,7 +2461,7 @@ dependencies = [ [[package]] name = "codex-connectors" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-app-server-protocol", @@ -2477,7 +2477,7 @@ dependencies = [ [[package]] name = "codex-core" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "arc-swap", @@ -2596,7 +2596,7 @@ dependencies = [ [[package]] name = "codex-core-api" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-analytics", "codex-app-server-protocol", @@ -2615,7 +2615,7 @@ dependencies = [ [[package]] name = "codex-core-plugins" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -2653,7 +2653,7 @@ dependencies = [ [[package]] name = "codex-core-skills" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-analytics", @@ -2684,7 +2684,7 @@ dependencies = [ [[package]] name = "codex-debug-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2696,7 +2696,7 @@ dependencies = [ [[package]] name = "codex-exec" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -2742,7 +2742,7 @@ dependencies = [ [[package]] name = "codex-exec-server" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "arc-swap", @@ -2783,7 +2783,7 @@ dependencies = [ [[package]] name = "codex-execpolicy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2801,7 +2801,7 @@ dependencies = [ [[package]] name = "codex-execpolicy-legacy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "allocative", "anyhow", @@ -2821,7 +2821,7 @@ dependencies = [ [[package]] name = "codex-experimental-api-macros" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "proc-macro2", "quote", @@ -2830,7 +2830,7 @@ dependencies = [ [[package]] name = "codex-extension-api" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-protocol", @@ -2839,7 +2839,7 @@ dependencies = [ [[package]] name = "codex-external-agent-migration" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-hooks", "pretty_assertions", @@ -2851,7 +2851,7 @@ dependencies = [ [[package]] name = "codex-external-agent-sessions" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "chrono", "codex-app-server-protocol", @@ -2865,7 +2865,7 @@ dependencies = [ [[package]] name = "codex-features" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-otel", "codex-protocol", @@ -2878,7 +2878,7 @@ dependencies = [ [[package]] name = "codex-feedback" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-login", @@ -2891,7 +2891,7 @@ dependencies = [ [[package]] name = "codex-file-search" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2907,7 +2907,7 @@ dependencies = [ [[package]] name = "codex-file-system" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-protocol", @@ -2917,7 +2917,7 @@ dependencies = [ [[package]] name = "codex-file-watcher" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "notify", "pretty_assertions", @@ -2928,7 +2928,7 @@ dependencies = [ [[package]] name = "codex-git-utils" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -2952,7 +2952,7 @@ dependencies = [ [[package]] name = "codex-goal-extension" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-extension-api", @@ -2964,7 +2964,7 @@ dependencies = [ [[package]] name = "codex-guardian" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-core", @@ -2974,7 +2974,7 @@ dependencies = [ [[package]] name = "codex-hooks" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -2997,7 +2997,7 @@ dependencies = [ [[package]] name = "codex-install-context" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-home-dir", "pretty_assertions", @@ -3006,7 +3006,7 @@ dependencies = [ [[package]] name = "codex-keyring-store" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "keyring", "tracing", @@ -3014,7 +3014,7 @@ dependencies = [ [[package]] name = "codex-linux-sandbox" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "clap", "codex-core", @@ -3037,7 +3037,7 @@ dependencies = [ [[package]] name = "codex-lmstudio" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-core", "codex-model-provider-info", @@ -3051,7 +3051,7 @@ dependencies = [ [[package]] name = "codex-login" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3093,7 +3093,7 @@ dependencies = [ [[package]] name = "codex-mcp" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-channel", @@ -3125,7 +3125,7 @@ dependencies = [ [[package]] name = "codex-mcp-server" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-arg0", @@ -3157,7 +3157,7 @@ dependencies = [ [[package]] name = "codex-memories-extension" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-core", @@ -3178,7 +3178,7 @@ dependencies = [ [[package]] name = "codex-memories-mcp" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-utils-absolute-path", @@ -3195,7 +3195,7 @@ dependencies = [ [[package]] name = "codex-memories-read" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-protocol", "codex-shell-command", @@ -3209,7 +3209,7 @@ dependencies = [ [[package]] name = "codex-memories-write" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -3244,7 +3244,7 @@ dependencies = [ [[package]] name = "codex-message-history" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-config", "pretty_assertions", @@ -3257,7 +3257,7 @@ dependencies = [ [[package]] name = "codex-model-provider" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-agent-identity", @@ -3281,7 +3281,7 @@ dependencies = [ [[package]] name = "codex-model-provider-info" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-api", "codex-app-server-protocol", @@ -3298,7 +3298,7 @@ dependencies = [ [[package]] name = "codex-models-manager" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "chrono", @@ -3319,7 +3319,7 @@ dependencies = [ [[package]] name = "codex-network-proxy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3350,7 +3350,7 @@ dependencies = [ [[package]] name = "codex-ollama" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "assert_matches", "async-stream", @@ -3369,7 +3369,7 @@ dependencies = [ [[package]] name = "codex-otel" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "chrono", "codex-api", @@ -3401,7 +3401,7 @@ dependencies = [ [[package]] name = "codex-plugin" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-config", "codex-utils-absolute-path", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "codex-process-hardening" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "libc", "pretty_assertions", @@ -3419,7 +3419,7 @@ dependencies = [ [[package]] name = "codex-protocol" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chardetng", @@ -3459,7 +3459,7 @@ dependencies = [ [[package]] name = "codex-realtime-webrtc" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "libwebrtc", "thiserror 2.0.18", @@ -3468,7 +3468,7 @@ dependencies = [ [[package]] name = "codex-response-debug-context" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "base64 0.22.1", "codex-api", @@ -3479,7 +3479,7 @@ dependencies = [ [[package]] name = "codex-responses-api-proxy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -3496,7 +3496,7 @@ dependencies = [ [[package]] name = "codex-rmcp-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "axum", @@ -3534,7 +3534,7 @@ dependencies = [ [[package]] name = "codex-rollout" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3559,7 +3559,7 @@ dependencies = [ [[package]] name = "codex-rollout-trace" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-code-mode", @@ -3575,7 +3575,7 @@ dependencies = [ [[package]] name = "codex-sandboxing" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3596,7 +3596,7 @@ dependencies = [ [[package]] name = "codex-secrets" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "age", "anyhow", @@ -3617,7 +3617,7 @@ dependencies = [ [[package]] name = "codex-shell-command" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -3637,7 +3637,7 @@ dependencies = [ [[package]] name = "codex-shell-escalation" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3658,7 +3658,7 @@ dependencies = [ [[package]] name = "codex-skills" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-absolute-path", "include_dir", @@ -3667,7 +3667,7 @@ dependencies = [ [[package]] name = "codex-state" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -3690,7 +3690,7 @@ dependencies = [ [[package]] name = "codex-stdio-to-uds" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-uds", @@ -3702,7 +3702,7 @@ dependencies = [ [[package]] name = "codex-terminal-detection" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "tracing", @@ -3710,7 +3710,7 @@ dependencies = [ [[package]] name = "codex-test-binary-support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-arg0", "tempfile", @@ -3718,7 +3718,7 @@ dependencies = [ [[package]] name = "codex-thread-manager-sample" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -3729,7 +3729,7 @@ dependencies = [ [[package]] name = "codex-thread-store" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "chrono", @@ -3750,7 +3750,7 @@ dependencies = [ [[package]] name = "codex-tools" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-app-server-protocol", @@ -3770,7 +3770,7 @@ dependencies = [ [[package]] name = "codex-tui" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "arboard", @@ -3881,7 +3881,7 @@ dependencies = [ [[package]] name = "codex-uds" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-io", "pretty_assertions", @@ -3893,7 +3893,7 @@ dependencies = [ [[package]] name = "codex-utils-absolute-path" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "dirs", "dunce", @@ -3907,14 +3907,14 @@ dependencies = [ [[package]] name = "codex-utils-approval-presets" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-protocol", ] [[package]] name = "codex-utils-cache" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "lru 0.16.3", "sha1", @@ -3923,7 +3923,7 @@ dependencies = [ [[package]] name = "codex-utils-cargo-bin" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "assert_cmd", "runfiles", @@ -3932,7 +3932,7 @@ dependencies = [ [[package]] name = "codex-utils-cli" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "clap", "codex-protocol", @@ -3944,15 +3944,19 @@ dependencies = [ [[package]] name = "codex-utils-elapsed" -version = "0.0.0" +version = "0.132.0-alpha.1" + +[[package]] +name = "codex-utils-file-lock" +version = "0.132.0-alpha.1" [[package]] name = "codex-utils-fuzzy-match" -version = "0.0.0" +version = "0.132.0-alpha.1" [[package]] name = "codex-utils-home-dir" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-absolute-path", "dirs", @@ -3962,7 +3966,7 @@ dependencies = [ [[package]] name = "codex-utils-image" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "base64 0.22.1", "codex-utils-cache", @@ -3974,7 +3978,7 @@ dependencies = [ [[package]] name = "codex-utils-json-to-toml" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "serde_json", @@ -3983,7 +3987,7 @@ dependencies = [ [[package]] name = "codex-utils-oss" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-core", "codex-lmstudio", @@ -3993,7 +3997,7 @@ dependencies = [ [[package]] name = "codex-utils-output-truncation" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-protocol", "codex-utils-string", @@ -4002,7 +4006,7 @@ dependencies = [ [[package]] name = "codex-utils-path" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-absolute-path", "dunce", @@ -4012,7 +4016,7 @@ dependencies = [ [[package]] name = "codex-utils-plugins" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-exec-server", "codex-login", @@ -4025,7 +4029,7 @@ dependencies = [ [[package]] name = "codex-utils-pty" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "filedescriptor", @@ -4041,7 +4045,7 @@ dependencies = [ [[package]] name = "codex-utils-readiness" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "assert_matches", "async-trait", @@ -4052,14 +4056,14 @@ dependencies = [ [[package]] name = "codex-utils-rustls-provider" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "rustls", ] [[package]] name = "codex-utils-sandbox-summary" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-core", "codex-model-provider-info", @@ -4070,7 +4074,7 @@ dependencies = [ [[package]] name = "codex-utils-sleep-inhibitor" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "core-foundation 0.9.4", "libc", @@ -4080,14 +4084,14 @@ dependencies = [ [[package]] name = "codex-utils-stream-parser" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", ] [[package]] name = "codex-utils-string" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "regex-lite", @@ -4097,14 +4101,14 @@ dependencies = [ [[package]] name = "codex-utils-template" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", ] [[package]] name = "codex-v8-poc" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "v8", @@ -4112,7 +4116,7 @@ dependencies = [ [[package]] name = "codex-windows-sandbox" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -4363,7 +4367,7 @@ dependencies = [ [[package]] name = "core_test_support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -5101,9 +5105,9 @@ dependencies = [ [[package]] name = "diplomat" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9adb46b05e2f53dcf6a7dfc242e4ce9eb60c369b6b6eb10826a01e93167f59c6" +checksum = "7935649d00000f5c5d735448ad3dc07b9738160727017914cf42138b8e8e6611" dependencies = [ "diplomat_core", "proc-macro2", @@ -5113,15 +5117,15 @@ dependencies = [ [[package]] name = "diplomat-runtime" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0569bd3caaf13829da7ee4e83dbf9197a0e1ecd72772da6d08f0b4c9285c8d29" +checksum = "970ac38ad677632efcee6d517e783958da9bc78ec206d8d5e35b459ffc5e4864" [[package]] name = "diplomat_core" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51731530ed7f2d4495019abc7df3744f53338e69e2863a6a64ae91821c763df1" +checksum = "9cf41b94101a4bce993febaf0098092b0bb31deaf0ecaf6e0a2562465f61b383" dependencies = [ "proc-macro2", "quote", @@ -5477,7 +5481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5665,9 +5669,9 @@ dependencies = [ [[package]] name = "fixed_decimal" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35eabf480f94d69182677e37571d3be065822acfafd12f2f085db44fbbcc8e57" +checksum = "79c3c892f121fff406e5dd6b28c1b30096b95111c30701a899d4f2b18da6d1bd" dependencies = [ "displaydoc", "smallvec", @@ -7510,9 +7514,9 @@ dependencies = [ [[package]] name = "icu_calendar" -version = "2.1.1" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e" +checksum = "a2b2acc6263f494f1df50685b53ff8e57869e47d5c6fe39c23d518ae9a4f3e45" dependencies = [ "calendrical_calculations", "displaydoc", @@ -7526,18 +7530,19 @@ dependencies = [ [[package]] name = "icu_calendar_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d" +checksum = "118577bcf3a0fa7c6ac0a7d6e951814da84ee56b9b1f68fb4d8d10b08cefaf4d" [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -7545,14 +7550,16 @@ dependencies = [ [[package]] name = "icu_decimal" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a38c52231bc348f9b982c1868a2af3195199623007ba2c7650f432038f5b3e8e" +checksum = "288247df2e32aa776ac54fdd64de552149ac43cb840f2761811f0e8d09719dd4" dependencies = [ + "displaydoc", "fixed_decimal", "icu_decimal_data", "icu_locale", "icu_locale_core", + "icu_plurals", "icu_provider", "writeable", "zerovec", @@ -7560,15 +7567,15 @@ dependencies = [ [[package]] name = "icu_decimal_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2905b4044eab2dd848fe84199f9195567b63ab3a93094711501363f63546fef7" +checksum = "6f14a5ca9e8af29eef62064f269078424283d90dbaffeac5225addf62aaabc22" [[package]] name = "icu_locale" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60" +checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" dependencies = [ "icu_collections", "icu_locale_core", @@ -7581,9 +7588,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -7595,15 +7602,15 @@ dependencies = [ [[package]] name = "icu_locale_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c5f1d16b4c3a2642d3a719f18f6b06070ab0aef246a6418130c955ae08aa831" +checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993" [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -7615,15 +7622,34 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_plurals" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a50023f1d49ad5c4333380328a0d4a19e4b9d6d842ec06639affd5ba47c8103" +dependencies = [ + "fixed_decimal", + "icu_locale", + "icu_plurals_data", + "icu_provider", + "zerovec", +] + +[[package]] +name = "icu_plurals_data" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "8485497155dc865f901decb93ecc20d3e467df67bfeceb91e3ba34e2b11e8e1d" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -7635,15 +7661,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -8540,7 +8566,7 @@ dependencies = [ [[package]] name = "mcp_test_support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-login", @@ -10898,9 +10924,9 @@ dependencies = [ [[package]] name = "resb" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a067ab3b5ca3b4dc307d0de9cf75f9f5e6ca9717b192b2f28a36c83e5de9e76" +checksum = "22d392791f3c6802a1905a509e9d1a6039cbbcb5e9e00e5a6d3661f7c874f390" dependencies = [ "potential_utf", "serde_core", @@ -12627,14 +12653,14 @@ dependencies = [ [[package]] name = "temporal_capi" -version = "0.1.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a151e402c2bdb6a3a2a2f3f225eddaead2e7ce7dd5d3fa2090deb11b17aa4ed8" +checksum = "8a2a1f001e756a9f5f2d175a9965c4c0b3a054f09f30de3a75ab49765f2deb36" dependencies = [ "diplomat", "diplomat-runtime", "icu_calendar", - "icu_locale", + "icu_locale_core", "num-traits", "temporal_rs", "timezone_provider", @@ -12644,13 +12670,14 @@ dependencies = [ [[package]] name = "temporal_rs" -version = "0.1.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88afde3bd75d2fc68d77a914bece426aa08aa7649ffd0cdd4a11c3d4d33474d1" +checksum = "9a902a45282e5175186b21d355efc92564601efe6e2d92818dc9e333d50bd4de" dependencies = [ + "calendrical_calculations", "core_maths", "icu_calendar", - "icu_locale", + "icu_locale_core", "ixdtf", "num-traits", "timezone_provider", @@ -12867,9 +12894,9 @@ dependencies = [ [[package]] name = "timezone_provider" -version = "0.1.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9ba0000e9e73862f3e7ca1ff159e2ddf915c9d8bb11e38a7874760f445d993" +checksum = "c48f9b04628a2b813051e4dfe97c65281e49625eabd09ec343190e31e399a8c2" dependencies = [ "tinystr", "zerotrie", @@ -12900,9 +12927,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "serde_core", @@ -13716,9 +13743,9 @@ dependencies = [ [[package]] name = "v8" -version = "146.9.0" +version = "147.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b21777080203cb33828681f177e53402fa2927a1e2d17adead072b0c26b401" +checksum = "2df8fffd507fb18ed000673a83d937f58e60fb07f3306b2274284125b15137cd" dependencies = [ "bindgen", "bitflags 2.10.0", @@ -14868,9 +14895,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -14879,9 +14906,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -15014,20 +15041,21 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "serde", "yoke", @@ -15037,9 +15065,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -15110,9 +15138,9 @@ checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" [[package]] name = "zoneinfo64" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2e5597efbe7c421da8a7fd396b20b571704e787c21a272eecf35dfe9d386f0" +checksum = "ed6eb2607e906160c457fd573e9297e65029669906b9ac8fb1b5cd5e055f0705" dependencies = [ "calendrical_calculations", "icu_locale_core", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 319f61b72be6..af11ae99099f 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -415,7 +415,7 @@ unicode-width = "0.2" url = "2" urlencoding = "2.1" uuid = "1" -v8 = "=146.9.0" +v8 = "=147.4.0" vt100 = "0.16.2" walkdir = "2.5.0" webbrowser = "1.0" From 14d5179319a2399e0140b45073ec55e6536d9953 Mon Sep 17 00:00:00 2001 From: Shijie Rao Date: Tue, 19 May 2026 20:16:15 -0700 Subject: [PATCH 06/25] Release 0.133.0-alpha.1 --- codex-rs/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index ec38a87cff97..91b73d562b5d 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -117,7 +117,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.0.0" +version = "0.133.0-alpha.1" # Track the edition for all workspace crates in one place. Individual # crates can still override this value, but keeping it here means new # crates created with `cargo new -w ...` automatically inherit the 2024 From 33bef1d7c0a719436652af6cf3afd607743042d5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 07:12:26 +0000 Subject: [PATCH 07/25] Seed Termux release automation --- .github/workflows/rust-release.yml | 1710 +++++++---------- .github/workflows/shell-tool-mcp.yml | 461 +++++ .../workflows/termux-release-checkpoint.yml | 103 + .github/workflows/termux-release-deploy.yml | 243 +++ .github/workflows/termux-release-promote.yml | 135 ++ scripts/termux-configure-git.sh | 40 + scripts/termux-create-checkpoint-pr.sh | 275 +++ ...ermux-create-or-update-mirrored-release.sh | 111 ++ scripts/termux-download-release-artifact.sh | 78 + scripts/termux-find-release-pr.sh | 53 + scripts/termux-read-release-metadata.sh | 77 + scripts/termux-release-asset-state.sh | 37 + scripts/termux-release-paths.sh | 55 + scripts/termux-resolve-release-ref.sh | 36 + scripts/termux-validate-gh-env.sh | 16 + 15 files changed, 2387 insertions(+), 1043 deletions(-) create mode 100644 .github/workflows/shell-tool-mcp.yml create mode 100644 .github/workflows/termux-release-checkpoint.yml create mode 100644 .github/workflows/termux-release-deploy.yml create mode 100644 .github/workflows/termux-release-promote.yml create mode 100755 scripts/termux-configure-git.sh create mode 100755 scripts/termux-create-checkpoint-pr.sh create mode 100755 scripts/termux-create-or-update-mirrored-release.sh create mode 100755 scripts/termux-download-release-artifact.sh create mode 100755 scripts/termux-find-release-pr.sh create mode 100755 scripts/termux-read-release-metadata.sh create mode 100755 scripts/termux-release-asset-state.sh create mode 100755 scripts/termux-release-paths.sh create mode 100755 scripts/termux-resolve-release-ref.sh create mode 100755 scripts/termux-validate-gh-env.sh diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index b6c293d6cdc2..248da7df3b7c 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -4,46 +4,49 @@ # git tag -a rust-v0.1.0 -m "Release 0.1.0" # git push origin rust-v0.1.0 # ``` -# -# To use external macOS signing, manually dispatch `release_mode=build_unsigned`, -# sign the unsigned macOS artifacts in a secure enclave, upload the signed handoff -# archive as a GitHub Release asset, then manually dispatch -# `release_mode=promote_signed` with `unsigned_run_id` and `signed_macos_asset`. -# The signed handoff archive should contain target or artifact directories such -# as `aarch64-apple-darwin/` with signed binaries. name: rust-release on: - push: - tags: - - "rust-v*.*.*" + pull_request: + branches: + - "release/**" workflow_dispatch: inputs: - release_mode: - description: "build_unsigned creates unsigned macOS handoff artifacts; promote_signed finishes a release from signed macOS handoff artifacts." + source_ref: + description: "Branch, tag, or commit SHA to build (code only; keeps .github from dispatched ref)" required: false + type: string + default: "" + build_target: + description: "Target to build" + required: true type: choice - default: build_unsigned + default: all options: - - build_unsigned - - promote_signed - sign_macos: - description: "Deprecated compatibility input; use release_mode instead." - required: false - type: boolean - default: false - unsigned_run_id: - description: "For promote_signed: workflow run id from the build_unsigned run." - required: false - type: string - signed_macos_asset: - description: "For promote_signed: exact GitHub Release asset name containing signed macOS handoff artifacts." - required: false - type: string - signed_macos_sha256: - description: "For promote_signed: optional SHA-256 of signed_macos_asset." - required: false - type: string + - all + - aarch64-apple-darwin + - x86_64-apple-darwin + - x86_64-unknown-linux-musl + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-musl + - aarch64-unknown-linux-gnu + - aarch64-linux-android + - x86_64-pc-windows-msvc + - aarch64-pc-windows-msvc + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read concurrency: group: ${{ github.workflow }} @@ -51,72 +54,26 @@ concurrency: jobs: tag-check: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - uses: dtolnay/rust-toolchain@1.92 - name: Validate tag matches Cargo.toml version shell: bash - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - REQUESTED_SIGN_MACOS: ${{ inputs.sign_macos }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} run: | set -euo pipefail echo "::group::Tag validation" - case "${RELEASE_MODE}" in - signed) - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - echo "❌ Manual rust-release runs must use release_mode=build_unsigned or release_mode=promote_signed" - exit 1 - fi - ;; - build_unsigned) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=build_unsigned is only valid for manual runs" - exit 1 - fi - ;; - promote_signed) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=promote_signed is only valid for manual runs" - exit 1 - fi - if [[ ! "${UNSIGNED_RUN_ID}" =~ ^[0-9]+$ ]]; then - echo "❌ release_mode=promote_signed requires unsigned_run_id to be a workflow run id" - exit 1 - fi - if [[ -z "${SIGNED_MACOS_ASSET}" ]]; then - echo "❌ release_mode=promote_signed requires signed_macos_asset" - exit 1 - fi - if [[ "${SIGNED_MACOS_ASSET}" == */* || "${SIGNED_MACOS_ASSET}" == *"*"* || "${SIGNED_MACOS_ASSET}" == *"?"* || "${SIGNED_MACOS_ASSET}" == *"["* ]]; then - echo "❌ signed_macos_asset must be an exact release asset name, not a path or glob" - exit 1 - fi - if [[ "${UNSIGNED_RUN_ID}" == "${GITHUB_RUN_ID}" ]]; then - echo "❌ unsigned_run_id must refer to the earlier build_unsigned run, not this run" - exit 1 - fi - ;; - *) - echo "❌ Unknown release_mode '${RELEASE_MODE}'" - exit 1 - ;; - esac - - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${REQUESTED_SIGN_MACOS}" == "true" ]]; then - echo "::warning title=Deprecated sign_macos input ignored::Use release_mode=build_unsigned or release_mode=promote_signed instead." - fi - # 1. Must be a tag and match the regex [[ "${GITHUB_REF_TYPE}" == "tag" ]] \ || { echo "❌ Not a tag push"; exit 1; } - [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \ + [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?(-termux)?$ ]] \ || { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; } # 2. Extract versions @@ -131,115 +88,198 @@ jobs: echo "✅ Tag and Cargo.toml agree (${tag_ver})" echo "::endgroup::" - build: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} + select-build-matrix: + if: always() && (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') needs: tag-check - name: Build - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: ${{ matrix.runs_on || matrix.runner }} - # Release builds can take a long time, so leave some headroom to avoid - # having to restart the full workflow due to a timeout. - timeout-minutes: 90 - permissions: - contents: read - id-token: write + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.select.outputs.matrix }} + build_version: ${{ steps.select.outputs.build_version }} + steps: + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - id: select + shell: bash + env: + REPO_OWNER: ${{ github.repository_owner }} + EVENT_NAME: ${{ github.event_name }} + BUILD_TARGET: ${{ inputs.build_target }} + run: | + set -euo pipefail + + matrix='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04","target":"aarch64-linux-android"}, + {"runner":"windows-latest","target":"x86_64-pc-windows-msvc"}, + {"runner":"windows-11-arm","target":"aarch64-pc-windows-msvc"} + ]' + + if [[ "${REPO_OWNER}" != "openai" ]]; then + matrix="$(jq -c '[.[] | select(.runner | startswith("macos") | not)]' <<< "${matrix}")" + fi + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + matrix="$(jq -c '[.[] | select(.target == "aarch64-linux-android")]' <<< "${matrix}")" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" && -n "${BUILD_TARGET}" && "${BUILD_TARGET}" != "all" ]]; then + matrix="$(jq -c --arg build_target "${BUILD_TARGET}" '[.[] | select(.target == $build_target)]' <<< "${matrix}")" + fi + + if [[ "$(jq 'length' <<< "${matrix}")" -eq 0 ]]; then + echo "No build targets selected after applying owner/dispatch filters." >&2 + exit 1 + fi + + echo "matrix={\"include\":${matrix}}" >> "$GITHUB_OUTPUT" + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + metadata=".github/termux-release.json" + if [[ ! -f "${metadata}" ]]; then + echo "${metadata} is required for release train PR builds" >&2 + exit 1 + fi + upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" + if [[ -z "${upstream_tag}" || "${upstream_tag}" != rust-v* ]]; then + echo "Unable to determine upstream rust tag from ${metadata}" >&2 + exit 1 + fi + build_version="${upstream_tag#rust-v}" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then + latest_release_tag="$( + GH_TOKEN="${{ github.token }}" gh api repos/openai/codex/releases/latest --jq '.tag_name' + )" + + if [[ -z "${latest_release_tag}" || "${latest_release_tag}" != rust-v* ]]; then + echo "Unable to determine latest stable release tag from openai/codex" >&2 + exit 1 + fi + + latest_release_version="${latest_release_tag#rust-v}" + build_version="${latest_release_version}-dev" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + fi + + build: + if: >- + always() && + (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') && + needs.select-build-matrix.result == 'success' + needs: + - tag-check + - select-build-matrix + name: Build - ${{ matrix.runner }} - ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + permissions: write-all + timeout-minutes: 120 defaults: run: working-directory: codex-rs env: - # 2026-03-04: temporarily change releases to use thin LTO because - # Ubuntu ARM is timing out at 60 minutes. - CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' }} + CODEX_BWRAP_ENABLE_FFI: ${{ contains(matrix.target, 'unknown-linux') && '1' || '0' }} + CODEX_BUILD_VERSION: ${{ needs.select-build-matrix.outputs.build_version }} + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" strategy: fail-fast: false - matrix: - include: - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - # Release artifacts intentionally ship MUSL-linked Linux binaries. - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - bundle: primary - artifact_name: x86_64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - bundle: app-server - artifact_name: x86_64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - bundle: primary - artifact_name: aarch64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - bundle: app-server - artifact_name: aarch64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" + matrix: ${{ fromJson(needs.select-build-matrix.outputs.matrix) }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main with: - persist-credentials: false - - name: Print runner specs (Linux) - if: ${{ runner.os == 'Linux' }} + verbose: true + - name: Checkout selected source ref + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + uses: actions/checkout@v6 + with: + ref: ${{ inputs.source_ref }} + path: __source__ + fetch-depth: 1 + - name: Overlay selected source ref (excluding .github) + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} shell: bash + working-directory: ${{ github.workspace }} run: | set -euo pipefail - cpu_model="$(lscpu | awk -F: '/Model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}')" - total_ram="$(awk '/MemTotal/ {printf "%.1f GiB\n", $2 / 1024 / 1024}' /proc/meminfo)" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(uname -a)" - echo "CPU model: ${cpu_model}" - echo "Logical CPUs: $(nproc)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . - - name: Print runner specs (macOS) - if: ${{ runner.os == 'macOS' }} + echo "Using source ref: ${{ inputs.source_ref }}" + + # Keep workflow/actions from the dispatch ref while replacing all other files. + find . -mindepth 1 -maxdepth 1 \ + ! -name .git \ + ! -name .github \ + ! -name __source__ \ + -exec rm -rf -- {} + + tar --exclude='.github' -C __source__ -cf - . | tar -xf - + rm -rf __source__ + + - name: Apply selected build version + if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} shell: bash + working-directory: ${{ github.workspace }} run: | set -euo pipefail - total_ram="$(sysctl -n hw.memsize | awk '{printf "%.1f GiB\n", $1 / 1024 / 1024 / 1024}')" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(sw_vers -productName) $(sw_vers -productVersion)" - echo "Hardware model: $(sysctl -n hw.model)" - echo "CPU architecture: $(uname -m)" - echo "Logical CPUs: $(sysctl -n hw.logicalcpu)" - echo "Physical CPUs: $(sysctl -n hw.physicalcpu)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . + if [[ -z "${CODEX_BUILD_VERSION}" ]]; then + echo "CODEX_BUILD_VERSION is empty for workflow_dispatch" >&2 + exit 1 + fi + echo "Using build version: ${CODEX_BUILD_VERSION}" + BUILD_VERSION="${CODEX_BUILD_VERSION}" perl -0777 -i -pe \ + 's/(\[workspace\.package\][^\[]*?version = ")([^"]+)(")/${1}$ENV{BUILD_VERSION}$3/s' \ + codex-rs/Cargo.toml + + grep -n "^\[workspace.package\]\|^version = " codex-rs/Cargo.toml | head -n 4 + + - name: Select Android NDK + if: ${{ matrix.target == 'aarch64-linux-android' }} + shell: bash + run: | + set -euo pipefail + + fallback_version="29.0.13113456" + ndk_root="${ANDROID_HOME}/ndk" + selected_ndk="" + + if [[ -d "${ndk_root}" ]]; then + while IFS= read -r candidate; do + toolchain="${candidate}/toolchains/llvm/prebuilt/linux-x86_64" + if [[ -x "${toolchain}/bin/aarch64-linux-android24-clang" ]]; then + selected_ndk="${candidate}" + break + fi + done < <(find "${ndk_root}" -mindepth 1 -maxdepth 1 -type d | sort -Vr) + fi + + if [[ -z "${selected_ndk}" ]]; then + echo "No usable preinstalled Android NDK found; installing ${fallback_version}" + yes | sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses || true + sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" "ndk;${fallback_version}" + selected_ndk="${ndk_root}/${fallback_version}" + fi + + if [[ ! -x "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" ]]; then + echo "Selected Android NDK does not contain a usable aarch64 API 24 clang: ${selected_ndk}" >&2 + exit 1 + fi + + echo "Selected Android NDK: ${selected_ndk}" + "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" --version + echo "ANDROID_NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" + echo "NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" - name: Install Linux bwrap build dependencies - if: ${{ runner.os == 'Linux' }} + if: ${{ runner.os == 'Linux' && matrix.target != 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail @@ -254,9 +294,173 @@ jobs: sudo apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 fi - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@1.93 with: targets: ${{ matrix.target }} + - name: Set up sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + uses: mozilla-actions/sccache-action@main + - name: Enable sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + shell: bash + run: | + set -euo pipefail + echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_VERSION=android-release-${{ matrix.target }}" >> "$GITHUB_ENV" + echo "SCCACHE_CACHE_SIZE=5G" >> "$GITHUB_ENV" + - name: Ensure Rust target is installed + shell: bash + run: | + set -euo pipefail + rustup target add "${{ matrix.target }}" + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android build environment + shell: bash + run: | + set -euo pipefail + ndk="${ANDROID_NDK_HOME}" + target="${{ matrix.target }}" + + # Set up the Android toolchain + toolchain="${ndk}/toolchains/llvm/prebuilt/linux-x86_64" + + # Use API level 24 to follow termux-build convention + api_level="24" + + # Configure Cargo to use the NDK linker + cargo_target_var="CARGO_TARGET_${target^^}_LINKER" + cargo_target_var="${cargo_target_var//-/_}" + echo "${cargo_target_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + # Set CC and AR for the target + target_cc_var="CC_${target}" + target_cc_var="${target_cc_var//-/_}" + echo "${target_cc_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + target_cxx_var="CXX_${target}" + target_cxx_var="${target_cxx_var//-/_}" + echo "${target_cxx_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang++" >> "$GITHUB_ENV" + + target_ar_var="AR_${target}" + target_ar_var="${target_ar_var//-/_}" + echo "${target_ar_var}=${toolchain}/bin/llvm-ar" >> "$GITHUB_ENV" + + # Add toolchain to PATH + echo "${toolchain}/bin" >> "$GITHUB_PATH" + + tls_align_source="${RUNNER_TEMP}/codex-android-tls-align.S" + tls_align_object="${RUNNER_TEMP}/codex-android-tls-align.o" + cat > "${tls_align_source}" <<'EOF' + .section .tdata.codex_android_tls_align,"awT",%progbits + .p2align 6 + .globl __codex_android_tls_align + .hidden __codex_android_tls_align + .type __codex_android_tls_align, %object + __codex_android_tls_align: + .byte 0 + .size __codex_android_tls_align, 1 + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${tls_align_source}" -o "${tls_align_object}" + + v8_compat_source="${RUNNER_TEMP}/codex-android-v8-compat.c" + v8_compat_object="${RUNNER_TEMP}/codex-android-v8-compat.o" + cat > "${v8_compat_source}" <<'EOF' + #include + #include + + extern int posix_memalign(void **memptr, size_t alignment, size_t size); + extern double strtod(const char *nptr, char **endptr); + extern float strtof(const char *nptr, char **endptr); + + void *aligned_alloc(size_t alignment, size_t size) { + void *ptr = 0; + if (posix_memalign(&ptr, alignment, size) != 0) { + return 0; + } + return ptr; + } + + double strtod_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtod(nptr, endptr); + } + + float strtof_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtof(nptr, endptr); + } + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${v8_compat_source}" -o "${v8_compat_object}" + + builtins_archive="$( + find "${toolchain}/lib/clang" \ + -path "*/lib/linux/libclang_rt.builtins-aarch64-android.a" \ + -print -quit + )" + if [[ -z "${builtins_archive}" ]]; then + echo "Could not find Android compiler-rt builtins archive under ${toolchain}/lib/clang" >&2 + exit 1 + fi + builtins_dir="$(dirname "${builtins_archive}")" + + # Provide compatibility symbols and C++ ABI needed by the Android rusty_v8 archive. + rustflags_var="CARGO_TARGET_${target^^}_RUSTFLAGS" + rustflags_var="${rustflags_var//-/_}" + echo "${rustflags_var}=-C link-arg=${tls_align_object} -C link-arg=${v8_compat_object} -C link-arg=-Wl,-u,__codex_android_tls_align -C link-arg=-L${builtins_dir} -C link-arg=-lclang_rt.builtins-aarch64-android -C link-arg=-lc++abi -C link-arg=-Wl,-z,max-page-size=65536" >> "$GITHUB_ENV" + + + # Configure for vendored OpenSSL build + echo "CARGO_BUILD_TARGET_APPLIES_TO_HOST=false" >> "$GITHUB_ENV" + echo "CARGO_BUILD_TARGET=${{ matrix.target }}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android rusty_v8 artifact overrides + env: + GH_TOKEN: ${{ github.token }} + TARGET: ${{ matrix.target }} + shell: bash + run: | + set -euo pipefail + binding_dir="${RUNNER_TEMP}/rusty_v8" + archive="${binding_dir}/librusty_v8_release_${TARGET}.a.gz" + binding_path="${binding_dir}/src_binding_release_${TARGET}.rs" + checksums_path="${binding_dir}/rusty_v8_release_${TARGET}.sha256" + artifact_repository="wallentx/codex-termux" + release_tag="rusty-v8-v147.4.0" + mkdir -p "${binding_dir}" + echo "Downloading Android rusty_v8 artifacts from ${artifact_repository}@${release_tag}" + gh release download "${release_tag}" \ + --repo "${artifact_repository}" \ + --dir "${binding_dir}" \ + --pattern "librusty_v8_release_${TARGET}.a.gz" \ + --pattern "src_binding_release_${TARGET}.rs" \ + --pattern "rusty_v8_release_${TARGET}.sha256" + (cd "${binding_dir}" && sha256sum -c "$(basename "${checksums_path}")") + echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV" + echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Install termux-elf-cleaner + shell: bash + run: | + set -euo pipefail + version="v3.0.1" + src_dir="${RUNNER_TEMP}/termux-elf-cleaner-src" + build_dir="${RUNNER_TEMP}/termux-elf-cleaner-build" + bin_dir="${RUNNER_TEMP}/termux-elf-cleaner-bin" + + rm -rf "${src_dir}" "${build_dir}" "${bin_dir}" + mkdir -p "${src_dir}" "${build_dir}" "${bin_dir}" + + curl -fsSL "https://github.com/termux/termux-elf-cleaner/archive/refs/tags/${version}.tar.gz" \ + | tar -xzf - --strip-components=1 -C "${src_dir}" + + cmake -S "${src_dir}" -B "${build_dir}" -DCMAKE_BUILD_TYPE=Release + cmake --build "${build_dir}" --parallel + + install "${build_dir}/termux-elf-cleaner" "${bin_dir}/termux-elf-cleaner" + echo "${bin_dir}" >> "$GITHUB_PATH" - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Use hermetic Cargo home (musl) @@ -269,12 +473,25 @@ jobs: echo "${cargo_home}/bin" >> "$GITHUB_PATH" : > "${cargo_home}/config.toml" + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ${{ github.workspace }}/.cargo-home/bin/ + ${{ github.workspace }}/.cargo-home/registry/index/ + ${{ github.workspace }}/.cargo-home/registry/cache/ + ${{ github.workspace }}/.cargo-home/git/db/ + ${{ github.workspace }}/codex-rs/target/ + key: cargo-${{ matrix.runner }}-${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }} + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install Zig - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + uses: mlugg/setup-zig@v2 with: version: 0.14.0 - use-cache: false - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install musl build tools @@ -310,12 +527,6 @@ jobs: shell: bash run: | set -euo pipefail - # Avoid problematic aws-lc jitter entropy code path on musl builders. - echo "AWS_LC_SYS_NO_JITTER_ENTROPY=1" >> "$GITHUB_ENV" - target_no_jitter="AWS_LC_SYS_NO_JITTER_ENTROPY_${{ matrix.target }}" - target_no_jitter="${target_no_jitter//-/_}" - echo "${target_no_jitter}=1" >> "$GITHUB_ENV" - # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. echo "RUSTFLAGS=" >> "$GITHUB_ENV" echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" @@ -340,94 +551,70 @@ jobs: echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" - - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }} - name: Configure musl rusty_v8 artifact overrides and verify checksums - uses: ./.github/actions/setup-rusty-v8-musl - with: - target: ${{ matrix.target }} - - - if: ${{ contains(matrix.target, 'linux') && matrix.bundle == 'primary' }} - name: Build bwrap and export digest + - name: Cargo build shell: bash run: | set -euo pipefail - target="${{ matrix.target }}" - cargo build --target "$target" --release --timings --bin bwrap - bwrap_path="target/${target}/release/bwrap" - if [[ ! -f "$bwrap_path" ]]; then - echo "bwrap binary ${bwrap_path} not found" - exit 1 + if [[ "${{ matrix.target }}" == 'aarch64-linux-android' ]]; then + export CARGO_BUILD_JOBS=4 + export CARGO_PROFILE_RELEASE_LTO=thin + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 + cargo build --target ${{ matrix.target }} --release --bin codex + exit 0 fi - digest="$(sha256sum "$bwrap_path" | awk '{print $1}')" - echo "CODEX_BWRAP_SHA256=${digest}" >> "$GITHUB_ENV" - echo "Built bwrap ${bwrap_path} with sha256:${digest}" - - - name: Cargo build + if [[ "${{ contains(matrix.target, 'windows') }}" == 'true' ]]; then + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy --bin codex-windows-sandbox-setup --bin codex-command-runner + else + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy + fi + - name: sccache stats (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash - run: | - build_args=() - for binary in ${{ matrix.binaries }}; do - build_args+=(--bin "$binary") - done - echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}" - cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}" + run: ${SCCACHE_PATH:-sccache} --show-stats || true - - name: Upload Cargo timings - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }} - path: codex-rs/target/**/cargo-timings/cargo-timing.html - if-no-files-found: warn - - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Stage unsigned macOS artifacts + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Normalize Android ELF for Termux shell: bash run: | set -euo pipefail - - target="${{ matrix.target }}" - release_dir="target/${target}/release" - dest="unsigned-dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - unsigned_name="${binary}-${target}-unsigned" - unsigned_path="${dest}/${unsigned_name}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 + for binary in codex codex-responses-api-proxy; do + binary_path="target/${{ matrix.target }}/release/${binary}" + if [[ -f "${binary_path}" ]]; then + termux-elf-cleaner --api-level 24 "${binary_path}" + chmod +x "${binary_path}" + # if [[ "${binary}" == "codex" ]]; then + # # Patch PT_TLS (0x7) to PT_NULL (0x0) at offset 400 to bypass Bionic alignment checks. + # printf '\x00\x00\x00\x00' | dd of="${binary_path}" bs=1 seek=400 count=4 conv=notrunc + # fi fi - - cp "${binary_path}" "${unsigned_path}" - tar -C "$dest" -czf "${unsigned_path}.tar.gz" "${unsigned_name}" - zstd -T0 -19 --rm "${unsigned_path}" done - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Upload unsigned macOS artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }}-unsigned - path: codex-rs/unsigned-dist/${{ matrix.target }}/* - if-no-files-found: error - - - if: ${{ contains(matrix.target, 'linux') }} + - if: ${{ contains(matrix.target, 'linux') && !contains(matrix.target, 'android') && github.repository_owner == 'openai' }} name: Cosign Linux artifacts uses: ./.github/actions/linux-code-sign with: target: ${{ matrix.target }} artifacts-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release - binaries: ${{ matrix.binaries }} - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS == 'true' }} + - if: ${{ contains(matrix.target, 'windows') && github.repository_owner == 'openai' }} + name: Sign Windows binaries with Azure Trusted Signing + uses: ./.github/actions/windows-code-sign + with: + target: ${{ matrix.target }} + client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID }} + endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} + account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} + + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (binaries) uses: ./.github/actions/macos-code-sign with: target: ${{ matrix.target }} - binaries: ${{ matrix.binaries }} sign-binaries: "true" sign-dmg: "false" apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }} @@ -436,7 +623,7 @@ jobs: apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: Build macOS dmg shell: bash run: | @@ -451,17 +638,23 @@ jobs: # The previous "MacOS code signing (binaries)" step signs + notarizes the # built artifacts in `${release_dir}`. This step packages *those same* # signed binaries into a dmg. + codex_binary_path="${release_dir}/codex" + proxy_binary_path="${release_dir}/codex-responses-api-proxy" + rm -rf "$dmg_root" mkdir -p "$dmg_root" - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 - fi - ditto "${binary_path}" "${dmg_root}/${binary}" - done + if [[ ! -f "$codex_binary_path" ]]; then + echo "Binary $codex_binary_path not found" + exit 1 + fi + if [[ ! -f "$proxy_binary_path" ]]; then + echo "Binary $proxy_binary_path not found" + exit 1 + fi + + ditto "$codex_binary_path" "${dmg_root}/codex" + ditto "$proxy_binary_path" "${dmg_root}/codex-responses-api-proxy" rm -f "$dmg_path" hdiutil create \ @@ -476,7 +669,7 @@ jobs: exit 1 fi - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (dmg) uses: ./.github/actions/macos-code-sign with: @@ -490,107 +683,65 @@ jobs: apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - name: Stage artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | dest="dist/${{ matrix.target }}" mkdir -p "$dest" - for binary in ${{ matrix.binaries }}; do - cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" - if [[ "${{ matrix.target }}" == *linux* ]]; then - cp "target/${{ matrix.target }}/release/${binary}.sigstore" \ - "$dest/${binary}-${{ matrix.target }}.sigstore" + if [[ "${{ matrix.runner }}" == windows* ]]; then + cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe" + else + # For Android, we want the binary to be named just 'codex' in the archive. + if [[ "${{ matrix.target }}" == "aarch64-linux-android" ]]; then + cp target/${{ matrix.target }}/release/codex "$dest/codex" + else + cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" + fi + if [[ -f target/${{ matrix.target }}/release/codex-responses-api-proxy ]]; then + cp target/${{ matrix.target }}/release/codex-responses-api-proxy "$dest/codex-responses-api-proxy-${{ matrix.target }}" fi - done - - if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.bundle }}" == "primary" ]]; then - bundle_root="${RUNNER_TEMP}/codex-${{ matrix.target }}-bundle" - rm -rf "$bundle_root" - mkdir -p "$bundle_root/codex-resources" - cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex" - cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap" - chmod 0755 "$bundle_root/codex" "$bundle_root/codex-resources/bwrap" - tar -C "$bundle_root" -cf - codex codex-resources/bwrap | - zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst" fi - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" + if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.target }}" != *android* ]]; then + cp target/${{ matrix.target }}/release/codex.sigstore "$dest/codex-${{ matrix.target }}.sigstore" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.sigstore "$dest/codex-responses-api-proxy-${{ matrix.target }}.sigstore" fi - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - aarch64-unknown-linux-musl) - platform_tag="musllinux_1_1_aarch64" - ;; - x86_64-unknown-linux-musl) - platform_tag="musllinux_1_1_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - # Do not install into the runner's system Python; macOS runners mark - # the Homebrew Python as externally managed under PEP 668. - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - stage_runtime_args=( - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" - stage-runtime - "$stage_dir" - "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/codex" - --codex-version "${GITHUB_REF_NAME}" - --platform-tag "$platform_tag" - ) - if [[ "${{ matrix.target }}" == *linux* ]]; then - # Keep bwrap in the runtime wheel so Linux sandbox fallback behavior - # matches the standalone release bundle on hosts without system bwrap. - stage_runtime_args+=( - --resource-binary - "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/bwrap" - ) + if [[ "${{ matrix.target }}" == *apple-darwin ]]; then + cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" fi - python3 "${stage_runtime_args[@]}" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error + - if: ${{ matrix.runner == 'windows-11-arm' }} + name: Install zstd + shell: powershell + run: choco install -y zstandard - name: Compress artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | # Path that contains the uncompressed binaries for the current # ${{ matrix.target }} dest="dist/${{ matrix.target }}" + repo_root=$PWD + + # We want to ship the raw Windows executables in the GitHub Release + # in addition to the compressed archives. Keep the originals for + # Windows targets; remove them elsewhere to limit the number of + # artifacts that end up in the GitHub Release. + keep_originals=false + if [[ "${{ matrix.runner }}" == windows* ]]; then + keep_originals=true + fi # For compatibility with environments that lack the `zstd` tool we - # additionally create a `.tar.gz` alongside every binary we publish. - # The end result is: + # additionally create a `.tar.gz` for all platforms and `.zip` for + # Windows alongside every single binary that we publish. The end result is: # codex-.zst (existing) # codex-.tar.gz (new) + # codex-.zip (only for Windows) # 1. Produce a .tar.gz for every file in the directory *before* we # run `zstd --rm`, because that flag deletes the original files. @@ -598,7 +749,7 @@ jobs: base="$(basename "$f")" # Skip files that are already archives (shouldn't happen, but be # safe). - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then + if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then continue fi @@ -608,330 +759,180 @@ jobs: fi # Create per-binary tar.gz - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - - # Also create .zst and remove the uncompressed binaries to keep - # non-Windows artifact directories small. - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - with: - name: ${{ matrix.artifact_name }} - # Upload the per-binary .zst files, .tar.gz equivalents, and any - # prebuilt archives staged above. - path: | - codex-rs/dist/${{ matrix.target }}/* - - stage-signed-macos: - if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode == 'promote_signed' }} - needs: tag-check - name: Stage signed macOS handoff - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: macos-15-xlarge - timeout-minutes: 30 - permissions: - contents: read - defaults: - run: - working-directory: codex-rs - - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Download signed macOS handoff - shell: bash - env: - GH_TOKEN: ${{ github.token }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - SIGNED_MACOS_SHA256: ${{ inputs.signed_macos_sha256 }} - run: | - set -euo pipefail - - download_dir="${RUNNER_TEMP}/signed-macos-download" - handoff_dir="${RUNNER_TEMP}/signed-macos-handoff" - rm -rf "$download_dir" "$handoff_dir" - mkdir -p "$download_dir" "$handoff_dir" - - gh release download "$GITHUB_REF_NAME" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern "$SIGNED_MACOS_ASSET" \ - --dir "$download_dir" - - asset_count="$(find "$download_dir" -maxdepth 1 -type f | wc -l | tr -d '[:space:]')" - if [[ "$asset_count" != "1" ]]; then - echo "Expected exactly one signed macOS handoff asset named ${SIGNED_MACOS_ASSET}; found ${asset_count}" - find "$download_dir" -maxdepth 1 -type f -print - exit 1 - fi - - asset_path="$(find "$download_dir" -maxdepth 1 -type f -print -quit)" - if [[ -n "${SIGNED_MACOS_SHA256}" ]]; then - expected_sha="$(printf '%s' "$SIGNED_MACOS_SHA256" | tr '[:upper:]' '[:lower:]')" - actual_sha="$(shasum -a 256 "$asset_path" | awk '{print $1}')" - if [[ "$actual_sha" != "$expected_sha" ]]; then - echo "signed_macos_sha256 mismatch for ${SIGNED_MACOS_ASSET}" - echo "expected: ${expected_sha}" - echo "actual: ${actual_sha}" - exit 1 - fi - fi - - asset_name="$(basename "$asset_path")" - case "$asset_name" in - *.tar.zst) - zstd -dc "$asset_path" | tar -C "$handoff_dir" -xf - - ;; - *.tar.gz|*.tgz) - tar -C "$handoff_dir" -xzf "$asset_path" - ;; - *.zip) - ditto -x -k "$asset_path" "$handoff_dir" - ;; - *) - echo "Unsupported signed macOS handoff archive format: ${asset_name}" - exit 1 - ;; - esac - - echo "SIGNED_MACOS_HANDOFF_DIR=$handoff_dir" >> "$GITHUB_ENV" - - - name: Stage signed macOS artifacts - shell: bash - run: | - set -euo pipefail - - target="${{ matrix.target }}" - artifact_name="${{ matrix.artifact_name }}" - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${target}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - fi - if [[ ! -d "$source_dir" ]]; then - echo "Signed macOS handoff is missing ${artifact_name}/" - echo "Expected either:" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${target}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - find "$SIGNED_MACOS_HANDOFF_DIR" -maxdepth 3 -type f -print - exit 1 - fi - - dest="dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - source_path="${source_dir}/${binary}" - if [[ ! -f "$source_path" ]]; then - source_path="${source_dir}/${binary}-${target}" + # For Android, we want the archive to have the target suffix even though the binary is just 'codex'. + archive_name="${base}" + if [[ "${{ matrix.target }}" == "aarch64-linux-android" && "${base}" == "codex" ]]; then + archive_name="codex-${{ matrix.target }}" fi - if [[ ! -f "$source_path" ]]; then - echo "Signed macOS handoff is missing ${binary} for ${artifact_name}" - exit 1 + tar -C "$dest" -czf "$dest/${archive_name}.tar.gz" "$base" + + # Create zip archive for Windows binaries + # Must run from inside the dest dir so 7z won't + # embed the directory path inside the zip. + if [[ "${{ matrix.runner }}" == windows* ]]; then + if [[ "$base" == "codex-${{ matrix.target }}.exe" ]]; then + # Bundle the sandbox helper binaries into the main codex zip so + # WinGet installs include the required helpers next to codex.exe. + # Fall back to the single-binary zip if the helpers are missing + # to avoid breaking releases. + bundle_dir="$(mktemp -d)" + runner_src="$dest/codex-command-runner-${{ matrix.target }}.exe" + setup_src="$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + if [[ -f "$runner_src" && -f "$setup_src" ]]; then + cp "$dest/$base" "$bundle_dir/$base" + cp "$runner_src" "$bundle_dir/codex-command-runner.exe" + cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" + # Use an absolute path so bundle zips land in the real dist + # dir even when 7z runs from a temp directory. + (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) + else + echo "warning: missing sandbox binaries; falling back to single-binary zip" + echo "warning: expected $runner_src and $setup_src" + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + rm -rf "$bundle_dir" + else + (cd "$dest" && 7z a "${base}.zip" "$base") + fi fi - release_path="${dest}/${binary}-${target}" - ditto "$source_path" "$release_path" - chmod 0755 "$release_path" - codesign --verify --strict --verbose=2 "$release_path" - done - - # DMG staging is disabled for signed promotion because we no longer - # distribute DMGs from this release path. Keep the branch here so the - # handoff can opt back in by flipping matrix.build_dmg if needed. - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - dmg_name="codex-${target}.dmg" - dmg_source="${source_dir}/${dmg_name}" - if [[ ! -f "$dmg_source" ]]; then - echo "Signed macOS handoff is missing ${dmg_name} for ${artifact_name}" - exit 1 + # Also create .zst (existing behaviour) *and* remove the original + # uncompressed binary to keep the directory small. + zstd_args=(-T0 -19) + if [[ "${keep_originals}" == false ]]; then + zstd_args+=(--rm) fi - codesign --verify --strict --verbose=2 "$dmg_source" - xcrun stapler validate "$dmg_source" - cp "$dmg_source" "$dest/$dmg_name" - fi + if [[ "${archive_name}" != "${base}" ]]; then + zstd "${zstd_args[@]}" "$dest/$base" -o "$dest/${archive_name}.zst" + else + zstd "${zstd_args[@]}" "$dest/$base" + fi + done - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} + - name: Add Termux release metadata + if: ${{ (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && matrix.target == 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail + if [[ ! -f "${GITHUB_WORKSPACE}/.github/termux-release.json" ]]; then + echo "No Termux release metadata found; skipping metadata attachment." + exit 0 + fi + dest="dist/${{ matrix.target }}" + cp "${GITHUB_WORKSPACE}/.github/termux-release.json" "${dest}/termux-release.json" + ( + cd "${dest}" + sha256sum ./* > SHA256SUMS + ) - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - python3 \ - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \ - stage-runtime \ - "$stage_dir" \ - "${GITHUB_WORKSPACE}/codex-rs/dist/${{ matrix.target }}/codex-${{ matrix.target }}" \ - --codex-version "${GITHUB_REF_NAME}" \ - --platform-tag "$platform_tag" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + - id: upload-artifact + uses: actions/upload-artifact@v6 with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error + name: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + # Upload the per-binary .zst files as well as the new .tar.gz + # equivalents we generated in the previous step. + path: | + codex-rs/dist/${{ matrix.target }}/* - - name: Compress artifacts + - name: Comment Termux artifact download link + if: ${{ github.event_name == 'pull_request' && startsWith(github.base_ref, 'release/') && matrix.target == 'aarch64-linux-android' }} shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_ID: ${{ steps.upload-artifact.outputs.artifact-id }} + ARTIFACT_NAME: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + PR_NUMBER: ${{ github.event.pull_request.number }} run: | set -euo pipefail + if [[ -z "${ARTIFACT_ID}" ]]; then + echo "upload-artifact did not return an artifact id" >&2 + exit 1 + fi - dest="dist/${{ matrix.target }}" - for f in "$dest"/*; do - base="$(basename "$f")" - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then - continue - fi - - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }} - path: | - codex-rs/dist/${{ matrix.target }}/* - - build-windows: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - needs: tag-check - uses: ./.github/workflows/rust-release-windows.yml - with: - release-lto: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }} - secrets: inherit + marker="" + artifact_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts/${ARTIFACT_ID}" + run_url="${GH_WORKFLOW_URL:-https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + body_path="${RUNNER_TEMP}/termux-artifact-comment.md" + { + echo "${marker}" + echo "Termux Android artifact ready for testing:" + echo + echo "- Artifact: [${ARTIFACT_NAME}](${artifact_url})" + echo "- Workflow run: [${GITHUB_RUN_ID}](${run_url})" + echo + echo "You must be signed in to GitHub with repository access to download Actions artifacts." + } > "${body_path}" + + existing_comment_id="$( + gh pr view "${PR_NUMBER}" \ + --repo "${GITHUB_REPOSITORY}" \ + --json comments \ + --jq ".comments | map(select(.author.is_bot == true and (.body | contains(\"${marker}\")))) | .[-1].id // \"\"" + )" + if [[ -n "${existing_comment_id}" ]]; then + gh api graphql \ + -f query=' + mutation($id: ID!, $body: String!) { + updateIssueComment(input: {id: $id, body: $body}) { + issueComment { + id + } + } + } + ' \ + -f id="${existing_comment_id}" \ + -f body="$(cat "${body_path}")" \ + >/dev/null + else + gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body-file "${body_path}" + fi - argument-comment-lint-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: argument-comment-lint release assets + gh label create binary-ready \ + --repo "${GITHUB_REPOSITORY}" \ + --color 0e8a16 \ + --description "Android binary is ready for testing" \ + --force + gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --add-label binary-ready + + shell-tool-mcp: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'openai' + name: shell-tool-mcp needs: tag-check - uses: ./.github/workflows/rust-release-argument-comment-lint.yml + permissions: + contents: read + id-token: write + uses: ./.github/workflows/shell-tool-mcp.yml with: + release-tag: ${{ github.ref_name }} publish: true - - zsh-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: zsh release assets - needs: tag-check - uses: ./.github/workflows/rust-release-zsh.yml + secrets: inherit release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') needs: - - tag-check - build - - stage-signed-macos - - build-windows - - argument-comment-lint-release-assets - - zsh-release-assets - if: >- - ${{ - always() && - needs.tag-check.result == 'success' && - ( - ( - github.event_name == 'workflow_dispatch' && - inputs.release_mode == 'promote_signed' && - needs.stage-signed-macos.result == 'success' && - needs.build.result == 'skipped' && - needs.build-windows.result == 'skipped' && - needs.argument-comment-lint-release-assets.result == 'skipped' && - needs.zsh-release-assets.result == 'skipped' - ) || - ( - (github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed') && - needs.build.result == 'success' && - needs.stage-signed-macos.result == 'skipped' && - needs.build-windows.result == 'success' && - needs.argument-comment-lint-release-assets.result == 'success' && - needs.zsh-release-assets.result == 'success' - ) - ) - }} + - shell-tool-mcp name: release runs-on: ubuntu-latest permissions: contents: write actions: read - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode == 'promote_signed' }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} outputs: version: ${{ steps.release_name.outputs.name }} tag: ${{ github.ref_name }} - sign_macos: ${{ steps.release_mode.outputs.sign_macos }} should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }} npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }} - should_publish_python_runtime: ${{ steps.python_runtime_publish_settings.outputs.should_publish }} steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + uses: actions/checkout@v6 - - name: Define release mode - id: release_mode - run: | - echo "release_mode=${RELEASE_MODE}" >> "$GITHUB_OUTPUT" - echo "sign_macos=${SIGN_MACOS}" >> "$GITHUB_OUTPUT" + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main - name: Generate release notes from tag commit message id: release_notes @@ -953,133 +954,18 @@ jobs: echo "path=${notes_path}" >> "${GITHUB_OUTPUT}" - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@v7 with: path: dist - - name: Validate unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - run_summary="$(gh run view "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --json conclusion,event,headBranch,headSha,status,workflowName,url \ - --jq '[.workflowName, .event, .headBranch, .headSha, .status, .conclusion, .url] | @tsv')" - IFS=$'\t' read -r workflow_name event head_branch head_sha status conclusion run_url <<< "$run_summary" - expected_head_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")" - - if [[ "$workflow_name" != "$GITHUB_WORKFLOW" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is for workflow '${workflow_name}', expected '${GITHUB_WORKFLOW}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$event" != "workflow_dispatch" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} was triggered by '${event}', expected 'workflow_dispatch'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_branch" != "$GITHUB_REF_NAME" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used ref '${head_branch}', expected '${GITHUB_REF_NAME}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_sha" != "$expected_head_sha" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used head SHA '${head_sha}', expected '${expected_head_sha}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is ${status}/${conclusion}, expected completed/success" - echo "Run URL: ${run_url}" - exit 1 - fi - - - name: Download artifacts from unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - gh run download "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --dir dist - - - name: Remove unsigned macOS staging artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - run: | - set -euo pipefail - find dist -mindepth 1 -maxdepth 1 -type d \ - -name '*-apple-darwin*-unsigned' \ - -exec rm -rf {} + - - - name: Re-upload promoted Linux x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-unknown-linux-musl - path: dist/x86_64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Linux arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-unknown-linux-musl - path: dist/aarch64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Windows x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-pc-windows-msvc - path: dist/x86_64-pc-windows-msvc/* - if-no-files-found: error - - - name: Re-upload promoted Windows arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-pc-windows-msvc - path: dist/aarch64-pc-windows-msvc/* - if-no-files-found: error - - name: List run: ls -R dist/ - - name: Prune artifacts excluded from unsigned macOS release - if: ${{ env.SIGN_MACOS == 'false' }} - run: | - find dist -mindepth 1 -maxdepth 1 -type d \ - ! -name '*-apple-darwin*-unsigned' \ - ! -name 'aarch64-unknown-linux-musl' \ - ! -name 'aarch64-unknown-linux-musl-app-server' \ - ! -name 'x86_64-unknown-linux-musl' \ - ! -name 'x86_64-unknown-linux-musl-app-server' \ - ! -name 'aarch64-pc-windows-msvc' \ - ! -name 'x86_64-pc-windows-msvc' \ - -exec rm -rf {} + - - if ! find dist -type f -name '*-apple-darwin*-unsigned*' | grep -q .; then - echo "No unsigned macOS artifacts found in downloaded workflow artifacts." - exit 1 - fi - + # This is a temporary fix: we should modify shell-tool-mcp.yml so these + # files do not end up in dist/ in the first place. - name: Delete entries from dist/ that should not go in the release run: | - rm -rf dist/windows-binaries* - # cargo-timing.html appears under multiple target-specific directories. - # If included in files: dist/**, release upload races on duplicate - # asset names and can fail with 404s. - find dist -type f -name 'cargo-timing.html' -delete - find dist -type d -empty -delete + rm -rf dist/shell-tool-mcp* ls -R dist/ @@ -1103,12 +989,6 @@ jobs: set -euo pipefail version="${VERSION}" - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - echo "npm_tag=" >> "$GITHUB_OUTPUT" - exit 0 - fi - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "should_publish=true" >> "$GITHUB_OUTPUT" echo "npm_tag=" >> "$GITHUB_OUTPUT" @@ -1120,132 +1000,55 @@ jobs: echo "npm_tag=" >> "$GITHUB_OUTPUT" fi - - name: Determine Python runtime publish settings - id: python_runtime_publish_settings - env: - VERSION: ${{ steps.release_name.outputs.name }} - run: | - set -euo pipefail - version="${VERSION}" - - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - else - echo "should_publish=false" >> "$GITHUB_OUTPUT" - fi - - name: Setup pnpm - if: ${{ env.SIGN_MACOS == 'true' }} - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + uses: pnpm/action-setup@v4 with: run_install: false - name: Setup Node.js for npm packaging - if: ${{ env.SIGN_MACOS == 'true' }} - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: node-version: 22 - name: Install dependencies - if: ${{ env.SIGN_MACOS == 'true' }} run: pnpm install --frozen-lockfile # stage_npm_packages.py requires DotSlash when staging releases. - - uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2 + - uses: facebook/install-dotslash@v2 - name: Stage npm packages - if: ${{ env.SIGN_MACOS == 'true' }} + if: github.repository_owner == 'openai' env: GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ steps.release_name.outputs.name }} run: | - workflow_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" ./scripts/stage_npm_packages.py \ - --release-version "$RELEASE_VERSION" \ - --workflow-url "$workflow_url" \ + --release-version "${{ steps.release_name.outputs.name }}" \ --package codex \ --package codex-responses-api-proxy \ --package codex-sdk - - name: Stage installer scripts - if: ${{ env.SIGN_MACOS == 'true' }} - run: | - cp scripts/install/install.sh dist/install.sh - cp scripts/install/install.ps1 dist/install.ps1 - - name: Create GitHub Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@v2 with: name: ${{ steps.release_name.outputs.name }} tag_name: ${{ github.ref_name }} body_path: ${{ steps.release_notes.outputs.path }} files: dist/** - overwrite_files: true - make_latest: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} # Mark as prerelease only when the version has a suffix after x.y.z # (e.g. -alpha, -beta). Otherwise publish a normal release. prerelease: ${{ contains(steps.release_name.outputs.name, '-') }} - - name: Clean up signed promotion handoff assets - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" --jq '.id')" - gh api --paginate "repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets" \ - --jq '.[] | [.id, .name] | @tsv' | - while IFS=$'\t' read -r asset_id asset_name; do - if [[ -z "$asset_id" || -z "$asset_name" ]]; then - continue - fi - - delete_asset=false - if [[ "$asset_name" == *unsigned* || "$asset_name" == "$SIGNED_MACOS_ASSET" ]]; then - delete_asset=true - fi - - if [[ "$delete_asset" == "true" ]]; then - echo "Deleting release asset ${asset_name}" - gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" - fi - done - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 + - if: github.repository_owner == 'openai' + uses: facebook/dotslash-publish-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag: ${{ github.ref_name }} config: .github/dotslash-config.json - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-zsh-config.json - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-argument-comment-lint-config.json - - name: Trigger developers.openai.com deploy # Only trigger the deploy if the release is not a pre-release. # The deploy is used to update the developers.openai.com website with the new config schema json file. - if: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} + if: ${{ !contains(steps.release_name.outputs.name, '-') && github.repository_owner == 'openai' }} continue-on-error: true env: DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }} @@ -1260,15 +1063,7 @@ jobs: # npm docs: https://docs.npmjs.com/trusted-publishers publish-npm: # Publish to npm for stable releases and alpha pre-releases with numeric suffixes. - # promote_signed intentionally skips build jobs that are ancestors of release; - # include the !cancelled() status function so Actions does not apply its implicit - # success() check to the whole dependency chain before evaluating release outputs. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_npm == 'true' - }} + if: ${{ needs.release.outputs.should_publish_npm == 'true' && github.repository_owner == 'openai' }} name: publish-npm needs: release runs-on: ubuntu-latest @@ -1278,37 +1073,36 @@ jobs: steps: - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: - # Node 24 bundles npm >= 11.5.1, which trusted publishing requires. - node-version: 24 + node-version: 22 registry-url: "https://registry.npmjs.org" scope: "@openai" + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + - name: Download npm tarballs from release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} run: | set -euo pipefail - version="$RELEASE_VERSION" - tag="$RELEASE_TAG" + version="${{ needs.release.outputs.version }}" + tag="${{ needs.release.outputs.tag }}" mkdir -p dist/npm - patterns=( - "codex-npm-${version}.tgz" - "codex-npm-linux-*-${version}.tgz" - "codex-npm-darwin-*-${version}.tgz" - "codex-npm-win32-*-${version}.tgz" - "codex-responses-api-proxy-npm-${version}.tgz" - "codex-sdk-npm-${version}.tgz" - ) - for pattern in "${patterns[@]}"; do - gh release download "$tag" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "$pattern" \ - --dir dist/npm - done + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-responses-api-proxy-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-sdk-npm-${version}.tgz" \ + --dir dist/npm # No NODE_AUTH_TOKEN needed because we use OIDC. - name: Publish to npm @@ -1317,193 +1111,23 @@ jobs: NPM_TAG: ${{ needs.release.outputs.npm_tag }} run: | set -euo pipefail - prefix="" + tag_args=() if [[ -n "${NPM_TAG}" ]]; then - prefix="${NPM_TAG}-" + tag_args+=(--tag "${NPM_TAG}") fi - root_tarball="dist/npm/codex-npm-${VERSION}.tgz" - sdk_tarball="dist/npm/codex-sdk-npm-${VERSION}.tgz" - # Keep this list in sync with CODEX_PLATFORM_PACKAGES in - # codex-cli/scripts/build_npm_package.py. The root wrapper advances - # @openai/codex@latest as soon as it publishes, so every platform - # package it aliases must already exist in the registry first. - platform_tarballs=( - "dist/npm/codex-npm-linux-x64-${VERSION}.tgz" - "dist/npm/codex-npm-linux-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-x64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-x64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-arm64-${VERSION}.tgz" - ) - - for required_tarball in "${platform_tarballs[@]}" "${root_tarball}"; do - if [[ ! -f "${required_tarball}" ]]; then - echo "Missing npm tarball: ${required_tarball}" - exit 1 - fi - done - - shopt -s nullglob - other_tarballs=() - for tarball in dist/npm/*-"${VERSION}".tgz; do - if [[ "${tarball}" == "${root_tarball}" || "${tarball}" == "${sdk_tarball}" ]]; then - continue - fi - - is_platform_tarball=false - for platform_tarball in "${platform_tarballs[@]}"; do - if [[ "${tarball}" == "${platform_tarball}" ]]; then - is_platform_tarball=true - break - fi - done - if [[ "${is_platform_tarball}" == true ]]; then - continue - fi - - other_tarballs+=("${tarball}") - done - - # Publish the platform packages before the root CLI wrapper. The root - # wrapper advances @openai/codex@latest, so it should only publish - # after the optional dependency versions it references exist. tarballs=( - "${platform_tarballs[@]}" - "${other_tarballs[@]}" - "${root_tarball}" + "codex-npm-${VERSION}.tgz" + "codex-responses-api-proxy-npm-${VERSION}.tgz" + "codex-sdk-npm-${VERSION}.tgz" ) - if [[ -f "${sdk_tarball}" ]]; then - tarballs+=("${sdk_tarball}") - fi for tarball in "${tarballs[@]}"; do - filename="$(basename "${tarball}")" - tag="" - - case "${filename}" in - codex-npm-linux-*-"${VERSION}".tgz|codex-npm-darwin-*-"${VERSION}".tgz|codex-npm-win32-*-"${VERSION}".tgz) - platform="${filename#codex-npm-}" - platform="${platform%-${VERSION}.tgz}" - tag="${prefix}${platform}" - ;; - codex-npm-"${VERSION}".tgz|codex-responses-api-proxy-npm-"${VERSION}".tgz|codex-sdk-npm-"${VERSION}".tgz) - tag="${NPM_TAG}" - ;; - *) - echo "Unexpected npm tarball: ${filename}" - exit 1 - ;; - esac - - publish_cmd=(npm publish "${GITHUB_WORKSPACE}/${tarball}") - if [[ -n "${tag}" ]]; then - publish_cmd+=(--tag "${tag}") - fi - - echo "+ ${publish_cmd[*]}" - set +e - publish_output="$("${publish_cmd[@]}" 2>&1)" - publish_status=$? - set -e - - echo "${publish_output}" - if [[ ${publish_status} -eq 0 ]]; then - continue - fi - - if grep -qiE "previously published|cannot publish over|version already exists" <<< "${publish_output}"; then - echo "Skipping already-published package version for ${filename}" - continue - fi - - exit "${publish_status}" + npm publish "${GITHUB_WORKSPACE}/dist/npm/${tarball}" "${tag_args[@]}" done - # Publish the platform-specific Python runtime wheels using PyPI trusted publishing. - # PyPI project configuration must trust this workflow and job. Keep this - # non-blocking while the Python runtime publishing path is new; failures still - # need release follow-up, but should not invalidate the Rust release itself. - publish-python-runtime: - # Publish to PyPI for stable releases and alpha pre-releases with numeric suffixes. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_python_runtime == 'true' - }} - name: publish-python-runtime - needs: release - runs-on: ubuntu-latest - continue-on-error: true - environment: pypi - permissions: - id-token: write # Required for PyPI trusted publishing. - contents: read - - steps: - - name: Download Python runtime wheels from release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} - run: | - set -euo pipefail - python_version="$RELEASE_VERSION" - python_version="${python_version/-alpha./a}" - python_version="${python_version/-beta./b}" - python_version="${python_version/-rc./rc}" - - mkdir -p dist/python-runtime - gh release download "$RELEASE_TAG" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "openai_codex_cli_bin-${python_version}-*.whl" \ - --dir dist/python-runtime - ls -lh dist/python-runtime - - - name: Publish Python runtime wheels to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - with: - packages-dir: dist/python-runtime - skip-existing: true - - winget: - name: winget - needs: release - # Only publish stable/mainline releases to WinGet; pre-releases include a - # '-' in the semver string (e.g., 1.2.3-alpha.1). - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' && - !contains(needs.release.outputs.version, '-') - }} - # This job only invokes a GitHub Action to open/update the winget-pkgs PR; - # it does not execute Windows-only tooling, so Linux is sufficient. - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Publish to WinGet - uses: vedantmgoyal9/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5 - with: - identifier: OpenAI.Codex - version: ${{ needs.release.outputs.version }} - release-tag: ${{ needs.release.outputs.tag }} - fork-user: openai-oss-forks - installers-regex: '^codex-(?:x86_64|aarch64)-pc-windows-msvc\.exe\.zip$' - token: ${{ secrets.WINGET_PUBLISH_PAT }} - update-branch: name: Update latest-alpha-cli branch - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' - }} permissions: contents: write needs: release diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml new file mode 100644 index 000000000000..66a76aa4d938 --- /dev/null +++ b/.github/workflows/shell-tool-mcp.yml @@ -0,0 +1,461 @@ +name: shell-tool-mcp + +on: + workflow_call: + inputs: + release-version: + description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v. + required: false + type: string + release-tag: + description: Tag name to use when downloading release artifacts (defaults to rust-v). + required: false + type: string + publish: + description: Whether to publish to npm when the version is releasable. + required: false + default: true + type: boolean + +env: + NODE_VERSION: 22 + +jobs: + metadata: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.compute.outputs.version }} + release_tag: ${{ steps.compute.outputs.release_tag }} + should_publish: ${{ steps.compute.outputs.should_publish }} + npm_tag: ${{ steps.compute.outputs.npm_tag }} + steps: + - name: Compute version and tags + id: compute + run: | + set -euo pipefail + + version="${{ inputs.release-version }}" + release_tag="${{ inputs.release-tag }}" + + if [[ -z "$version" ]]; then + if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then + version="${release_tag#rust-v}" + elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then + version="${GITHUB_REF_NAME#rust-v}" + release_tag="${GITHUB_REF_NAME}" + else + echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag." + exit 1 + fi + fi + + if [[ -z "$release_tag" ]]; then + release_tag="rust-v${version}" + fi + + npm_tag="" + should_publish="false" + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + should_publish="true" + elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then + should_publish="true" + npm_tag="alpha" + fi + + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" + echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT" + echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT" + + matrix-setup: + runs-on: ubuntu-latest + outputs: + rust-binaries: ${{ steps.compute.outputs.rust-binaries }} + bash-linux: ${{ steps.compute.outputs.bash-linux }} + bash-darwin: ${{ steps.compute.outputs.bash-darwin }} + steps: + - name: Compute matrices + id: compute + shell: bash + run: | + set -euo pipefail + is_openai="${{ github.repository_owner == 'openai' }}" + + # rust-binaries: always include x86_64-musl; conditionally include paid/fork runners + if [[ "$is_openai" == "true" ]]; then + rust='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","install_musl":true} + ]' + else + rust='[{"runner":"macos-latest","target":"aarch64-apple-darwin"}]' + fi + rust=$(echo "$rust" | jq -c '. + [{"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","install_musl":true}]') + echo "rust-binaries={\"include\":$rust}" >> "$GITHUB_OUTPUT" + + # bash-linux: always include x86_64 variants; add arm64 on openai + bash_linux='[ + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-24.04","image":"ubuntu:24.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-22.04","image":"ubuntu:22.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-12","image":"debian:12"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-11","image":"debian:11"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]' + if [[ "$is_openai" == "true" ]]; then + bash_linux=$(echo "$bash_linux" | jq -c '. + [ + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-24.04","image":"arm64v8/ubuntu:24.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-22.04","image":"arm64v8/ubuntu:22.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-20.04","image":"arm64v8/ubuntu:20.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-12","image":"arm64v8/debian:12"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-11","image":"arm64v8/debian:11"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]') + fi + echo "bash-linux={\"include\":$(echo "$bash_linux" | jq -c)}" >> "$GITHUB_OUTPUT" + + # bash-darwin: always include macos-14; add macos-15-xlarge on openai + bash_darwin='[{"runner":"macos-14","target":"aarch64-apple-darwin","variant":"macos-14"}]' + if [[ "$is_openai" == "true" ]]; then + bash_darwin=$(echo "$bash_darwin" | jq -c '. + [ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin","variant":"macos-15"} + ]') + fi + echo "bash-darwin={\"include\":$(echo "$bash_darwin" | jq -c)}" >> "$GITHUB_OUTPUT" + + rust-binaries: + name: Build Rust - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + defaults: + run: + working-directory: codex-rs + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.rust-binaries) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install UBSan runtime (musl) + if: ${{ matrix.install_musl }} + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -y + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 + fi + + - uses: dtolnay/rust-toolchain@1.93 + with: + targets: ${{ matrix.target }} + + - if: ${{ matrix.install_musl }} + name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.14.0 + + - if: ${{ matrix.install_musl }} + name: Install musl build dependencies + env: + TARGET: ${{ matrix.target }} + run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh" + + - if: ${{ matrix.install_musl }} + name: Configure rustc UBSan wrapper (musl host) + shell: bash + run: | + set -euo pipefail + ubsan="" + if command -v ldconfig >/dev/null 2>&1; then + ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')" + fi + wrapper_root="${RUNNER_TEMP:-/tmp}" + wrapper="${wrapper_root}/rustc-ubsan-wrapper" + cat > "${wrapper}" <> "$GITHUB_ENV" + echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV" + + - if: ${{ matrix.install_musl }} + name: Clear sanitizer flags (musl) + shell: bash + run: | + set -euo pipefail + # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. + echo "RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV" + # Override any runner-level Cargo config rustflags as well. + echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + + sanitize_flags() { + local input="$1" + input="${input//-fsanitize=undefined/}" + input="${input//-fno-sanitize-recover=undefined/}" + input="${input//-fno-sanitize-trap=undefined/}" + echo "$input" + } + + cflags="$(sanitize_flags "${CFLAGS-}")" + cxxflags="$(sanitize_flags "${CXXFLAGS-}")" + echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" + echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + + - name: Build exec server binaries + run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper + + - name: Stage exec server binaries + run: | + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}" + mkdir -p "$dest" + cp "target/${{ matrix.target }}/release/codex-exec-mcp-server" "$dest/" + cp "target/${{ matrix.target }}/release/codex-execve-wrapper" "$dest/" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-rust-${{ matrix.target }} + path: artifacts/** + if-no-files-found: error + + bash-linux: + name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + container: + image: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-linux) }} + steps: + - name: Install build prerequisites + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext + elif command -v dnf >/dev/null 2>&1; then + dnf install -y git gcc gcc-c++ make bison autoconf gettext + elif command -v yum >/dev/null 2>&1; then + yum install -y git gcc gcc-c++ make bison autoconf gettext + else + echo "Unsupported package manager in container" + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + bash-darwin: + name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-darwin) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + package: + name: Package npm module + needs: + - metadata + - rust-binaries + - bash-linux + - bash-darwin + runs-on: ubuntu-latest + env: + PACKAGE_VERSION: ${{ needs.metadata.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install JavaScript dependencies + run: pnpm install --frozen-lockfile + + - name: Build (shell-tool-mcp) + run: pnpm --filter @openai/codex-shell-tool-mcp run build + + - name: Download build artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts + + - name: Assemble staging directory + id: staging + shell: bash + run: | + set -euo pipefail + staging="${STAGING_DIR}" + mkdir -p "$staging" "$staging/vendor" + cp shell-tool-mcp/README.md "$staging/" + cp shell-tool-mcp/package.json "$staging/" + cp -R shell-tool-mcp/bin "$staging/" + + found_vendor="false" + shopt -s nullglob + for vendor_dir in artifacts/*/vendor; do + rsync -av "$vendor_dir/" "$staging/vendor/" + found_vendor="true" + done + if [[ "$found_vendor" == "false" ]]; then + echo "No vendor payloads were downloaded." + exit 1 + fi + + node - <<'NODE' + import fs from "node:fs"; + import path from "node:path"; + + const stagingDir = process.env.STAGING_DIR; + const version = process.env.PACKAGE_VERSION; + const pkgPath = path.join(stagingDir, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + pkg.version = version; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + NODE + + echo "dir=$staging" >> "$GITHUB_OUTPUT" + env: + STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp + + - name: Ensure binaries are executable + run: | + set -euo pipefail + staging="${{ steps.staging.outputs.dir }}" + chmod +x \ + "$staging"/vendor/*/codex-exec-mcp-server \ + "$staging"/vendor/*/codex-execve-wrapper \ + "$staging"/vendor/*/bash/*/bash + + - name: Create npm tarball + shell: bash + run: | + set -euo pipefail + mkdir -p dist/npm + staging="${{ steps.staging.outputs.dir }}" + pack_info=$(cd "$staging" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm") + filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);') + mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz" + + - uses: actions/upload-artifact@v6 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz + if-no-files-found: error + + publish: + name: Publish npm package + needs: + - metadata + - package + if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }} + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: https://registry.npmjs.org + scope: "@openai" + + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + + - name: Download npm tarball + uses: actions/download-artifact@v7 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm + + - name: Publish to npm + if: github.repository == 'openai/codex' + env: + NPM_TAG: ${{ needs.metadata.outputs.npm_tag }} + VERSION: ${{ needs.metadata.outputs.version }} + shell: bash + run: | + set -euo pipefail + tag_args=() + if [[ -n "${NPM_TAG}" ]]; then + tag_args+=(--tag "${NPM_TAG}") + fi + npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}" diff --git a/.github/workflows/termux-release-checkpoint.yml b/.github/workflows/termux-release-checkpoint.yml new file mode 100644 index 000000000000..0347e6f1b3d6 --- /dev/null +++ b/.github/workflows/termux-release-checkpoint.yml @@ -0,0 +1,103 @@ +name: termux-release-checkpoint + +on: + workflow_dispatch: + inputs: + source_branch: + description: "Release branch to checkpoint from, for example release/0.123.0" + required: false + type: string + default: "" + source_sha: + description: "Specific source commit SHA to checkpoint; defaults to the source branch tip" + required: false + type: string + default: "" + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + type: string + default: "wallentx/termux-target" + reviewer: + description: "GitHub username to request as reviewer" + required: false + type: string + default: "wallentx" + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-checkpoint-${{ inputs.source_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + checkpoint: + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + env: + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + REQUESTED_SOURCE_BRANCH: ${{ inputs.source_branch }} + REQUESTED_SOURCE_SHA: ${{ inputs.source_sha }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ inputs.source_branch || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + source_branch="${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${source_branch}" + + - name: Create checkpoint PR + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" diff --git a/.github/workflows/termux-release-deploy.yml b/.github/workflows/termux-release-deploy.yml new file mode 100644 index 000000000000..2071611dacc4 --- /dev/null +++ b/.github/workflows/termux-release-deploy.yml @@ -0,0 +1,243 @@ +name: termux-release-deploy + +on: + push: + branches: + - "release/**" + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to deploy, for example release/0.124.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to deploy. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged release PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged release PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + default: "wallentx/termux-target" + type: string + reviewer: + description: "GitHub username to request as reviewer on the checkpoint PR" + required: false + default: "wallentx" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-deploy-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + deploy: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' || !startsWith(github.event.head_commit.message, 'Seed Termux release automation') }} + permissions: + actions: read + contents: write + deployments: write + issues: write + pull-requests: write + env: + REQUESTED_RELEASE_BRANCH: ${{ inputs.release_branch }} + REQUESTED_RELEASE_SHA: ${{ inputs.release_sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + release_branch="${REQUESTED_RELEASE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${release_branch}" + + - name: Resolve release ref + id: release-ref + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: deploy + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Create deployment + if: steps.metadata.outputs.deploy == 'true' + id: deployment + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + deployment_id="$( + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments" \ + -f ref="${RELEASE_SHA}" \ + -f environment="termux-release" \ + -F auto_merge=false \ + -F required_contexts[] \ + -f description="Termux release deployment for ${TERMUX_TAG}" \ + --jq '.id' + )" + echo "id=${deployment_id}" >> "$GITHUB_OUTPUT" + + - name: Mark deployment in progress + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="in_progress" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Promoting Termux release artifact and preparing checkpoint PR" \ + >/dev/null + + - name: Locate merged pull request + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" + + - name: Ensure checkpoint PR + if: steps.metadata.outputs.deploy == 'true' + id: checkpoint + env: + SOURCE_BRANCH: ${{ steps.release-ref.outputs.branch }} + SOURCE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" + + - name: Mark deployment success + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="success" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment completed for ${TERMUX_TAG}" \ + >/dev/null + + - name: Mark deployment failure + if: failure() && steps.deployment.outputs.id != '' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="failure" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment failed" \ + >/dev/null diff --git a/.github/workflows/termux-release-promote.yml b/.github/workflows/termux-release-promote.yml new file mode 100644 index 000000000000..22c277c21a70 --- /dev/null +++ b/.github/workflows/termux-release-promote.yml @@ -0,0 +1,135 @@ +name: termux-release-promote + +on: + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to promote, for example release/0.122.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to promote. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-promote-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + promote: + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: write + pull-requests: read + env: + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Resolve release ref + id: release-ref + env: + INPUT_RELEASE_BRANCH: ${{ inputs.release_branch }} + INPUT_RELEASE_SHA: ${{ inputs.release_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: promote + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Locate merged pull request + if: steps.metadata.outputs.promote == 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.promote == 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.promote == 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" diff --git a/scripts/termux-configure-git.sh b/scripts/termux-configure-git.sh new file mode 100755 index 000000000000..fae3abcbab48 --- /dev/null +++ b/scripts/termux-configure-git.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Configure the explicit bot identity used by release automation and fetch any +# refs requested by the calling workflow. + +set -euo pipefail + +git config user.name "github-actions[bot]" +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + +while (($#)); do + case "$1" in + --origin) + shift + origin_refs=() + while (($#)) && [[ "$1" != --* ]]; do + origin_refs+=("$1") + shift + done + if ((${#origin_refs[@]})); then + git fetch --prune origin "${origin_refs[@]}" + fi + ;; + --upstream-tag) + if (($# < 3)); then + echo "--upstream-tag requires ." >&2 + exit 1 + fi + upstream_repo="$2" + upstream_tag="$3" + git remote add upstream "https://github.com/${upstream_repo}.git" 2>/dev/null || true + git fetch --prune --no-tags upstream "+refs/tags/${upstream_tag}:refs/tags/${upstream_tag}" + shift 3 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done diff --git a/scripts/termux-create-checkpoint-pr.sh b/scripts/termux-create-checkpoint-pr.sh new file mode 100755 index 000000000000..ba07c289952d --- /dev/null +++ b/scripts/termux-create-checkpoint-pr.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/termux-release-paths.sh +source "${script_dir}/termux-release-paths.sh" + +source_branch="${SOURCE_BRANCH:-${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}}" +source_sha="${SOURCE_SHA:-${REQUESTED_SOURCE_SHA:-}}" +if [[ -z "${source_sha}" ]]; then + if [[ "${GITHUB_EVENT_NAME:-}" == "push" && "${source_branch}" == "${GITHUB_REF_NAME:-}" ]]; then + source_sha="${GITHUB_SHA}" + else + source_sha="$(git rev-parse "origin/${source_branch}")" + fi +fi + +if [[ -z "${DESTINATION_BRANCH:-}" ]]; then + echo "DESTINATION_BRANCH is required." >&2 + exit 1 +fi + +release_only_checkpoint_paths() { + printf '%s\n' "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +} + +resolve_source_version_conflicts() { + local path="$1" + local resolved_path + resolved_path="$(mktemp)" + + if ! awk ' + function normalize_versions(text) { + gsub(/version = "[^"]+"/, "version = \"\"", text) + return text + } + + BEGIN { + in_block = 0 + side = "" + ours = "" + theirs = "" + blocks = 0 + } + + /^<<<<<<< / { + if (in_block) { + exit 1 + } + in_block = 1 + side = "ours" + ours = "" + theirs = "" + blocks++ + next + } + + /^=======$/ && in_block { + side = "theirs" + next + } + + /^>>>>>>> / && in_block { + if (normalize_versions(ours) != normalize_versions(theirs)) { + exit 1 + } + printf "%s", theirs + in_block = 0 + side = "" + next + } + + { + if (!in_block) { + print + } else if (side == "ours") { + ours = ours $0 ORS + } else if (side == "theirs") { + theirs = theirs $0 ORS + } else { + exit 1 + } + } + + END { + if (in_block || blocks == 0) { + exit 1 + } + } + ' "${path}" > "${resolved_path}"; then + rm -f "${resolved_path}" + return 1 + fi + + cp "${resolved_path}" "${path}" + rm -f "${resolved_path}" +} + +short_sha="${source_sha:0:12}" +source_slug="${source_branch//\//_}" +dest_slug="${DESTINATION_BRANCH//\//_}" +checkpoint_branch="checkpoint/${dest_slug}_from_${source_slug}_${short_sha}" +pr_title="checkpoint: into ${DESTINATION_BRANCH} from ${source_branch} @ ${short_sha}" +merge_conflicted=false +conflict_summary="" + +existing_pr="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --head "${checkpoint_branch}" \ + --state all \ + --json number,state,mergedAt,url \ + --jq '[.[] | select(.state == "OPEN" or .mergedAt != null)] | .[0] // empty' +)" +if [[ -n "${existing_pr}" ]]; then + existing_url="$(jq -r '.url' <<< "${existing_pr}")" + existing_state="$(jq -r '.state' <<< "${existing_pr}")" + echo "Checkpoint PR already exists for ${checkpoint_branch}: ${existing_url} (${existing_state})." + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${existing_url}" >> "${GITHUB_OUTPUT}" + fi + exit 0 +fi + +git checkout -B "${checkpoint_branch}" "origin/${DESTINATION_BRANCH}" + +if ! git merge --no-ff --no-edit "${source_sha}"; then + mapfile -t conflicted_paths < <(git diff --name-only --diff-filter=U) + for conflicted_path in "${conflicted_paths[@]}"; do + if termux_is_checkpoint_release_only_path "${conflicted_path}"; then + echo "Auto-resolving release-only checkpoint conflict in ${conflicted_path} by keeping ${DESTINATION_BRANCH}." + if git cat-file -e "HEAD:${conflicted_path}" 2>/dev/null; then + git checkout --ours -- "${conflicted_path}" + git add "${conflicted_path}" + else + git rm -f --ignore-unmatch "${conflicted_path}" + fi + fi + done + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if ((${#remaining_conflicts[@]})); then + cargo_version_conflicts=true + for remaining_conflict in "${remaining_conflicts[@]}"; do + case "${remaining_conflict}" in + codex-rs/Cargo.toml|codex-rs/Cargo.lock) + ;; + *) + cargo_version_conflicts=false + ;; + esac + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + for remaining_conflict in "${remaining_conflicts[@]}"; do + if ! resolve_source_version_conflicts "${remaining_conflict}"; then + cargo_version_conflicts=false + break + fi + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + echo "Auto-resolving recurring Cargo version checkpoint conflicts by keeping ${source_branch} versions." + git add -- "${remaining_conflicts[@]}" + fi + fi + fi + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if [[ "${#remaining_conflicts[@]}" -eq 0 ]]; then + git commit --no-edit + else + merge_conflicted=true + conflict_summary="$( + printf '%s\n' "${remaining_conflicts[@]}" | awk '{ print "- `" $0 "`" }' + )" + echo "Automatic checkpoint merge failed; creating a manual-resolution PR instead." >&2 + if git rev-parse -q --verify MERGE_HEAD >/dev/null; then + git merge --abort + fi + git checkout -B "${checkpoint_branch}" "${source_sha}" + fi +fi + +if git cat-file -e "origin/${DESTINATION_BRANCH}:.github" 2>/dev/null; then + git restore --source="origin/${DESTINATION_BRANCH}" --staged --worktree -- .github + mapfile -t added_github_paths < <( + git diff --name-only --diff-filter=A "origin/${DESTINATION_BRANCH}" -- .github + ) + if ((${#added_github_paths[@]})); then + git rm -f --ignore-unmatch -- "${added_github_paths[@]}" + fi +else + git rm -r --ignore-unmatch .github +fi + +while IFS= read -r release_only_path; do + if git cat-file -e "origin/${DESTINATION_BRANCH}:${release_only_path}" 2>/dev/null; then + git restore --source="origin/${DESTINATION_BRANCH}" --staged --worktree -- "${release_only_path}" + else + git rm -f --ignore-unmatch -- "${release_only_path}" + fi +done < <(release_only_checkpoint_paths) + +if ! git diff --quiet || ! git diff --cached --quiet; then + if [[ -e .github || -L .github ]] || git ls-files --error-unmatch -- .github >/dev/null 2>&1; then + git add -A .github + fi + while IFS= read -r release_only_path; do + if [[ -e "${release_only_path}" || -L "${release_only_path}" ]] || git ls-files --error-unmatch -- "${release_only_path}" >/dev/null 2>&1; then + git add -A -- "${release_only_path}" + fi + done < <(release_only_checkpoint_paths) + if [[ "${merge_conflicted}" == "true" ]]; then + git commit -m "checkpoint: prepare ${source_branch} for ${DESTINATION_BRANCH}" + else + git commit --amend --no-edit + fi +fi + +if git diff --quiet "origin/${DESTINATION_BRANCH}" HEAD; then + echo "Checkpoint merge produced no destination changes after release-only files were restored." + exit 0 +fi + +git push --force-with-lease origin "${checkpoint_branch}" + +remaining="$( + git log --first-parent --pretty=format:%H "${source_sha}..origin/${source_branch}" | wc -w +)" + +body_path="${RUNNER_TEMP}/termux-checkpoint-pr.md" +{ + echo "## Termux release checkpoint" + echo + echo "- Source branch: \`${source_branch}\`" + echo "- Source hash: \`${source_sha}\`" + echo "- Destination branch: \`${DESTINATION_BRANCH}\`" + echo "- Remaining first-parent commits on source: ${remaining}" + echo + echo "This PR carries release-train conflict fixes and follow-up changes back into the reusable Termux patch branch." + if [[ "${merge_conflicted}" == "true" ]]; then + echo + echo "## Merge conflicts" + echo + echo "GitHub Actions could not create the checkpoint merge commit automatically, so this PR was created from the source branch state for manual conflict resolution." + echo + echo "Conflicted paths from the failed merge attempt:" + if [[ -n "${conflict_summary}" ]]; then + printf '%s\n' "${conflict_summary}" + else + echo "- Conflict details unavailable" + fi + fi + echo + echo "Release-only workflow files and metadata under \`.github\` were restored to the destination branch versions before opening this PR." +} > "${body_path}" + +pr_url="$( + gh pr create \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${DESTINATION_BRANCH}" \ + --head "${checkpoint_branch}" \ + --title "${pr_title}" \ + --body-file "${body_path}" +)" +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-reviewer "${REVIEWER}" || true +gh label create checkpoint --repo "${GITHUB_REPOSITORY}" --color c5def5 --description "Checkpoint merge" --force +gh label create termux-release --repo "${GITHUB_REPOSITORY}" --color 0e8a16 --description "Termux release automation" --force +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-label "checkpoint" --add-label "termux-release" + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${pr_url}" >> "${GITHUB_OUTPUT}" +fi diff --git a/scripts/termux-create-or-update-mirrored-release.sh b/scripts/termux-create-or-update-mirrored-release.sh new file mode 100755 index 000000000000..5787e894582b --- /dev/null +++ b/scripts/termux-create-or-update-mirrored-release.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +# Create a mirrored Termux release or repair an existing release that is missing +# the Android tarball. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" +: "${RUNNER_TEMP:?RUNNER_TEMP is required}" +: "${UPSTREAM_TAG:?UPSTREAM_TAG is required}" +: "${UPSTREAM_REPO:?UPSTREAM_REPO is required}" +: "${UPSTREAM_NAME?UPSTREAM_NAME is required}" +: "${TERMUX_TAG:?TERMUX_TAG is required}" +: "${UPSTREAM_PRERELEASE:?UPSTREAM_PRERELEASE is required}" +: "${UPSTREAM_HTML_URL?UPSTREAM_HTML_URL is required}" +: "${RELEASE_TRAIN?RELEASE_TRAIN is required}" +: "${RELEASE_EXISTS:?RELEASE_EXISTS is required}" +: "${PR_NUMBER:?PR_NUMBER is required}" +: "${HEAD_SHA:?HEAD_SHA is required}" +: "${RELEASE_SHA:?RELEASE_SHA is required}" + +promoted_dir="${PROMOTED_DIR:-promoted}" +asset_path="${promoted_dir}/codex-aarch64-linux-android.tar.gz" + +body_path="${RUNNER_TEMP}/release-body.md" +upstream_body_path="${RUNNER_TEMP}/upstream-release-body.md" +upstream_body_without_changelog_path="${RUNNER_TEMP}/upstream-release-body-without-changelog.md" +if gh release view "${UPSTREAM_TAG}" \ + --repo "${UPSTREAM_REPO}" \ + --json body \ + --jq '.body // ""' > "${upstream_body_path}"; then + awk ' + function heading_level(line, text) { + if (match(line, /^(#{1,6})[[:space:]]+(.+)$/, parts)) { + text = tolower(parts[2]) + sub(/[[:space:]]+#+[[:space:]]*$/, "", text) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", text) + if (text == "changelog" || text == "change log") { + return length(parts[1]) + } + } + return 0 + } + + { + if (!skip) { + level = heading_level($0) + if (level > 0) { + skip = 1 + skip_level = level + next + } + print + next + } + + if (match($0, /^(#{1,6})[[:space:]]+/, parts) && length(parts[1]) <= skip_level) { + skip = 0 + print + } + } + ' "${upstream_body_path}" > "${upstream_body_without_changelog_path}" +else + echo "::warning title=Upstream release notes unavailable::Could not read ${UPSTREAM_REPO} release ${UPSTREAM_TAG}." + : > "${upstream_body_without_changelog_path}" +fi + +{ + echo "Termux Android build for ${UPSTREAM_TAG}." + echo + echo "- Upstream release: ${UPSTREAM_HTML_URL}" + echo "- Release train: \`${RELEASE_TRAIN}\`" + echo "- Promoted PR: #${PR_NUMBER}" + echo "- Promoted PR head SHA: \`${HEAD_SHA}\`" +} > "${body_path}" +if [[ -s "${upstream_body_without_changelog_path}" ]]; then + { + echo + echo "## Upstream release notes" + echo + cat "${upstream_body_without_changelog_path}" + } >> "${body_path}" +fi + +release_title="${UPSTREAM_NAME}" +if [[ -z "${release_title}" || "${release_title}" == "null" ]]; then + release_title="${TERMUX_TAG}" +fi + +if [[ "${RELEASE_EXISTS}" == "true" ]]; then + gh release upload \ + "${TERMUX_TAG}" \ + "${asset_path}#codex-termux" \ + --repo "${GITHUB_REPOSITORY}" \ + --clobber + exit 0 +fi + +release_args=( + gh release create "${TERMUX_TAG}" + --repo "${GITHUB_REPOSITORY}" + --target "${RELEASE_SHA}" + --title "${release_title}" + --notes-file "${body_path}" +) +if [[ "${UPSTREAM_PRERELEASE}" == "true" ]]; then + release_args+=(--prerelease) +fi +release_args+=("${asset_path}#codex-termux") +"${release_args[@]}" diff --git a/scripts/termux-download-release-artifact.sh b/scripts/termux-download-release-artifact.sh new file mode 100755 index 000000000000..2acdba9025a0 --- /dev/null +++ b/scripts/termux-download-release-artifact.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${HEAD_SHA:-}" ]]; then + echo "HEAD_SHA is required." >&2 + exit 1 +fi + +if [[ -z "${PR_ARTIFACT_NAME:-}" ]]; then + echo "PR_ARTIFACT_NAME is required." >&2 + exit 1 +fi + +promoted_dir="${PROMOTED_DIR:-promoted}" + +find_run_id() { + local event="$1" + gh run list \ + --repo "${GITHUB_REPOSITORY}" \ + --workflow rust-release.yml \ + --event "${event}" \ + --status success \ + --commit "${HEAD_SHA}" \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId // empty' +} + +find_artifact_run_id() { + local artifact_name="$1" + + gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=${artifact_name}" \ + | jq -rs \ + --arg artifact_name "${artifact_name}" \ + --arg head_sha "${HEAD_SHA}" \ + ' + [ + .[].artifacts[] + | select(.name == $artifact_name) + | select(.expired == false) + | select(.workflow_run.head_sha == $head_sha) + ] + | sort_by(.created_at) + | reverse + | .[0].workflow_run.id // empty + ' +} + +artifact_name="${PR_ARTIFACT_NAME}" +run_id="$(find_artifact_run_id "${artifact_name}")" +mkdir -p "${promoted_dir}" +if [[ -n "${run_id}" ]] && gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}"; then + : +else + artifact_name="aarch64-linux-android" + run_id="$(find_run_id workflow_dispatch)" + if [[ -z "${run_id}" ]]; then + echo "No successful rust-release run found for ${HEAD_SHA}" >&2 + exit 1 + fi + gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}" +fi + +ls -la "${promoted_dir}" +if [[ -f "${promoted_dir}/SHA256SUMS" ]]; then + (cd "${promoted_dir}" && sha256sum -c SHA256SUMS) +fi +if [[ ! -f "${promoted_dir}/codex-aarch64-linux-android.tar.gz" ]]; then + echo "Expected ${promoted_dir}/codex-aarch64-linux-android.tar.gz in the downloaded artifact." >&2 + exit 1 +fi diff --git a/scripts/termux-find-release-pr.sh b/scripts/termux-find-release-pr.sh new file mode 100755 index 000000000000..3e35fa42eabe --- /dev/null +++ b/scripts/termux-find-release-pr.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -n "${INPUT_PR_NUMBER:-}" || -n "${INPUT_PR_HEAD_SHA:-}" ]]; then + if [[ -z "${INPUT_PR_NUMBER:-}" || -z "${INPUT_PR_HEAD_SHA:-}" ]]; then + echo "workflow_dispatch inputs pr_number and pr_head_sha must be provided together." >&2 + exit 1 + fi + + pr_number="${INPUT_PR_NUMBER}" + head_sha="${INPUT_PR_HEAD_SHA}" + head_ref="" +else + if [[ -z "${RELEASE_BRANCH:-}" || -z "${RELEASE_SHA:-}" ]]; then + echo "RELEASE_BRANCH and RELEASE_SHA are required." >&2 + exit 1 + fi + + pr_json="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${RELEASE_BRANCH}" \ + --state merged \ + --limit 100 \ + --json number,headRefOid,headRefName,mergeCommit \ + --jq "[.[] | select(.mergeCommit.oid == \"${RELEASE_SHA}\")] | sort_by(.number) | reverse | .[0] // empty" + )" + if [[ -z "${pr_json}" ]]; then + echo "Unable to find merged PR for ${RELEASE_SHA} into ${RELEASE_BRANCH}" >&2 + exit 1 + fi + + pr_number="$(jq -r '.number' <<< "${pr_json}")" + head_sha="$(jq -r '.headRefOid // .head.sha' <<< "${pr_json}")" + head_ref="$(jq -r '.headRefName // .head.ref' <<< "${pr_json}")" +fi + +artifact_name="termux-android-pr-${pr_number}-${head_sha}" +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "number=${pr_number}" + echo "head_sha=${head_sha}" + echo "head_ref=${head_ref}" + echo "artifact_name=${artifact_name}" + } >> "${GITHUB_OUTPUT}" +else + printf 'number=%s\nhead_sha=%s\nhead_ref=%s\nartifact_name=%s\n' \ + "${pr_number}" \ + "${head_sha}" \ + "${head_ref}" \ + "${artifact_name}" +fi diff --git a/scripts/termux-read-release-metadata.sh b/scripts/termux-read-release-metadata.sh new file mode 100755 index 000000000000..519a0635a368 --- /dev/null +++ b/scripts/termux-read-release-metadata.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# Parse .github/termux-release.json and emit workflow outputs for deploy or +# promote jobs. TERMUX_RELEASE_ACTION must be "deploy" or "promote". + +set -euo pipefail + +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +metadata=".github/termux-release.json" +action="${TERMUX_RELEASE_ACTION:-deploy}" +case "${action}" in + deploy) + missing_context="deployment" + ;; + promote) + missing_context="promotion" + ;; + *) + echo "TERMUX_RELEASE_ACTION must be deploy or promote." >&2 + exit 1 + ;; +esac + +if [[ ! -f "${metadata}" ]]; then + echo "No ${metadata}; this push is not a Termux release ${missing_context}." + echo "${action}=false" >> "${GITHUB_OUTPUT}" + exit 0 +fi + +upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" +upstream_name="$(jq -r '.upstream_name // .upstream_tag // empty' "${metadata}")" +termux_tag="$(jq -r '.termux_tag // empty' "${metadata}")" +upstream_version="${upstream_tag#rust-v}" +upstream_version="${upstream_version%-termux}" +upstream_prerelease=false +if [[ "${upstream_version}" == *-* ]]; then + upstream_prerelease=true +fi +upstream_html_url="$(jq -r '.upstream_html_url // ""' "${metadata}")" +upstream_repo="$(jq -r '.upstream_repo // "openai/codex"' "${metadata}")" +release_train="$(jq -r '.release_train // ""' "${metadata}")" +if [[ -z "${upstream_tag}" || -z "${termux_tag}" ]]; then + echo "Missing upstream_tag or termux_tag in ${metadata}" >&2 + exit 1 +fi + +release_state="$(TERMUX_TAG="${termux_tag}" "${script_dir}/termux-release-asset-state.sh")" +release_exists="$(awk -F= '$1 == "release_exists" { print $2 }' <<< "${release_state}")" +asset_exists="$(awk -F= '$1 == "asset_exists" { print $2 }' <<< "${release_state}")" + +if [[ "${action}" == "promote" ]]; then + if [[ "${asset_exists}" == "true" ]]; then + echo "${termux_tag} already exists with codex-aarch64-linux-android.tar.gz; skipping promotion." + echo "promote=false" >> "${GITHUB_OUTPUT}" + exit 0 + fi + if [[ "${release_exists}" == "true" ]]; then + echo "${termux_tag} exists but is missing codex-aarch64-linux-android.tar.gz; repairing promotion." + fi +fi + +{ + echo "${action}=true" + echo "upstream_tag=${upstream_tag}" + echo "upstream_name=${upstream_name}" + echo "termux_tag=${termux_tag}" + echo "upstream_prerelease=${upstream_prerelease}" + echo "upstream_html_url=${upstream_html_url}" + echo "upstream_repo=${upstream_repo}" + echo "release_train=${release_train}" + echo "release_exists=${release_exists}" + echo "asset_exists=${asset_exists}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-release-asset-state.sh b/scripts/termux-release-asset-state.sh new file mode 100755 index 000000000000..8694480b6a36 --- /dev/null +++ b/scripts/termux-release-asset-state.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +termux_tag="${1:-${TERMUX_TAG:-}}" +release_repo="${RELEASE_REPO:-${GITHUB_REPOSITORY:-}}" +asset_name="${ASSET_NAME:-codex-aarch64-linux-android.tar.gz}" + +if [[ -z "${termux_tag}" ]]; then + echo "TERMUX_TAG or tag argument is required." >&2 + exit 1 +fi + +if [[ -z "${release_repo}" ]]; then + echo "GITHUB_REPOSITORY or RELEASE_REPO is required." >&2 + exit 1 +fi + +release_exists=false +asset_exists=false + +if gh release view "${termux_tag}" --repo "${release_repo}" >/dev/null 2>&1; then + release_exists=true + release_asset_exists="$( + gh release view "${termux_tag}" \ + --repo "${release_repo}" \ + --json assets \ + | jq -r --arg asset_name "${asset_name}" \ + '.assets | map(.name) | any(. == $asset_name)' + )" + if [[ "${release_asset_exists}" == "true" ]]; then + asset_exists=true + fi +fi + +printf 'release_exists=%s\n' "${release_exists}" +printf 'asset_exists=%s\n' "${asset_exists}" diff --git a/scripts/termux-release-paths.sh b/scripts/termux-release-paths.sh new file mode 100755 index 000000000000..6990f323ec44 --- /dev/null +++ b/scripts/termux-release-paths.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# Sourceable path lists for files owned by the Termux release automation. + +set -euo pipefail + +readonly -a TERMUX_RELEASE_WORKFLOW_PATHS=( + .github/workflows/rust-release.yml + .github/workflows/shell-tool-mcp.yml + .github/workflows/termux-release-checkpoint.yml + .github/workflows/termux-release-deploy.yml + .github/workflows/termux-release-promote.yml +) + +readonly -a TERMUX_RELEASE_BRANCH_SCRIPT_PATHS=( + scripts/termux-configure-git.sh + scripts/termux-create-checkpoint-pr.sh + scripts/termux-create-or-update-mirrored-release.sh + scripts/termux-download-release-artifact.sh + scripts/termux-find-release-pr.sh + scripts/termux-read-release-metadata.sh + scripts/termux-release-asset-state.sh + scripts/termux-release-paths.sh + scripts/termux-resolve-release-ref.sh + scripts/termux-validate-gh-env.sh +) + +readonly -a TERMUX_RELEASE_AUTOMATION_PATHS=( + "${TERMUX_RELEASE_WORKFLOW_PATHS[@]}" + "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +) + +readonly -a TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS=( + "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" + .github/termux-release.json +) + +termux_path_in_list() { + local candidate="$1" + shift + local listed_path + + for listed_path in "$@"; do + [[ "${candidate}" != "${listed_path}" ]] || return 0 + done + return 1 +} + +termux_is_release_automation_path() { + termux_path_in_list "$1" "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" +} + +termux_is_checkpoint_release_only_path() { + termux_path_in_list "$1" "${TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS[@]}" +} diff --git a/scripts/termux-resolve-release-ref.sh b/scripts/termux-resolve-release-ref.sh new file mode 100755 index 000000000000..194773d45bed --- /dev/null +++ b/scripts/termux-resolve-release-ref.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Resolve the release branch and SHA for deploy/promote workflows. + +set -euo pipefail + +: "${GITHUB_EVENT_NAME:?GITHUB_EVENT_NAME is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" + +input_release_branch="${INPUT_RELEASE_BRANCH:-${REQUESTED_RELEASE_BRANCH:-}}" +input_release_sha="${INPUT_RELEASE_SHA:-${REQUESTED_RELEASE_SHA:-}}" + +if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then + if [[ -z "${input_release_branch}" ]]; then + echo "release_branch is required for workflow_dispatch." >&2 + exit 1 + fi + + release_branch="${input_release_branch}" + if [[ -n "${input_release_sha}" ]]; then + git checkout --detach "${input_release_sha}" + release_sha="${input_release_sha}" + else + release_sha="$(git rev-parse HEAD)" + fi +else + : "${GITHUB_REF_NAME:?GITHUB_REF_NAME is required}" + : "${GITHUB_SHA:?GITHUB_SHA is required}" + release_branch="${GITHUB_REF_NAME}" + release_sha="${GITHUB_SHA}" +fi + +{ + echo "branch=${release_branch}" + echo "sha=${release_sha}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-validate-gh-env.sh b/scripts/termux-validate-gh-env.sh new file mode 100755 index 000000000000..02581fdebceb --- /dev/null +++ b/scripts/termux-validate-gh-env.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Lightweight post-toolbox check for jobs that rely on authenticated gh calls. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +command -v gh +gh auth status --hostname github.com + +printf 'GITHUB_REPOSITORY=%s\n' "${GITHUB_REPOSITORY}" +printf 'REPO=%s\n' "${REPO:-}" +printf 'GH_REPO_URL=%s\n' "${GH_REPO_URL:-}" +printf 'GH_WORKFLOW_URL=%s\n' "${GH_WORKFLOW_URL:-}" From bb38ff12834cab48b37638880da7f94c23371d6e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 07:12:29 +0000 Subject: [PATCH 08/25] Prepare Termux rust-v0.132.0 --- .github/termux-release.json | 15 ++ codex-rs/Cargo.lock | 247 ++++++++++++++------------- codex-rs/Cargo.toml | 2 + codex-rs/arg0/Cargo.toml | 1 + codex-rs/arg0/src/lib.rs | 29 +++- codex-rs/core/Cargo.toml | 5 + codex-rs/core/src/installation_id.rs | 8 +- codex-rs/execpolicy/Cargo.toml | 1 + codex-rs/execpolicy/src/amend.rs | 23 ++- codex-rs/tui/Cargo.toml | 2 +- codex-rs/tui/src/lib.rs | 8 +- codex-rs/utils/file-lock/BUILD.bazel | 6 + codex-rs/utils/file-lock/Cargo.toml | 10 ++ codex-rs/utils/file-lock/src/lib.rs | 168 ++++++++++++++++++ justfile | 5 + scripts/termux-lock-audit.sh | 126 ++++++++++++++ 16 files changed, 518 insertions(+), 138 deletions(-) create mode 100644 .github/termux-release.json create mode 100644 codex-rs/utils/file-lock/BUILD.bazel create mode 100644 codex-rs/utils/file-lock/Cargo.toml create mode 100644 codex-rs/utils/file-lock/src/lib.rs create mode 100755 scripts/termux-lock-audit.sh diff --git a/.github/termux-release.json b/.github/termux-release.json new file mode 100644 index 000000000000..3c4f4d419162 --- /dev/null +++ b/.github/termux-release.json @@ -0,0 +1,15 @@ +{ + "upstream_repo": "openai/codex", + "upstream_tag": "rust-v0.132.0", + "upstream_name": "0.132.0", + "upstream_html_url": "https://github.com/openai/codex/releases/tag/rust-v0.132.0", + "upstream_target": "main", + "upstream_release_id": "325545332", + "upstream_prerelease": false, + "release_train": "0.132.0", + "release_branch": "release/0.132.0", + "work_branch": "upstream/rust-v0.132.0", + "patch_branch": "wallentx/termux-target", + "patch_source_sha": "0e4b5a7e6b6a316c6e2f66b8223a490b9f093842", + "termux_tag": "rust-v0.132.0-termux" +} diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 5ced1803fa4c..2f9714c07d2c 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -402,7 +402,7 @@ checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "app_test_support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -1778,7 +1778,7 @@ dependencies = [ [[package]] name = "codex-agent-graph-store" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-protocol", @@ -1793,7 +1793,7 @@ dependencies = [ [[package]] name = "codex-agent-identity" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -1812,7 +1812,7 @@ dependencies = [ [[package]] name = "codex-analytics" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-app-server-protocol", "codex-git-utils", @@ -1832,7 +1832,7 @@ dependencies = [ [[package]] name = "codex-ansi-escape" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "ansi-to-tui", "ratatui", @@ -1841,7 +1841,7 @@ dependencies = [ [[package]] name = "codex-api" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_matches", @@ -1875,7 +1875,7 @@ dependencies = [ [[package]] name = "codex-app-server" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "app_test_support", @@ -1960,7 +1960,7 @@ dependencies = [ [[package]] name = "codex-app-server-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-app-server", "codex-app-server-protocol", @@ -1987,7 +1987,7 @@ dependencies = [ [[package]] name = "codex-app-server-daemon" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-app-server-protocol", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "codex-app-server-protocol" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2035,7 +2035,7 @@ dependencies = [ [[package]] name = "codex-app-server-test-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2056,7 +2056,7 @@ dependencies = [ [[package]] name = "codex-app-server-transport" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "axum", @@ -2095,7 +2095,7 @@ dependencies = [ [[package]] name = "codex-apply-patch" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -2114,7 +2114,7 @@ dependencies = [ [[package]] name = "codex-arg0" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-apply-patch", @@ -2123,6 +2123,7 @@ dependencies = [ "codex-sandboxing", "codex-shell-escalation", "codex-utils-absolute-path", + "codex-utils-file-lock", "codex-utils-home-dir", "dotenvy", "tempfile", @@ -2131,7 +2132,7 @@ dependencies = [ [[package]] name = "codex-async-utils" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "pretty_assertions", @@ -2141,7 +2142,7 @@ dependencies = [ [[package]] name = "codex-aws-auth" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "aws-config", "aws-credential-types", @@ -2156,7 +2157,7 @@ dependencies = [ [[package]] name = "codex-backend-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-api", @@ -2173,7 +2174,7 @@ dependencies = [ [[package]] name = "codex-backend-openapi-models" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "serde", "serde_json", @@ -2182,7 +2183,7 @@ dependencies = [ [[package]] name = "codex-bwrap" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "cc", "libc", @@ -2191,7 +2192,7 @@ dependencies = [ [[package]] name = "codex-chatgpt" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2214,7 +2215,7 @@ dependencies = [ [[package]] name = "codex-cli" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -2279,7 +2280,7 @@ dependencies = [ [[package]] name = "codex-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "bytes", @@ -2310,7 +2311,7 @@ dependencies = [ [[package]] name = "codex-cloud-requirements" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "base64 0.22.1", @@ -2335,7 +2336,7 @@ dependencies = [ [[package]] name = "codex-cloud-tasks" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -2367,7 +2368,7 @@ dependencies = [ [[package]] name = "codex-cloud-tasks-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -2382,7 +2383,7 @@ dependencies = [ [[package]] name = "codex-cloud-tasks-mock-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "chrono", @@ -2392,7 +2393,7 @@ dependencies = [ [[package]] name = "codex-code-mode" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-channel", "async-trait", @@ -2409,11 +2410,11 @@ dependencies = [ [[package]] name = "codex-collaboration-mode-templates" -version = "0.0.0" +version = "0.132.0-alpha.1" [[package]] name = "codex-config" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -2460,7 +2461,7 @@ dependencies = [ [[package]] name = "codex-connectors" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-app-server-protocol", @@ -2476,7 +2477,7 @@ dependencies = [ [[package]] name = "codex-core" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "arc-swap", @@ -2530,6 +2531,7 @@ dependencies = [ "codex-utils-absolute-path", "codex-utils-cache", "codex-utils-cargo-bin", + "codex-utils-file-lock", "codex-utils-home-dir", "codex-utils-image", "codex-utils-output-truncation", @@ -2594,7 +2596,7 @@ dependencies = [ [[package]] name = "codex-core-api" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-analytics", "codex-app-server-protocol", @@ -2613,7 +2615,7 @@ dependencies = [ [[package]] name = "codex-core-plugins" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -2651,7 +2653,7 @@ dependencies = [ [[package]] name = "codex-core-skills" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-analytics", @@ -2682,7 +2684,7 @@ dependencies = [ [[package]] name = "codex-debug-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2694,7 +2696,7 @@ dependencies = [ [[package]] name = "codex-exec" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -2740,7 +2742,7 @@ dependencies = [ [[package]] name = "codex-exec-server" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "arc-swap", @@ -2781,11 +2783,12 @@ dependencies = [ [[package]] name = "codex-execpolicy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", "codex-utils-absolute-path", + "codex-utils-file-lock", "multimap", "pretty_assertions", "serde", @@ -2798,7 +2801,7 @@ dependencies = [ [[package]] name = "codex-execpolicy-legacy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "allocative", "anyhow", @@ -2818,7 +2821,7 @@ dependencies = [ [[package]] name = "codex-experimental-api-macros" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "proc-macro2", "quote", @@ -2827,7 +2830,7 @@ dependencies = [ [[package]] name = "codex-extension-api" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-protocol", @@ -2836,7 +2839,7 @@ dependencies = [ [[package]] name = "codex-external-agent-migration" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-hooks", "pretty_assertions", @@ -2848,7 +2851,7 @@ dependencies = [ [[package]] name = "codex-external-agent-sessions" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "chrono", "codex-app-server-protocol", @@ -2862,7 +2865,7 @@ dependencies = [ [[package]] name = "codex-features" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-otel", "codex-protocol", @@ -2875,7 +2878,7 @@ dependencies = [ [[package]] name = "codex-feedback" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-login", @@ -2888,7 +2891,7 @@ dependencies = [ [[package]] name = "codex-file-search" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2904,7 +2907,7 @@ dependencies = [ [[package]] name = "codex-file-system" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-protocol", @@ -2914,7 +2917,7 @@ dependencies = [ [[package]] name = "codex-file-watcher" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "notify", "pretty_assertions", @@ -2925,7 +2928,7 @@ dependencies = [ [[package]] name = "codex-git-utils" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -2949,7 +2952,7 @@ dependencies = [ [[package]] name = "codex-goal-extension" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-extension-api", @@ -2961,7 +2964,7 @@ dependencies = [ [[package]] name = "codex-guardian" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-core", @@ -2971,7 +2974,7 @@ dependencies = [ [[package]] name = "codex-hooks" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -2994,7 +2997,7 @@ dependencies = [ [[package]] name = "codex-install-context" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-home-dir", "pretty_assertions", @@ -3003,7 +3006,7 @@ dependencies = [ [[package]] name = "codex-keyring-store" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "keyring", "tracing", @@ -3011,7 +3014,7 @@ dependencies = [ [[package]] name = "codex-linux-sandbox" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "clap", "codex-core", @@ -3034,7 +3037,7 @@ dependencies = [ [[package]] name = "codex-lmstudio" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-core", "codex-model-provider-info", @@ -3048,7 +3051,7 @@ dependencies = [ [[package]] name = "codex-login" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3090,7 +3093,7 @@ dependencies = [ [[package]] name = "codex-mcp" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-channel", @@ -3122,7 +3125,7 @@ dependencies = [ [[package]] name = "codex-mcp-server" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-arg0", @@ -3154,7 +3157,7 @@ dependencies = [ [[package]] name = "codex-memories-extension" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-core", @@ -3175,7 +3178,7 @@ dependencies = [ [[package]] name = "codex-memories-mcp" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-utils-absolute-path", @@ -3192,7 +3195,7 @@ dependencies = [ [[package]] name = "codex-memories-read" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-protocol", "codex-shell-command", @@ -3206,7 +3209,7 @@ dependencies = [ [[package]] name = "codex-memories-write" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -3241,7 +3244,7 @@ dependencies = [ [[package]] name = "codex-message-history" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-config", "pretty_assertions", @@ -3254,7 +3257,7 @@ dependencies = [ [[package]] name = "codex-model-provider" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-agent-identity", @@ -3278,7 +3281,7 @@ dependencies = [ [[package]] name = "codex-model-provider-info" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-api", "codex-app-server-protocol", @@ -3295,7 +3298,7 @@ dependencies = [ [[package]] name = "codex-models-manager" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "chrono", @@ -3316,7 +3319,7 @@ dependencies = [ [[package]] name = "codex-network-proxy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3347,7 +3350,7 @@ dependencies = [ [[package]] name = "codex-ollama" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "assert_matches", "async-stream", @@ -3366,7 +3369,7 @@ dependencies = [ [[package]] name = "codex-otel" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "chrono", "codex-api", @@ -3398,7 +3401,7 @@ dependencies = [ [[package]] name = "codex-plugin" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-config", "codex-utils-absolute-path", @@ -3408,7 +3411,7 @@ dependencies = [ [[package]] name = "codex-process-hardening" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "libc", "pretty_assertions", @@ -3416,7 +3419,7 @@ dependencies = [ [[package]] name = "codex-protocol" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chardetng", @@ -3456,7 +3459,7 @@ dependencies = [ [[package]] name = "codex-realtime-webrtc" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "libwebrtc", "thiserror 2.0.18", @@ -3465,7 +3468,7 @@ dependencies = [ [[package]] name = "codex-response-debug-context" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "base64 0.22.1", "codex-api", @@ -3476,7 +3479,7 @@ dependencies = [ [[package]] name = "codex-responses-api-proxy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -3493,7 +3496,7 @@ dependencies = [ [[package]] name = "codex-rmcp-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "axum", @@ -3531,7 +3534,7 @@ dependencies = [ [[package]] name = "codex-rollout" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3556,7 +3559,7 @@ dependencies = [ [[package]] name = "codex-rollout-trace" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-code-mode", @@ -3572,7 +3575,7 @@ dependencies = [ [[package]] name = "codex-sandboxing" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3593,7 +3596,7 @@ dependencies = [ [[package]] name = "codex-secrets" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "age", "anyhow", @@ -3614,7 +3617,7 @@ dependencies = [ [[package]] name = "codex-shell-command" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -3634,7 +3637,7 @@ dependencies = [ [[package]] name = "codex-shell-escalation" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3655,7 +3658,7 @@ dependencies = [ [[package]] name = "codex-skills" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-absolute-path", "include_dir", @@ -3664,7 +3667,7 @@ dependencies = [ [[package]] name = "codex-state" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -3687,7 +3690,7 @@ dependencies = [ [[package]] name = "codex-stdio-to-uds" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-uds", @@ -3699,7 +3702,7 @@ dependencies = [ [[package]] name = "codex-terminal-detection" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "tracing", @@ -3707,7 +3710,7 @@ dependencies = [ [[package]] name = "codex-test-binary-support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-arg0", "tempfile", @@ -3715,7 +3718,7 @@ dependencies = [ [[package]] name = "codex-thread-manager-sample" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -3726,7 +3729,7 @@ dependencies = [ [[package]] name = "codex-thread-store" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "chrono", @@ -3747,7 +3750,7 @@ dependencies = [ [[package]] name = "codex-tools" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-app-server-protocol", @@ -3767,7 +3770,7 @@ dependencies = [ [[package]] name = "codex-tui" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "arboard", @@ -3878,7 +3881,7 @@ dependencies = [ [[package]] name = "codex-uds" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-io", "pretty_assertions", @@ -3890,7 +3893,7 @@ dependencies = [ [[package]] name = "codex-utils-absolute-path" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "dirs", "dunce", @@ -3904,14 +3907,14 @@ dependencies = [ [[package]] name = "codex-utils-approval-presets" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-protocol", ] [[package]] name = "codex-utils-cache" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "lru 0.16.3", "sha1", @@ -3920,7 +3923,7 @@ dependencies = [ [[package]] name = "codex-utils-cargo-bin" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "assert_cmd", "runfiles", @@ -3929,7 +3932,7 @@ dependencies = [ [[package]] name = "codex-utils-cli" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "clap", "codex-protocol", @@ -3941,15 +3944,19 @@ dependencies = [ [[package]] name = "codex-utils-elapsed" -version = "0.0.0" +version = "0.132.0-alpha.1" + +[[package]] +name = "codex-utils-file-lock" +version = "0.132.0-alpha.1" [[package]] name = "codex-utils-fuzzy-match" -version = "0.0.0" +version = "0.132.0-alpha.1" [[package]] name = "codex-utils-home-dir" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-absolute-path", "dirs", @@ -3959,7 +3966,7 @@ dependencies = [ [[package]] name = "codex-utils-image" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "base64 0.22.1", "codex-utils-cache", @@ -3971,7 +3978,7 @@ dependencies = [ [[package]] name = "codex-utils-json-to-toml" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "serde_json", @@ -3980,7 +3987,7 @@ dependencies = [ [[package]] name = "codex-utils-oss" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-core", "codex-lmstudio", @@ -3990,7 +3997,7 @@ dependencies = [ [[package]] name = "codex-utils-output-truncation" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-protocol", "codex-utils-string", @@ -3999,7 +4006,7 @@ dependencies = [ [[package]] name = "codex-utils-path" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-absolute-path", "dunce", @@ -4009,7 +4016,7 @@ dependencies = [ [[package]] name = "codex-utils-plugins" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-exec-server", "codex-login", @@ -4022,7 +4029,7 @@ dependencies = [ [[package]] name = "codex-utils-pty" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "filedescriptor", @@ -4038,7 +4045,7 @@ dependencies = [ [[package]] name = "codex-utils-readiness" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "assert_matches", "async-trait", @@ -4049,14 +4056,14 @@ dependencies = [ [[package]] name = "codex-utils-rustls-provider" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "rustls", ] [[package]] name = "codex-utils-sandbox-summary" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-core", "codex-model-provider-info", @@ -4067,7 +4074,7 @@ dependencies = [ [[package]] name = "codex-utils-sleep-inhibitor" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "core-foundation 0.9.4", "libc", @@ -4077,14 +4084,14 @@ dependencies = [ [[package]] name = "codex-utils-stream-parser" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", ] [[package]] name = "codex-utils-string" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "regex-lite", @@ -4094,14 +4101,14 @@ dependencies = [ [[package]] name = "codex-utils-template" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", ] [[package]] name = "codex-v8-poc" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "v8", @@ -4109,7 +4116,7 @@ dependencies = [ [[package]] name = "codex-windows-sandbox" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -4360,7 +4367,7 @@ dependencies = [ [[package]] name = "core_test_support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -5474,7 +5481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8537,7 +8544,7 @@ dependencies = [ [[package]] name = "mcp_test_support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-login", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 5e0b16c4cb11..447c680eabd0 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -81,6 +81,7 @@ members = [ "v8-poc", "utils/absolute-path", "utils/cargo-bin", + "utils/file-lock", "git-utils", "utils/cache", "utils/image", @@ -219,6 +220,7 @@ codex-utils-cache = { path = "utils/cache" } codex-utils-cargo-bin = { path = "utils/cargo-bin" } codex-utils-cli = { path = "utils/cli" } codex-utils-elapsed = { path = "utils/elapsed" } +codex-utils-file-lock = { path = "utils/file-lock" } codex-utils-fuzzy-match = { path = "utils/fuzzy-match" } codex-utils-home-dir = { path = "utils/home-dir" } codex-utils-image = { path = "utils/image" } diff --git a/codex-rs/arg0/Cargo.toml b/codex-rs/arg0/Cargo.toml index 7ee21a770e49..7c2c17692020 100644 --- a/codex-rs/arg0/Cargo.toml +++ b/codex-rs/arg0/Cargo.toml @@ -20,6 +20,7 @@ codex-linux-sandbox = { workspace = true } codex-sandboxing = { workspace = true } codex-shell-escalation = { workspace = true } codex-utils-absolute-path = { workspace = true } +codex-utils-file-lock = { workspace = true } codex-utils-home-dir = { workspace = true } dotenvy = { workspace = true } tempfile = { workspace = true } diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index 2f6ae4653c65..5a558769a7e5 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -6,6 +6,8 @@ use std::path::PathBuf; use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1; use codex_exec_server::CODEX_FS_HELPER_ARG1; use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; +use codex_utils_file_lock::TryFileLockOutcome; +use codex_utils_file_lock::try_lock_exclusive_optional; use codex_utils_home_dir::find_codex_home; #[cfg(unix)] use std::os::unix::fs::symlink; @@ -33,12 +35,12 @@ pub struct Arg0DispatchPaths { /// Keeps the per-session PATH entry alive and locked for the process lifetime. pub struct Arg0PathEntryGuard { _temp_dir: TempDir, - _lock_file: File, + _lock_file: Option, paths: Arg0DispatchPaths, } impl Arg0PathEntryGuard { - fn new(temp_dir: TempDir, lock_file: File, paths: Arg0DispatchPaths) -> Self { + fn new(temp_dir: TempDir, lock_file: Option, paths: Arg0DispatchPaths) -> Self { Self { _temp_dir: temp_dir, _lock_file: lock_file, @@ -326,7 +328,13 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result Some(lock_file), + TryFileLockOutcome::Unsupported => None, + TryFileLockOutcome::WouldBlock => { + return Err(std::io::Error::from(std::io::ErrorKind::WouldBlock).into()); + } + }; for filename in &[ APPLY_PATCH_ARG0, @@ -445,10 +453,9 @@ fn try_lock_dir(dir: &Path) -> std::io::Result> { Err(err) => return Err(err), }; - match lock_file.try_lock() { - Ok(()) => Ok(Some(lock_file)), - Err(std::fs::TryLockError::WouldBlock) => Ok(None), - Err(err) => Err(err.into()), + match try_lock_exclusive_optional(&lock_file)? { + TryFileLockOutcome::Acquired => Ok(Some(lock_file)), + TryFileLockOutcome::WouldBlock | TryFileLockOutcome::Unsupported => Ok(None), } } @@ -562,7 +569,13 @@ mod tests { let dir = root.path().join("locked"); fs::create_dir(&dir)?; let lock_file = create_lock(&dir)?; - lock_file.try_lock()?; + match try_lock_exclusive_optional(&lock_file)? { + TryFileLockOutcome::Acquired => {} + TryFileLockOutcome::Unsupported => return Ok(()), + TryFileLockOutcome::WouldBlock => { + panic!("newly created lock file should not be locked"); + } + } janitor_cleanup(root.path())?; diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 8472918dcae9..fdf1d4b18a9e 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -63,6 +63,7 @@ codex-thread-store = { workspace = true } codex-tools = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-cache = { workspace = true } +codex-utils-file-lock = { workspace = true } codex-utils-image = { workspace = true } codex-utils-home-dir = { workspace = true } codex-utils-output-truncation = { workspace = true } @@ -125,6 +126,10 @@ openssl-sys = { workspace = true, features = ["vendored"] } [target.aarch64-unknown-linux-musl.dependencies] openssl-sys = { workspace = true, features = ["vendored"] } +# Build OpenSSL from source for Android builds. +[target.aarch64-linux-android.dependencies] +openssl-sys = { workspace = true, features = ["vendored"] } + [target.'cfg(unix)'.dependencies] codex-shell-escalation = { workspace = true } diff --git a/codex-rs/core/src/installation_id.rs b/codex-rs/core/src/installation_id.rs index a42e6b6d8353..a87c660f6132 100644 --- a/codex-rs/core/src/installation_id.rs +++ b/codex-rs/core/src/installation_id.rs @@ -11,6 +11,9 @@ use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::PermissionsExt; use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_file_lock::FileLockOutcome; +use codex_utils_file_lock::acquire_sibling_lock_dir; +use codex_utils_file_lock::lock_exclusive_optional; use tokio::fs; use uuid::Uuid; @@ -29,7 +32,10 @@ pub async fn resolve_installation_id(codex_home: &AbsolutePathBuf) -> Result None, + FileLockOutcome::Unsupported => Some(acquire_sibling_lock_dir(&path)?), + }; #[cfg(unix)] { diff --git a/codex-rs/execpolicy/Cargo.toml b/codex-rs/execpolicy/Cargo.toml index b22226a79e4b..b87af084d7d1 100644 --- a/codex-rs/execpolicy/Cargo.toml +++ b/codex-rs/execpolicy/Cargo.toml @@ -21,6 +21,7 @@ workspace = true anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] } codex-utils-absolute-path = { workspace = true } +codex-utils-file-lock = { workspace = true } multimap = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/codex-rs/execpolicy/src/amend.rs b/codex-rs/execpolicy/src/amend.rs index e25fd1bd9144..1ef36b1ff5d8 100644 --- a/codex-rs/execpolicy/src/amend.rs +++ b/codex-rs/execpolicy/src/amend.rs @@ -6,6 +6,10 @@ use std::io::Write; use std::path::Path; use std::path::PathBuf; +use codex_utils_file_lock::FileLockOutcome; +use codex_utils_file_lock::acquire_sibling_lock_dir; +use codex_utils_file_lock::lock_exclusive_optional; + use crate::decision::Decision; use crate::rule::NetworkRuleProtocol; use crate::rule::normalize_network_rule_host; @@ -154,10 +158,21 @@ fn append_locked_line(policy_path: &Path, line: &str) -> Result<(), AmendError> path: policy_path.to_path_buf(), source, })?; - file.lock().map_err(|source| AmendError::LockPolicyFile { - path: policy_path.to_path_buf(), - source, - })?; + let _lock_dir_guard = + match lock_exclusive_optional(&file).map_err(|source| AmendError::LockPolicyFile { + path: policy_path.to_path_buf(), + source, + })? { + FileLockOutcome::Acquired => None, + FileLockOutcome::Unsupported => { + Some(acquire_sibling_lock_dir(policy_path).map_err(|source| { + AmendError::LockPolicyFile { + path: policy_path.to_path_buf(), + source, + } + })?) + } + }; file.seek(SeekFrom::Start(0)) .map_err(|source| AmendError::SeekPolicyFile { diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index c213e92bae41..40624baba0d0 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -126,7 +126,7 @@ uuid = { workspace = true } codex-windows-sandbox = { workspace = true } tokio-util = { workspace = true, features = ["time"] } -[target.'cfg(not(target_os = "linux"))'.dependencies] +[target.'cfg(all(not(target_os = "linux"), not(target_os = "android")))'.dependencies] cpal = "0.15" [target.'cfg(unix)'.dependencies] diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 9ece5e3776f5..fbd1d7eb79bc 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -88,9 +88,9 @@ mod app_server_approval_conversions; mod app_server_session; mod approval_events; mod ascii_animation; -#[cfg(not(target_os = "linux"))] +#[cfg(not(any(target_os = "linux", target_os = "android")))] mod audio_device; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] #[allow(dead_code)] mod audio_device { use crate::app_event::RealtimeAudioDeviceKind; @@ -194,11 +194,11 @@ mod update_prompt; mod update_versions; mod updates; mod version; -#[cfg(not(target_os = "linux"))] +#[cfg(not(any(target_os = "linux", target_os = "android")))] mod voice; mod width; mod workspace_command; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] #[allow(dead_code)] mod voice { use crate::app_event_sender::AppEventSender; diff --git a/codex-rs/utils/file-lock/BUILD.bazel b/codex-rs/utils/file-lock/BUILD.bazel new file mode 100644 index 000000000000..face70c53518 --- /dev/null +++ b/codex-rs/utils/file-lock/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "file-lock", + crate_name = "codex_utils_file_lock", +) diff --git a/codex-rs/utils/file-lock/Cargo.toml b/codex-rs/utils/file-lock/Cargo.toml new file mode 100644 index 000000000000..5e3877fc1630 --- /dev/null +++ b/codex-rs/utils/file-lock/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "codex-utils-file-lock" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] diff --git a/codex-rs/utils/file-lock/src/lib.rs b/codex-rs/utils/file-lock/src/lib.rs new file mode 100644 index 000000000000..fdcfbdb81e75 --- /dev/null +++ b/codex-rs/utils/file-lock/src/lib.rs @@ -0,0 +1,168 @@ +use std::fs::File; +use std::fs::create_dir; +use std::fs::remove_dir; +use std::io; +use std::path::Path; +use std::path::PathBuf; +use std::thread; +use std::time::Duration; + +const LOCK_DIR_RETRY_SLEEP: Duration = Duration::from_millis(100); + +/// Result of acquiring a blocking advisory file lock. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileLockOutcome { + Acquired, + Unsupported, +} + +/// Result of attempting to acquire a non-blocking advisory file lock. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryFileLockOutcome { + Acquired, + WouldBlock, + Unsupported, +} + +/// Result of attempting to acquire a non-blocking lock directory. +#[derive(Debug, PartialEq, Eq)] +pub enum TryLockDirOutcome { + Acquired(LockDirGuard), + WouldBlock, +} + +/// Guard for a sibling lock directory created with an atomic `mkdir`. +#[derive(Debug, PartialEq, Eq)] +pub struct LockDirGuard { + path: PathBuf, +} + +impl Drop for LockDirGuard { + fn drop(&mut self) { + let _ = remove_dir(&self.path); + } +} + +/// Acquires an exclusive advisory file lock, treating unsupported file locking +/// as a distinct outcome for platforms such as Termux. +pub fn lock_exclusive_optional(file: &File) -> io::Result { + match file.lock() { + Ok(()) => Ok(FileLockOutcome::Acquired), + Err(err) if err.kind() == io::ErrorKind::Unsupported => Ok(FileLockOutcome::Unsupported), + Err(err) => Err(err), + } +} + +/// Attempts to acquire an exclusive advisory file lock without blocking, +/// preserving `WouldBlock` and unsupported file locking as distinct outcomes. +pub fn try_lock_exclusive_optional(file: &File) -> io::Result { + match file.try_lock() { + Ok(()) => Ok(TryFileLockOutcome::Acquired), + Err(std::fs::TryLockError::WouldBlock) => Ok(TryFileLockOutcome::WouldBlock), + Err(std::fs::TryLockError::Error(err)) if err.kind() == io::ErrorKind::Unsupported => { + Ok(TryFileLockOutcome::Unsupported) + } + Err(std::fs::TryLockError::Error(err)) => Err(err), + } +} + +/// Returns the sibling directory path used as a fallback lock for `path`. +pub fn sibling_lock_dir(path: &Path) -> PathBuf { + let Some(file_name) = path.file_name() else { + return path.with_file_name(".lock"); + }; + + let mut lock_name = file_name.to_os_string(); + lock_name.push(".lock"); + path.with_file_name(lock_name) +} + +/// Acquires a sibling lock directory, blocking until it is available. +pub fn acquire_sibling_lock_dir(path: &Path) -> io::Result { + loop { + match try_acquire_sibling_lock_dir(path)? { + TryLockDirOutcome::Acquired(guard) => return Ok(guard), + TryLockDirOutcome::WouldBlock => thread::sleep(LOCK_DIR_RETRY_SLEEP), + } + } +} + +/// Attempts to acquire a sibling lock directory without blocking. +pub fn try_acquire_sibling_lock_dir(path: &Path) -> io::Result { + let lock_dir = sibling_lock_dir(path); + match create_dir(&lock_dir) { + Ok(()) => Ok(TryLockDirOutcome::Acquired(LockDirGuard { path: lock_dir })), + Err(err) if err.kind() == io::ErrorKind::AlreadyExists => Ok(TryLockDirOutcome::WouldBlock), + Err(err) => Err(err), + } +} + +/// Attempts to acquire a shared advisory file lock without blocking, +/// preserving `WouldBlock` and unsupported file locking as distinct outcomes. +pub fn try_lock_shared_optional(file: &File) -> io::Result { + match file.try_lock_shared() { + Ok(()) => Ok(TryFileLockOutcome::Acquired), + Err(std::fs::TryLockError::WouldBlock) => Ok(TryFileLockOutcome::WouldBlock), + Err(std::fs::TryLockError::Error(err)) if err.kind() == io::ErrorKind::Unsupported => { + Ok(TryFileLockOutcome::Unsupported) + } + Err(std::fs::TryLockError::Error(err)) => Err(err), + } +} + +#[cfg(test)] +mod tests { + use super::TryLockDirOutcome; + use super::sibling_lock_dir; + use super::try_acquire_sibling_lock_dir; + use std::fs; + use std::path::PathBuf; + use std::time::SystemTime; + + fn unique_temp_file_path(name: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("system clock should be after Unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "codex-file-lock-{name}-{}-{nanos}", + std::process::id() + )) + } + + #[test] + fn sibling_lock_dir_appends_lock_suffix() { + let path = PathBuf::from("/tmp/history.jsonl"); + + assert_eq!( + sibling_lock_dir(&path), + PathBuf::from("/tmp/history.jsonl.lock") + ); + } + + #[test] + fn try_acquire_sibling_lock_dir_is_exclusive_until_drop() { + let path = unique_temp_file_path("exclusive"); + let lock_dir = sibling_lock_dir(&path); + let _ = fs::remove_dir_all(&lock_dir); + + let guard = match try_acquire_sibling_lock_dir(&path).expect("acquire lock dir") { + TryLockDirOutcome::Acquired(guard) => guard, + TryLockDirOutcome::WouldBlock => panic!("first lock attempt should acquire"), + }; + assert!(lock_dir.is_dir()); + + assert!(matches!( + try_acquire_sibling_lock_dir(&path).expect("try acquire held lock dir"), + TryLockDirOutcome::WouldBlock + )); + + drop(guard); + assert!(!lock_dir.exists()); + + let reacquired = try_acquire_sibling_lock_dir(&path).expect("reacquire lock dir"); + assert!(matches!(reacquired, TryLockDirOutcome::Acquired(_))); + + let _ = fs::remove_dir_all(lock_dir); + } +} diff --git a/justfile b/justfile index ab2fbc63629a..569a04695216 100644 --- a/justfile +++ b/justfile @@ -116,6 +116,11 @@ argument-comment-lint *args: argument-comment-lint-from-source *args: {{ justfile_directory() }}/tools/argument-comment-lint/run.py "$@" +# Audit advisory file locks that may need Termux Unsupported handling. +[no-cd] +termux-lock-audit *args: + {{ justfile_directory() }}/scripts/termux-lock-audit.sh "$@" + # Tail logs from the state SQLite database log *args: if [ "${1:-}" = "--" ]; then shift; fi; cargo run -p codex-state --bin logs_client -- "$@" diff --git a/scripts/termux-lock-audit.sh b/scripts/termux-lock-audit.sh new file mode 100755 index 000000000000..ac40f8e57246 --- /dev/null +++ b/scripts/termux-lock-audit.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: scripts/termux-lock-audit.sh [--strict] + +Audits Rust advisory file-lock usage that may need Termux compatibility handling. + +By default this script prints findings and exits successfully. With --strict, it +exits non-zero when candidate file-lock calls are found in files that do not also +mention Unsupported/TryLockError handling. +USAGE +} + +strict=false +for arg in "$@"; do + case "$arg" in + --strict) + strict=true + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "unknown argument: $arg" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if ! command -v rg >/dev/null 2>&1; then + echo "error: rg is required for this audit" >&2 + exit 1 +fi + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +unsupported_pattern='std::fs::TryLockError|fs::TryLockError|TryLockError::|ErrorKind::Unsupported|std::io::ErrorKind::Unsupported' +lock_context_pattern='(\.lock\(\)|\.try_lock\(|\.try_lock_shared\(|TryLockError|ErrorKind::Unsupported)' +candidate_file_lock_pattern='\b([A-Za-z_][A-Za-z0-9_]*_file|file)\.(lock|try_lock|try_lock_shared)\(' +candidate_false_positive_pattern='poisoned' +helper_pattern='codex_utils_file_lock|FileLockOutcome|TryFileLockOutcome|lock_exclusive_optional|try_lock_exclusive_optional|try_lock_shared_optional' + +print_section() { + printf '\n== %s ==\n' "$1" +} + +print_section "Unsupported-aware lock handling" +echo "These files already mention Unsupported/TryLockError and are likely patched or intentionally reviewed:" + +unsupported_count=0 +while IFS= read -r file; do + if rg -q "$lock_context_pattern" "$file"; then + rg -n -H "$lock_context_pattern" "$file" + unsupported_count=$((unsupported_count + 1)) + fi +done < <(rg -l "$unsupported_pattern" codex-rs -g '*.rs' || true) + +if [ "$unsupported_count" -eq 0 ]; then + echo "No unsupported-aware lock handling found." +fi + +print_section "Optional file-lock helper usage" +echo "These files use the shared optional advisory file-lock helper:" + +helper_count=0 +while IFS= read -r file; do + rg -n -H "$helper_pattern" "$file" + helper_count=$((helper_count + 1)) +done < <(rg -l "$helper_pattern" codex-rs -g '*.rs' || true) + +if [ "$helper_count" -eq 0 ]; then + echo "No optional file-lock helper usage found." +fi + +print_section "Candidate file-lock calls for manual review" +echo "These receiver names may be std::fs::File advisory locks, but the file does not mention Unsupported/TryLockError handling." +echo "This section can include false positives, such as mutexes wrapping a file." + +review_count=0 +while IFS= read -r file; do + if ! rg -q "$unsupported_pattern" "$file"; then + matches="$(rg -n -H "$candidate_file_lock_pattern" "$file" | rg -v "$candidate_false_positive_pattern" || true)" + if [ -n "$matches" ]; then + echo "$matches" + review_count=$((review_count + 1)) + fi + fi +done < <(rg -l "$candidate_file_lock_pattern" codex-rs -g '*.rs' || true) + +if [ "$review_count" -eq 0 ]; then + echo "No unpatched candidate file-lock calls found." +fi + +print_section "Candidate file-lock calls already in unsupported-aware files" +echo "These are candidate file-lock calls in files that already mention Unsupported/TryLockError handling:" + +aware_candidate_count=0 +while IFS= read -r file; do + if rg -q "$unsupported_pattern" "$file"; then + matches="$(rg -n -H "$candidate_file_lock_pattern" "$file" | rg -v "$candidate_false_positive_pattern" || true)" + if [ -n "$matches" ]; then + echo "$matches" + aware_candidate_count=$((aware_candidate_count + 1)) + fi + fi +done < <(rg -l "$candidate_file_lock_pattern" codex-rs -g '*.rs' || true) + +if [ "$aware_candidate_count" -eq 0 ]; then + echo "No unsupported-aware candidate file-lock calls found." +fi + +print_section "Summary" +echo "unsupported-aware files: $unsupported_count" +echo "optional helper files: $helper_count" +echo "review candidate files: $review_count" +echo "unsupported-aware candidate files: $aware_candidate_count" + +if [ "$strict" = true ] && [ "$review_count" -gt 0 ]; then + echo "strict mode: manual-review candidate files were found" >&2 + exit 1 +fi From d84294c0d67dc0cca7b687ca4ade38579d26fa04 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 16:38:37 +0000 Subject: [PATCH 09/25] Seed Termux release automation --- .github/workflows/rust-release.yml | 1765 +++++++---------- .github/workflows/shell-tool-mcp.yml | 461 +++++ .../workflows/termux-release-checkpoint.yml | 103 + .github/workflows/termux-release-deploy.yml | 243 +++ .github/workflows/termux-release-promote.yml | 135 ++ scripts/termux-configure-git.sh | 40 + scripts/termux-create-checkpoint-pr.sh | 275 +++ ...ermux-create-or-update-mirrored-release.sh | 111 ++ scripts/termux-download-release-artifact.sh | 78 + scripts/termux-find-release-pr.sh | 53 + scripts/termux-read-release-metadata.sh | 77 + scripts/termux-release-asset-state.sh | 37 + scripts/termux-release-paths.sh | 55 + scripts/termux-resolve-release-ref.sh | 36 + scripts/termux-validate-gh-env.sh | 16 + 15 files changed, 2444 insertions(+), 1041 deletions(-) create mode 100644 .github/workflows/shell-tool-mcp.yml create mode 100644 .github/workflows/termux-release-checkpoint.yml create mode 100644 .github/workflows/termux-release-deploy.yml create mode 100644 .github/workflows/termux-release-promote.yml create mode 100755 scripts/termux-configure-git.sh create mode 100755 scripts/termux-create-checkpoint-pr.sh create mode 100755 scripts/termux-create-or-update-mirrored-release.sh create mode 100755 scripts/termux-download-release-artifact.sh create mode 100755 scripts/termux-find-release-pr.sh create mode 100755 scripts/termux-read-release-metadata.sh create mode 100755 scripts/termux-release-asset-state.sh create mode 100755 scripts/termux-release-paths.sh create mode 100755 scripts/termux-resolve-release-ref.sh create mode 100755 scripts/termux-validate-gh-env.sh diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 50953506d325..56d0977e6e67 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -4,46 +4,49 @@ # git tag -a rust-v0.1.0 -m "Release 0.1.0" # git push origin rust-v0.1.0 # ``` -# -# To use external macOS signing, manually dispatch `release_mode=build_unsigned`, -# sign the unsigned macOS artifacts in a secure enclave, upload the signed handoff -# archive as a GitHub Release asset, then manually dispatch -# `release_mode=promote_signed` with `unsigned_run_id` and `signed_macos_asset`. -# The signed handoff archive should contain target or artifact directories such -# as `aarch64-apple-darwin/` with signed binaries. name: rust-release on: - push: - tags: - - "rust-v*.*.*" + pull_request: + branches: + - "release/**" workflow_dispatch: inputs: - release_mode: - description: "build_unsigned creates unsigned macOS handoff artifacts; promote_signed finishes a release from signed macOS handoff artifacts." + source_ref: + description: "Branch, tag, or commit SHA to build (code only; keeps .github from dispatched ref)" required: false + type: string + default: "" + build_target: + description: "Target to build" + required: true type: choice - default: build_unsigned + default: all options: - - build_unsigned - - promote_signed - sign_macos: - description: "Deprecated compatibility input; use release_mode instead." - required: false - type: boolean - default: false - unsigned_run_id: - description: "For promote_signed: workflow run id from the build_unsigned run." - required: false - type: string - signed_macos_asset: - description: "For promote_signed: exact GitHub Release asset name containing signed macOS handoff artifacts." - required: false - type: string - signed_macos_sha256: - description: "For promote_signed: optional SHA-256 of signed_macos_asset." - required: false - type: string + - all + - aarch64-apple-darwin + - x86_64-apple-darwin + - x86_64-unknown-linux-musl + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-musl + - aarch64-unknown-linux-gnu + - aarch64-linux-android + - x86_64-pc-windows-msvc + - aarch64-pc-windows-msvc + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read concurrency: group: ${{ github.workflow }} @@ -51,72 +54,26 @@ concurrency: jobs: tag-check: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - uses: dtolnay/rust-toolchain@1.92 - name: Validate tag matches Cargo.toml version shell: bash - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - REQUESTED_SIGN_MACOS: ${{ inputs.sign_macos }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} run: | set -euo pipefail echo "::group::Tag validation" - case "${RELEASE_MODE}" in - signed) - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - echo "❌ Manual rust-release runs must use release_mode=build_unsigned or release_mode=promote_signed" - exit 1 - fi - ;; - build_unsigned) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=build_unsigned is only valid for manual runs" - exit 1 - fi - ;; - promote_signed) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=promote_signed is only valid for manual runs" - exit 1 - fi - if [[ ! "${UNSIGNED_RUN_ID}" =~ ^[0-9]+$ ]]; then - echo "❌ release_mode=promote_signed requires unsigned_run_id to be a workflow run id" - exit 1 - fi - if [[ -z "${SIGNED_MACOS_ASSET}" ]]; then - echo "❌ release_mode=promote_signed requires signed_macos_asset" - exit 1 - fi - if [[ "${SIGNED_MACOS_ASSET}" == */* || "${SIGNED_MACOS_ASSET}" == *"*"* || "${SIGNED_MACOS_ASSET}" == *"?"* || "${SIGNED_MACOS_ASSET}" == *"["* ]]; then - echo "❌ signed_macos_asset must be an exact release asset name, not a path or glob" - exit 1 - fi - if [[ "${UNSIGNED_RUN_ID}" == "${GITHUB_RUN_ID}" ]]; then - echo "❌ unsigned_run_id must refer to the earlier build_unsigned run, not this run" - exit 1 - fi - ;; - *) - echo "❌ Unknown release_mode '${RELEASE_MODE}'" - exit 1 - ;; - esac - - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${REQUESTED_SIGN_MACOS}" == "true" ]]; then - echo "::warning title=Deprecated sign_macos input ignored::Use release_mode=build_unsigned or release_mode=promote_signed instead." - fi - # 1. Must be a tag and match the regex [[ "${GITHUB_REF_TYPE}" == "tag" ]] \ || { echo "❌ Not a tag push"; exit 1; } - [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \ + [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?(-termux)?$ ]] \ || { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; } # 2. Extract versions @@ -131,115 +88,198 @@ jobs: echo "✅ Tag and Cargo.toml agree (${tag_ver})" echo "::endgroup::" - build: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} + select-build-matrix: + if: always() && (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') needs: tag-check - name: Build - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: ${{ matrix.runs_on || matrix.runner }} - # Release builds can take a long time, so leave some headroom to avoid - # having to restart the full workflow due to a timeout. - timeout-minutes: 90 - permissions: - contents: read - id-token: write + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.select.outputs.matrix }} + build_version: ${{ steps.select.outputs.build_version }} + steps: + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - id: select + shell: bash + env: + REPO_OWNER: ${{ github.repository_owner }} + EVENT_NAME: ${{ github.event_name }} + BUILD_TARGET: ${{ inputs.build_target }} + run: | + set -euo pipefail + + matrix='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04","target":"aarch64-linux-android"}, + {"runner":"windows-latest","target":"x86_64-pc-windows-msvc"}, + {"runner":"windows-11-arm","target":"aarch64-pc-windows-msvc"} + ]' + + if [[ "${REPO_OWNER}" != "openai" ]]; then + matrix="$(jq -c '[.[] | select(.runner | startswith("macos") | not)]' <<< "${matrix}")" + fi + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + matrix="$(jq -c '[.[] | select(.target == "aarch64-linux-android")]' <<< "${matrix}")" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" && -n "${BUILD_TARGET}" && "${BUILD_TARGET}" != "all" ]]; then + matrix="$(jq -c --arg build_target "${BUILD_TARGET}" '[.[] | select(.target == $build_target)]' <<< "${matrix}")" + fi + + if [[ "$(jq 'length' <<< "${matrix}")" -eq 0 ]]; then + echo "No build targets selected after applying owner/dispatch filters." >&2 + exit 1 + fi + + echo "matrix={\"include\":${matrix}}" >> "$GITHUB_OUTPUT" + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + metadata=".github/termux-release.json" + if [[ ! -f "${metadata}" ]]; then + echo "${metadata} is required for release train PR builds" >&2 + exit 1 + fi + upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" + if [[ -z "${upstream_tag}" || "${upstream_tag}" != rust-v* ]]; then + echo "Unable to determine upstream rust tag from ${metadata}" >&2 + exit 1 + fi + build_version="${upstream_tag#rust-v}" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then + latest_release_tag="$( + GH_TOKEN="${{ github.token }}" gh api repos/openai/codex/releases/latest --jq '.tag_name' + )" + + if [[ -z "${latest_release_tag}" || "${latest_release_tag}" != rust-v* ]]; then + echo "Unable to determine latest stable release tag from openai/codex" >&2 + exit 1 + fi + + latest_release_version="${latest_release_tag#rust-v}" + build_version="${latest_release_version}-dev" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + fi + + build: + if: >- + always() && + (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') && + needs.select-build-matrix.result == 'success' + needs: + - tag-check + - select-build-matrix + name: Build - ${{ matrix.runner }} - ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + permissions: write-all + timeout-minutes: 120 defaults: run: working-directory: codex-rs env: - # 2026-03-04: temporarily change releases to use thin LTO because - # Ubuntu ARM is timing out at 60 minutes. - CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' }} + CODEX_BWRAP_ENABLE_FFI: ${{ contains(matrix.target, 'unknown-linux') && '1' || '0' }} + CODEX_BUILD_VERSION: ${{ needs.select-build-matrix.outputs.build_version }} + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" strategy: fail-fast: false - matrix: - include: - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - # Release artifacts intentionally ship MUSL-linked Linux binaries. - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - bundle: primary - artifact_name: x86_64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - bundle: app-server - artifact_name: x86_64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - bundle: primary - artifact_name: aarch64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - bundle: app-server - artifact_name: aarch64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" + matrix: ${{ fromJson(needs.select-build-matrix.outputs.matrix) }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main with: - persist-credentials: false - - name: Print runner specs (Linux) - if: ${{ runner.os == 'Linux' }} + verbose: true + - name: Checkout selected source ref + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + uses: actions/checkout@v6 + with: + ref: ${{ inputs.source_ref }} + path: __source__ + fetch-depth: 1 + - name: Overlay selected source ref (excluding .github) + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} shell: bash + working-directory: ${{ github.workspace }} run: | set -euo pipefail - cpu_model="$(lscpu | awk -F: '/Model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}')" - total_ram="$(awk '/MemTotal/ {printf "%.1f GiB\n", $2 / 1024 / 1024}' /proc/meminfo)" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(uname -a)" - echo "CPU model: ${cpu_model}" - echo "Logical CPUs: $(nproc)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . - - name: Print runner specs (macOS) - if: ${{ runner.os == 'macOS' }} + echo "Using source ref: ${{ inputs.source_ref }}" + + # Keep workflow/actions from the dispatch ref while replacing all other files. + find . -mindepth 1 -maxdepth 1 \ + ! -name .git \ + ! -name .github \ + ! -name __source__ \ + -exec rm -rf -- {} + + tar --exclude='.github' -C __source__ -cf - . | tar -xf - + rm -rf __source__ + + - name: Apply selected build version + if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} shell: bash + working-directory: ${{ github.workspace }} run: | set -euo pipefail - total_ram="$(sysctl -n hw.memsize | awk '{printf "%.1f GiB\n", $1 / 1024 / 1024 / 1024}')" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(sw_vers -productName) $(sw_vers -productVersion)" - echo "Hardware model: $(sysctl -n hw.model)" - echo "CPU architecture: $(uname -m)" - echo "Logical CPUs: $(sysctl -n hw.logicalcpu)" - echo "Physical CPUs: $(sysctl -n hw.physicalcpu)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . + if [[ -z "${CODEX_BUILD_VERSION}" ]]; then + echo "CODEX_BUILD_VERSION is empty for workflow_dispatch" >&2 + exit 1 + fi + echo "Using build version: ${CODEX_BUILD_VERSION}" + BUILD_VERSION="${CODEX_BUILD_VERSION}" perl -0777 -i -pe \ + 's/(\[workspace\.package\][^\[]*?version = ")([^"]+)(")/${1}$ENV{BUILD_VERSION}$3/s' \ + codex-rs/Cargo.toml + + grep -n "^\[workspace.package\]\|^version = " codex-rs/Cargo.toml | head -n 4 + + - name: Select Android NDK + if: ${{ matrix.target == 'aarch64-linux-android' }} + shell: bash + run: | + set -euo pipefail + + fallback_version="29.0.13113456" + ndk_root="${ANDROID_HOME}/ndk" + selected_ndk="" + + if [[ -d "${ndk_root}" ]]; then + while IFS= read -r candidate; do + toolchain="${candidate}/toolchains/llvm/prebuilt/linux-x86_64" + if [[ -x "${toolchain}/bin/aarch64-linux-android24-clang" ]]; then + selected_ndk="${candidate}" + break + fi + done < <(find "${ndk_root}" -mindepth 1 -maxdepth 1 -type d | sort -Vr) + fi + + if [[ -z "${selected_ndk}" ]]; then + echo "No usable preinstalled Android NDK found; installing ${fallback_version}" + yes | sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses || true + sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" "ndk;${fallback_version}" + selected_ndk="${ndk_root}/${fallback_version}" + fi + + if [[ ! -x "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" ]]; then + echo "Selected Android NDK does not contain a usable aarch64 API 24 clang: ${selected_ndk}" >&2 + exit 1 + fi + + echo "Selected Android NDK: ${selected_ndk}" + "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" --version + echo "ANDROID_NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" + echo "NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" - name: Install Linux bwrap build dependencies - if: ${{ runner.os == 'Linux' }} + if: ${{ runner.os == 'Linux' && matrix.target != 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail @@ -254,9 +294,173 @@ jobs: sudo apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 fi - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@1.93 with: targets: ${{ matrix.target }} + - name: Set up sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + uses: mozilla-actions/sccache-action@main + - name: Enable sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + shell: bash + run: | + set -euo pipefail + echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_VERSION=android-release-${{ matrix.target }}" >> "$GITHUB_ENV" + echo "SCCACHE_CACHE_SIZE=5G" >> "$GITHUB_ENV" + - name: Ensure Rust target is installed + shell: bash + run: | + set -euo pipefail + rustup target add "${{ matrix.target }}" + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android build environment + shell: bash + run: | + set -euo pipefail + ndk="${ANDROID_NDK_HOME}" + target="${{ matrix.target }}" + + # Set up the Android toolchain + toolchain="${ndk}/toolchains/llvm/prebuilt/linux-x86_64" + + # Use API level 24 to follow termux-build convention + api_level="24" + + # Configure Cargo to use the NDK linker + cargo_target_var="CARGO_TARGET_${target^^}_LINKER" + cargo_target_var="${cargo_target_var//-/_}" + echo "${cargo_target_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + # Set CC and AR for the target + target_cc_var="CC_${target}" + target_cc_var="${target_cc_var//-/_}" + echo "${target_cc_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + target_cxx_var="CXX_${target}" + target_cxx_var="${target_cxx_var//-/_}" + echo "${target_cxx_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang++" >> "$GITHUB_ENV" + + target_ar_var="AR_${target}" + target_ar_var="${target_ar_var//-/_}" + echo "${target_ar_var}=${toolchain}/bin/llvm-ar" >> "$GITHUB_ENV" + + # Add toolchain to PATH + echo "${toolchain}/bin" >> "$GITHUB_PATH" + + tls_align_source="${RUNNER_TEMP}/codex-android-tls-align.S" + tls_align_object="${RUNNER_TEMP}/codex-android-tls-align.o" + cat > "${tls_align_source}" <<'EOF' + .section .tdata.codex_android_tls_align,"awT",%progbits + .p2align 6 + .globl __codex_android_tls_align + .hidden __codex_android_tls_align + .type __codex_android_tls_align, %object + __codex_android_tls_align: + .byte 0 + .size __codex_android_tls_align, 1 + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${tls_align_source}" -o "${tls_align_object}" + + v8_compat_source="${RUNNER_TEMP}/codex-android-v8-compat.c" + v8_compat_object="${RUNNER_TEMP}/codex-android-v8-compat.o" + cat > "${v8_compat_source}" <<'EOF' + #include + #include + + extern int posix_memalign(void **memptr, size_t alignment, size_t size); + extern double strtod(const char *nptr, char **endptr); + extern float strtof(const char *nptr, char **endptr); + + void *aligned_alloc(size_t alignment, size_t size) { + void *ptr = 0; + if (posix_memalign(&ptr, alignment, size) != 0) { + return 0; + } + return ptr; + } + + double strtod_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtod(nptr, endptr); + } + + float strtof_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtof(nptr, endptr); + } + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${v8_compat_source}" -o "${v8_compat_object}" + + builtins_archive="$( + find "${toolchain}/lib/clang" \ + -path "*/lib/linux/libclang_rt.builtins-aarch64-android.a" \ + -print -quit + )" + if [[ -z "${builtins_archive}" ]]; then + echo "Could not find Android compiler-rt builtins archive under ${toolchain}/lib/clang" >&2 + exit 1 + fi + builtins_dir="$(dirname "${builtins_archive}")" + + # Provide compatibility symbols and C++ ABI needed by the Android rusty_v8 archive. + rustflags_var="CARGO_TARGET_${target^^}_RUSTFLAGS" + rustflags_var="${rustflags_var//-/_}" + echo "${rustflags_var}=-C link-arg=${tls_align_object} -C link-arg=${v8_compat_object} -C link-arg=-Wl,-u,__codex_android_tls_align -C link-arg=-L${builtins_dir} -C link-arg=-lclang_rt.builtins-aarch64-android -C link-arg=-lc++abi -C link-arg=-Wl,-z,max-page-size=65536" >> "$GITHUB_ENV" + + + # Configure for vendored OpenSSL build + echo "CARGO_BUILD_TARGET_APPLIES_TO_HOST=false" >> "$GITHUB_ENV" + echo "CARGO_BUILD_TARGET=${{ matrix.target }}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android rusty_v8 artifact overrides + env: + GH_TOKEN: ${{ github.token }} + TARGET: ${{ matrix.target }} + shell: bash + run: | + set -euo pipefail + binding_dir="${RUNNER_TEMP}/rusty_v8" + archive="${binding_dir}/librusty_v8_release_${TARGET}.a.gz" + binding_path="${binding_dir}/src_binding_release_${TARGET}.rs" + checksums_path="${binding_dir}/rusty_v8_release_${TARGET}.sha256" + artifact_repository="wallentx/codex-termux" + release_tag="rusty-v8-v147.4.0" + mkdir -p "${binding_dir}" + echo "Downloading Android rusty_v8 artifacts from ${artifact_repository}@${release_tag}" + gh release download "${release_tag}" \ + --repo "${artifact_repository}" \ + --dir "${binding_dir}" \ + --pattern "librusty_v8_release_${TARGET}.a.gz" \ + --pattern "src_binding_release_${TARGET}.rs" \ + --pattern "rusty_v8_release_${TARGET}.sha256" + (cd "${binding_dir}" && sha256sum -c "$(basename "${checksums_path}")") + echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV" + echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Install termux-elf-cleaner + shell: bash + run: | + set -euo pipefail + version="v3.0.1" + src_dir="${RUNNER_TEMP}/termux-elf-cleaner-src" + build_dir="${RUNNER_TEMP}/termux-elf-cleaner-build" + bin_dir="${RUNNER_TEMP}/termux-elf-cleaner-bin" + + rm -rf "${src_dir}" "${build_dir}" "${bin_dir}" + mkdir -p "${src_dir}" "${build_dir}" "${bin_dir}" + + curl -fsSL "https://github.com/termux/termux-elf-cleaner/archive/refs/tags/${version}.tar.gz" \ + | tar -xzf - --strip-components=1 -C "${src_dir}" + + cmake -S "${src_dir}" -B "${build_dir}" -DCMAKE_BUILD_TYPE=Release + cmake --build "${build_dir}" --parallel + + install "${build_dir}/termux-elf-cleaner" "${bin_dir}/termux-elf-cleaner" + echo "${bin_dir}" >> "$GITHUB_PATH" - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Use hermetic Cargo home (musl) @@ -269,12 +473,25 @@ jobs: echo "${cargo_home}/bin" >> "$GITHUB_PATH" : > "${cargo_home}/config.toml" + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ${{ github.workspace }}/.cargo-home/bin/ + ${{ github.workspace }}/.cargo-home/registry/index/ + ${{ github.workspace }}/.cargo-home/registry/cache/ + ${{ github.workspace }}/.cargo-home/git/db/ + ${{ github.workspace }}/codex-rs/target/ + key: cargo-${{ matrix.runner }}-${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }} + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install Zig - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + uses: mlugg/setup-zig@v2 with: version: 0.14.0 - use-cache: false - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install musl build tools @@ -310,12 +527,6 @@ jobs: shell: bash run: | set -euo pipefail - # Avoid problematic aws-lc jitter entropy code path on musl builders. - echo "AWS_LC_SYS_NO_JITTER_ENTROPY=1" >> "$GITHUB_ENV" - target_no_jitter="AWS_LC_SYS_NO_JITTER_ENTROPY_${{ matrix.target }}" - target_no_jitter="${target_no_jitter//-/_}" - echo "${target_no_jitter}=1" >> "$GITHUB_ENV" - # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. echo "RUSTFLAGS=" >> "$GITHUB_ENV" echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" @@ -340,94 +551,70 @@ jobs: echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" - - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }} - name: Configure musl rusty_v8 artifact overrides and verify checksums - uses: ./.github/actions/setup-rusty-v8-musl - with: - target: ${{ matrix.target }} - - - if: ${{ contains(matrix.target, 'linux') && matrix.bundle == 'primary' }} - name: Build bwrap and export digest + - name: Cargo build shell: bash run: | set -euo pipefail - target="${{ matrix.target }}" - cargo build --target "$target" --release --timings --bin bwrap - bwrap_path="target/${target}/release/bwrap" - if [[ ! -f "$bwrap_path" ]]; then - echo "bwrap binary ${bwrap_path} not found" - exit 1 + if [[ "${{ matrix.target }}" == 'aarch64-linux-android' ]]; then + export CARGO_BUILD_JOBS=4 + export CARGO_PROFILE_RELEASE_LTO=thin + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 + cargo build --target ${{ matrix.target }} --release --bin codex + exit 0 fi - digest="$(sha256sum "$bwrap_path" | awk '{print $1}')" - echo "CODEX_BWRAP_SHA256=${digest}" >> "$GITHUB_ENV" - echo "Built bwrap ${bwrap_path} with sha256:${digest}" - - - name: Cargo build + if [[ "${{ contains(matrix.target, 'windows') }}" == 'true' ]]; then + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy --bin codex-windows-sandbox-setup --bin codex-command-runner + else + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy + fi + - name: sccache stats (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash - run: | - build_args=() - for binary in ${{ matrix.binaries }}; do - build_args+=(--bin "$binary") - done - echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}" - cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}" + run: ${SCCACHE_PATH:-sccache} --show-stats || true - - name: Upload Cargo timings - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }} - path: codex-rs/target/**/cargo-timings/cargo-timing.html - if-no-files-found: warn - - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Stage unsigned macOS artifacts + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Normalize Android ELF for Termux shell: bash run: | set -euo pipefail - - target="${{ matrix.target }}" - release_dir="target/${target}/release" - dest="unsigned-dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - unsigned_name="${binary}-${target}-unsigned" - unsigned_path="${dest}/${unsigned_name}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 + for binary in codex codex-responses-api-proxy; do + binary_path="target/${{ matrix.target }}/release/${binary}" + if [[ -f "${binary_path}" ]]; then + termux-elf-cleaner --api-level 24 "${binary_path}" + chmod +x "${binary_path}" + # if [[ "${binary}" == "codex" ]]; then + # # Patch PT_TLS (0x7) to PT_NULL (0x0) at offset 400 to bypass Bionic alignment checks. + # printf '\x00\x00\x00\x00' | dd of="${binary_path}" bs=1 seek=400 count=4 conv=notrunc + # fi fi - - cp "${binary_path}" "${unsigned_path}" - tar -C "$dest" -czf "${unsigned_path}.tar.gz" "${unsigned_name}" - zstd -T0 -19 --rm "${unsigned_path}" done - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Upload unsigned macOS artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }}-unsigned - path: codex-rs/unsigned-dist/${{ matrix.target }}/* - if-no-files-found: error - - - if: ${{ contains(matrix.target, 'linux') }} + - if: ${{ contains(matrix.target, 'linux') && !contains(matrix.target, 'android') && github.repository_owner == 'openai' }} name: Cosign Linux artifacts uses: ./.github/actions/linux-code-sign with: target: ${{ matrix.target }} artifacts-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release - binaries: ${{ matrix.binaries }} - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS == 'true' }} + - if: ${{ contains(matrix.target, 'windows') && github.repository_owner == 'openai' }} + name: Sign Windows binaries with Azure Trusted Signing + uses: ./.github/actions/windows-code-sign + with: + target: ${{ matrix.target }} + client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID }} + endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} + account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} + + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (binaries) uses: ./.github/actions/macos-code-sign with: target: ${{ matrix.target }} - binaries: ${{ matrix.binaries }} sign-binaries: "true" sign-dmg: "false" apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }} @@ -436,7 +623,7 @@ jobs: apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: Build macOS dmg shell: bash run: | @@ -451,17 +638,23 @@ jobs: # The previous "MacOS code signing (binaries)" step signs + notarizes the # built artifacts in `${release_dir}`. This step packages *those same* # signed binaries into a dmg. + codex_binary_path="${release_dir}/codex" + proxy_binary_path="${release_dir}/codex-responses-api-proxy" + rm -rf "$dmg_root" mkdir -p "$dmg_root" - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 - fi - ditto "${binary_path}" "${dmg_root}/${binary}" - done + if [[ ! -f "$codex_binary_path" ]]; then + echo "Binary $codex_binary_path not found" + exit 1 + fi + if [[ ! -f "$proxy_binary_path" ]]; then + echo "Binary $proxy_binary_path not found" + exit 1 + fi + + ditto "$codex_binary_path" "${dmg_root}/codex" + ditto "$proxy_binary_path" "${dmg_root}/codex-responses-api-proxy" rm -f "$dmg_path" hdiutil create \ @@ -476,7 +669,7 @@ jobs: exit 1 fi - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (dmg) uses: ./.github/actions/macos-code-sign with: @@ -490,107 +683,65 @@ jobs: apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - name: Stage artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | dest="dist/${{ matrix.target }}" mkdir -p "$dest" - for binary in ${{ matrix.binaries }}; do - cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" - if [[ "${{ matrix.target }}" == *linux* ]]; then - cp "target/${{ matrix.target }}/release/${binary}.sigstore" \ - "$dest/${binary}-${{ matrix.target }}.sigstore" + if [[ "${{ matrix.runner }}" == windows* ]]; then + cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe" + else + # For Android, we want the binary to be named just 'codex' in the archive. + if [[ "${{ matrix.target }}" == "aarch64-linux-android" ]]; then + cp target/${{ matrix.target }}/release/codex "$dest/codex" + else + cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" + fi + if [[ -f target/${{ matrix.target }}/release/codex-responses-api-proxy ]]; then + cp target/${{ matrix.target }}/release/codex-responses-api-proxy "$dest/codex-responses-api-proxy-${{ matrix.target }}" fi - done - - if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.bundle }}" == "primary" ]]; then - bundle_root="${RUNNER_TEMP}/codex-${{ matrix.target }}-bundle" - rm -rf "$bundle_root" - mkdir -p "$bundle_root/codex-resources" - cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex" - cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap" - chmod 0755 "$bundle_root/codex" "$bundle_root/codex-resources/bwrap" - tar -C "$bundle_root" -cf - codex codex-resources/bwrap | - zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst" fi - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" + if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.target }}" != *android* ]]; then + cp target/${{ matrix.target }}/release/codex.sigstore "$dest/codex-${{ matrix.target }}.sigstore" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.sigstore "$dest/codex-responses-api-proxy-${{ matrix.target }}.sigstore" fi - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - aarch64-unknown-linux-musl) - platform_tag="manylinux_2_17_aarch64" - ;; - x86_64-unknown-linux-musl) - platform_tag="manylinux_2_17_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - # Do not install into the runner's system Python; macOS runners mark - # the Homebrew Python as externally managed under PEP 668. - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - stage_runtime_args=( - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" - stage-runtime - "$stage_dir" - "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/codex" - --codex-version "${GITHUB_REF_NAME}" - --platform-tag "$platform_tag" - ) - if [[ "${{ matrix.target }}" == *linux* ]]; then - # Keep bwrap in the runtime wheel so Linux sandbox fallback behavior - # matches the standalone release bundle on hosts without system bwrap. - stage_runtime_args+=( - --resource-binary - "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/bwrap" - ) + if [[ "${{ matrix.target }}" == *apple-darwin ]]; then + cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" fi - python3 "${stage_runtime_args[@]}" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error + - if: ${{ matrix.runner == 'windows-11-arm' }} + name: Install zstd + shell: powershell + run: choco install -y zstandard - name: Compress artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | # Path that contains the uncompressed binaries for the current # ${{ matrix.target }} dest="dist/${{ matrix.target }}" + repo_root=$PWD + + # We want to ship the raw Windows executables in the GitHub Release + # in addition to the compressed archives. Keep the originals for + # Windows targets; remove them elsewhere to limit the number of + # artifacts that end up in the GitHub Release. + keep_originals=false + if [[ "${{ matrix.runner }}" == windows* ]]; then + keep_originals=true + fi # For compatibility with environments that lack the `zstd` tool we - # additionally create a `.tar.gz` alongside every binary we publish. - # The end result is: + # additionally create a `.tar.gz` for all platforms and `.zip` for + # Windows alongside every single binary that we publish. The end result is: # codex-.zst (existing) # codex-.tar.gz (new) + # codex-.zip (only for Windows) # 1. Produce a .tar.gz for every file in the directory *before* we # run `zstd --rm`, because that flag deletes the original files. @@ -598,7 +749,7 @@ jobs: base="$(basename "$f")" # Skip files that are already archives (shouldn't happen, but be # safe). - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then + if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then continue fi @@ -608,330 +759,239 @@ jobs: fi # Create per-binary tar.gz - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - - # Also create .zst and remove the uncompressed binaries to keep - # non-Windows artifact directories small. - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - with: - name: ${{ matrix.artifact_name }} - # Upload the per-binary .zst files, .tar.gz equivalents, and any - # prebuilt archives staged above. - path: | - codex-rs/dist/${{ matrix.target }}/* - - stage-signed-macos: - if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode == 'promote_signed' }} - needs: tag-check - name: Stage signed macOS handoff - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: macos-15-xlarge - timeout-minutes: 30 - permissions: - contents: read - defaults: - run: - working-directory: codex-rs + # For Android, we want the archive to have the target suffix even though the binary is just 'codex'. + archive_name="${base}" + if [[ "${{ matrix.target }}" == "aarch64-linux-android" && "${base}" == "codex" ]]; then + archive_name="codex-${{ matrix.target }}" + fi + tar -C "$dest" -czf "$dest/${archive_name}.tar.gz" "$base" + + # Create zip archive for Windows binaries + # Must run from inside the dest dir so 7z won't + # embed the directory path inside the zip. + if [[ "${{ matrix.runner }}" == windows* ]]; then + if [[ "$base" == "codex-${{ matrix.target }}.exe" ]]; then + # Bundle the sandbox helper binaries into the main codex zip so + # WinGet installs include the required helpers next to codex.exe. + # Fall back to the single-binary zip if the helpers are missing + # to avoid breaking releases. + bundle_dir="$(mktemp -d)" + runner_src="$dest/codex-command-runner-${{ matrix.target }}.exe" + setup_src="$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + if [[ -f "$runner_src" && -f "$setup_src" ]]; then + cp "$dest/$base" "$bundle_dir/$base" + cp "$runner_src" "$bundle_dir/codex-command-runner.exe" + cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" + # Use an absolute path so bundle zips land in the real dist + # dir even when 7z runs from a temp directory. + (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) + else + echo "warning: missing sandbox binaries; falling back to single-binary zip" + echo "warning: expected $runner_src and $setup_src" + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + rm -rf "$bundle_dir" + else + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + fi - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" + # Also create .zst (existing behaviour) *and* remove the original + # uncompressed binary to keep the directory small. + zstd_args=(-T0 -19) + if [[ "${keep_originals}" == false ]]; then + zstd_args+=(--rm) + fi - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + if [[ "${archive_name}" != "${base}" ]]; then + zstd "${zstd_args[@]}" "$dest/$base" -o "$dest/${archive_name}.zst" + else + zstd "${zstd_args[@]}" "$dest/$base" + fi + done - - name: Download signed macOS handoff + - name: Add Termux release metadata + if: ${{ (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && matrix.target == 'aarch64-linux-android' }} shell: bash - env: - GH_TOKEN: ${{ github.token }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - SIGNED_MACOS_SHA256: ${{ inputs.signed_macos_sha256 }} run: | set -euo pipefail - - download_dir="${RUNNER_TEMP}/signed-macos-download" - handoff_dir="${RUNNER_TEMP}/signed-macos-handoff" - rm -rf "$download_dir" "$handoff_dir" - mkdir -p "$download_dir" "$handoff_dir" - - gh release download "$GITHUB_REF_NAME" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern "$SIGNED_MACOS_ASSET" \ - --dir "$download_dir" - - asset_count="$(find "$download_dir" -maxdepth 1 -type f | wc -l | tr -d '[:space:]')" - if [[ "$asset_count" != "1" ]]; then - echo "Expected exactly one signed macOS handoff asset named ${SIGNED_MACOS_ASSET}; found ${asset_count}" - find "$download_dir" -maxdepth 1 -type f -print - exit 1 - fi - - asset_path="$(find "$download_dir" -maxdepth 1 -type f -print -quit)" - if [[ -n "${SIGNED_MACOS_SHA256}" ]]; then - expected_sha="$(printf '%s' "$SIGNED_MACOS_SHA256" | tr '[:upper:]' '[:lower:]')" - actual_sha="$(shasum -a 256 "$asset_path" | awk '{print $1}')" - if [[ "$actual_sha" != "$expected_sha" ]]; then - echo "signed_macos_sha256 mismatch for ${SIGNED_MACOS_ASSET}" - echo "expected: ${expected_sha}" - echo "actual: ${actual_sha}" - exit 1 - fi + if [[ ! -f "${GITHUB_WORKSPACE}/.github/termux-release.json" ]]; then + echo "No Termux release metadata found; skipping metadata attachment." + exit 0 fi + dest="dist/${{ matrix.target }}" + cp "${GITHUB_WORKSPACE}/.github/termux-release.json" "${dest}/termux-release.json" + ( + cd "${dest}" + sha256sum ./* > SHA256SUMS + ) - asset_name="$(basename "$asset_path")" - case "$asset_name" in - *.tar.zst) - zstd -dc "$asset_path" | tar -C "$handoff_dir" -xf - - ;; - *.tar.gz|*.tgz) - tar -C "$handoff_dir" -xzf "$asset_path" - ;; - *.zip) - ditto -x -k "$asset_path" "$handoff_dir" - ;; - *) - echo "Unsupported signed macOS handoff archive format: ${asset_name}" - exit 1 - ;; - esac - - echo "SIGNED_MACOS_HANDOFF_DIR=$handoff_dir" >> "$GITHUB_ENV" + - id: upload-artifact + uses: actions/upload-artifact@v6 + with: + name: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + # Upload the per-binary .zst files as well as the new .tar.gz + # equivalents we generated in the previous step. + path: | + codex-rs/dist/${{ matrix.target }}/* - - name: Stage signed macOS artifacts + - name: Publish Termux artifact notification + if: ${{ github.event_name == 'pull_request' && startsWith(github.base_ref, 'release/') && matrix.target == 'aarch64-linux-android' }} shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_ID: ${{ steps.upload-artifact.outputs.artifact-id }} + ARTIFACT_NAME: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + PR_NUMBER: ${{ github.event.pull_request.number }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | set -euo pipefail - - target="${{ matrix.target }}" - artifact_name="${{ matrix.artifact_name }}" - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${target}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - fi - if [[ ! -d "$source_dir" ]]; then - echo "Signed macOS handoff is missing ${artifact_name}/" - echo "Expected either:" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${target}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - find "$SIGNED_MACOS_HANDOFF_DIR" -maxdepth 3 -type f -print + if [[ -z "${ARTIFACT_ID}" ]]; then + echo "upload-artifact did not return an artifact id" >&2 exit 1 fi - dest="dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - source_path="${source_dir}/${binary}" - if [[ ! -f "$source_path" ]]; then - source_path="${source_dir}/${binary}-${target}" - fi - if [[ ! -f "$source_path" ]]; then - echo "Signed macOS handoff is missing ${binary} for ${artifact_name}" - exit 1 - fi - - release_path="${dest}/${binary}-${target}" - ditto "$source_path" "$release_path" - chmod 0755 "$release_path" - codesign --verify --strict --verbose=2 "$release_path" - done - - # DMG staging is disabled for signed promotion because we no longer - # distribute DMGs from this release path. Keep the branch here so the - # handoff can opt back in by flipping matrix.build_dmg if needed. - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - dmg_name="codex-${target}.dmg" - dmg_source="${source_dir}/${dmg_name}" - if [[ ! -f "$dmg_source" ]]; then - echo "Signed macOS handoff is missing ${dmg_name} for ${artifact_name}" - exit 1 - fi - - codesign --verify --strict --verbose=2 "$dmg_source" - xcrun stapler validate "$dmg_source" - cp "$dmg_source" "$dest/$dmg_name" + marker="" + artifact_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts/${ARTIFACT_ID}" + run_url="${GH_WORKFLOW_URL:-https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + body_path="${RUNNER_TEMP}/termux-artifact-comment.md" + { + echo "${marker}" + echo "Termux Android artifact ready for testing:" + echo + echo "- Artifact: [${ARTIFACT_NAME}](${artifact_url})" + echo "- Workflow run: [${GITHUB_RUN_ID}](${run_url})" + echo + echo "You must be signed in to GitHub with repository access to download Actions artifacts." + } > "${body_path}" + + existing_comment_id="$( + gh pr view "${PR_NUMBER}" \ + --repo "${GITHUB_REPOSITORY}" \ + --json comments \ + --jq ".comments | map(select(.author.is_bot == true and (.body | contains(\"${marker}\")))) | .[-1].id // \"\"" + )" + if [[ -n "${existing_comment_id}" ]]; then + gh api graphql \ + -f query=' + mutation($id: ID!, $body: String!) { + updateIssueComment(input: {id: $id, body: $body}) { + issueComment { + id + } + } + } + ' \ + -f id="${existing_comment_id}" \ + -f body="$(cat "${body_path}")" \ + >/dev/null + else + gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body-file "${body_path}" fi - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - python3 \ - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \ - stage-runtime \ - "$stage_dir" \ - "${GITHUB_WORKSPACE}/codex-rs/dist/${{ matrix.target }}/codex-${{ matrix.target }}" \ - --codex-version "${GITHUB_REF_NAME}" \ - --platform-tag "$platform_tag" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error - - - name: Compress artifacts - shell: bash - run: | - set -euo pipefail - - dest="dist/${{ matrix.target }}" - for f in "$dest"/*; do - base="$(basename "$f")" - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then - continue + gh label create binary-ready \ + --repo "${GITHUB_REPOSITORY}" \ + --color 0e8a16 \ + --description "Android binary is ready for testing" \ + --force + gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --add-label binary-ready + + if [[ -n "${SLACK_WEBHOOK_URL:-}" ]]; then + slack_payload_path="${RUNNER_TEMP}/termux-artifact-slack.json" + pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}" + jq -n \ + --arg artifact_name "${ARTIFACT_NAME}" \ + --arg artifact_url "${artifact_url}" \ + --arg pr_number "${PR_NUMBER}" \ + --arg pr_url "${pr_url}" \ + --arg run_id "${GITHUB_RUN_ID}" \ + --arg run_url "${run_url}" \ + '{ + text: ("Termux Android artifact ready for testing: " + $artifact_url), + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: ("*Termux Android artifact ready for testing*\n" + $artifact_url) + } + }, + { + type: "section", + fields: [ + { + type: "mrkdwn", + text: ("*Artifact:*\n<" + $artifact_url + "|" + $artifact_name + ">") + }, + { + type: "mrkdwn", + text: ("*Pull request:*\n<" + $pr_url + "|#" + $pr_number + ">") + }, + { + type: "mrkdwn", + text: ("*Workflow run:*\n<" + $run_url + "|" + $run_id + ">") + } + ] + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: "You must be signed in to GitHub with repository access to download Actions artifacts." + } + ] + } + ] + }' > "${slack_payload_path}" + + if ! curl -fsS \ + -X POST \ + -H "Content-type: application/json" \ + --data @"${slack_payload_path}" \ + "${SLACK_WEBHOOK_URL}"; then + echo "::warning::Failed to send Slack artifact notification" fi + fi - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }} - path: | - codex-rs/dist/${{ matrix.target }}/* - - build-windows: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - needs: tag-check - uses: ./.github/workflows/rust-release-windows.yml - with: - release-lto: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }} - secrets: inherit - - argument-comment-lint-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: argument-comment-lint release assets + shell-tool-mcp: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'openai' + name: shell-tool-mcp needs: tag-check - uses: ./.github/workflows/rust-release-argument-comment-lint.yml + permissions: + contents: read + id-token: write + uses: ./.github/workflows/shell-tool-mcp.yml with: + release-tag: ${{ github.ref_name }} publish: true - - zsh-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: zsh release assets - needs: tag-check - uses: ./.github/workflows/rust-release-zsh.yml + secrets: inherit release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') needs: - - tag-check - build - - stage-signed-macos - - build-windows - - argument-comment-lint-release-assets - - zsh-release-assets - if: >- - ${{ - always() && - needs.tag-check.result == 'success' && - ( - ( - github.event_name == 'workflow_dispatch' && - inputs.release_mode == 'promote_signed' && - needs.stage-signed-macos.result == 'success' && - needs.build.result == 'skipped' && - needs.build-windows.result == 'skipped' && - needs.argument-comment-lint-release-assets.result == 'skipped' && - needs.zsh-release-assets.result == 'skipped' - ) || - ( - (github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed') && - needs.build.result == 'success' && - needs.stage-signed-macos.result == 'skipped' && - needs.build-windows.result == 'success' && - needs.argument-comment-lint-release-assets.result == 'success' && - needs.zsh-release-assets.result == 'success' - ) - ) - }} + - shell-tool-mcp name: release runs-on: ubuntu-latest permissions: contents: write actions: read - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode == 'promote_signed' }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} outputs: version: ${{ steps.release_name.outputs.name }} tag: ${{ github.ref_name }} - sign_macos: ${{ steps.release_mode.outputs.sign_macos }} should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }} npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }} - should_publish_python_runtime: ${{ steps.python_runtime_publish_settings.outputs.should_publish }} steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + uses: actions/checkout@v6 - - name: Define release mode - id: release_mode - run: | - echo "release_mode=${RELEASE_MODE}" >> "$GITHUB_OUTPUT" - echo "sign_macos=${SIGN_MACOS}" >> "$GITHUB_OUTPUT" + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main - name: Generate release notes from tag commit message id: release_notes @@ -953,133 +1013,18 @@ jobs: echo "path=${notes_path}" >> "${GITHUB_OUTPUT}" - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@v7 with: path: dist - - name: Validate unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - run_summary="$(gh run view "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --json conclusion,event,headBranch,headSha,status,workflowName,url \ - --jq '[.workflowName, .event, .headBranch, .headSha, .status, .conclusion, .url] | @tsv')" - IFS=$'\t' read -r workflow_name event head_branch head_sha status conclusion run_url <<< "$run_summary" - expected_head_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")" - - if [[ "$workflow_name" != "$GITHUB_WORKFLOW" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is for workflow '${workflow_name}', expected '${GITHUB_WORKFLOW}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$event" != "workflow_dispatch" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} was triggered by '${event}', expected 'workflow_dispatch'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_branch" != "$GITHUB_REF_NAME" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used ref '${head_branch}', expected '${GITHUB_REF_NAME}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_sha" != "$expected_head_sha" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used head SHA '${head_sha}', expected '${expected_head_sha}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is ${status}/${conclusion}, expected completed/success" - echo "Run URL: ${run_url}" - exit 1 - fi - - - name: Download artifacts from unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - gh run download "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --dir dist - - - name: Remove unsigned macOS staging artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - run: | - set -euo pipefail - find dist -mindepth 1 -maxdepth 1 -type d \ - -name '*-apple-darwin*-unsigned' \ - -exec rm -rf {} + - - - name: Re-upload promoted Linux x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-unknown-linux-musl - path: dist/x86_64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Linux arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-unknown-linux-musl - path: dist/aarch64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Windows x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-pc-windows-msvc - path: dist/x86_64-pc-windows-msvc/* - if-no-files-found: error - - - name: Re-upload promoted Windows arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-pc-windows-msvc - path: dist/aarch64-pc-windows-msvc/* - if-no-files-found: error - - name: List run: ls -R dist/ - - name: Prune artifacts excluded from unsigned macOS release - if: ${{ env.SIGN_MACOS == 'false' }} - run: | - find dist -mindepth 1 -maxdepth 1 -type d \ - ! -name '*-apple-darwin*-unsigned' \ - ! -name 'aarch64-unknown-linux-musl' \ - ! -name 'aarch64-unknown-linux-musl-app-server' \ - ! -name 'x86_64-unknown-linux-musl' \ - ! -name 'x86_64-unknown-linux-musl-app-server' \ - ! -name 'aarch64-pc-windows-msvc' \ - ! -name 'x86_64-pc-windows-msvc' \ - -exec rm -rf {} + - - if ! find dist -type f -name '*-apple-darwin*-unsigned*' | grep -q .; then - echo "No unsigned macOS artifacts found in downloaded workflow artifacts." - exit 1 - fi - + # This is a temporary fix: we should modify shell-tool-mcp.yml so these + # files do not end up in dist/ in the first place. - name: Delete entries from dist/ that should not go in the release run: | - rm -rf dist/windows-binaries* - # cargo-timing.html appears under multiple target-specific directories. - # If included in files: dist/**, release upload races on duplicate - # asset names and can fail with 404s. - find dist -type f -name 'cargo-timing.html' -delete - find dist -type d -empty -delete + rm -rf dist/shell-tool-mcp* ls -R dist/ @@ -1103,12 +1048,6 @@ jobs: set -euo pipefail version="${VERSION}" - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - echo "npm_tag=" >> "$GITHUB_OUTPUT" - exit 0 - fi - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "should_publish=true" >> "$GITHUB_OUTPUT" echo "npm_tag=" >> "$GITHUB_OUTPUT" @@ -1120,132 +1059,55 @@ jobs: echo "npm_tag=" >> "$GITHUB_OUTPUT" fi - - name: Determine Python runtime publish settings - id: python_runtime_publish_settings - env: - VERSION: ${{ steps.release_name.outputs.name }} - run: | - set -euo pipefail - version="${VERSION}" - - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - else - echo "should_publish=false" >> "$GITHUB_OUTPUT" - fi - - name: Setup pnpm - if: ${{ env.SIGN_MACOS == 'true' }} - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + uses: pnpm/action-setup@v4 with: run_install: false - name: Setup Node.js for npm packaging - if: ${{ env.SIGN_MACOS == 'true' }} - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: node-version: 22 - name: Install dependencies - if: ${{ env.SIGN_MACOS == 'true' }} run: pnpm install --frozen-lockfile # stage_npm_packages.py requires DotSlash when staging releases. - - uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2 + - uses: facebook/install-dotslash@v2 - name: Stage npm packages - if: ${{ env.SIGN_MACOS == 'true' }} + if: github.repository_owner == 'openai' env: GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ steps.release_name.outputs.name }} run: | - workflow_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" ./scripts/stage_npm_packages.py \ - --release-version "$RELEASE_VERSION" \ - --workflow-url "$workflow_url" \ + --release-version "${{ steps.release_name.outputs.name }}" \ --package codex \ --package codex-responses-api-proxy \ --package codex-sdk - - name: Stage installer scripts - if: ${{ env.SIGN_MACOS == 'true' }} - run: | - cp scripts/install/install.sh dist/install.sh - cp scripts/install/install.ps1 dist/install.ps1 - - name: Create GitHub Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@v2 with: name: ${{ steps.release_name.outputs.name }} tag_name: ${{ github.ref_name }} body_path: ${{ steps.release_notes.outputs.path }} files: dist/** - overwrite_files: true - make_latest: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} # Mark as prerelease only when the version has a suffix after x.y.z # (e.g. -alpha, -beta). Otherwise publish a normal release. prerelease: ${{ contains(steps.release_name.outputs.name, '-') }} - - name: Clean up signed promotion handoff assets - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" --jq '.id')" - gh api --paginate "repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets" \ - --jq '.[] | [.id, .name] | @tsv' | - while IFS=$'\t' read -r asset_id asset_name; do - if [[ -z "$asset_id" || -z "$asset_name" ]]; then - continue - fi - - delete_asset=false - if [[ "$asset_name" == *unsigned* || "$asset_name" == "$SIGNED_MACOS_ASSET" ]]; then - delete_asset=true - fi - - if [[ "$delete_asset" == "true" ]]; then - echo "Deleting release asset ${asset_name}" - gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" - fi - done - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 + - if: github.repository_owner == 'openai' + uses: facebook/dotslash-publish-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag: ${{ github.ref_name }} config: .github/dotslash-config.json - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-zsh-config.json - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-argument-comment-lint-config.json - - name: Trigger developers.openai.com deploy # Only trigger the deploy if the release is not a pre-release. # The deploy is used to update the developers.openai.com website with the new config schema json file. - if: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} + if: ${{ !contains(steps.release_name.outputs.name, '-') && github.repository_owner == 'openai' }} continue-on-error: true env: DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }} @@ -1260,15 +1122,7 @@ jobs: # npm docs: https://docs.npmjs.com/trusted-publishers publish-npm: # Publish to npm for stable releases and alpha pre-releases with numeric suffixes. - # promote_signed intentionally skips build jobs that are ancestors of release; - # include the !cancelled() status function so Actions does not apply its implicit - # success() check to the whole dependency chain before evaluating release outputs. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_npm == 'true' - }} + if: ${{ needs.release.outputs.should_publish_npm == 'true' && github.repository_owner == 'openai' }} name: publish-npm needs: release runs-on: ubuntu-latest @@ -1278,37 +1132,36 @@ jobs: steps: - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: - # Node 24 bundles npm >= 11.5.1, which trusted publishing requires. - node-version: 24 + node-version: 22 registry-url: "https://registry.npmjs.org" scope: "@openai" + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + - name: Download npm tarballs from release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} run: | set -euo pipefail - version="$RELEASE_VERSION" - tag="$RELEASE_TAG" + version="${{ needs.release.outputs.version }}" + tag="${{ needs.release.outputs.tag }}" mkdir -p dist/npm - patterns=( - "codex-npm-${version}.tgz" - "codex-npm-linux-*-${version}.tgz" - "codex-npm-darwin-*-${version}.tgz" - "codex-npm-win32-*-${version}.tgz" - "codex-responses-api-proxy-npm-${version}.tgz" - "codex-sdk-npm-${version}.tgz" - ) - for pattern in "${patterns[@]}"; do - gh release download "$tag" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "$pattern" \ - --dir dist/npm - done + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-responses-api-proxy-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-sdk-npm-${version}.tgz" \ + --dir dist/npm # No NODE_AUTH_TOKEN needed because we use OIDC. - name: Publish to npm @@ -1317,193 +1170,23 @@ jobs: NPM_TAG: ${{ needs.release.outputs.npm_tag }} run: | set -euo pipefail - prefix="" + tag_args=() if [[ -n "${NPM_TAG}" ]]; then - prefix="${NPM_TAG}-" + tag_args+=(--tag "${NPM_TAG}") fi - root_tarball="dist/npm/codex-npm-${VERSION}.tgz" - sdk_tarball="dist/npm/codex-sdk-npm-${VERSION}.tgz" - # Keep this list in sync with CODEX_PLATFORM_PACKAGES in - # codex-cli/scripts/build_npm_package.py. The root wrapper advances - # @openai/codex@latest as soon as it publishes, so every platform - # package it aliases must already exist in the registry first. - platform_tarballs=( - "dist/npm/codex-npm-linux-x64-${VERSION}.tgz" - "dist/npm/codex-npm-linux-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-x64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-x64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-arm64-${VERSION}.tgz" - ) - - for required_tarball in "${platform_tarballs[@]}" "${root_tarball}"; do - if [[ ! -f "${required_tarball}" ]]; then - echo "Missing npm tarball: ${required_tarball}" - exit 1 - fi - done - - shopt -s nullglob - other_tarballs=() - for tarball in dist/npm/*-"${VERSION}".tgz; do - if [[ "${tarball}" == "${root_tarball}" || "${tarball}" == "${sdk_tarball}" ]]; then - continue - fi - - is_platform_tarball=false - for platform_tarball in "${platform_tarballs[@]}"; do - if [[ "${tarball}" == "${platform_tarball}" ]]; then - is_platform_tarball=true - break - fi - done - if [[ "${is_platform_tarball}" == true ]]; then - continue - fi - - other_tarballs+=("${tarball}") - done - - # Publish the platform packages before the root CLI wrapper. The root - # wrapper advances @openai/codex@latest, so it should only publish - # after the optional dependency versions it references exist. tarballs=( - "${platform_tarballs[@]}" - "${other_tarballs[@]}" - "${root_tarball}" + "codex-npm-${VERSION}.tgz" + "codex-responses-api-proxy-npm-${VERSION}.tgz" + "codex-sdk-npm-${VERSION}.tgz" ) - if [[ -f "${sdk_tarball}" ]]; then - tarballs+=("${sdk_tarball}") - fi for tarball in "${tarballs[@]}"; do - filename="$(basename "${tarball}")" - tag="" - - case "${filename}" in - codex-npm-linux-*-"${VERSION}".tgz|codex-npm-darwin-*-"${VERSION}".tgz|codex-npm-win32-*-"${VERSION}".tgz) - platform="${filename#codex-npm-}" - platform="${platform%-${VERSION}.tgz}" - tag="${prefix}${platform}" - ;; - codex-npm-"${VERSION}".tgz|codex-responses-api-proxy-npm-"${VERSION}".tgz|codex-sdk-npm-"${VERSION}".tgz) - tag="${NPM_TAG}" - ;; - *) - echo "Unexpected npm tarball: ${filename}" - exit 1 - ;; - esac - - publish_cmd=(npm publish "${GITHUB_WORKSPACE}/${tarball}") - if [[ -n "${tag}" ]]; then - publish_cmd+=(--tag "${tag}") - fi - - echo "+ ${publish_cmd[*]}" - set +e - publish_output="$("${publish_cmd[@]}" 2>&1)" - publish_status=$? - set -e - - echo "${publish_output}" - if [[ ${publish_status} -eq 0 ]]; then - continue - fi - - if grep -qiE "previously published|cannot publish over|version already exists" <<< "${publish_output}"; then - echo "Skipping already-published package version for ${filename}" - continue - fi - - exit "${publish_status}" + npm publish "${GITHUB_WORKSPACE}/dist/npm/${tarball}" "${tag_args[@]}" done - # Publish the platform-specific Python runtime wheels using PyPI trusted publishing. - # PyPI project configuration must trust this workflow and job. Keep this - # non-blocking while the Python runtime publishing path is new; failures still - # need release follow-up, but should not invalidate the Rust release itself. - publish-python-runtime: - # Publish to PyPI for stable releases and alpha pre-releases with numeric suffixes. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_python_runtime == 'true' - }} - name: publish-python-runtime - needs: release - runs-on: ubuntu-latest - continue-on-error: true - environment: pypi - permissions: - id-token: write # Required for PyPI trusted publishing. - contents: read - - steps: - - name: Download Python runtime wheels from release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} - run: | - set -euo pipefail - python_version="$RELEASE_VERSION" - python_version="${python_version/-alpha./a}" - python_version="${python_version/-beta./b}" - python_version="${python_version/-rc./rc}" - - mkdir -p dist/python-runtime - gh release download "$RELEASE_TAG" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "openai_codex_cli_bin-${python_version}-*.whl" \ - --dir dist/python-runtime - ls -lh dist/python-runtime - - - name: Publish Python runtime wheels to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - with: - packages-dir: dist/python-runtime - skip-existing: true - - winget: - name: winget - needs: release - # Only publish stable/mainline releases to WinGet; pre-releases include a - # '-' in the semver string (e.g., 1.2.3-alpha.1). - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' && - !contains(needs.release.outputs.version, '-') - }} - # This job only invokes a GitHub Action to open/update the winget-pkgs PR; - # it does not execute Windows-only tooling, so Linux is sufficient. - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Publish to WinGet - uses: vedantmgoyal9/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5 - with: - identifier: OpenAI.Codex - version: ${{ needs.release.outputs.version }} - release-tag: ${{ needs.release.outputs.tag }} - fork-user: openai-oss-forks - installers-regex: '^codex-(?:x86_64|aarch64)-pc-windows-msvc\.exe\.zip$' - token: ${{ secrets.WINGET_PUBLISH_PAT }} - update-branch: name: Update latest-alpha-cli branch - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' - }} permissions: contents: write needs: release diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml new file mode 100644 index 000000000000..66a76aa4d938 --- /dev/null +++ b/.github/workflows/shell-tool-mcp.yml @@ -0,0 +1,461 @@ +name: shell-tool-mcp + +on: + workflow_call: + inputs: + release-version: + description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v. + required: false + type: string + release-tag: + description: Tag name to use when downloading release artifacts (defaults to rust-v). + required: false + type: string + publish: + description: Whether to publish to npm when the version is releasable. + required: false + default: true + type: boolean + +env: + NODE_VERSION: 22 + +jobs: + metadata: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.compute.outputs.version }} + release_tag: ${{ steps.compute.outputs.release_tag }} + should_publish: ${{ steps.compute.outputs.should_publish }} + npm_tag: ${{ steps.compute.outputs.npm_tag }} + steps: + - name: Compute version and tags + id: compute + run: | + set -euo pipefail + + version="${{ inputs.release-version }}" + release_tag="${{ inputs.release-tag }}" + + if [[ -z "$version" ]]; then + if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then + version="${release_tag#rust-v}" + elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then + version="${GITHUB_REF_NAME#rust-v}" + release_tag="${GITHUB_REF_NAME}" + else + echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag." + exit 1 + fi + fi + + if [[ -z "$release_tag" ]]; then + release_tag="rust-v${version}" + fi + + npm_tag="" + should_publish="false" + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + should_publish="true" + elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then + should_publish="true" + npm_tag="alpha" + fi + + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" + echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT" + echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT" + + matrix-setup: + runs-on: ubuntu-latest + outputs: + rust-binaries: ${{ steps.compute.outputs.rust-binaries }} + bash-linux: ${{ steps.compute.outputs.bash-linux }} + bash-darwin: ${{ steps.compute.outputs.bash-darwin }} + steps: + - name: Compute matrices + id: compute + shell: bash + run: | + set -euo pipefail + is_openai="${{ github.repository_owner == 'openai' }}" + + # rust-binaries: always include x86_64-musl; conditionally include paid/fork runners + if [[ "$is_openai" == "true" ]]; then + rust='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","install_musl":true} + ]' + else + rust='[{"runner":"macos-latest","target":"aarch64-apple-darwin"}]' + fi + rust=$(echo "$rust" | jq -c '. + [{"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","install_musl":true}]') + echo "rust-binaries={\"include\":$rust}" >> "$GITHUB_OUTPUT" + + # bash-linux: always include x86_64 variants; add arm64 on openai + bash_linux='[ + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-24.04","image":"ubuntu:24.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-22.04","image":"ubuntu:22.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-12","image":"debian:12"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-11","image":"debian:11"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]' + if [[ "$is_openai" == "true" ]]; then + bash_linux=$(echo "$bash_linux" | jq -c '. + [ + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-24.04","image":"arm64v8/ubuntu:24.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-22.04","image":"arm64v8/ubuntu:22.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-20.04","image":"arm64v8/ubuntu:20.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-12","image":"arm64v8/debian:12"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-11","image":"arm64v8/debian:11"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]') + fi + echo "bash-linux={\"include\":$(echo "$bash_linux" | jq -c)}" >> "$GITHUB_OUTPUT" + + # bash-darwin: always include macos-14; add macos-15-xlarge on openai + bash_darwin='[{"runner":"macos-14","target":"aarch64-apple-darwin","variant":"macos-14"}]' + if [[ "$is_openai" == "true" ]]; then + bash_darwin=$(echo "$bash_darwin" | jq -c '. + [ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin","variant":"macos-15"} + ]') + fi + echo "bash-darwin={\"include\":$(echo "$bash_darwin" | jq -c)}" >> "$GITHUB_OUTPUT" + + rust-binaries: + name: Build Rust - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + defaults: + run: + working-directory: codex-rs + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.rust-binaries) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install UBSan runtime (musl) + if: ${{ matrix.install_musl }} + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -y + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 + fi + + - uses: dtolnay/rust-toolchain@1.93 + with: + targets: ${{ matrix.target }} + + - if: ${{ matrix.install_musl }} + name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.14.0 + + - if: ${{ matrix.install_musl }} + name: Install musl build dependencies + env: + TARGET: ${{ matrix.target }} + run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh" + + - if: ${{ matrix.install_musl }} + name: Configure rustc UBSan wrapper (musl host) + shell: bash + run: | + set -euo pipefail + ubsan="" + if command -v ldconfig >/dev/null 2>&1; then + ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')" + fi + wrapper_root="${RUNNER_TEMP:-/tmp}" + wrapper="${wrapper_root}/rustc-ubsan-wrapper" + cat > "${wrapper}" <> "$GITHUB_ENV" + echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV" + + - if: ${{ matrix.install_musl }} + name: Clear sanitizer flags (musl) + shell: bash + run: | + set -euo pipefail + # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. + echo "RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV" + # Override any runner-level Cargo config rustflags as well. + echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + + sanitize_flags() { + local input="$1" + input="${input//-fsanitize=undefined/}" + input="${input//-fno-sanitize-recover=undefined/}" + input="${input//-fno-sanitize-trap=undefined/}" + echo "$input" + } + + cflags="$(sanitize_flags "${CFLAGS-}")" + cxxflags="$(sanitize_flags "${CXXFLAGS-}")" + echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" + echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + + - name: Build exec server binaries + run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper + + - name: Stage exec server binaries + run: | + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}" + mkdir -p "$dest" + cp "target/${{ matrix.target }}/release/codex-exec-mcp-server" "$dest/" + cp "target/${{ matrix.target }}/release/codex-execve-wrapper" "$dest/" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-rust-${{ matrix.target }} + path: artifacts/** + if-no-files-found: error + + bash-linux: + name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + container: + image: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-linux) }} + steps: + - name: Install build prerequisites + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext + elif command -v dnf >/dev/null 2>&1; then + dnf install -y git gcc gcc-c++ make bison autoconf gettext + elif command -v yum >/dev/null 2>&1; then + yum install -y git gcc gcc-c++ make bison autoconf gettext + else + echo "Unsupported package manager in container" + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + bash-darwin: + name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-darwin) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + package: + name: Package npm module + needs: + - metadata + - rust-binaries + - bash-linux + - bash-darwin + runs-on: ubuntu-latest + env: + PACKAGE_VERSION: ${{ needs.metadata.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install JavaScript dependencies + run: pnpm install --frozen-lockfile + + - name: Build (shell-tool-mcp) + run: pnpm --filter @openai/codex-shell-tool-mcp run build + + - name: Download build artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts + + - name: Assemble staging directory + id: staging + shell: bash + run: | + set -euo pipefail + staging="${STAGING_DIR}" + mkdir -p "$staging" "$staging/vendor" + cp shell-tool-mcp/README.md "$staging/" + cp shell-tool-mcp/package.json "$staging/" + cp -R shell-tool-mcp/bin "$staging/" + + found_vendor="false" + shopt -s nullglob + for vendor_dir in artifacts/*/vendor; do + rsync -av "$vendor_dir/" "$staging/vendor/" + found_vendor="true" + done + if [[ "$found_vendor" == "false" ]]; then + echo "No vendor payloads were downloaded." + exit 1 + fi + + node - <<'NODE' + import fs from "node:fs"; + import path from "node:path"; + + const stagingDir = process.env.STAGING_DIR; + const version = process.env.PACKAGE_VERSION; + const pkgPath = path.join(stagingDir, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + pkg.version = version; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + NODE + + echo "dir=$staging" >> "$GITHUB_OUTPUT" + env: + STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp + + - name: Ensure binaries are executable + run: | + set -euo pipefail + staging="${{ steps.staging.outputs.dir }}" + chmod +x \ + "$staging"/vendor/*/codex-exec-mcp-server \ + "$staging"/vendor/*/codex-execve-wrapper \ + "$staging"/vendor/*/bash/*/bash + + - name: Create npm tarball + shell: bash + run: | + set -euo pipefail + mkdir -p dist/npm + staging="${{ steps.staging.outputs.dir }}" + pack_info=$(cd "$staging" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm") + filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);') + mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz" + + - uses: actions/upload-artifact@v6 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz + if-no-files-found: error + + publish: + name: Publish npm package + needs: + - metadata + - package + if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }} + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: https://registry.npmjs.org + scope: "@openai" + + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + + - name: Download npm tarball + uses: actions/download-artifact@v7 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm + + - name: Publish to npm + if: github.repository == 'openai/codex' + env: + NPM_TAG: ${{ needs.metadata.outputs.npm_tag }} + VERSION: ${{ needs.metadata.outputs.version }} + shell: bash + run: | + set -euo pipefail + tag_args=() + if [[ -n "${NPM_TAG}" ]]; then + tag_args+=(--tag "${NPM_TAG}") + fi + npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}" diff --git a/.github/workflows/termux-release-checkpoint.yml b/.github/workflows/termux-release-checkpoint.yml new file mode 100644 index 000000000000..0347e6f1b3d6 --- /dev/null +++ b/.github/workflows/termux-release-checkpoint.yml @@ -0,0 +1,103 @@ +name: termux-release-checkpoint + +on: + workflow_dispatch: + inputs: + source_branch: + description: "Release branch to checkpoint from, for example release/0.123.0" + required: false + type: string + default: "" + source_sha: + description: "Specific source commit SHA to checkpoint; defaults to the source branch tip" + required: false + type: string + default: "" + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + type: string + default: "wallentx/termux-target" + reviewer: + description: "GitHub username to request as reviewer" + required: false + type: string + default: "wallentx" + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-checkpoint-${{ inputs.source_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + checkpoint: + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + env: + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + REQUESTED_SOURCE_BRANCH: ${{ inputs.source_branch }} + REQUESTED_SOURCE_SHA: ${{ inputs.source_sha }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ inputs.source_branch || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + source_branch="${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${source_branch}" + + - name: Create checkpoint PR + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" diff --git a/.github/workflows/termux-release-deploy.yml b/.github/workflows/termux-release-deploy.yml new file mode 100644 index 000000000000..2071611dacc4 --- /dev/null +++ b/.github/workflows/termux-release-deploy.yml @@ -0,0 +1,243 @@ +name: termux-release-deploy + +on: + push: + branches: + - "release/**" + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to deploy, for example release/0.124.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to deploy. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged release PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged release PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + default: "wallentx/termux-target" + type: string + reviewer: + description: "GitHub username to request as reviewer on the checkpoint PR" + required: false + default: "wallentx" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-deploy-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + deploy: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' || !startsWith(github.event.head_commit.message, 'Seed Termux release automation') }} + permissions: + actions: read + contents: write + deployments: write + issues: write + pull-requests: write + env: + REQUESTED_RELEASE_BRANCH: ${{ inputs.release_branch }} + REQUESTED_RELEASE_SHA: ${{ inputs.release_sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + release_branch="${REQUESTED_RELEASE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${release_branch}" + + - name: Resolve release ref + id: release-ref + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: deploy + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Create deployment + if: steps.metadata.outputs.deploy == 'true' + id: deployment + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + deployment_id="$( + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments" \ + -f ref="${RELEASE_SHA}" \ + -f environment="termux-release" \ + -F auto_merge=false \ + -F required_contexts[] \ + -f description="Termux release deployment for ${TERMUX_TAG}" \ + --jq '.id' + )" + echo "id=${deployment_id}" >> "$GITHUB_OUTPUT" + + - name: Mark deployment in progress + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="in_progress" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Promoting Termux release artifact and preparing checkpoint PR" \ + >/dev/null + + - name: Locate merged pull request + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" + + - name: Ensure checkpoint PR + if: steps.metadata.outputs.deploy == 'true' + id: checkpoint + env: + SOURCE_BRANCH: ${{ steps.release-ref.outputs.branch }} + SOURCE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" + + - name: Mark deployment success + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="success" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment completed for ${TERMUX_TAG}" \ + >/dev/null + + - name: Mark deployment failure + if: failure() && steps.deployment.outputs.id != '' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="failure" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment failed" \ + >/dev/null diff --git a/.github/workflows/termux-release-promote.yml b/.github/workflows/termux-release-promote.yml new file mode 100644 index 000000000000..22c277c21a70 --- /dev/null +++ b/.github/workflows/termux-release-promote.yml @@ -0,0 +1,135 @@ +name: termux-release-promote + +on: + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to promote, for example release/0.122.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to promote. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-promote-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + promote: + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: write + pull-requests: read + env: + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Resolve release ref + id: release-ref + env: + INPUT_RELEASE_BRANCH: ${{ inputs.release_branch }} + INPUT_RELEASE_SHA: ${{ inputs.release_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: promote + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Locate merged pull request + if: steps.metadata.outputs.promote == 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.promote == 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.promote == 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" diff --git a/scripts/termux-configure-git.sh b/scripts/termux-configure-git.sh new file mode 100755 index 000000000000..fae3abcbab48 --- /dev/null +++ b/scripts/termux-configure-git.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Configure the explicit bot identity used by release automation and fetch any +# refs requested by the calling workflow. + +set -euo pipefail + +git config user.name "github-actions[bot]" +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + +while (($#)); do + case "$1" in + --origin) + shift + origin_refs=() + while (($#)) && [[ "$1" != --* ]]; do + origin_refs+=("$1") + shift + done + if ((${#origin_refs[@]})); then + git fetch --prune origin "${origin_refs[@]}" + fi + ;; + --upstream-tag) + if (($# < 3)); then + echo "--upstream-tag requires ." >&2 + exit 1 + fi + upstream_repo="$2" + upstream_tag="$3" + git remote add upstream "https://github.com/${upstream_repo}.git" 2>/dev/null || true + git fetch --prune --no-tags upstream "+refs/tags/${upstream_tag}:refs/tags/${upstream_tag}" + shift 3 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done diff --git a/scripts/termux-create-checkpoint-pr.sh b/scripts/termux-create-checkpoint-pr.sh new file mode 100755 index 000000000000..ba07c289952d --- /dev/null +++ b/scripts/termux-create-checkpoint-pr.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/termux-release-paths.sh +source "${script_dir}/termux-release-paths.sh" + +source_branch="${SOURCE_BRANCH:-${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}}" +source_sha="${SOURCE_SHA:-${REQUESTED_SOURCE_SHA:-}}" +if [[ -z "${source_sha}" ]]; then + if [[ "${GITHUB_EVENT_NAME:-}" == "push" && "${source_branch}" == "${GITHUB_REF_NAME:-}" ]]; then + source_sha="${GITHUB_SHA}" + else + source_sha="$(git rev-parse "origin/${source_branch}")" + fi +fi + +if [[ -z "${DESTINATION_BRANCH:-}" ]]; then + echo "DESTINATION_BRANCH is required." >&2 + exit 1 +fi + +release_only_checkpoint_paths() { + printf '%s\n' "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +} + +resolve_source_version_conflicts() { + local path="$1" + local resolved_path + resolved_path="$(mktemp)" + + if ! awk ' + function normalize_versions(text) { + gsub(/version = "[^"]+"/, "version = \"\"", text) + return text + } + + BEGIN { + in_block = 0 + side = "" + ours = "" + theirs = "" + blocks = 0 + } + + /^<<<<<<< / { + if (in_block) { + exit 1 + } + in_block = 1 + side = "ours" + ours = "" + theirs = "" + blocks++ + next + } + + /^=======$/ && in_block { + side = "theirs" + next + } + + /^>>>>>>> / && in_block { + if (normalize_versions(ours) != normalize_versions(theirs)) { + exit 1 + } + printf "%s", theirs + in_block = 0 + side = "" + next + } + + { + if (!in_block) { + print + } else if (side == "ours") { + ours = ours $0 ORS + } else if (side == "theirs") { + theirs = theirs $0 ORS + } else { + exit 1 + } + } + + END { + if (in_block || blocks == 0) { + exit 1 + } + } + ' "${path}" > "${resolved_path}"; then + rm -f "${resolved_path}" + return 1 + fi + + cp "${resolved_path}" "${path}" + rm -f "${resolved_path}" +} + +short_sha="${source_sha:0:12}" +source_slug="${source_branch//\//_}" +dest_slug="${DESTINATION_BRANCH//\//_}" +checkpoint_branch="checkpoint/${dest_slug}_from_${source_slug}_${short_sha}" +pr_title="checkpoint: into ${DESTINATION_BRANCH} from ${source_branch} @ ${short_sha}" +merge_conflicted=false +conflict_summary="" + +existing_pr="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --head "${checkpoint_branch}" \ + --state all \ + --json number,state,mergedAt,url \ + --jq '[.[] | select(.state == "OPEN" or .mergedAt != null)] | .[0] // empty' +)" +if [[ -n "${existing_pr}" ]]; then + existing_url="$(jq -r '.url' <<< "${existing_pr}")" + existing_state="$(jq -r '.state' <<< "${existing_pr}")" + echo "Checkpoint PR already exists for ${checkpoint_branch}: ${existing_url} (${existing_state})." + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${existing_url}" >> "${GITHUB_OUTPUT}" + fi + exit 0 +fi + +git checkout -B "${checkpoint_branch}" "origin/${DESTINATION_BRANCH}" + +if ! git merge --no-ff --no-edit "${source_sha}"; then + mapfile -t conflicted_paths < <(git diff --name-only --diff-filter=U) + for conflicted_path in "${conflicted_paths[@]}"; do + if termux_is_checkpoint_release_only_path "${conflicted_path}"; then + echo "Auto-resolving release-only checkpoint conflict in ${conflicted_path} by keeping ${DESTINATION_BRANCH}." + if git cat-file -e "HEAD:${conflicted_path}" 2>/dev/null; then + git checkout --ours -- "${conflicted_path}" + git add "${conflicted_path}" + else + git rm -f --ignore-unmatch "${conflicted_path}" + fi + fi + done + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if ((${#remaining_conflicts[@]})); then + cargo_version_conflicts=true + for remaining_conflict in "${remaining_conflicts[@]}"; do + case "${remaining_conflict}" in + codex-rs/Cargo.toml|codex-rs/Cargo.lock) + ;; + *) + cargo_version_conflicts=false + ;; + esac + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + for remaining_conflict in "${remaining_conflicts[@]}"; do + if ! resolve_source_version_conflicts "${remaining_conflict}"; then + cargo_version_conflicts=false + break + fi + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + echo "Auto-resolving recurring Cargo version checkpoint conflicts by keeping ${source_branch} versions." + git add -- "${remaining_conflicts[@]}" + fi + fi + fi + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if [[ "${#remaining_conflicts[@]}" -eq 0 ]]; then + git commit --no-edit + else + merge_conflicted=true + conflict_summary="$( + printf '%s\n' "${remaining_conflicts[@]}" | awk '{ print "- `" $0 "`" }' + )" + echo "Automatic checkpoint merge failed; creating a manual-resolution PR instead." >&2 + if git rev-parse -q --verify MERGE_HEAD >/dev/null; then + git merge --abort + fi + git checkout -B "${checkpoint_branch}" "${source_sha}" + fi +fi + +if git cat-file -e "origin/${DESTINATION_BRANCH}:.github" 2>/dev/null; then + git restore --source="origin/${DESTINATION_BRANCH}" --staged --worktree -- .github + mapfile -t added_github_paths < <( + git diff --name-only --diff-filter=A "origin/${DESTINATION_BRANCH}" -- .github + ) + if ((${#added_github_paths[@]})); then + git rm -f --ignore-unmatch -- "${added_github_paths[@]}" + fi +else + git rm -r --ignore-unmatch .github +fi + +while IFS= read -r release_only_path; do + if git cat-file -e "origin/${DESTINATION_BRANCH}:${release_only_path}" 2>/dev/null; then + git restore --source="origin/${DESTINATION_BRANCH}" --staged --worktree -- "${release_only_path}" + else + git rm -f --ignore-unmatch -- "${release_only_path}" + fi +done < <(release_only_checkpoint_paths) + +if ! git diff --quiet || ! git diff --cached --quiet; then + if [[ -e .github || -L .github ]] || git ls-files --error-unmatch -- .github >/dev/null 2>&1; then + git add -A .github + fi + while IFS= read -r release_only_path; do + if [[ -e "${release_only_path}" || -L "${release_only_path}" ]] || git ls-files --error-unmatch -- "${release_only_path}" >/dev/null 2>&1; then + git add -A -- "${release_only_path}" + fi + done < <(release_only_checkpoint_paths) + if [[ "${merge_conflicted}" == "true" ]]; then + git commit -m "checkpoint: prepare ${source_branch} for ${DESTINATION_BRANCH}" + else + git commit --amend --no-edit + fi +fi + +if git diff --quiet "origin/${DESTINATION_BRANCH}" HEAD; then + echo "Checkpoint merge produced no destination changes after release-only files were restored." + exit 0 +fi + +git push --force-with-lease origin "${checkpoint_branch}" + +remaining="$( + git log --first-parent --pretty=format:%H "${source_sha}..origin/${source_branch}" | wc -w +)" + +body_path="${RUNNER_TEMP}/termux-checkpoint-pr.md" +{ + echo "## Termux release checkpoint" + echo + echo "- Source branch: \`${source_branch}\`" + echo "- Source hash: \`${source_sha}\`" + echo "- Destination branch: \`${DESTINATION_BRANCH}\`" + echo "- Remaining first-parent commits on source: ${remaining}" + echo + echo "This PR carries release-train conflict fixes and follow-up changes back into the reusable Termux patch branch." + if [[ "${merge_conflicted}" == "true" ]]; then + echo + echo "## Merge conflicts" + echo + echo "GitHub Actions could not create the checkpoint merge commit automatically, so this PR was created from the source branch state for manual conflict resolution." + echo + echo "Conflicted paths from the failed merge attempt:" + if [[ -n "${conflict_summary}" ]]; then + printf '%s\n' "${conflict_summary}" + else + echo "- Conflict details unavailable" + fi + fi + echo + echo "Release-only workflow files and metadata under \`.github\` were restored to the destination branch versions before opening this PR." +} > "${body_path}" + +pr_url="$( + gh pr create \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${DESTINATION_BRANCH}" \ + --head "${checkpoint_branch}" \ + --title "${pr_title}" \ + --body-file "${body_path}" +)" +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-reviewer "${REVIEWER}" || true +gh label create checkpoint --repo "${GITHUB_REPOSITORY}" --color c5def5 --description "Checkpoint merge" --force +gh label create termux-release --repo "${GITHUB_REPOSITORY}" --color 0e8a16 --description "Termux release automation" --force +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-label "checkpoint" --add-label "termux-release" + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${pr_url}" >> "${GITHUB_OUTPUT}" +fi diff --git a/scripts/termux-create-or-update-mirrored-release.sh b/scripts/termux-create-or-update-mirrored-release.sh new file mode 100755 index 000000000000..5787e894582b --- /dev/null +++ b/scripts/termux-create-or-update-mirrored-release.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +# Create a mirrored Termux release or repair an existing release that is missing +# the Android tarball. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" +: "${RUNNER_TEMP:?RUNNER_TEMP is required}" +: "${UPSTREAM_TAG:?UPSTREAM_TAG is required}" +: "${UPSTREAM_REPO:?UPSTREAM_REPO is required}" +: "${UPSTREAM_NAME?UPSTREAM_NAME is required}" +: "${TERMUX_TAG:?TERMUX_TAG is required}" +: "${UPSTREAM_PRERELEASE:?UPSTREAM_PRERELEASE is required}" +: "${UPSTREAM_HTML_URL?UPSTREAM_HTML_URL is required}" +: "${RELEASE_TRAIN?RELEASE_TRAIN is required}" +: "${RELEASE_EXISTS:?RELEASE_EXISTS is required}" +: "${PR_NUMBER:?PR_NUMBER is required}" +: "${HEAD_SHA:?HEAD_SHA is required}" +: "${RELEASE_SHA:?RELEASE_SHA is required}" + +promoted_dir="${PROMOTED_DIR:-promoted}" +asset_path="${promoted_dir}/codex-aarch64-linux-android.tar.gz" + +body_path="${RUNNER_TEMP}/release-body.md" +upstream_body_path="${RUNNER_TEMP}/upstream-release-body.md" +upstream_body_without_changelog_path="${RUNNER_TEMP}/upstream-release-body-without-changelog.md" +if gh release view "${UPSTREAM_TAG}" \ + --repo "${UPSTREAM_REPO}" \ + --json body \ + --jq '.body // ""' > "${upstream_body_path}"; then + awk ' + function heading_level(line, text) { + if (match(line, /^(#{1,6})[[:space:]]+(.+)$/, parts)) { + text = tolower(parts[2]) + sub(/[[:space:]]+#+[[:space:]]*$/, "", text) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", text) + if (text == "changelog" || text == "change log") { + return length(parts[1]) + } + } + return 0 + } + + { + if (!skip) { + level = heading_level($0) + if (level > 0) { + skip = 1 + skip_level = level + next + } + print + next + } + + if (match($0, /^(#{1,6})[[:space:]]+/, parts) && length(parts[1]) <= skip_level) { + skip = 0 + print + } + } + ' "${upstream_body_path}" > "${upstream_body_without_changelog_path}" +else + echo "::warning title=Upstream release notes unavailable::Could not read ${UPSTREAM_REPO} release ${UPSTREAM_TAG}." + : > "${upstream_body_without_changelog_path}" +fi + +{ + echo "Termux Android build for ${UPSTREAM_TAG}." + echo + echo "- Upstream release: ${UPSTREAM_HTML_URL}" + echo "- Release train: \`${RELEASE_TRAIN}\`" + echo "- Promoted PR: #${PR_NUMBER}" + echo "- Promoted PR head SHA: \`${HEAD_SHA}\`" +} > "${body_path}" +if [[ -s "${upstream_body_without_changelog_path}" ]]; then + { + echo + echo "## Upstream release notes" + echo + cat "${upstream_body_without_changelog_path}" + } >> "${body_path}" +fi + +release_title="${UPSTREAM_NAME}" +if [[ -z "${release_title}" || "${release_title}" == "null" ]]; then + release_title="${TERMUX_TAG}" +fi + +if [[ "${RELEASE_EXISTS}" == "true" ]]; then + gh release upload \ + "${TERMUX_TAG}" \ + "${asset_path}#codex-termux" \ + --repo "${GITHUB_REPOSITORY}" \ + --clobber + exit 0 +fi + +release_args=( + gh release create "${TERMUX_TAG}" + --repo "${GITHUB_REPOSITORY}" + --target "${RELEASE_SHA}" + --title "${release_title}" + --notes-file "${body_path}" +) +if [[ "${UPSTREAM_PRERELEASE}" == "true" ]]; then + release_args+=(--prerelease) +fi +release_args+=("${asset_path}#codex-termux") +"${release_args[@]}" diff --git a/scripts/termux-download-release-artifact.sh b/scripts/termux-download-release-artifact.sh new file mode 100755 index 000000000000..2acdba9025a0 --- /dev/null +++ b/scripts/termux-download-release-artifact.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${HEAD_SHA:-}" ]]; then + echo "HEAD_SHA is required." >&2 + exit 1 +fi + +if [[ -z "${PR_ARTIFACT_NAME:-}" ]]; then + echo "PR_ARTIFACT_NAME is required." >&2 + exit 1 +fi + +promoted_dir="${PROMOTED_DIR:-promoted}" + +find_run_id() { + local event="$1" + gh run list \ + --repo "${GITHUB_REPOSITORY}" \ + --workflow rust-release.yml \ + --event "${event}" \ + --status success \ + --commit "${HEAD_SHA}" \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId // empty' +} + +find_artifact_run_id() { + local artifact_name="$1" + + gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=${artifact_name}" \ + | jq -rs \ + --arg artifact_name "${artifact_name}" \ + --arg head_sha "${HEAD_SHA}" \ + ' + [ + .[].artifacts[] + | select(.name == $artifact_name) + | select(.expired == false) + | select(.workflow_run.head_sha == $head_sha) + ] + | sort_by(.created_at) + | reverse + | .[0].workflow_run.id // empty + ' +} + +artifact_name="${PR_ARTIFACT_NAME}" +run_id="$(find_artifact_run_id "${artifact_name}")" +mkdir -p "${promoted_dir}" +if [[ -n "${run_id}" ]] && gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}"; then + : +else + artifact_name="aarch64-linux-android" + run_id="$(find_run_id workflow_dispatch)" + if [[ -z "${run_id}" ]]; then + echo "No successful rust-release run found for ${HEAD_SHA}" >&2 + exit 1 + fi + gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}" +fi + +ls -la "${promoted_dir}" +if [[ -f "${promoted_dir}/SHA256SUMS" ]]; then + (cd "${promoted_dir}" && sha256sum -c SHA256SUMS) +fi +if [[ ! -f "${promoted_dir}/codex-aarch64-linux-android.tar.gz" ]]; then + echo "Expected ${promoted_dir}/codex-aarch64-linux-android.tar.gz in the downloaded artifact." >&2 + exit 1 +fi diff --git a/scripts/termux-find-release-pr.sh b/scripts/termux-find-release-pr.sh new file mode 100755 index 000000000000..3e35fa42eabe --- /dev/null +++ b/scripts/termux-find-release-pr.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -n "${INPUT_PR_NUMBER:-}" || -n "${INPUT_PR_HEAD_SHA:-}" ]]; then + if [[ -z "${INPUT_PR_NUMBER:-}" || -z "${INPUT_PR_HEAD_SHA:-}" ]]; then + echo "workflow_dispatch inputs pr_number and pr_head_sha must be provided together." >&2 + exit 1 + fi + + pr_number="${INPUT_PR_NUMBER}" + head_sha="${INPUT_PR_HEAD_SHA}" + head_ref="" +else + if [[ -z "${RELEASE_BRANCH:-}" || -z "${RELEASE_SHA:-}" ]]; then + echo "RELEASE_BRANCH and RELEASE_SHA are required." >&2 + exit 1 + fi + + pr_json="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${RELEASE_BRANCH}" \ + --state merged \ + --limit 100 \ + --json number,headRefOid,headRefName,mergeCommit \ + --jq "[.[] | select(.mergeCommit.oid == \"${RELEASE_SHA}\")] | sort_by(.number) | reverse | .[0] // empty" + )" + if [[ -z "${pr_json}" ]]; then + echo "Unable to find merged PR for ${RELEASE_SHA} into ${RELEASE_BRANCH}" >&2 + exit 1 + fi + + pr_number="$(jq -r '.number' <<< "${pr_json}")" + head_sha="$(jq -r '.headRefOid // .head.sha' <<< "${pr_json}")" + head_ref="$(jq -r '.headRefName // .head.ref' <<< "${pr_json}")" +fi + +artifact_name="termux-android-pr-${pr_number}-${head_sha}" +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "number=${pr_number}" + echo "head_sha=${head_sha}" + echo "head_ref=${head_ref}" + echo "artifact_name=${artifact_name}" + } >> "${GITHUB_OUTPUT}" +else + printf 'number=%s\nhead_sha=%s\nhead_ref=%s\nartifact_name=%s\n' \ + "${pr_number}" \ + "${head_sha}" \ + "${head_ref}" \ + "${artifact_name}" +fi diff --git a/scripts/termux-read-release-metadata.sh b/scripts/termux-read-release-metadata.sh new file mode 100755 index 000000000000..519a0635a368 --- /dev/null +++ b/scripts/termux-read-release-metadata.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# Parse .github/termux-release.json and emit workflow outputs for deploy or +# promote jobs. TERMUX_RELEASE_ACTION must be "deploy" or "promote". + +set -euo pipefail + +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +metadata=".github/termux-release.json" +action="${TERMUX_RELEASE_ACTION:-deploy}" +case "${action}" in + deploy) + missing_context="deployment" + ;; + promote) + missing_context="promotion" + ;; + *) + echo "TERMUX_RELEASE_ACTION must be deploy or promote." >&2 + exit 1 + ;; +esac + +if [[ ! -f "${metadata}" ]]; then + echo "No ${metadata}; this push is not a Termux release ${missing_context}." + echo "${action}=false" >> "${GITHUB_OUTPUT}" + exit 0 +fi + +upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" +upstream_name="$(jq -r '.upstream_name // .upstream_tag // empty' "${metadata}")" +termux_tag="$(jq -r '.termux_tag // empty' "${metadata}")" +upstream_version="${upstream_tag#rust-v}" +upstream_version="${upstream_version%-termux}" +upstream_prerelease=false +if [[ "${upstream_version}" == *-* ]]; then + upstream_prerelease=true +fi +upstream_html_url="$(jq -r '.upstream_html_url // ""' "${metadata}")" +upstream_repo="$(jq -r '.upstream_repo // "openai/codex"' "${metadata}")" +release_train="$(jq -r '.release_train // ""' "${metadata}")" +if [[ -z "${upstream_tag}" || -z "${termux_tag}" ]]; then + echo "Missing upstream_tag or termux_tag in ${metadata}" >&2 + exit 1 +fi + +release_state="$(TERMUX_TAG="${termux_tag}" "${script_dir}/termux-release-asset-state.sh")" +release_exists="$(awk -F= '$1 == "release_exists" { print $2 }' <<< "${release_state}")" +asset_exists="$(awk -F= '$1 == "asset_exists" { print $2 }' <<< "${release_state}")" + +if [[ "${action}" == "promote" ]]; then + if [[ "${asset_exists}" == "true" ]]; then + echo "${termux_tag} already exists with codex-aarch64-linux-android.tar.gz; skipping promotion." + echo "promote=false" >> "${GITHUB_OUTPUT}" + exit 0 + fi + if [[ "${release_exists}" == "true" ]]; then + echo "${termux_tag} exists but is missing codex-aarch64-linux-android.tar.gz; repairing promotion." + fi +fi + +{ + echo "${action}=true" + echo "upstream_tag=${upstream_tag}" + echo "upstream_name=${upstream_name}" + echo "termux_tag=${termux_tag}" + echo "upstream_prerelease=${upstream_prerelease}" + echo "upstream_html_url=${upstream_html_url}" + echo "upstream_repo=${upstream_repo}" + echo "release_train=${release_train}" + echo "release_exists=${release_exists}" + echo "asset_exists=${asset_exists}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-release-asset-state.sh b/scripts/termux-release-asset-state.sh new file mode 100755 index 000000000000..8694480b6a36 --- /dev/null +++ b/scripts/termux-release-asset-state.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +termux_tag="${1:-${TERMUX_TAG:-}}" +release_repo="${RELEASE_REPO:-${GITHUB_REPOSITORY:-}}" +asset_name="${ASSET_NAME:-codex-aarch64-linux-android.tar.gz}" + +if [[ -z "${termux_tag}" ]]; then + echo "TERMUX_TAG or tag argument is required." >&2 + exit 1 +fi + +if [[ -z "${release_repo}" ]]; then + echo "GITHUB_REPOSITORY or RELEASE_REPO is required." >&2 + exit 1 +fi + +release_exists=false +asset_exists=false + +if gh release view "${termux_tag}" --repo "${release_repo}" >/dev/null 2>&1; then + release_exists=true + release_asset_exists="$( + gh release view "${termux_tag}" \ + --repo "${release_repo}" \ + --json assets \ + | jq -r --arg asset_name "${asset_name}" \ + '.assets | map(.name) | any(. == $asset_name)' + )" + if [[ "${release_asset_exists}" == "true" ]]; then + asset_exists=true + fi +fi + +printf 'release_exists=%s\n' "${release_exists}" +printf 'asset_exists=%s\n' "${asset_exists}" diff --git a/scripts/termux-release-paths.sh b/scripts/termux-release-paths.sh new file mode 100755 index 000000000000..6990f323ec44 --- /dev/null +++ b/scripts/termux-release-paths.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# Sourceable path lists for files owned by the Termux release automation. + +set -euo pipefail + +readonly -a TERMUX_RELEASE_WORKFLOW_PATHS=( + .github/workflows/rust-release.yml + .github/workflows/shell-tool-mcp.yml + .github/workflows/termux-release-checkpoint.yml + .github/workflows/termux-release-deploy.yml + .github/workflows/termux-release-promote.yml +) + +readonly -a TERMUX_RELEASE_BRANCH_SCRIPT_PATHS=( + scripts/termux-configure-git.sh + scripts/termux-create-checkpoint-pr.sh + scripts/termux-create-or-update-mirrored-release.sh + scripts/termux-download-release-artifact.sh + scripts/termux-find-release-pr.sh + scripts/termux-read-release-metadata.sh + scripts/termux-release-asset-state.sh + scripts/termux-release-paths.sh + scripts/termux-resolve-release-ref.sh + scripts/termux-validate-gh-env.sh +) + +readonly -a TERMUX_RELEASE_AUTOMATION_PATHS=( + "${TERMUX_RELEASE_WORKFLOW_PATHS[@]}" + "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +) + +readonly -a TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS=( + "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" + .github/termux-release.json +) + +termux_path_in_list() { + local candidate="$1" + shift + local listed_path + + for listed_path in "$@"; do + [[ "${candidate}" != "${listed_path}" ]] || return 0 + done + return 1 +} + +termux_is_release_automation_path() { + termux_path_in_list "$1" "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" +} + +termux_is_checkpoint_release_only_path() { + termux_path_in_list "$1" "${TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS[@]}" +} diff --git a/scripts/termux-resolve-release-ref.sh b/scripts/termux-resolve-release-ref.sh new file mode 100755 index 000000000000..194773d45bed --- /dev/null +++ b/scripts/termux-resolve-release-ref.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Resolve the release branch and SHA for deploy/promote workflows. + +set -euo pipefail + +: "${GITHUB_EVENT_NAME:?GITHUB_EVENT_NAME is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" + +input_release_branch="${INPUT_RELEASE_BRANCH:-${REQUESTED_RELEASE_BRANCH:-}}" +input_release_sha="${INPUT_RELEASE_SHA:-${REQUESTED_RELEASE_SHA:-}}" + +if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then + if [[ -z "${input_release_branch}" ]]; then + echo "release_branch is required for workflow_dispatch." >&2 + exit 1 + fi + + release_branch="${input_release_branch}" + if [[ -n "${input_release_sha}" ]]; then + git checkout --detach "${input_release_sha}" + release_sha="${input_release_sha}" + else + release_sha="$(git rev-parse HEAD)" + fi +else + : "${GITHUB_REF_NAME:?GITHUB_REF_NAME is required}" + : "${GITHUB_SHA:?GITHUB_SHA is required}" + release_branch="${GITHUB_REF_NAME}" + release_sha="${GITHUB_SHA}" +fi + +{ + echo "branch=${release_branch}" + echo "sha=${release_sha}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-validate-gh-env.sh b/scripts/termux-validate-gh-env.sh new file mode 100755 index 000000000000..02581fdebceb --- /dev/null +++ b/scripts/termux-validate-gh-env.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Lightweight post-toolbox check for jobs that rely on authenticated gh calls. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +command -v gh +gh auth status --hostname github.com + +printf 'GITHUB_REPOSITORY=%s\n' "${GITHUB_REPOSITORY}" +printf 'REPO=%s\n' "${REPO:-}" +printf 'GH_REPO_URL=%s\n' "${GH_REPO_URL:-}" +printf 'GH_WORKFLOW_URL=%s\n' "${GH_WORKFLOW_URL:-}" From 820e9df09476c5d357fabcd7ee3e050e1856b3cb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 16:38:39 +0000 Subject: [PATCH 10/25] Prepare Termux rust-v0.133.0-alpha.1 --- .github/termux-release.json | 15 ++ codex-rs/Cargo.lock | 247 ++++++++++++++------------- codex-rs/Cargo.toml | 2 + codex-rs/arg0/Cargo.toml | 1 + codex-rs/arg0/src/lib.rs | 29 +++- codex-rs/core/Cargo.toml | 5 + codex-rs/core/src/installation_id.rs | 8 +- codex-rs/execpolicy/Cargo.toml | 1 + codex-rs/execpolicy/src/amend.rs | 23 ++- codex-rs/tui/Cargo.toml | 2 +- codex-rs/tui/src/lib.rs | 8 +- codex-rs/utils/file-lock/BUILD.bazel | 6 + codex-rs/utils/file-lock/Cargo.toml | 10 ++ codex-rs/utils/file-lock/src/lib.rs | 168 ++++++++++++++++++ justfile | 5 + scripts/termux-lock-audit.sh | 126 ++++++++++++++ 16 files changed, 518 insertions(+), 138 deletions(-) create mode 100644 .github/termux-release.json create mode 100644 codex-rs/utils/file-lock/BUILD.bazel create mode 100644 codex-rs/utils/file-lock/Cargo.toml create mode 100644 codex-rs/utils/file-lock/src/lib.rs create mode 100755 scripts/termux-lock-audit.sh diff --git a/.github/termux-release.json b/.github/termux-release.json new file mode 100644 index 000000000000..3cf1ce21c8eb --- /dev/null +++ b/.github/termux-release.json @@ -0,0 +1,15 @@ +{ + "upstream_repo": "openai/codex", + "upstream_tag": "rust-v0.133.0-alpha.1", + "upstream_name": "0.133.0-alpha.1", + "upstream_html_url": "https://github.com/openai/codex/releases/tag/rust-v0.133.0-alpha.1", + "upstream_target": "main", + "upstream_release_id": "326079618", + "upstream_prerelease": true, + "release_train": "0.133.0", + "release_branch": "release/0.133.0", + "work_branch": "upstream/rust-v0.133.0", + "patch_branch": "wallentx/termux-target", + "patch_source_sha": "b32aadb5610138201c219029a65486f448139b53", + "termux_tag": "rust-v0.133.0-alpha.1-termux" +} diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 1950939e652e..54e1f7a1a078 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -402,7 +402,7 @@ checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "app_test_support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -1778,7 +1778,7 @@ dependencies = [ [[package]] name = "codex-agent-graph-store" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-protocol", @@ -1793,7 +1793,7 @@ dependencies = [ [[package]] name = "codex-agent-identity" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -1812,7 +1812,7 @@ dependencies = [ [[package]] name = "codex-analytics" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-app-server-protocol", "codex-git-utils", @@ -1832,7 +1832,7 @@ dependencies = [ [[package]] name = "codex-ansi-escape" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "ansi-to-tui", "ratatui", @@ -1841,7 +1841,7 @@ dependencies = [ [[package]] name = "codex-api" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_matches", @@ -1875,7 +1875,7 @@ dependencies = [ [[package]] name = "codex-app-server" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "app_test_support", @@ -1960,7 +1960,7 @@ dependencies = [ [[package]] name = "codex-app-server-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-app-server", "codex-app-server-protocol", @@ -1987,7 +1987,7 @@ dependencies = [ [[package]] name = "codex-app-server-daemon" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-app-server-protocol", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "codex-app-server-protocol" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2035,7 +2035,7 @@ dependencies = [ [[package]] name = "codex-app-server-test-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2056,7 +2056,7 @@ dependencies = [ [[package]] name = "codex-app-server-transport" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "axum", @@ -2095,7 +2095,7 @@ dependencies = [ [[package]] name = "codex-apply-patch" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -2114,7 +2114,7 @@ dependencies = [ [[package]] name = "codex-arg0" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-apply-patch", @@ -2123,6 +2123,7 @@ dependencies = [ "codex-sandboxing", "codex-shell-escalation", "codex-utils-absolute-path", + "codex-utils-file-lock", "codex-utils-home-dir", "dotenvy", "tempfile", @@ -2131,7 +2132,7 @@ dependencies = [ [[package]] name = "codex-async-utils" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "pretty_assertions", @@ -2141,7 +2142,7 @@ dependencies = [ [[package]] name = "codex-aws-auth" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "aws-config", "aws-credential-types", @@ -2156,7 +2157,7 @@ dependencies = [ [[package]] name = "codex-backend-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-api", @@ -2173,7 +2174,7 @@ dependencies = [ [[package]] name = "codex-backend-openapi-models" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "serde", "serde_json", @@ -2182,7 +2183,7 @@ dependencies = [ [[package]] name = "codex-bwrap" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "cc", "libc", @@ -2191,7 +2192,7 @@ dependencies = [ [[package]] name = "codex-chatgpt" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2214,7 +2215,7 @@ dependencies = [ [[package]] name = "codex-cli" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -2279,7 +2280,7 @@ dependencies = [ [[package]] name = "codex-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "bytes", @@ -2310,7 +2311,7 @@ dependencies = [ [[package]] name = "codex-cloud-requirements" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "base64 0.22.1", @@ -2335,7 +2336,7 @@ dependencies = [ [[package]] name = "codex-cloud-tasks" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -2367,7 +2368,7 @@ dependencies = [ [[package]] name = "codex-cloud-tasks-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -2382,7 +2383,7 @@ dependencies = [ [[package]] name = "codex-cloud-tasks-mock-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "chrono", @@ -2392,7 +2393,7 @@ dependencies = [ [[package]] name = "codex-code-mode" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-channel", "async-trait", @@ -2409,11 +2410,11 @@ dependencies = [ [[package]] name = "codex-collaboration-mode-templates" -version = "0.0.0" +version = "0.132.0-alpha.1" [[package]] name = "codex-config" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -2460,7 +2461,7 @@ dependencies = [ [[package]] name = "codex-connectors" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-app-server-protocol", @@ -2476,7 +2477,7 @@ dependencies = [ [[package]] name = "codex-core" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "arc-swap", @@ -2530,6 +2531,7 @@ dependencies = [ "codex-utils-absolute-path", "codex-utils-cache", "codex-utils-cargo-bin", + "codex-utils-file-lock", "codex-utils-home-dir", "codex-utils-image", "codex-utils-output-truncation", @@ -2594,7 +2596,7 @@ dependencies = [ [[package]] name = "codex-core-api" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-analytics", "codex-app-server-protocol", @@ -2613,7 +2615,7 @@ dependencies = [ [[package]] name = "codex-core-plugins" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -2652,7 +2654,7 @@ dependencies = [ [[package]] name = "codex-core-skills" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-analytics", @@ -2683,7 +2685,7 @@ dependencies = [ [[package]] name = "codex-debug-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2695,7 +2697,7 @@ dependencies = [ [[package]] name = "codex-exec" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -2741,7 +2743,7 @@ dependencies = [ [[package]] name = "codex-exec-server" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "arc-swap", @@ -2782,11 +2784,12 @@ dependencies = [ [[package]] name = "codex-execpolicy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", "codex-utils-absolute-path", + "codex-utils-file-lock", "multimap", "pretty_assertions", "serde", @@ -2799,7 +2802,7 @@ dependencies = [ [[package]] name = "codex-execpolicy-legacy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "allocative", "anyhow", @@ -2819,7 +2822,7 @@ dependencies = [ [[package]] name = "codex-experimental-api-macros" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "proc-macro2", "quote", @@ -2828,7 +2831,7 @@ dependencies = [ [[package]] name = "codex-extension-api" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-protocol", @@ -2837,7 +2840,7 @@ dependencies = [ [[package]] name = "codex-external-agent-migration" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-hooks", "pretty_assertions", @@ -2849,7 +2852,7 @@ dependencies = [ [[package]] name = "codex-external-agent-sessions" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "chrono", "codex-app-server-protocol", @@ -2863,7 +2866,7 @@ dependencies = [ [[package]] name = "codex-features" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-otel", "codex-protocol", @@ -2876,7 +2879,7 @@ dependencies = [ [[package]] name = "codex-feedback" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-login", @@ -2889,7 +2892,7 @@ dependencies = [ [[package]] name = "codex-file-search" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2905,7 +2908,7 @@ dependencies = [ [[package]] name = "codex-file-system" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-protocol", @@ -2915,7 +2918,7 @@ dependencies = [ [[package]] name = "codex-file-watcher" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "notify", "pretty_assertions", @@ -2926,7 +2929,7 @@ dependencies = [ [[package]] name = "codex-git-utils" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -2950,7 +2953,7 @@ dependencies = [ [[package]] name = "codex-goal-extension" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-extension-api", @@ -2962,7 +2965,7 @@ dependencies = [ [[package]] name = "codex-guardian" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-core", @@ -2972,7 +2975,7 @@ dependencies = [ [[package]] name = "codex-hooks" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -2995,7 +2998,7 @@ dependencies = [ [[package]] name = "codex-install-context" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-home-dir", "pretty_assertions", @@ -3004,7 +3007,7 @@ dependencies = [ [[package]] name = "codex-keyring-store" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "keyring", "tracing", @@ -3012,7 +3015,7 @@ dependencies = [ [[package]] name = "codex-linux-sandbox" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "clap", "codex-core", @@ -3035,7 +3038,7 @@ dependencies = [ [[package]] name = "codex-lmstudio" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-core", "codex-model-provider-info", @@ -3049,7 +3052,7 @@ dependencies = [ [[package]] name = "codex-login" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3091,7 +3094,7 @@ dependencies = [ [[package]] name = "codex-mcp" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-channel", @@ -3123,7 +3126,7 @@ dependencies = [ [[package]] name = "codex-mcp-server" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-arg0", @@ -3155,7 +3158,7 @@ dependencies = [ [[package]] name = "codex-memories-extension" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-core", @@ -3176,7 +3179,7 @@ dependencies = [ [[package]] name = "codex-memories-mcp" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-utils-absolute-path", @@ -3193,7 +3196,7 @@ dependencies = [ [[package]] name = "codex-memories-read" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-protocol", "codex-shell-command", @@ -3207,7 +3210,7 @@ dependencies = [ [[package]] name = "codex-memories-write" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -3242,7 +3245,7 @@ dependencies = [ [[package]] name = "codex-message-history" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-config", "pretty_assertions", @@ -3255,7 +3258,7 @@ dependencies = [ [[package]] name = "codex-model-provider" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-agent-identity", @@ -3279,7 +3282,7 @@ dependencies = [ [[package]] name = "codex-model-provider-info" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-api", "codex-app-server-protocol", @@ -3296,7 +3299,7 @@ dependencies = [ [[package]] name = "codex-models-manager" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "chrono", @@ -3317,7 +3320,7 @@ dependencies = [ [[package]] name = "codex-network-proxy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3348,7 +3351,7 @@ dependencies = [ [[package]] name = "codex-ollama" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "assert_matches", "async-stream", @@ -3367,7 +3370,7 @@ dependencies = [ [[package]] name = "codex-otel" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "chrono", "codex-api", @@ -3399,7 +3402,7 @@ dependencies = [ [[package]] name = "codex-plugin" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-config", "codex-utils-absolute-path", @@ -3409,7 +3412,7 @@ dependencies = [ [[package]] name = "codex-process-hardening" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "libc", "pretty_assertions", @@ -3417,7 +3420,7 @@ dependencies = [ [[package]] name = "codex-protocol" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chardetng", @@ -3457,7 +3460,7 @@ dependencies = [ [[package]] name = "codex-realtime-webrtc" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "libwebrtc", "thiserror 2.0.18", @@ -3466,7 +3469,7 @@ dependencies = [ [[package]] name = "codex-response-debug-context" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "base64 0.22.1", "codex-api", @@ -3477,7 +3480,7 @@ dependencies = [ [[package]] name = "codex-responses-api-proxy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -3494,7 +3497,7 @@ dependencies = [ [[package]] name = "codex-rmcp-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "axum", @@ -3532,7 +3535,7 @@ dependencies = [ [[package]] name = "codex-rollout" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3557,7 +3560,7 @@ dependencies = [ [[package]] name = "codex-rollout-trace" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-code-mode", @@ -3573,7 +3576,7 @@ dependencies = [ [[package]] name = "codex-sandboxing" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3594,7 +3597,7 @@ dependencies = [ [[package]] name = "codex-secrets" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "age", "anyhow", @@ -3615,7 +3618,7 @@ dependencies = [ [[package]] name = "codex-shell-command" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -3635,7 +3638,7 @@ dependencies = [ [[package]] name = "codex-shell-escalation" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3656,7 +3659,7 @@ dependencies = [ [[package]] name = "codex-skills" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-absolute-path", "include_dir", @@ -3665,7 +3668,7 @@ dependencies = [ [[package]] name = "codex-state" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -3688,7 +3691,7 @@ dependencies = [ [[package]] name = "codex-stdio-to-uds" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-uds", @@ -3700,7 +3703,7 @@ dependencies = [ [[package]] name = "codex-terminal-detection" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "tracing", @@ -3708,7 +3711,7 @@ dependencies = [ [[package]] name = "codex-test-binary-support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-arg0", "tempfile", @@ -3716,7 +3719,7 @@ dependencies = [ [[package]] name = "codex-thread-manager-sample" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -3727,7 +3730,7 @@ dependencies = [ [[package]] name = "codex-thread-store" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "chrono", @@ -3748,7 +3751,7 @@ dependencies = [ [[package]] name = "codex-tools" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-app-server-protocol", @@ -3768,7 +3771,7 @@ dependencies = [ [[package]] name = "codex-tui" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "arboard", @@ -3879,7 +3882,7 @@ dependencies = [ [[package]] name = "codex-uds" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-io", "pretty_assertions", @@ -3891,7 +3894,7 @@ dependencies = [ [[package]] name = "codex-utils-absolute-path" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "dirs", "dunce", @@ -3905,14 +3908,14 @@ dependencies = [ [[package]] name = "codex-utils-approval-presets" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-protocol", ] [[package]] name = "codex-utils-cache" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "lru 0.16.3", "sha1", @@ -3921,7 +3924,7 @@ dependencies = [ [[package]] name = "codex-utils-cargo-bin" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "assert_cmd", "runfiles", @@ -3930,7 +3933,7 @@ dependencies = [ [[package]] name = "codex-utils-cli" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "clap", "codex-protocol", @@ -3942,15 +3945,19 @@ dependencies = [ [[package]] name = "codex-utils-elapsed" -version = "0.0.0" +version = "0.132.0-alpha.1" + +[[package]] +name = "codex-utils-file-lock" +version = "0.132.0-alpha.1" [[package]] name = "codex-utils-fuzzy-match" -version = "0.0.0" +version = "0.132.0-alpha.1" [[package]] name = "codex-utils-home-dir" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-absolute-path", "dirs", @@ -3960,7 +3967,7 @@ dependencies = [ [[package]] name = "codex-utils-image" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "base64 0.22.1", "codex-utils-cache", @@ -3972,7 +3979,7 @@ dependencies = [ [[package]] name = "codex-utils-json-to-toml" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "serde_json", @@ -3981,7 +3988,7 @@ dependencies = [ [[package]] name = "codex-utils-oss" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-core", "codex-lmstudio", @@ -3991,7 +3998,7 @@ dependencies = [ [[package]] name = "codex-utils-output-truncation" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-protocol", "codex-utils-string", @@ -4000,7 +4007,7 @@ dependencies = [ [[package]] name = "codex-utils-path" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-absolute-path", "dunce", @@ -4010,7 +4017,7 @@ dependencies = [ [[package]] name = "codex-utils-plugins" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-exec-server", "codex-login", @@ -4023,7 +4030,7 @@ dependencies = [ [[package]] name = "codex-utils-pty" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "filedescriptor", @@ -4039,7 +4046,7 @@ dependencies = [ [[package]] name = "codex-utils-readiness" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "assert_matches", "async-trait", @@ -4050,14 +4057,14 @@ dependencies = [ [[package]] name = "codex-utils-rustls-provider" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "rustls", ] [[package]] name = "codex-utils-sandbox-summary" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-core", "codex-model-provider-info", @@ -4068,7 +4075,7 @@ dependencies = [ [[package]] name = "codex-utils-sleep-inhibitor" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "core-foundation 0.9.4", "libc", @@ -4078,14 +4085,14 @@ dependencies = [ [[package]] name = "codex-utils-stream-parser" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", ] [[package]] name = "codex-utils-string" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "regex-lite", @@ -4095,14 +4102,14 @@ dependencies = [ [[package]] name = "codex-utils-template" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", ] [[package]] name = "codex-v8-poc" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "v8", @@ -4110,7 +4117,7 @@ dependencies = [ [[package]] name = "codex-windows-sandbox" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -4361,7 +4368,7 @@ dependencies = [ [[package]] name = "core_test_support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -5475,7 +5482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8560,7 +8567,7 @@ dependencies = [ [[package]] name = "mcp_test_support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-login", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 91b73d562b5d..c2ae163ee1cb 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -81,6 +81,7 @@ members = [ "v8-poc", "utils/absolute-path", "utils/cargo-bin", + "utils/file-lock", "git-utils", "utils/cache", "utils/image", @@ -219,6 +220,7 @@ codex-utils-cache = { path = "utils/cache" } codex-utils-cargo-bin = { path = "utils/cargo-bin" } codex-utils-cli = { path = "utils/cli" } codex-utils-elapsed = { path = "utils/elapsed" } +codex-utils-file-lock = { path = "utils/file-lock" } codex-utils-fuzzy-match = { path = "utils/fuzzy-match" } codex-utils-home-dir = { path = "utils/home-dir" } codex-utils-image = { path = "utils/image" } diff --git a/codex-rs/arg0/Cargo.toml b/codex-rs/arg0/Cargo.toml index 7ee21a770e49..7c2c17692020 100644 --- a/codex-rs/arg0/Cargo.toml +++ b/codex-rs/arg0/Cargo.toml @@ -20,6 +20,7 @@ codex-linux-sandbox = { workspace = true } codex-sandboxing = { workspace = true } codex-shell-escalation = { workspace = true } codex-utils-absolute-path = { workspace = true } +codex-utils-file-lock = { workspace = true } codex-utils-home-dir = { workspace = true } dotenvy = { workspace = true } tempfile = { workspace = true } diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index 2f6ae4653c65..5a558769a7e5 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -6,6 +6,8 @@ use std::path::PathBuf; use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1; use codex_exec_server::CODEX_FS_HELPER_ARG1; use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; +use codex_utils_file_lock::TryFileLockOutcome; +use codex_utils_file_lock::try_lock_exclusive_optional; use codex_utils_home_dir::find_codex_home; #[cfg(unix)] use std::os::unix::fs::symlink; @@ -33,12 +35,12 @@ pub struct Arg0DispatchPaths { /// Keeps the per-session PATH entry alive and locked for the process lifetime. pub struct Arg0PathEntryGuard { _temp_dir: TempDir, - _lock_file: File, + _lock_file: Option, paths: Arg0DispatchPaths, } impl Arg0PathEntryGuard { - fn new(temp_dir: TempDir, lock_file: File, paths: Arg0DispatchPaths) -> Self { + fn new(temp_dir: TempDir, lock_file: Option, paths: Arg0DispatchPaths) -> Self { Self { _temp_dir: temp_dir, _lock_file: lock_file, @@ -326,7 +328,13 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result Some(lock_file), + TryFileLockOutcome::Unsupported => None, + TryFileLockOutcome::WouldBlock => { + return Err(std::io::Error::from(std::io::ErrorKind::WouldBlock).into()); + } + }; for filename in &[ APPLY_PATCH_ARG0, @@ -445,10 +453,9 @@ fn try_lock_dir(dir: &Path) -> std::io::Result> { Err(err) => return Err(err), }; - match lock_file.try_lock() { - Ok(()) => Ok(Some(lock_file)), - Err(std::fs::TryLockError::WouldBlock) => Ok(None), - Err(err) => Err(err.into()), + match try_lock_exclusive_optional(&lock_file)? { + TryFileLockOutcome::Acquired => Ok(Some(lock_file)), + TryFileLockOutcome::WouldBlock | TryFileLockOutcome::Unsupported => Ok(None), } } @@ -562,7 +569,13 @@ mod tests { let dir = root.path().join("locked"); fs::create_dir(&dir)?; let lock_file = create_lock(&dir)?; - lock_file.try_lock()?; + match try_lock_exclusive_optional(&lock_file)? { + TryFileLockOutcome::Acquired => {} + TryFileLockOutcome::Unsupported => return Ok(()), + TryFileLockOutcome::WouldBlock => { + panic!("newly created lock file should not be locked"); + } + } janitor_cleanup(root.path())?; diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 8472918dcae9..fdf1d4b18a9e 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -63,6 +63,7 @@ codex-thread-store = { workspace = true } codex-tools = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-cache = { workspace = true } +codex-utils-file-lock = { workspace = true } codex-utils-image = { workspace = true } codex-utils-home-dir = { workspace = true } codex-utils-output-truncation = { workspace = true } @@ -125,6 +126,10 @@ openssl-sys = { workspace = true, features = ["vendored"] } [target.aarch64-unknown-linux-musl.dependencies] openssl-sys = { workspace = true, features = ["vendored"] } +# Build OpenSSL from source for Android builds. +[target.aarch64-linux-android.dependencies] +openssl-sys = { workspace = true, features = ["vendored"] } + [target.'cfg(unix)'.dependencies] codex-shell-escalation = { workspace = true } diff --git a/codex-rs/core/src/installation_id.rs b/codex-rs/core/src/installation_id.rs index a42e6b6d8353..a87c660f6132 100644 --- a/codex-rs/core/src/installation_id.rs +++ b/codex-rs/core/src/installation_id.rs @@ -11,6 +11,9 @@ use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::PermissionsExt; use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_file_lock::FileLockOutcome; +use codex_utils_file_lock::acquire_sibling_lock_dir; +use codex_utils_file_lock::lock_exclusive_optional; use tokio::fs; use uuid::Uuid; @@ -29,7 +32,10 @@ pub async fn resolve_installation_id(codex_home: &AbsolutePathBuf) -> Result None, + FileLockOutcome::Unsupported => Some(acquire_sibling_lock_dir(&path)?), + }; #[cfg(unix)] { diff --git a/codex-rs/execpolicy/Cargo.toml b/codex-rs/execpolicy/Cargo.toml index b22226a79e4b..b87af084d7d1 100644 --- a/codex-rs/execpolicy/Cargo.toml +++ b/codex-rs/execpolicy/Cargo.toml @@ -21,6 +21,7 @@ workspace = true anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] } codex-utils-absolute-path = { workspace = true } +codex-utils-file-lock = { workspace = true } multimap = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/codex-rs/execpolicy/src/amend.rs b/codex-rs/execpolicy/src/amend.rs index e25fd1bd9144..1ef36b1ff5d8 100644 --- a/codex-rs/execpolicy/src/amend.rs +++ b/codex-rs/execpolicy/src/amend.rs @@ -6,6 +6,10 @@ use std::io::Write; use std::path::Path; use std::path::PathBuf; +use codex_utils_file_lock::FileLockOutcome; +use codex_utils_file_lock::acquire_sibling_lock_dir; +use codex_utils_file_lock::lock_exclusive_optional; + use crate::decision::Decision; use crate::rule::NetworkRuleProtocol; use crate::rule::normalize_network_rule_host; @@ -154,10 +158,21 @@ fn append_locked_line(policy_path: &Path, line: &str) -> Result<(), AmendError> path: policy_path.to_path_buf(), source, })?; - file.lock().map_err(|source| AmendError::LockPolicyFile { - path: policy_path.to_path_buf(), - source, - })?; + let _lock_dir_guard = + match lock_exclusive_optional(&file).map_err(|source| AmendError::LockPolicyFile { + path: policy_path.to_path_buf(), + source, + })? { + FileLockOutcome::Acquired => None, + FileLockOutcome::Unsupported => { + Some(acquire_sibling_lock_dir(policy_path).map_err(|source| { + AmendError::LockPolicyFile { + path: policy_path.to_path_buf(), + source, + } + })?) + } + }; file.seek(SeekFrom::Start(0)) .map_err(|source| AmendError::SeekPolicyFile { diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index c213e92bae41..40624baba0d0 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -126,7 +126,7 @@ uuid = { workspace = true } codex-windows-sandbox = { workspace = true } tokio-util = { workspace = true, features = ["time"] } -[target.'cfg(not(target_os = "linux"))'.dependencies] +[target.'cfg(all(not(target_os = "linux"), not(target_os = "android")))'.dependencies] cpal = "0.15" [target.'cfg(unix)'.dependencies] diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 40cd121c7135..8a71fd738e1e 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -89,9 +89,9 @@ mod app_server_approval_conversions; mod app_server_session; mod approval_events; mod ascii_animation; -#[cfg(not(target_os = "linux"))] +#[cfg(not(any(target_os = "linux", target_os = "android")))] mod audio_device; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] #[allow(dead_code)] mod audio_device { use crate::app_event::RealtimeAudioDeviceKind; @@ -195,11 +195,11 @@ mod update_prompt; mod update_versions; mod updates; mod version; -#[cfg(not(target_os = "linux"))] +#[cfg(not(any(target_os = "linux", target_os = "android")))] mod voice; mod width; mod workspace_command; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] #[allow(dead_code)] mod voice { use crate::app_event_sender::AppEventSender; diff --git a/codex-rs/utils/file-lock/BUILD.bazel b/codex-rs/utils/file-lock/BUILD.bazel new file mode 100644 index 000000000000..face70c53518 --- /dev/null +++ b/codex-rs/utils/file-lock/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "file-lock", + crate_name = "codex_utils_file_lock", +) diff --git a/codex-rs/utils/file-lock/Cargo.toml b/codex-rs/utils/file-lock/Cargo.toml new file mode 100644 index 000000000000..5e3877fc1630 --- /dev/null +++ b/codex-rs/utils/file-lock/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "codex-utils-file-lock" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] diff --git a/codex-rs/utils/file-lock/src/lib.rs b/codex-rs/utils/file-lock/src/lib.rs new file mode 100644 index 000000000000..fdcfbdb81e75 --- /dev/null +++ b/codex-rs/utils/file-lock/src/lib.rs @@ -0,0 +1,168 @@ +use std::fs::File; +use std::fs::create_dir; +use std::fs::remove_dir; +use std::io; +use std::path::Path; +use std::path::PathBuf; +use std::thread; +use std::time::Duration; + +const LOCK_DIR_RETRY_SLEEP: Duration = Duration::from_millis(100); + +/// Result of acquiring a blocking advisory file lock. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileLockOutcome { + Acquired, + Unsupported, +} + +/// Result of attempting to acquire a non-blocking advisory file lock. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryFileLockOutcome { + Acquired, + WouldBlock, + Unsupported, +} + +/// Result of attempting to acquire a non-blocking lock directory. +#[derive(Debug, PartialEq, Eq)] +pub enum TryLockDirOutcome { + Acquired(LockDirGuard), + WouldBlock, +} + +/// Guard for a sibling lock directory created with an atomic `mkdir`. +#[derive(Debug, PartialEq, Eq)] +pub struct LockDirGuard { + path: PathBuf, +} + +impl Drop for LockDirGuard { + fn drop(&mut self) { + let _ = remove_dir(&self.path); + } +} + +/// Acquires an exclusive advisory file lock, treating unsupported file locking +/// as a distinct outcome for platforms such as Termux. +pub fn lock_exclusive_optional(file: &File) -> io::Result { + match file.lock() { + Ok(()) => Ok(FileLockOutcome::Acquired), + Err(err) if err.kind() == io::ErrorKind::Unsupported => Ok(FileLockOutcome::Unsupported), + Err(err) => Err(err), + } +} + +/// Attempts to acquire an exclusive advisory file lock without blocking, +/// preserving `WouldBlock` and unsupported file locking as distinct outcomes. +pub fn try_lock_exclusive_optional(file: &File) -> io::Result { + match file.try_lock() { + Ok(()) => Ok(TryFileLockOutcome::Acquired), + Err(std::fs::TryLockError::WouldBlock) => Ok(TryFileLockOutcome::WouldBlock), + Err(std::fs::TryLockError::Error(err)) if err.kind() == io::ErrorKind::Unsupported => { + Ok(TryFileLockOutcome::Unsupported) + } + Err(std::fs::TryLockError::Error(err)) => Err(err), + } +} + +/// Returns the sibling directory path used as a fallback lock for `path`. +pub fn sibling_lock_dir(path: &Path) -> PathBuf { + let Some(file_name) = path.file_name() else { + return path.with_file_name(".lock"); + }; + + let mut lock_name = file_name.to_os_string(); + lock_name.push(".lock"); + path.with_file_name(lock_name) +} + +/// Acquires a sibling lock directory, blocking until it is available. +pub fn acquire_sibling_lock_dir(path: &Path) -> io::Result { + loop { + match try_acquire_sibling_lock_dir(path)? { + TryLockDirOutcome::Acquired(guard) => return Ok(guard), + TryLockDirOutcome::WouldBlock => thread::sleep(LOCK_DIR_RETRY_SLEEP), + } + } +} + +/// Attempts to acquire a sibling lock directory without blocking. +pub fn try_acquire_sibling_lock_dir(path: &Path) -> io::Result { + let lock_dir = sibling_lock_dir(path); + match create_dir(&lock_dir) { + Ok(()) => Ok(TryLockDirOutcome::Acquired(LockDirGuard { path: lock_dir })), + Err(err) if err.kind() == io::ErrorKind::AlreadyExists => Ok(TryLockDirOutcome::WouldBlock), + Err(err) => Err(err), + } +} + +/// Attempts to acquire a shared advisory file lock without blocking, +/// preserving `WouldBlock` and unsupported file locking as distinct outcomes. +pub fn try_lock_shared_optional(file: &File) -> io::Result { + match file.try_lock_shared() { + Ok(()) => Ok(TryFileLockOutcome::Acquired), + Err(std::fs::TryLockError::WouldBlock) => Ok(TryFileLockOutcome::WouldBlock), + Err(std::fs::TryLockError::Error(err)) if err.kind() == io::ErrorKind::Unsupported => { + Ok(TryFileLockOutcome::Unsupported) + } + Err(std::fs::TryLockError::Error(err)) => Err(err), + } +} + +#[cfg(test)] +mod tests { + use super::TryLockDirOutcome; + use super::sibling_lock_dir; + use super::try_acquire_sibling_lock_dir; + use std::fs; + use std::path::PathBuf; + use std::time::SystemTime; + + fn unique_temp_file_path(name: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("system clock should be after Unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "codex-file-lock-{name}-{}-{nanos}", + std::process::id() + )) + } + + #[test] + fn sibling_lock_dir_appends_lock_suffix() { + let path = PathBuf::from("/tmp/history.jsonl"); + + assert_eq!( + sibling_lock_dir(&path), + PathBuf::from("/tmp/history.jsonl.lock") + ); + } + + #[test] + fn try_acquire_sibling_lock_dir_is_exclusive_until_drop() { + let path = unique_temp_file_path("exclusive"); + let lock_dir = sibling_lock_dir(&path); + let _ = fs::remove_dir_all(&lock_dir); + + let guard = match try_acquire_sibling_lock_dir(&path).expect("acquire lock dir") { + TryLockDirOutcome::Acquired(guard) => guard, + TryLockDirOutcome::WouldBlock => panic!("first lock attempt should acquire"), + }; + assert!(lock_dir.is_dir()); + + assert!(matches!( + try_acquire_sibling_lock_dir(&path).expect("try acquire held lock dir"), + TryLockDirOutcome::WouldBlock + )); + + drop(guard); + assert!(!lock_dir.exists()); + + let reacquired = try_acquire_sibling_lock_dir(&path).expect("reacquire lock dir"); + assert!(matches!(reacquired, TryLockDirOutcome::Acquired(_))); + + let _ = fs::remove_dir_all(lock_dir); + } +} diff --git a/justfile b/justfile index ab2fbc63629a..569a04695216 100644 --- a/justfile +++ b/justfile @@ -116,6 +116,11 @@ argument-comment-lint *args: argument-comment-lint-from-source *args: {{ justfile_directory() }}/tools/argument-comment-lint/run.py "$@" +# Audit advisory file locks that may need Termux Unsupported handling. +[no-cd] +termux-lock-audit *args: + {{ justfile_directory() }}/scripts/termux-lock-audit.sh "$@" + # Tail logs from the state SQLite database log *args: if [ "${1:-}" = "--" ]; then shift; fi; cargo run -p codex-state --bin logs_client -- "$@" diff --git a/scripts/termux-lock-audit.sh b/scripts/termux-lock-audit.sh new file mode 100755 index 000000000000..ac40f8e57246 --- /dev/null +++ b/scripts/termux-lock-audit.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: scripts/termux-lock-audit.sh [--strict] + +Audits Rust advisory file-lock usage that may need Termux compatibility handling. + +By default this script prints findings and exits successfully. With --strict, it +exits non-zero when candidate file-lock calls are found in files that do not also +mention Unsupported/TryLockError handling. +USAGE +} + +strict=false +for arg in "$@"; do + case "$arg" in + --strict) + strict=true + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "unknown argument: $arg" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if ! command -v rg >/dev/null 2>&1; then + echo "error: rg is required for this audit" >&2 + exit 1 +fi + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +unsupported_pattern='std::fs::TryLockError|fs::TryLockError|TryLockError::|ErrorKind::Unsupported|std::io::ErrorKind::Unsupported' +lock_context_pattern='(\.lock\(\)|\.try_lock\(|\.try_lock_shared\(|TryLockError|ErrorKind::Unsupported)' +candidate_file_lock_pattern='\b([A-Za-z_][A-Za-z0-9_]*_file|file)\.(lock|try_lock|try_lock_shared)\(' +candidate_false_positive_pattern='poisoned' +helper_pattern='codex_utils_file_lock|FileLockOutcome|TryFileLockOutcome|lock_exclusive_optional|try_lock_exclusive_optional|try_lock_shared_optional' + +print_section() { + printf '\n== %s ==\n' "$1" +} + +print_section "Unsupported-aware lock handling" +echo "These files already mention Unsupported/TryLockError and are likely patched or intentionally reviewed:" + +unsupported_count=0 +while IFS= read -r file; do + if rg -q "$lock_context_pattern" "$file"; then + rg -n -H "$lock_context_pattern" "$file" + unsupported_count=$((unsupported_count + 1)) + fi +done < <(rg -l "$unsupported_pattern" codex-rs -g '*.rs' || true) + +if [ "$unsupported_count" -eq 0 ]; then + echo "No unsupported-aware lock handling found." +fi + +print_section "Optional file-lock helper usage" +echo "These files use the shared optional advisory file-lock helper:" + +helper_count=0 +while IFS= read -r file; do + rg -n -H "$helper_pattern" "$file" + helper_count=$((helper_count + 1)) +done < <(rg -l "$helper_pattern" codex-rs -g '*.rs' || true) + +if [ "$helper_count" -eq 0 ]; then + echo "No optional file-lock helper usage found." +fi + +print_section "Candidate file-lock calls for manual review" +echo "These receiver names may be std::fs::File advisory locks, but the file does not mention Unsupported/TryLockError handling." +echo "This section can include false positives, such as mutexes wrapping a file." + +review_count=0 +while IFS= read -r file; do + if ! rg -q "$unsupported_pattern" "$file"; then + matches="$(rg -n -H "$candidate_file_lock_pattern" "$file" | rg -v "$candidate_false_positive_pattern" || true)" + if [ -n "$matches" ]; then + echo "$matches" + review_count=$((review_count + 1)) + fi + fi +done < <(rg -l "$candidate_file_lock_pattern" codex-rs -g '*.rs' || true) + +if [ "$review_count" -eq 0 ]; then + echo "No unpatched candidate file-lock calls found." +fi + +print_section "Candidate file-lock calls already in unsupported-aware files" +echo "These are candidate file-lock calls in files that already mention Unsupported/TryLockError handling:" + +aware_candidate_count=0 +while IFS= read -r file; do + if rg -q "$unsupported_pattern" "$file"; then + matches="$(rg -n -H "$candidate_file_lock_pattern" "$file" | rg -v "$candidate_false_positive_pattern" || true)" + if [ -n "$matches" ]; then + echo "$matches" + aware_candidate_count=$((aware_candidate_count + 1)) + fi + fi +done < <(rg -l "$candidate_file_lock_pattern" codex-rs -g '*.rs' || true) + +if [ "$aware_candidate_count" -eq 0 ]; then + echo "No unsupported-aware candidate file-lock calls found." +fi + +print_section "Summary" +echo "unsupported-aware files: $unsupported_count" +echo "optional helper files: $helper_count" +echo "review candidate files: $review_count" +echo "unsupported-aware candidate files: $aware_candidate_count" + +if [ "$strict" = true ] && [ "$review_count" -gt 0 ]; then + echo "strict mode: manual-review candidate files were found" >&2 + exit 1 +fi From b5d89d1555efea55f65bfe60b12b04dd4805a192 Mon Sep 17 00:00:00 2001 From: Shijie Rao Date: Wed, 20 May 2026 14:54:59 -0700 Subject: [PATCH 11/25] Release 0.133.0-alpha.3 --- codex-rs/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index ec38a87cff97..9dfb0bf3d316 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -117,7 +117,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.0.0" +version = "0.133.0-alpha.3" # Track the edition for all workspace crates in one place. Individual # crates can still override this value, but keeping it here means new # crates created with `cargo new -w ...` automatically inherit the 2024 From a282351abcb5a3a87b8433ddbeec2a16bb667c0f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 01:23:49 +0000 Subject: [PATCH 12/25] Seed Termux release automation --- .github/workflows/rust-release.yml | 1816 +++++++---------- .github/workflows/shell-tool-mcp.yml | 461 +++++ .../workflows/termux-release-checkpoint.yml | 103 + .github/workflows/termux-release-deploy.yml | 243 +++ .github/workflows/termux-release-promote.yml | 135 ++ scripts/termux-configure-git.sh | 40 + scripts/termux-create-checkpoint-pr.sh | 275 +++ ...ermux-create-or-update-mirrored-release.sh | 111 + scripts/termux-download-release-artifact.sh | 78 + scripts/termux-find-release-pr.sh | 53 + scripts/termux-read-release-metadata.sh | 77 + scripts/termux-release-asset-state.sh | 37 + scripts/termux-release-paths.sh | 55 + scripts/termux-resolve-release-ref.sh | 36 + scripts/termux-validate-gh-env.sh | 16 + 15 files changed, 2445 insertions(+), 1091 deletions(-) create mode 100644 .github/workflows/shell-tool-mcp.yml create mode 100644 .github/workflows/termux-release-checkpoint.yml create mode 100644 .github/workflows/termux-release-deploy.yml create mode 100644 .github/workflows/termux-release-promote.yml create mode 100755 scripts/termux-configure-git.sh create mode 100755 scripts/termux-create-checkpoint-pr.sh create mode 100755 scripts/termux-create-or-update-mirrored-release.sh create mode 100755 scripts/termux-download-release-artifact.sh create mode 100755 scripts/termux-find-release-pr.sh create mode 100755 scripts/termux-read-release-metadata.sh create mode 100755 scripts/termux-release-asset-state.sh create mode 100755 scripts/termux-release-paths.sh create mode 100755 scripts/termux-resolve-release-ref.sh create mode 100755 scripts/termux-validate-gh-env.sh diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index c88fede7fa8b..56d0977e6e67 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -4,46 +4,49 @@ # git tag -a rust-v0.1.0 -m "Release 0.1.0" # git push origin rust-v0.1.0 # ``` -# -# To use external macOS signing, manually dispatch `release_mode=build_unsigned`, -# sign the unsigned macOS artifacts in a secure enclave, upload the signed handoff -# archive as a GitHub Release asset, then manually dispatch -# `release_mode=promote_signed` with `unsigned_run_id` and `signed_macos_asset`. -# The signed handoff archive should contain target or artifact directories such -# as `aarch64-apple-darwin/` with signed binaries. name: rust-release on: - push: - tags: - - "rust-v*.*.*" + pull_request: + branches: + - "release/**" workflow_dispatch: inputs: - release_mode: - description: "build_unsigned creates unsigned macOS handoff artifacts; promote_signed finishes a release from signed macOS handoff artifacts." + source_ref: + description: "Branch, tag, or commit SHA to build (code only; keeps .github from dispatched ref)" required: false + type: string + default: "" + build_target: + description: "Target to build" + required: true type: choice - default: build_unsigned + default: all options: - - build_unsigned - - promote_signed - sign_macos: - description: "Deprecated compatibility input; use release_mode instead." - required: false - type: boolean - default: false - unsigned_run_id: - description: "For promote_signed: workflow run id from the build_unsigned run." - required: false - type: string - signed_macos_asset: - description: "For promote_signed: exact GitHub Release asset name containing signed macOS handoff artifacts." - required: false - type: string - signed_macos_sha256: - description: "For promote_signed: optional SHA-256 of signed_macos_asset." - required: false - type: string + - all + - aarch64-apple-darwin + - x86_64-apple-darwin + - x86_64-unknown-linux-musl + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-musl + - aarch64-unknown-linux-gnu + - aarch64-linux-android + - x86_64-pc-windows-msvc + - aarch64-pc-windows-msvc + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read concurrency: group: ${{ github.workflow }} @@ -51,72 +54,26 @@ concurrency: jobs: tag-check: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - uses: dtolnay/rust-toolchain@1.92 - name: Validate tag matches Cargo.toml version shell: bash - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - REQUESTED_SIGN_MACOS: ${{ inputs.sign_macos }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} run: | set -euo pipefail echo "::group::Tag validation" - case "${RELEASE_MODE}" in - signed) - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - echo "❌ Manual rust-release runs must use release_mode=build_unsigned or release_mode=promote_signed" - exit 1 - fi - ;; - build_unsigned) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=build_unsigned is only valid for manual runs" - exit 1 - fi - ;; - promote_signed) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=promote_signed is only valid for manual runs" - exit 1 - fi - if [[ ! "${UNSIGNED_RUN_ID}" =~ ^[0-9]+$ ]]; then - echo "❌ release_mode=promote_signed requires unsigned_run_id to be a workflow run id" - exit 1 - fi - if [[ -z "${SIGNED_MACOS_ASSET}" ]]; then - echo "❌ release_mode=promote_signed requires signed_macos_asset" - exit 1 - fi - if [[ "${SIGNED_MACOS_ASSET}" == */* || "${SIGNED_MACOS_ASSET}" == *"*"* || "${SIGNED_MACOS_ASSET}" == *"?"* || "${SIGNED_MACOS_ASSET}" == *"["* ]]; then - echo "❌ signed_macos_asset must be an exact release asset name, not a path or glob" - exit 1 - fi - if [[ "${UNSIGNED_RUN_ID}" == "${GITHUB_RUN_ID}" ]]; then - echo "❌ unsigned_run_id must refer to the earlier build_unsigned run, not this run" - exit 1 - fi - ;; - *) - echo "❌ Unknown release_mode '${RELEASE_MODE}'" - exit 1 - ;; - esac - - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${REQUESTED_SIGN_MACOS}" == "true" ]]; then - echo "::warning title=Deprecated sign_macos input ignored::Use release_mode=build_unsigned or release_mode=promote_signed instead." - fi - # 1. Must be a tag and match the regex [[ "${GITHUB_REF_TYPE}" == "tag" ]] \ || { echo "❌ Not a tag push"; exit 1; } - [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \ + [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?(-termux)?$ ]] \ || { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; } # 2. Extract versions @@ -131,115 +88,198 @@ jobs: echo "✅ Tag and Cargo.toml agree (${tag_ver})" echo "::endgroup::" - build: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} + select-build-matrix: + if: always() && (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') needs: tag-check - name: Build - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: ${{ matrix.runs_on || matrix.runner }} - # Release builds can take a long time, so leave some headroom to avoid - # having to restart the full workflow due to a timeout. - timeout-minutes: 90 - permissions: - contents: read - id-token: write + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.select.outputs.matrix }} + build_version: ${{ steps.select.outputs.build_version }} + steps: + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - id: select + shell: bash + env: + REPO_OWNER: ${{ github.repository_owner }} + EVENT_NAME: ${{ github.event_name }} + BUILD_TARGET: ${{ inputs.build_target }} + run: | + set -euo pipefail + + matrix='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04","target":"aarch64-linux-android"}, + {"runner":"windows-latest","target":"x86_64-pc-windows-msvc"}, + {"runner":"windows-11-arm","target":"aarch64-pc-windows-msvc"} + ]' + + if [[ "${REPO_OWNER}" != "openai" ]]; then + matrix="$(jq -c '[.[] | select(.runner | startswith("macos") | not)]' <<< "${matrix}")" + fi + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + matrix="$(jq -c '[.[] | select(.target == "aarch64-linux-android")]' <<< "${matrix}")" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" && -n "${BUILD_TARGET}" && "${BUILD_TARGET}" != "all" ]]; then + matrix="$(jq -c --arg build_target "${BUILD_TARGET}" '[.[] | select(.target == $build_target)]' <<< "${matrix}")" + fi + + if [[ "$(jq 'length' <<< "${matrix}")" -eq 0 ]]; then + echo "No build targets selected after applying owner/dispatch filters." >&2 + exit 1 + fi + + echo "matrix={\"include\":${matrix}}" >> "$GITHUB_OUTPUT" + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + metadata=".github/termux-release.json" + if [[ ! -f "${metadata}" ]]; then + echo "${metadata} is required for release train PR builds" >&2 + exit 1 + fi + upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" + if [[ -z "${upstream_tag}" || "${upstream_tag}" != rust-v* ]]; then + echo "Unable to determine upstream rust tag from ${metadata}" >&2 + exit 1 + fi + build_version="${upstream_tag#rust-v}" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then + latest_release_tag="$( + GH_TOKEN="${{ github.token }}" gh api repos/openai/codex/releases/latest --jq '.tag_name' + )" + + if [[ -z "${latest_release_tag}" || "${latest_release_tag}" != rust-v* ]]; then + echo "Unable to determine latest stable release tag from openai/codex" >&2 + exit 1 + fi + + latest_release_version="${latest_release_tag#rust-v}" + build_version="${latest_release_version}-dev" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + fi + + build: + if: >- + always() && + (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') && + needs.select-build-matrix.result == 'success' + needs: + - tag-check + - select-build-matrix + name: Build - ${{ matrix.runner }} - ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + permissions: write-all + timeout-minutes: 120 defaults: run: working-directory: codex-rs env: - # 2026-03-04: temporarily change releases to use thin LTO because - # Ubuntu ARM is timing out at 60 minutes. - CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' }} + CODEX_BWRAP_ENABLE_FFI: ${{ contains(matrix.target, 'unknown-linux') && '1' || '0' }} + CODEX_BUILD_VERSION: ${{ needs.select-build-matrix.outputs.build_version }} + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" strategy: fail-fast: false - matrix: - include: - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - # Release artifacts intentionally ship MUSL-linked Linux binaries. - - runner: codex-linux-x64-xl - target: x86_64-unknown-linux-musl - bundle: primary - artifact_name: x86_64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: codex-linux-x64-xl - target: x86_64-unknown-linux-musl - bundle: app-server - artifact_name: x86_64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: codex-linux-arm64 - target: aarch64-unknown-linux-musl - bundle: primary - artifact_name: aarch64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: codex-linux-arm64 - target: aarch64-unknown-linux-musl - bundle: app-server - artifact_name: aarch64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" + matrix: ${{ fromJson(needs.select-build-matrix.outputs.matrix) }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + with: + verbose: true + - name: Checkout selected source ref + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + uses: actions/checkout@v6 with: - persist-credentials: false - - name: Print runner specs (Linux) - if: ${{ runner.os == 'Linux' }} + ref: ${{ inputs.source_ref }} + path: __source__ + fetch-depth: 1 + - name: Overlay selected source ref (excluding .github) + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + shell: bash + working-directory: ${{ github.workspace }} + run: | + set -euo pipefail + echo "Using source ref: ${{ inputs.source_ref }}" + + # Keep workflow/actions from the dispatch ref while replacing all other files. + find . -mindepth 1 -maxdepth 1 \ + ! -name .git \ + ! -name .github \ + ! -name __source__ \ + -exec rm -rf -- {} + + tar --exclude='.github' -C __source__ -cf - . | tar -xf - + rm -rf __source__ + + - name: Apply selected build version + if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} shell: bash + working-directory: ${{ github.workspace }} run: | set -euo pipefail - cpu_model="$(lscpu | awk -F: '/Model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}')" - total_ram="$(awk '/MemTotal/ {printf "%.1f GiB\n", $2 / 1024 / 1024}' /proc/meminfo)" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(uname -a)" - echo "CPU model: ${cpu_model}" - echo "Logical CPUs: $(nproc)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . - - name: Print runner specs (macOS) - if: ${{ runner.os == 'macOS' }} + if [[ -z "${CODEX_BUILD_VERSION}" ]]; then + echo "CODEX_BUILD_VERSION is empty for workflow_dispatch" >&2 + exit 1 + fi + echo "Using build version: ${CODEX_BUILD_VERSION}" + BUILD_VERSION="${CODEX_BUILD_VERSION}" perl -0777 -i -pe \ + 's/(\[workspace\.package\][^\[]*?version = ")([^"]+)(")/${1}$ENV{BUILD_VERSION}$3/s' \ + codex-rs/Cargo.toml + + grep -n "^\[workspace.package\]\|^version = " codex-rs/Cargo.toml | head -n 4 + + - name: Select Android NDK + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail - total_ram="$(sysctl -n hw.memsize | awk '{printf "%.1f GiB\n", $1 / 1024 / 1024 / 1024}')" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(sw_vers -productName) $(sw_vers -productVersion)" - echo "Hardware model: $(sysctl -n hw.model)" - echo "CPU architecture: $(uname -m)" - echo "Logical CPUs: $(sysctl -n hw.logicalcpu)" - echo "Physical CPUs: $(sysctl -n hw.physicalcpu)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . + + fallback_version="29.0.13113456" + ndk_root="${ANDROID_HOME}/ndk" + selected_ndk="" + + if [[ -d "${ndk_root}" ]]; then + while IFS= read -r candidate; do + toolchain="${candidate}/toolchains/llvm/prebuilt/linux-x86_64" + if [[ -x "${toolchain}/bin/aarch64-linux-android24-clang" ]]; then + selected_ndk="${candidate}" + break + fi + done < <(find "${ndk_root}" -mindepth 1 -maxdepth 1 -type d | sort -Vr) + fi + + if [[ -z "${selected_ndk}" ]]; then + echo "No usable preinstalled Android NDK found; installing ${fallback_version}" + yes | sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses || true + sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" "ndk;${fallback_version}" + selected_ndk="${ndk_root}/${fallback_version}" + fi + + if [[ ! -x "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" ]]; then + echo "Selected Android NDK does not contain a usable aarch64 API 24 clang: ${selected_ndk}" >&2 + exit 1 + fi + + echo "Selected Android NDK: ${selected_ndk}" + "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" --version + echo "ANDROID_NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" + echo "NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" - name: Install Linux bwrap build dependencies - if: ${{ runner.os == 'Linux' }} + if: ${{ runner.os == 'Linux' && matrix.target != 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail @@ -254,9 +294,173 @@ jobs: sudo apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 fi - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@1.93 with: targets: ${{ matrix.target }} + - name: Set up sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + uses: mozilla-actions/sccache-action@main + - name: Enable sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + shell: bash + run: | + set -euo pipefail + echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_VERSION=android-release-${{ matrix.target }}" >> "$GITHUB_ENV" + echo "SCCACHE_CACHE_SIZE=5G" >> "$GITHUB_ENV" + - name: Ensure Rust target is installed + shell: bash + run: | + set -euo pipefail + rustup target add "${{ matrix.target }}" + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android build environment + shell: bash + run: | + set -euo pipefail + ndk="${ANDROID_NDK_HOME}" + target="${{ matrix.target }}" + + # Set up the Android toolchain + toolchain="${ndk}/toolchains/llvm/prebuilt/linux-x86_64" + + # Use API level 24 to follow termux-build convention + api_level="24" + + # Configure Cargo to use the NDK linker + cargo_target_var="CARGO_TARGET_${target^^}_LINKER" + cargo_target_var="${cargo_target_var//-/_}" + echo "${cargo_target_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + # Set CC and AR for the target + target_cc_var="CC_${target}" + target_cc_var="${target_cc_var//-/_}" + echo "${target_cc_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + target_cxx_var="CXX_${target}" + target_cxx_var="${target_cxx_var//-/_}" + echo "${target_cxx_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang++" >> "$GITHUB_ENV" + + target_ar_var="AR_${target}" + target_ar_var="${target_ar_var//-/_}" + echo "${target_ar_var}=${toolchain}/bin/llvm-ar" >> "$GITHUB_ENV" + + # Add toolchain to PATH + echo "${toolchain}/bin" >> "$GITHUB_PATH" + + tls_align_source="${RUNNER_TEMP}/codex-android-tls-align.S" + tls_align_object="${RUNNER_TEMP}/codex-android-tls-align.o" + cat > "${tls_align_source}" <<'EOF' + .section .tdata.codex_android_tls_align,"awT",%progbits + .p2align 6 + .globl __codex_android_tls_align + .hidden __codex_android_tls_align + .type __codex_android_tls_align, %object + __codex_android_tls_align: + .byte 0 + .size __codex_android_tls_align, 1 + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${tls_align_source}" -o "${tls_align_object}" + + v8_compat_source="${RUNNER_TEMP}/codex-android-v8-compat.c" + v8_compat_object="${RUNNER_TEMP}/codex-android-v8-compat.o" + cat > "${v8_compat_source}" <<'EOF' + #include + #include + + extern int posix_memalign(void **memptr, size_t alignment, size_t size); + extern double strtod(const char *nptr, char **endptr); + extern float strtof(const char *nptr, char **endptr); + + void *aligned_alloc(size_t alignment, size_t size) { + void *ptr = 0; + if (posix_memalign(&ptr, alignment, size) != 0) { + return 0; + } + return ptr; + } + + double strtod_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtod(nptr, endptr); + } + + float strtof_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtof(nptr, endptr); + } + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${v8_compat_source}" -o "${v8_compat_object}" + + builtins_archive="$( + find "${toolchain}/lib/clang" \ + -path "*/lib/linux/libclang_rt.builtins-aarch64-android.a" \ + -print -quit + )" + if [[ -z "${builtins_archive}" ]]; then + echo "Could not find Android compiler-rt builtins archive under ${toolchain}/lib/clang" >&2 + exit 1 + fi + builtins_dir="$(dirname "${builtins_archive}")" + + # Provide compatibility symbols and C++ ABI needed by the Android rusty_v8 archive. + rustflags_var="CARGO_TARGET_${target^^}_RUSTFLAGS" + rustflags_var="${rustflags_var//-/_}" + echo "${rustflags_var}=-C link-arg=${tls_align_object} -C link-arg=${v8_compat_object} -C link-arg=-Wl,-u,__codex_android_tls_align -C link-arg=-L${builtins_dir} -C link-arg=-lclang_rt.builtins-aarch64-android -C link-arg=-lc++abi -C link-arg=-Wl,-z,max-page-size=65536" >> "$GITHUB_ENV" + + + # Configure for vendored OpenSSL build + echo "CARGO_BUILD_TARGET_APPLIES_TO_HOST=false" >> "$GITHUB_ENV" + echo "CARGO_BUILD_TARGET=${{ matrix.target }}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android rusty_v8 artifact overrides + env: + GH_TOKEN: ${{ github.token }} + TARGET: ${{ matrix.target }} + shell: bash + run: | + set -euo pipefail + binding_dir="${RUNNER_TEMP}/rusty_v8" + archive="${binding_dir}/librusty_v8_release_${TARGET}.a.gz" + binding_path="${binding_dir}/src_binding_release_${TARGET}.rs" + checksums_path="${binding_dir}/rusty_v8_release_${TARGET}.sha256" + artifact_repository="wallentx/codex-termux" + release_tag="rusty-v8-v147.4.0" + mkdir -p "${binding_dir}" + echo "Downloading Android rusty_v8 artifacts from ${artifact_repository}@${release_tag}" + gh release download "${release_tag}" \ + --repo "${artifact_repository}" \ + --dir "${binding_dir}" \ + --pattern "librusty_v8_release_${TARGET}.a.gz" \ + --pattern "src_binding_release_${TARGET}.rs" \ + --pattern "rusty_v8_release_${TARGET}.sha256" + (cd "${binding_dir}" && sha256sum -c "$(basename "${checksums_path}")") + echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV" + echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Install termux-elf-cleaner + shell: bash + run: | + set -euo pipefail + version="v3.0.1" + src_dir="${RUNNER_TEMP}/termux-elf-cleaner-src" + build_dir="${RUNNER_TEMP}/termux-elf-cleaner-build" + bin_dir="${RUNNER_TEMP}/termux-elf-cleaner-bin" + + rm -rf "${src_dir}" "${build_dir}" "${bin_dir}" + mkdir -p "${src_dir}" "${build_dir}" "${bin_dir}" + + curl -fsSL "https://github.com/termux/termux-elf-cleaner/archive/refs/tags/${version}.tar.gz" \ + | tar -xzf - --strip-components=1 -C "${src_dir}" + + cmake -S "${src_dir}" -B "${build_dir}" -DCMAKE_BUILD_TYPE=Release + cmake --build "${build_dir}" --parallel + + install "${build_dir}/termux-elf-cleaner" "${bin_dir}/termux-elf-cleaner" + echo "${bin_dir}" >> "$GITHUB_PATH" - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Use hermetic Cargo home (musl) @@ -269,12 +473,25 @@ jobs: echo "${cargo_home}/bin" >> "$GITHUB_PATH" : > "${cargo_home}/config.toml" + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ${{ github.workspace }}/.cargo-home/bin/ + ${{ github.workspace }}/.cargo-home/registry/index/ + ${{ github.workspace }}/.cargo-home/registry/cache/ + ${{ github.workspace }}/.cargo-home/git/db/ + ${{ github.workspace }}/codex-rs/target/ + key: cargo-${{ matrix.runner }}-${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }} + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install Zig - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + uses: mlugg/setup-zig@v2 with: version: 0.14.0 - use-cache: false - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install musl build tools @@ -310,12 +527,6 @@ jobs: shell: bash run: | set -euo pipefail - # Avoid problematic aws-lc jitter entropy code path on musl builders. - echo "AWS_LC_SYS_NO_JITTER_ENTROPY=1" >> "$GITHUB_ENV" - target_no_jitter="AWS_LC_SYS_NO_JITTER_ENTROPY_${{ matrix.target }}" - target_no_jitter="${target_no_jitter//-/_}" - echo "${target_no_jitter}=1" >> "$GITHUB_ENV" - # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. echo "RUSTFLAGS=" >> "$GITHUB_ENV" echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" @@ -340,94 +551,70 @@ jobs: echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" - - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }} - name: Configure musl rusty_v8 artifact overrides and verify checksums - uses: ./.github/actions/setup-rusty-v8-musl - with: - target: ${{ matrix.target }} - - - if: ${{ contains(matrix.target, 'linux') }} - name: Build bwrap and export digest + - name: Cargo build shell: bash run: | set -euo pipefail - target="${{ matrix.target }}" - cargo build --target "$target" --release --timings --bin bwrap - bwrap_path="target/${target}/release/bwrap" - if [[ ! -f "$bwrap_path" ]]; then - echo "bwrap binary ${bwrap_path} not found" - exit 1 + if [[ "${{ matrix.target }}" == 'aarch64-linux-android' ]]; then + export CARGO_BUILD_JOBS=4 + export CARGO_PROFILE_RELEASE_LTO=thin + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 + cargo build --target ${{ matrix.target }} --release --bin codex + exit 0 fi - digest="$(sha256sum "$bwrap_path" | awk '{print $1}')" - echo "CODEX_BWRAP_SHA256=${digest}" >> "$GITHUB_ENV" - echo "Built bwrap ${bwrap_path} with sha256:${digest}" - - - name: Cargo build + if [[ "${{ contains(matrix.target, 'windows') }}" == 'true' ]]; then + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy --bin codex-windows-sandbox-setup --bin codex-command-runner + else + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy + fi + - name: sccache stats (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash - run: | - build_args=() - for binary in ${{ matrix.binaries }}; do - build_args+=(--bin "$binary") - done - echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}" - cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}" - - - name: Upload Cargo timings - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }} - path: codex-rs/target/**/cargo-timings/cargo-timing.html - if-no-files-found: warn + run: ${SCCACHE_PATH:-sccache} --show-stats || true - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Stage unsigned macOS artifacts + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Normalize Android ELF for Termux shell: bash run: | set -euo pipefail - - target="${{ matrix.target }}" - release_dir="target/${target}/release" - dest="unsigned-dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - unsigned_name="${binary}-${target}-unsigned" - unsigned_path="${dest}/${unsigned_name}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 + for binary in codex codex-responses-api-proxy; do + binary_path="target/${{ matrix.target }}/release/${binary}" + if [[ -f "${binary_path}" ]]; then + termux-elf-cleaner --api-level 24 "${binary_path}" + chmod +x "${binary_path}" + # if [[ "${binary}" == "codex" ]]; then + # # Patch PT_TLS (0x7) to PT_NULL (0x0) at offset 400 to bypass Bionic alignment checks. + # printf '\x00\x00\x00\x00' | dd of="${binary_path}" bs=1 seek=400 count=4 conv=notrunc + # fi fi - - cp "${binary_path}" "${unsigned_path}" - tar -C "$dest" -czf "${unsigned_path}.tar.gz" "${unsigned_name}" - zstd -T0 -19 --rm "${unsigned_path}" done - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Upload unsigned macOS artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }}-unsigned - path: codex-rs/unsigned-dist/${{ matrix.target }}/* - if-no-files-found: error - - - if: ${{ contains(matrix.target, 'linux') }} + - if: ${{ contains(matrix.target, 'linux') && !contains(matrix.target, 'android') && github.repository_owner == 'openai' }} name: Cosign Linux artifacts uses: ./.github/actions/linux-code-sign with: target: ${{ matrix.target }} artifacts-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release - binaries: ${{ matrix.binaries }} - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS == 'true' }} + - if: ${{ contains(matrix.target, 'windows') && github.repository_owner == 'openai' }} + name: Sign Windows binaries with Azure Trusted Signing + uses: ./.github/actions/windows-code-sign + with: + target: ${{ matrix.target }} + client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID }} + endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} + account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} + + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (binaries) uses: ./.github/actions/macos-code-sign with: target: ${{ matrix.target }} - binaries: ${{ matrix.binaries }} sign-binaries: "true" sign-dmg: "false" apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }} @@ -436,7 +623,7 @@ jobs: apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: Build macOS dmg shell: bash run: | @@ -451,17 +638,23 @@ jobs: # The previous "MacOS code signing (binaries)" step signs + notarizes the # built artifacts in `${release_dir}`. This step packages *those same* # signed binaries into a dmg. + codex_binary_path="${release_dir}/codex" + proxy_binary_path="${release_dir}/codex-responses-api-proxy" + rm -rf "$dmg_root" mkdir -p "$dmg_root" - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 - fi - ditto "${binary_path}" "${dmg_root}/${binary}" - done + if [[ ! -f "$codex_binary_path" ]]; then + echo "Binary $codex_binary_path not found" + exit 1 + fi + if [[ ! -f "$proxy_binary_path" ]]; then + echo "Binary $proxy_binary_path not found" + exit 1 + fi + + ditto "$codex_binary_path" "${dmg_root}/codex" + ditto "$proxy_binary_path" "${dmg_root}/codex-responses-api-proxy" rm -f "$dmg_path" hdiutil create \ @@ -476,7 +669,7 @@ jobs: exit 1 fi - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (dmg) uses: ./.github/actions/macos-code-sign with: @@ -490,121 +683,65 @@ jobs: apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - name: Stage artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | dest="dist/${{ matrix.target }}" mkdir -p "$dest" - for binary in ${{ matrix.binaries }}; do - cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" - if [[ "${{ matrix.target }}" == *linux* ]]; then - cp "target/${{ matrix.target }}/release/${binary}.sigstore" \ - "$dest/${binary}-${{ matrix.target }}.sigstore" + if [[ "${{ matrix.runner }}" == windows* ]]; then + cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe" + else + # For Android, we want the binary to be named just 'codex' in the archive. + if [[ "${{ matrix.target }}" == "aarch64-linux-android" ]]; then + cp target/${{ matrix.target }}/release/codex "$dest/codex" + else + cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" + fi + if [[ -f target/${{ matrix.target }}/release/codex-responses-api-proxy ]]; then + cp target/${{ matrix.target }}/release/codex-responses-api-proxy "$dest/codex-responses-api-proxy-${{ matrix.target }}" fi - done - - if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.bundle }}" == "primary" ]]; then - bundle_root="${RUNNER_TEMP}/codex-${{ matrix.target }}-bundle" - rm -rf "$bundle_root" - mkdir -p "$bundle_root/codex-resources" - cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex" - cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap" - chmod 0755 "$bundle_root/codex" "$bundle_root/codex-resources/bwrap" - tar -C "$bundle_root" -cf - codex codex-resources/bwrap | - zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst" fi - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" + if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.target }}" != *android* ]]; then + cp target/${{ matrix.target }}/release/codex.sigstore "$dest/codex-${{ matrix.target }}.sigstore" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.sigstore "$dest/codex-responses-api-proxy-${{ matrix.target }}.sigstore" fi - - name: Build Codex package archive - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "target/${TARGET}/release" \ - --archive-dir "dist/${TARGET}" - - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - aarch64-unknown-linux-musl) - platform_tag="manylinux_2_17_aarch64" - ;; - x86_64-unknown-linux-musl) - platform_tag="manylinux_2_17_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - # Do not install into the runner's system Python; macOS runners mark - # the Homebrew Python as externally managed under PEP 668. - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - stage_runtime_args=( - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" - stage-runtime - "$stage_dir" - "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/codex" - --codex-version "${GITHUB_REF_NAME}" - --platform-tag "$platform_tag" - ) - if [[ "${{ matrix.target }}" == *linux* ]]; then - # Keep bwrap in the runtime wheel so Linux sandbox fallback behavior - # matches the standalone release bundle on hosts without system bwrap. - stage_runtime_args+=( - --resource-binary - "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/bwrap" - ) + if [[ "${{ matrix.target }}" == *apple-darwin ]]; then + cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" fi - python3 "${stage_runtime_args[@]}" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error + - if: ${{ matrix.runner == 'windows-11-arm' }} + name: Install zstd + shell: powershell + run: choco install -y zstandard - name: Compress artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | # Path that contains the uncompressed binaries for the current # ${{ matrix.target }} dest="dist/${{ matrix.target }}" + repo_root=$PWD + + # We want to ship the raw Windows executables in the GitHub Release + # in addition to the compressed archives. Keep the originals for + # Windows targets; remove them elsewhere to limit the number of + # artifacts that end up in the GitHub Release. + keep_originals=false + if [[ "${{ matrix.runner }}" == windows* ]]; then + keep_originals=true + fi # For compatibility with environments that lack the `zstd` tool we - # additionally create a `.tar.gz` alongside every binary we publish. - # The end result is: + # additionally create a `.tar.gz` for all platforms and `.zip` for + # Windows alongside every single binary that we publish. The end result is: # codex-.zst (existing) # codex-.tar.gz (new) + # codex-.zip (only for Windows) # 1. Produce a .tar.gz for every file in the directory *before* we # run `zstd --rm`, because that flag deletes the original files. @@ -612,7 +749,7 @@ jobs: base="$(basename "$f")" # Skip files that are already archives (shouldn't happen, but be # safe). - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then + if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then continue fi @@ -622,344 +759,239 @@ jobs: fi # Create per-binary tar.gz - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - - # Also create .zst and remove the uncompressed binaries to keep - # non-Windows artifact directories small. - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - with: - name: ${{ matrix.artifact_name }} - # Upload the per-binary .zst files, .tar.gz equivalents, and any - # prebuilt archives staged above. - path: | - codex-rs/dist/${{ matrix.target }}/* - - stage-signed-macos: - if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode == 'promote_signed' }} - needs: tag-check - name: Stage signed macOS handoff - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: macos-15-xlarge - timeout-minutes: 30 - permissions: - contents: read - defaults: - run: - working-directory: codex-rs + # For Android, we want the archive to have the target suffix even though the binary is just 'codex'. + archive_name="${base}" + if [[ "${{ matrix.target }}" == "aarch64-linux-android" && "${base}" == "codex" ]]; then + archive_name="codex-${{ matrix.target }}" + fi + tar -C "$dest" -czf "$dest/${archive_name}.tar.gz" "$base" + + # Create zip archive for Windows binaries + # Must run from inside the dest dir so 7z won't + # embed the directory path inside the zip. + if [[ "${{ matrix.runner }}" == windows* ]]; then + if [[ "$base" == "codex-${{ matrix.target }}.exe" ]]; then + # Bundle the sandbox helper binaries into the main codex zip so + # WinGet installs include the required helpers next to codex.exe. + # Fall back to the single-binary zip if the helpers are missing + # to avoid breaking releases. + bundle_dir="$(mktemp -d)" + runner_src="$dest/codex-command-runner-${{ matrix.target }}.exe" + setup_src="$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + if [[ -f "$runner_src" && -f "$setup_src" ]]; then + cp "$dest/$base" "$bundle_dir/$base" + cp "$runner_src" "$bundle_dir/codex-command-runner.exe" + cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" + # Use an absolute path so bundle zips land in the real dist + # dir even when 7z runs from a temp directory. + (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) + else + echo "warning: missing sandbox binaries; falling back to single-binary zip" + echo "warning: expected $runner_src and $setup_src" + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + rm -rf "$bundle_dir" + else + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + fi - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" + # Also create .zst (existing behaviour) *and* remove the original + # uncompressed binary to keep the directory small. + zstd_args=(-T0 -19) + if [[ "${keep_originals}" == false ]]; then + zstd_args+=(--rm) + fi - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + if [[ "${archive_name}" != "${base}" ]]; then + zstd "${zstd_args[@]}" "$dest/$base" -o "$dest/${archive_name}.zst" + else + zstd "${zstd_args[@]}" "$dest/$base" + fi + done - - name: Download signed macOS handoff + - name: Add Termux release metadata + if: ${{ (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && matrix.target == 'aarch64-linux-android' }} shell: bash - env: - GH_TOKEN: ${{ github.token }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - SIGNED_MACOS_SHA256: ${{ inputs.signed_macos_sha256 }} run: | set -euo pipefail - - download_dir="${RUNNER_TEMP}/signed-macos-download" - handoff_dir="${RUNNER_TEMP}/signed-macos-handoff" - rm -rf "$download_dir" "$handoff_dir" - mkdir -p "$download_dir" "$handoff_dir" - - gh release download "$GITHUB_REF_NAME" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern "$SIGNED_MACOS_ASSET" \ - --dir "$download_dir" - - asset_count="$(find "$download_dir" -maxdepth 1 -type f | wc -l | tr -d '[:space:]')" - if [[ "$asset_count" != "1" ]]; then - echo "Expected exactly one signed macOS handoff asset named ${SIGNED_MACOS_ASSET}; found ${asset_count}" - find "$download_dir" -maxdepth 1 -type f -print - exit 1 - fi - - asset_path="$(find "$download_dir" -maxdepth 1 -type f -print -quit)" - if [[ -n "${SIGNED_MACOS_SHA256}" ]]; then - expected_sha="$(printf '%s' "$SIGNED_MACOS_SHA256" | tr '[:upper:]' '[:lower:]')" - actual_sha="$(shasum -a 256 "$asset_path" | awk '{print $1}')" - if [[ "$actual_sha" != "$expected_sha" ]]; then - echo "signed_macos_sha256 mismatch for ${SIGNED_MACOS_ASSET}" - echo "expected: ${expected_sha}" - echo "actual: ${actual_sha}" - exit 1 - fi + if [[ ! -f "${GITHUB_WORKSPACE}/.github/termux-release.json" ]]; then + echo "No Termux release metadata found; skipping metadata attachment." + exit 0 fi + dest="dist/${{ matrix.target }}" + cp "${GITHUB_WORKSPACE}/.github/termux-release.json" "${dest}/termux-release.json" + ( + cd "${dest}" + sha256sum ./* > SHA256SUMS + ) - asset_name="$(basename "$asset_path")" - case "$asset_name" in - *.tar.zst) - zstd -dc "$asset_path" | tar -C "$handoff_dir" -xf - - ;; - *.tar.gz|*.tgz) - tar -C "$handoff_dir" -xzf "$asset_path" - ;; - *.zip) - ditto -x -k "$asset_path" "$handoff_dir" - ;; - *) - echo "Unsupported signed macOS handoff archive format: ${asset_name}" - exit 1 - ;; - esac - - echo "SIGNED_MACOS_HANDOFF_DIR=$handoff_dir" >> "$GITHUB_ENV" + - id: upload-artifact + uses: actions/upload-artifact@v6 + with: + name: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + # Upload the per-binary .zst files as well as the new .tar.gz + # equivalents we generated in the previous step. + path: | + codex-rs/dist/${{ matrix.target }}/* - - name: Stage signed macOS artifacts + - name: Publish Termux artifact notification + if: ${{ github.event_name == 'pull_request' && startsWith(github.base_ref, 'release/') && matrix.target == 'aarch64-linux-android' }} shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_ID: ${{ steps.upload-artifact.outputs.artifact-id }} + ARTIFACT_NAME: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + PR_NUMBER: ${{ github.event.pull_request.number }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | set -euo pipefail - - target="${{ matrix.target }}" - artifact_name="${{ matrix.artifact_name }}" - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${target}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - fi - if [[ ! -d "$source_dir" ]]; then - echo "Signed macOS handoff is missing ${artifact_name}/" - echo "Expected either:" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${target}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - find "$SIGNED_MACOS_HANDOFF_DIR" -maxdepth 3 -type f -print + if [[ -z "${ARTIFACT_ID}" ]]; then + echo "upload-artifact did not return an artifact id" >&2 exit 1 fi - dest="dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - source_path="${source_dir}/${binary}" - if [[ ! -f "$source_path" ]]; then - source_path="${source_dir}/${binary}-${target}" - fi - if [[ ! -f "$source_path" ]]; then - echo "Signed macOS handoff is missing ${binary} for ${artifact_name}" - exit 1 - fi - - release_path="${dest}/${binary}-${target}" - ditto "$source_path" "$release_path" - chmod 0755 "$release_path" - codesign --verify --strict --verbose=2 "$release_path" - done - - # DMG staging is disabled for signed promotion because we no longer - # distribute DMGs from this release path. Keep the branch here so the - # handoff can opt back in by flipping matrix.build_dmg if needed. - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - dmg_name="codex-${target}.dmg" - dmg_source="${source_dir}/${dmg_name}" - if [[ ! -f "$dmg_source" ]]; then - echo "Signed macOS handoff is missing ${dmg_name} for ${artifact_name}" - exit 1 - fi - - codesign --verify --strict --verbose=2 "$dmg_source" - xcrun stapler validate "$dmg_source" - cp "$dmg_source" "$dest/$dmg_name" + marker="" + artifact_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts/${ARTIFACT_ID}" + run_url="${GH_WORKFLOW_URL:-https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + body_path="${RUNNER_TEMP}/termux-artifact-comment.md" + { + echo "${marker}" + echo "Termux Android artifact ready for testing:" + echo + echo "- Artifact: [${ARTIFACT_NAME}](${artifact_url})" + echo "- Workflow run: [${GITHUB_RUN_ID}](${run_url})" + echo + echo "You must be signed in to GitHub with repository access to download Actions artifacts." + } > "${body_path}" + + existing_comment_id="$( + gh pr view "${PR_NUMBER}" \ + --repo "${GITHUB_REPOSITORY}" \ + --json comments \ + --jq ".comments | map(select(.author.is_bot == true and (.body | contains(\"${marker}\")))) | .[-1].id // \"\"" + )" + if [[ -n "${existing_comment_id}" ]]; then + gh api graphql \ + -f query=' + mutation($id: ID!, $body: String!) { + updateIssueComment(input: {id: $id, body: $body}) { + issueComment { + id + } + } + } + ' \ + -f id="${existing_comment_id}" \ + -f body="$(cat "${body_path}")" \ + >/dev/null + else + gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body-file "${body_path}" fi - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - python3 \ - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \ - stage-runtime \ - "$stage_dir" \ - "${GITHUB_WORKSPACE}/codex-rs/dist/${{ matrix.target }}/codex-${{ matrix.target }}" \ - --codex-version "${GITHUB_REF_NAME}" \ - --platform-tag "$platform_tag" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - - name: Build Codex package archive - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "dist/${TARGET}" \ - --archive-dir "dist/${TARGET}" \ - --target-suffixed-entrypoint - - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error - - - name: Compress artifacts - shell: bash - run: | - set -euo pipefail - - dest="dist/${{ matrix.target }}" - for f in "$dest"/*; do - base="$(basename "$f")" - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then - continue + gh label create binary-ready \ + --repo "${GITHUB_REPOSITORY}" \ + --color 0e8a16 \ + --description "Android binary is ready for testing" \ + --force + gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --add-label binary-ready + + if [[ -n "${SLACK_WEBHOOK_URL:-}" ]]; then + slack_payload_path="${RUNNER_TEMP}/termux-artifact-slack.json" + pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}" + jq -n \ + --arg artifact_name "${ARTIFACT_NAME}" \ + --arg artifact_url "${artifact_url}" \ + --arg pr_number "${PR_NUMBER}" \ + --arg pr_url "${pr_url}" \ + --arg run_id "${GITHUB_RUN_ID}" \ + --arg run_url "${run_url}" \ + '{ + text: ("Termux Android artifact ready for testing: " + $artifact_url), + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: ("*Termux Android artifact ready for testing*\n" + $artifact_url) + } + }, + { + type: "section", + fields: [ + { + type: "mrkdwn", + text: ("*Artifact:*\n<" + $artifact_url + "|" + $artifact_name + ">") + }, + { + type: "mrkdwn", + text: ("*Pull request:*\n<" + $pr_url + "|#" + $pr_number + ">") + }, + { + type: "mrkdwn", + text: ("*Workflow run:*\n<" + $run_url + "|" + $run_id + ">") + } + ] + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: "You must be signed in to GitHub with repository access to download Actions artifacts." + } + ] + } + ] + }' > "${slack_payload_path}" + + if ! curl -fsS \ + -X POST \ + -H "Content-type: application/json" \ + --data @"${slack_payload_path}" \ + "${SLACK_WEBHOOK_URL}"; then + echo "::warning::Failed to send Slack artifact notification" fi + fi - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }} - path: | - codex-rs/dist/${{ matrix.target }}/* - - build-windows: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - needs: tag-check - uses: ./.github/workflows/rust-release-windows.yml - with: - release-lto: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }} - secrets: inherit - - argument-comment-lint-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: argument-comment-lint release assets + shell-tool-mcp: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'openai' + name: shell-tool-mcp needs: tag-check - uses: ./.github/workflows/rust-release-argument-comment-lint.yml + permissions: + contents: read + id-token: write + uses: ./.github/workflows/shell-tool-mcp.yml with: + release-tag: ${{ github.ref_name }} publish: true - - zsh-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: zsh release assets - needs: tag-check - uses: ./.github/workflows/rust-release-zsh.yml + secrets: inherit release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') needs: - - tag-check - build - - stage-signed-macos - - build-windows - - argument-comment-lint-release-assets - - zsh-release-assets - if: >- - ${{ - always() && - needs.tag-check.result == 'success' && - ( - ( - github.event_name == 'workflow_dispatch' && - inputs.release_mode == 'promote_signed' && - needs.stage-signed-macos.result == 'success' && - needs.build.result == 'skipped' && - needs.build-windows.result == 'skipped' && - needs.argument-comment-lint-release-assets.result == 'skipped' && - needs.zsh-release-assets.result == 'skipped' - ) || - ( - (github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed') && - needs.build.result == 'success' && - needs.stage-signed-macos.result == 'skipped' && - needs.build-windows.result == 'success' && - needs.argument-comment-lint-release-assets.result == 'success' && - needs.zsh-release-assets.result == 'success' - ) - ) - }} + - shell-tool-mcp name: release runs-on: ubuntu-latest permissions: contents: write actions: read - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode == 'promote_signed' }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} outputs: version: ${{ steps.release_name.outputs.name }} tag: ${{ github.ref_name }} - sign_macos: ${{ steps.release_mode.outputs.sign_macos }} should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }} npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }} - should_publish_python_runtime: ${{ steps.python_runtime_publish_settings.outputs.should_publish }} steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + uses: actions/checkout@v6 - - name: Define release mode - id: release_mode - run: | - echo "release_mode=${RELEASE_MODE}" >> "$GITHUB_OUTPUT" - echo "sign_macos=${SIGN_MACOS}" >> "$GITHUB_OUTPUT" + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main - name: Generate release notes from tag commit message id: release_notes @@ -981,159 +1013,21 @@ jobs: echo "path=${notes_path}" >> "${GITHUB_OUTPUT}" - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@v7 with: path: dist - - name: Validate unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - run_summary="$(gh run view "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --json conclusion,event,headBranch,headSha,status,workflowName,url \ - --jq '[.workflowName, .event, .headBranch, .headSha, .status, .conclusion, .url] | @tsv')" - IFS=$'\t' read -r workflow_name event head_branch head_sha status conclusion run_url <<< "$run_summary" - expected_head_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")" - - if [[ "$workflow_name" != "$GITHUB_WORKFLOW" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is for workflow '${workflow_name}', expected '${GITHUB_WORKFLOW}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$event" != "workflow_dispatch" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} was triggered by '${event}', expected 'workflow_dispatch'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_branch" != "$GITHUB_REF_NAME" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used ref '${head_branch}', expected '${GITHUB_REF_NAME}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_sha" != "$expected_head_sha" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used head SHA '${head_sha}', expected '${expected_head_sha}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is ${status}/${conclusion}, expected completed/success" - echo "Run URL: ${run_url}" - exit 1 - fi - - - name: Download artifacts from unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - gh run download "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --dir dist - - - name: Remove unsigned macOS staging artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - run: | - set -euo pipefail - find dist -mindepth 1 -maxdepth 1 -type d \ - -name '*-apple-darwin*-unsigned' \ - -exec rm -rf {} + - - - name: Re-upload promoted Linux x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-unknown-linux-musl - path: dist/x86_64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Linux arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-unknown-linux-musl - path: dist/aarch64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Windows x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-pc-windows-msvc - path: dist/x86_64-pc-windows-msvc/* - if-no-files-found: error - - - name: Re-upload promoted Windows arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-pc-windows-msvc - path: dist/aarch64-pc-windows-msvc/* - if-no-files-found: error - - name: List run: ls -R dist/ - - name: Prune artifacts excluded from unsigned macOS release - if: ${{ env.SIGN_MACOS == 'false' }} - run: | - find dist -mindepth 1 -maxdepth 1 -type d \ - ! -name '*-apple-darwin*-unsigned' \ - ! -name 'aarch64-unknown-linux-musl' \ - ! -name 'aarch64-unknown-linux-musl-app-server' \ - ! -name 'x86_64-unknown-linux-musl' \ - ! -name 'x86_64-unknown-linux-musl-app-server' \ - ! -name 'aarch64-pc-windows-msvc' \ - ! -name 'x86_64-pc-windows-msvc' \ - -exec rm -rf {} + - - if ! find dist -type f -name '*-apple-darwin*-unsigned*' | grep -q .; then - echo "No unsigned macOS artifacts found in downloaded workflow artifacts." - exit 1 - fi - + # This is a temporary fix: we should modify shell-tool-mcp.yml so these + # files do not end up in dist/ in the first place. - name: Delete entries from dist/ that should not go in the release run: | - rm -rf dist/windows-binaries* - # cargo-timing.html appears under multiple target-specific directories. - # If included in files: dist/**, release upload races on duplicate - # asset names and can fail with 404s. - find dist -type f -name 'cargo-timing.html' -delete - find dist -type d -empty -delete + rm -rf dist/shell-tool-mcp* ls -R dist/ - - name: Add Codex package checksum manifest - run: | - set -euo pipefail - - manifest="dist/codex-package_SHA256SUMS" - tmp_manifest="$(mktemp)" - find dist -type f \ - \( -name 'codex-package-*.tar.gz' -o -name 'codex-app-server-package-*.tar.gz' \) \ - -print | - sort | - while IFS= read -r archive; do - sha256sum "$archive" | - awk -v name="$(basename "$archive")" '{ print $1 " " name }' - done > "$tmp_manifest" - - if [[ ! -s "$tmp_manifest" ]]; then - echo "No Codex package archives found for checksum manifest" - exit 1 - fi - - mv "$tmp_manifest" "$manifest" - cat "$manifest" - - name: Add config schema release asset run: | cp codex-rs/core/config.schema.json dist/config-schema.json @@ -1154,12 +1048,6 @@ jobs: set -euo pipefail version="${VERSION}" - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - echo "npm_tag=" >> "$GITHUB_OUTPUT" - exit 0 - fi - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "should_publish=true" >> "$GITHUB_OUTPUT" echo "npm_tag=" >> "$GITHUB_OUTPUT" @@ -1171,130 +1059,55 @@ jobs: echo "npm_tag=" >> "$GITHUB_OUTPUT" fi - - name: Determine Python runtime publish settings - id: python_runtime_publish_settings - env: - VERSION: ${{ steps.release_name.outputs.name }} - run: | - set -euo pipefail - version="${VERSION}" - - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - else - echo "should_publish=false" >> "$GITHUB_OUTPUT" - fi - - name: Setup pnpm - if: ${{ env.SIGN_MACOS == 'true' }} - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + uses: pnpm/action-setup@v4 with: run_install: false - name: Setup Node.js for npm packaging - if: ${{ env.SIGN_MACOS == 'true' }} - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: node-version: 22 - name: Install dependencies - if: ${{ env.SIGN_MACOS == 'true' }} run: pnpm install --frozen-lockfile + # stage_npm_packages.py requires DotSlash when staging releases. + - uses: facebook/install-dotslash@v2 - name: Stage npm packages - if: ${{ env.SIGN_MACOS == 'true' }} + if: github.repository_owner == 'openai' env: GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ steps.release_name.outputs.name }} run: | - workflow_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" ./scripts/stage_npm_packages.py \ - --release-version "$RELEASE_VERSION" \ - --workflow-url "$workflow_url" \ + --release-version "${{ steps.release_name.outputs.name }}" \ --package codex \ --package codex-responses-api-proxy \ --package codex-sdk - - name: Stage installer scripts - if: ${{ env.SIGN_MACOS == 'true' }} - run: | - cp scripts/install/install.sh dist/install.sh - cp scripts/install/install.ps1 dist/install.ps1 - - name: Create GitHub Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@v2 with: name: ${{ steps.release_name.outputs.name }} tag_name: ${{ github.ref_name }} body_path: ${{ steps.release_notes.outputs.path }} files: dist/** - overwrite_files: true - make_latest: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} # Mark as prerelease only when the version has a suffix after x.y.z # (e.g. -alpha, -beta). Otherwise publish a normal release. prerelease: ${{ contains(steps.release_name.outputs.name, '-') }} - - name: Clean up signed promotion handoff assets - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" --jq '.id')" - gh api --paginate "repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets" \ - --jq '.[] | [.id, .name] | @tsv' | - while IFS=$'\t' read -r asset_id asset_name; do - if [[ -z "$asset_id" || -z "$asset_name" ]]; then - continue - fi - - delete_asset=false - if [[ "$asset_name" == *unsigned* || "$asset_name" == "$SIGNED_MACOS_ASSET" ]]; then - delete_asset=true - fi - - if [[ "$delete_asset" == "true" ]]; then - echo "Deleting release asset ${asset_name}" - gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" - fi - done - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 + - if: github.repository_owner == 'openai' + uses: facebook/dotslash-publish-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag: ${{ github.ref_name }} config: .github/dotslash-config.json - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-zsh-config.json - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-argument-comment-lint-config.json - - name: Trigger developers.openai.com deploy # Only trigger the deploy if the release is not a pre-release. # The deploy is used to update the developers.openai.com website with the new config schema json file. - if: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} + if: ${{ !contains(steps.release_name.outputs.name, '-') && github.repository_owner == 'openai' }} continue-on-error: true env: DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }} @@ -1309,15 +1122,7 @@ jobs: # npm docs: https://docs.npmjs.com/trusted-publishers publish-npm: # Publish to npm for stable releases and alpha pre-releases with numeric suffixes. - # promote_signed intentionally skips build jobs that are ancestors of release; - # include the !cancelled() status function so Actions does not apply its implicit - # success() check to the whole dependency chain before evaluating release outputs. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_npm == 'true' - }} + if: ${{ needs.release.outputs.should_publish_npm == 'true' && github.repository_owner == 'openai' }} name: publish-npm needs: release runs-on: ubuntu-latest @@ -1327,37 +1132,36 @@ jobs: steps: - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: - # Node 24 bundles npm >= 11.5.1, which trusted publishing requires. - node-version: 24 + node-version: 22 registry-url: "https://registry.npmjs.org" scope: "@openai" + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + - name: Download npm tarballs from release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} run: | set -euo pipefail - version="$RELEASE_VERSION" - tag="$RELEASE_TAG" + version="${{ needs.release.outputs.version }}" + tag="${{ needs.release.outputs.tag }}" mkdir -p dist/npm - patterns=( - "codex-npm-${version}.tgz" - "codex-npm-linux-*-${version}.tgz" - "codex-npm-darwin-*-${version}.tgz" - "codex-npm-win32-*-${version}.tgz" - "codex-responses-api-proxy-npm-${version}.tgz" - "codex-sdk-npm-${version}.tgz" - ) - for pattern in "${patterns[@]}"; do - gh release download "$tag" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "$pattern" \ - --dir dist/npm - done + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-responses-api-proxy-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-sdk-npm-${version}.tgz" \ + --dir dist/npm # No NODE_AUTH_TOKEN needed because we use OIDC. - name: Publish to npm @@ -1366,193 +1170,23 @@ jobs: NPM_TAG: ${{ needs.release.outputs.npm_tag }} run: | set -euo pipefail - prefix="" + tag_args=() if [[ -n "${NPM_TAG}" ]]; then - prefix="${NPM_TAG}-" + tag_args+=(--tag "${NPM_TAG}") fi - root_tarball="dist/npm/codex-npm-${VERSION}.tgz" - sdk_tarball="dist/npm/codex-sdk-npm-${VERSION}.tgz" - # Keep this list in sync with CODEX_PLATFORM_PACKAGES in - # codex-cli/scripts/build_npm_package.py. The root wrapper advances - # @openai/codex@latest as soon as it publishes, so every platform - # package it aliases must already exist in the registry first. - platform_tarballs=( - "dist/npm/codex-npm-linux-x64-${VERSION}.tgz" - "dist/npm/codex-npm-linux-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-x64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-x64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-arm64-${VERSION}.tgz" - ) - - for required_tarball in "${platform_tarballs[@]}" "${root_tarball}"; do - if [[ ! -f "${required_tarball}" ]]; then - echo "Missing npm tarball: ${required_tarball}" - exit 1 - fi - done - - shopt -s nullglob - other_tarballs=() - for tarball in dist/npm/*-"${VERSION}".tgz; do - if [[ "${tarball}" == "${root_tarball}" || "${tarball}" == "${sdk_tarball}" ]]; then - continue - fi - - is_platform_tarball=false - for platform_tarball in "${platform_tarballs[@]}"; do - if [[ "${tarball}" == "${platform_tarball}" ]]; then - is_platform_tarball=true - break - fi - done - if [[ "${is_platform_tarball}" == true ]]; then - continue - fi - - other_tarballs+=("${tarball}") - done - - # Publish the platform packages before the root CLI wrapper. The root - # wrapper advances @openai/codex@latest, so it should only publish - # after the optional dependency versions it references exist. tarballs=( - "${platform_tarballs[@]}" - "${other_tarballs[@]}" - "${root_tarball}" + "codex-npm-${VERSION}.tgz" + "codex-responses-api-proxy-npm-${VERSION}.tgz" + "codex-sdk-npm-${VERSION}.tgz" ) - if [[ -f "${sdk_tarball}" ]]; then - tarballs+=("${sdk_tarball}") - fi for tarball in "${tarballs[@]}"; do - filename="$(basename "${tarball}")" - tag="" - - case "${filename}" in - codex-npm-linux-*-"${VERSION}".tgz|codex-npm-darwin-*-"${VERSION}".tgz|codex-npm-win32-*-"${VERSION}".tgz) - platform="${filename#codex-npm-}" - platform="${platform%-${VERSION}.tgz}" - tag="${prefix}${platform}" - ;; - codex-npm-"${VERSION}".tgz|codex-responses-api-proxy-npm-"${VERSION}".tgz|codex-sdk-npm-"${VERSION}".tgz) - tag="${NPM_TAG}" - ;; - *) - echo "Unexpected npm tarball: ${filename}" - exit 1 - ;; - esac - - publish_cmd=(npm publish "${GITHUB_WORKSPACE}/${tarball}") - if [[ -n "${tag}" ]]; then - publish_cmd+=(--tag "${tag}") - fi - - echo "+ ${publish_cmd[*]}" - set +e - publish_output="$("${publish_cmd[@]}" 2>&1)" - publish_status=$? - set -e - - echo "${publish_output}" - if [[ ${publish_status} -eq 0 ]]; then - continue - fi - - if grep -qiE "previously published|cannot publish over|version already exists" <<< "${publish_output}"; then - echo "Skipping already-published package version for ${filename}" - continue - fi - - exit "${publish_status}" + npm publish "${GITHUB_WORKSPACE}/dist/npm/${tarball}" "${tag_args[@]}" done - # Publish the platform-specific Python runtime wheels using PyPI trusted publishing. - # PyPI project configuration must trust this workflow and job. Keep this - # non-blocking while the Python runtime publishing path is new; failures still - # need release follow-up, but should not invalidate the Rust release itself. - publish-python-runtime: - # Publish to PyPI for stable releases and alpha pre-releases with numeric suffixes. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_python_runtime == 'true' - }} - name: publish-python-runtime - needs: release - runs-on: ubuntu-latest - continue-on-error: true - environment: pypi - permissions: - id-token: write # Required for PyPI trusted publishing. - contents: read - - steps: - - name: Download Python runtime wheels from release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} - run: | - set -euo pipefail - python_version="$RELEASE_VERSION" - python_version="${python_version/-alpha./a}" - python_version="${python_version/-beta./b}" - python_version="${python_version/-rc./rc}" - - mkdir -p dist/python-runtime - gh release download "$RELEASE_TAG" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "openai_codex_cli_bin-${python_version}-*.whl" \ - --dir dist/python-runtime - ls -lh dist/python-runtime - - - name: Publish Python runtime wheels to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - with: - packages-dir: dist/python-runtime - skip-existing: true - - winget: - name: winget - needs: release - # Only publish stable/mainline releases to WinGet; pre-releases include a - # '-' in the semver string (e.g., 1.2.3-alpha.1). - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' && - !contains(needs.release.outputs.version, '-') - }} - # This job only invokes a GitHub Action to open/update the winget-pkgs PR; - # it does not execute Windows-only tooling, so Linux is sufficient. - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Publish to WinGet - uses: vedantmgoyal9/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5 - with: - identifier: OpenAI.Codex - version: ${{ needs.release.outputs.version }} - release-tag: ${{ needs.release.outputs.tag }} - fork-user: openai-oss-forks - installers-regex: '^codex-(?:x86_64|aarch64)-pc-windows-msvc\.exe\.zip$' - token: ${{ secrets.WINGET_PUBLISH_PAT }} - update-branch: name: Update latest-alpha-cli branch - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' - }} permissions: contents: write needs: release diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml new file mode 100644 index 000000000000..66a76aa4d938 --- /dev/null +++ b/.github/workflows/shell-tool-mcp.yml @@ -0,0 +1,461 @@ +name: shell-tool-mcp + +on: + workflow_call: + inputs: + release-version: + description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v. + required: false + type: string + release-tag: + description: Tag name to use when downloading release artifacts (defaults to rust-v). + required: false + type: string + publish: + description: Whether to publish to npm when the version is releasable. + required: false + default: true + type: boolean + +env: + NODE_VERSION: 22 + +jobs: + metadata: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.compute.outputs.version }} + release_tag: ${{ steps.compute.outputs.release_tag }} + should_publish: ${{ steps.compute.outputs.should_publish }} + npm_tag: ${{ steps.compute.outputs.npm_tag }} + steps: + - name: Compute version and tags + id: compute + run: | + set -euo pipefail + + version="${{ inputs.release-version }}" + release_tag="${{ inputs.release-tag }}" + + if [[ -z "$version" ]]; then + if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then + version="${release_tag#rust-v}" + elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then + version="${GITHUB_REF_NAME#rust-v}" + release_tag="${GITHUB_REF_NAME}" + else + echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag." + exit 1 + fi + fi + + if [[ -z "$release_tag" ]]; then + release_tag="rust-v${version}" + fi + + npm_tag="" + should_publish="false" + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + should_publish="true" + elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then + should_publish="true" + npm_tag="alpha" + fi + + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" + echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT" + echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT" + + matrix-setup: + runs-on: ubuntu-latest + outputs: + rust-binaries: ${{ steps.compute.outputs.rust-binaries }} + bash-linux: ${{ steps.compute.outputs.bash-linux }} + bash-darwin: ${{ steps.compute.outputs.bash-darwin }} + steps: + - name: Compute matrices + id: compute + shell: bash + run: | + set -euo pipefail + is_openai="${{ github.repository_owner == 'openai' }}" + + # rust-binaries: always include x86_64-musl; conditionally include paid/fork runners + if [[ "$is_openai" == "true" ]]; then + rust='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","install_musl":true} + ]' + else + rust='[{"runner":"macos-latest","target":"aarch64-apple-darwin"}]' + fi + rust=$(echo "$rust" | jq -c '. + [{"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","install_musl":true}]') + echo "rust-binaries={\"include\":$rust}" >> "$GITHUB_OUTPUT" + + # bash-linux: always include x86_64 variants; add arm64 on openai + bash_linux='[ + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-24.04","image":"ubuntu:24.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-22.04","image":"ubuntu:22.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-12","image":"debian:12"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-11","image":"debian:11"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]' + if [[ "$is_openai" == "true" ]]; then + bash_linux=$(echo "$bash_linux" | jq -c '. + [ + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-24.04","image":"arm64v8/ubuntu:24.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-22.04","image":"arm64v8/ubuntu:22.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-20.04","image":"arm64v8/ubuntu:20.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-12","image":"arm64v8/debian:12"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-11","image":"arm64v8/debian:11"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]') + fi + echo "bash-linux={\"include\":$(echo "$bash_linux" | jq -c)}" >> "$GITHUB_OUTPUT" + + # bash-darwin: always include macos-14; add macos-15-xlarge on openai + bash_darwin='[{"runner":"macos-14","target":"aarch64-apple-darwin","variant":"macos-14"}]' + if [[ "$is_openai" == "true" ]]; then + bash_darwin=$(echo "$bash_darwin" | jq -c '. + [ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin","variant":"macos-15"} + ]') + fi + echo "bash-darwin={\"include\":$(echo "$bash_darwin" | jq -c)}" >> "$GITHUB_OUTPUT" + + rust-binaries: + name: Build Rust - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + defaults: + run: + working-directory: codex-rs + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.rust-binaries) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install UBSan runtime (musl) + if: ${{ matrix.install_musl }} + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -y + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 + fi + + - uses: dtolnay/rust-toolchain@1.93 + with: + targets: ${{ matrix.target }} + + - if: ${{ matrix.install_musl }} + name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.14.0 + + - if: ${{ matrix.install_musl }} + name: Install musl build dependencies + env: + TARGET: ${{ matrix.target }} + run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh" + + - if: ${{ matrix.install_musl }} + name: Configure rustc UBSan wrapper (musl host) + shell: bash + run: | + set -euo pipefail + ubsan="" + if command -v ldconfig >/dev/null 2>&1; then + ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')" + fi + wrapper_root="${RUNNER_TEMP:-/tmp}" + wrapper="${wrapper_root}/rustc-ubsan-wrapper" + cat > "${wrapper}" <> "$GITHUB_ENV" + echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV" + + - if: ${{ matrix.install_musl }} + name: Clear sanitizer flags (musl) + shell: bash + run: | + set -euo pipefail + # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. + echo "RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV" + # Override any runner-level Cargo config rustflags as well. + echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + + sanitize_flags() { + local input="$1" + input="${input//-fsanitize=undefined/}" + input="${input//-fno-sanitize-recover=undefined/}" + input="${input//-fno-sanitize-trap=undefined/}" + echo "$input" + } + + cflags="$(sanitize_flags "${CFLAGS-}")" + cxxflags="$(sanitize_flags "${CXXFLAGS-}")" + echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" + echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + + - name: Build exec server binaries + run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper + + - name: Stage exec server binaries + run: | + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}" + mkdir -p "$dest" + cp "target/${{ matrix.target }}/release/codex-exec-mcp-server" "$dest/" + cp "target/${{ matrix.target }}/release/codex-execve-wrapper" "$dest/" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-rust-${{ matrix.target }} + path: artifacts/** + if-no-files-found: error + + bash-linux: + name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + container: + image: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-linux) }} + steps: + - name: Install build prerequisites + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext + elif command -v dnf >/dev/null 2>&1; then + dnf install -y git gcc gcc-c++ make bison autoconf gettext + elif command -v yum >/dev/null 2>&1; then + yum install -y git gcc gcc-c++ make bison autoconf gettext + else + echo "Unsupported package manager in container" + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + bash-darwin: + name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-darwin) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + package: + name: Package npm module + needs: + - metadata + - rust-binaries + - bash-linux + - bash-darwin + runs-on: ubuntu-latest + env: + PACKAGE_VERSION: ${{ needs.metadata.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install JavaScript dependencies + run: pnpm install --frozen-lockfile + + - name: Build (shell-tool-mcp) + run: pnpm --filter @openai/codex-shell-tool-mcp run build + + - name: Download build artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts + + - name: Assemble staging directory + id: staging + shell: bash + run: | + set -euo pipefail + staging="${STAGING_DIR}" + mkdir -p "$staging" "$staging/vendor" + cp shell-tool-mcp/README.md "$staging/" + cp shell-tool-mcp/package.json "$staging/" + cp -R shell-tool-mcp/bin "$staging/" + + found_vendor="false" + shopt -s nullglob + for vendor_dir in artifacts/*/vendor; do + rsync -av "$vendor_dir/" "$staging/vendor/" + found_vendor="true" + done + if [[ "$found_vendor" == "false" ]]; then + echo "No vendor payloads were downloaded." + exit 1 + fi + + node - <<'NODE' + import fs from "node:fs"; + import path from "node:path"; + + const stagingDir = process.env.STAGING_DIR; + const version = process.env.PACKAGE_VERSION; + const pkgPath = path.join(stagingDir, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + pkg.version = version; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + NODE + + echo "dir=$staging" >> "$GITHUB_OUTPUT" + env: + STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp + + - name: Ensure binaries are executable + run: | + set -euo pipefail + staging="${{ steps.staging.outputs.dir }}" + chmod +x \ + "$staging"/vendor/*/codex-exec-mcp-server \ + "$staging"/vendor/*/codex-execve-wrapper \ + "$staging"/vendor/*/bash/*/bash + + - name: Create npm tarball + shell: bash + run: | + set -euo pipefail + mkdir -p dist/npm + staging="${{ steps.staging.outputs.dir }}" + pack_info=$(cd "$staging" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm") + filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);') + mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz" + + - uses: actions/upload-artifact@v6 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz + if-no-files-found: error + + publish: + name: Publish npm package + needs: + - metadata + - package + if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }} + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: https://registry.npmjs.org + scope: "@openai" + + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + + - name: Download npm tarball + uses: actions/download-artifact@v7 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm + + - name: Publish to npm + if: github.repository == 'openai/codex' + env: + NPM_TAG: ${{ needs.metadata.outputs.npm_tag }} + VERSION: ${{ needs.metadata.outputs.version }} + shell: bash + run: | + set -euo pipefail + tag_args=() + if [[ -n "${NPM_TAG}" ]]; then + tag_args+=(--tag "${NPM_TAG}") + fi + npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}" diff --git a/.github/workflows/termux-release-checkpoint.yml b/.github/workflows/termux-release-checkpoint.yml new file mode 100644 index 000000000000..0347e6f1b3d6 --- /dev/null +++ b/.github/workflows/termux-release-checkpoint.yml @@ -0,0 +1,103 @@ +name: termux-release-checkpoint + +on: + workflow_dispatch: + inputs: + source_branch: + description: "Release branch to checkpoint from, for example release/0.123.0" + required: false + type: string + default: "" + source_sha: + description: "Specific source commit SHA to checkpoint; defaults to the source branch tip" + required: false + type: string + default: "" + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + type: string + default: "wallentx/termux-target" + reviewer: + description: "GitHub username to request as reviewer" + required: false + type: string + default: "wallentx" + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-checkpoint-${{ inputs.source_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + checkpoint: + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + env: + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + REQUESTED_SOURCE_BRANCH: ${{ inputs.source_branch }} + REQUESTED_SOURCE_SHA: ${{ inputs.source_sha }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ inputs.source_branch || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + source_branch="${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${source_branch}" + + - name: Create checkpoint PR + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" diff --git a/.github/workflows/termux-release-deploy.yml b/.github/workflows/termux-release-deploy.yml new file mode 100644 index 000000000000..2071611dacc4 --- /dev/null +++ b/.github/workflows/termux-release-deploy.yml @@ -0,0 +1,243 @@ +name: termux-release-deploy + +on: + push: + branches: + - "release/**" + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to deploy, for example release/0.124.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to deploy. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged release PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged release PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + default: "wallentx/termux-target" + type: string + reviewer: + description: "GitHub username to request as reviewer on the checkpoint PR" + required: false + default: "wallentx" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-deploy-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + deploy: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' || !startsWith(github.event.head_commit.message, 'Seed Termux release automation') }} + permissions: + actions: read + contents: write + deployments: write + issues: write + pull-requests: write + env: + REQUESTED_RELEASE_BRANCH: ${{ inputs.release_branch }} + REQUESTED_RELEASE_SHA: ${{ inputs.release_sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + release_branch="${REQUESTED_RELEASE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${release_branch}" + + - name: Resolve release ref + id: release-ref + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: deploy + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Create deployment + if: steps.metadata.outputs.deploy == 'true' + id: deployment + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + deployment_id="$( + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments" \ + -f ref="${RELEASE_SHA}" \ + -f environment="termux-release" \ + -F auto_merge=false \ + -F required_contexts[] \ + -f description="Termux release deployment for ${TERMUX_TAG}" \ + --jq '.id' + )" + echo "id=${deployment_id}" >> "$GITHUB_OUTPUT" + + - name: Mark deployment in progress + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="in_progress" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Promoting Termux release artifact and preparing checkpoint PR" \ + >/dev/null + + - name: Locate merged pull request + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" + + - name: Ensure checkpoint PR + if: steps.metadata.outputs.deploy == 'true' + id: checkpoint + env: + SOURCE_BRANCH: ${{ steps.release-ref.outputs.branch }} + SOURCE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" + + - name: Mark deployment success + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="success" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment completed for ${TERMUX_TAG}" \ + >/dev/null + + - name: Mark deployment failure + if: failure() && steps.deployment.outputs.id != '' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="failure" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment failed" \ + >/dev/null diff --git a/.github/workflows/termux-release-promote.yml b/.github/workflows/termux-release-promote.yml new file mode 100644 index 000000000000..22c277c21a70 --- /dev/null +++ b/.github/workflows/termux-release-promote.yml @@ -0,0 +1,135 @@ +name: termux-release-promote + +on: + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to promote, for example release/0.122.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to promote. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-promote-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + promote: + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: write + pull-requests: read + env: + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Resolve release ref + id: release-ref + env: + INPUT_RELEASE_BRANCH: ${{ inputs.release_branch }} + INPUT_RELEASE_SHA: ${{ inputs.release_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: promote + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Locate merged pull request + if: steps.metadata.outputs.promote == 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.promote == 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.promote == 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" diff --git a/scripts/termux-configure-git.sh b/scripts/termux-configure-git.sh new file mode 100755 index 000000000000..fae3abcbab48 --- /dev/null +++ b/scripts/termux-configure-git.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Configure the explicit bot identity used by release automation and fetch any +# refs requested by the calling workflow. + +set -euo pipefail + +git config user.name "github-actions[bot]" +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + +while (($#)); do + case "$1" in + --origin) + shift + origin_refs=() + while (($#)) && [[ "$1" != --* ]]; do + origin_refs+=("$1") + shift + done + if ((${#origin_refs[@]})); then + git fetch --prune origin "${origin_refs[@]}" + fi + ;; + --upstream-tag) + if (($# < 3)); then + echo "--upstream-tag requires ." >&2 + exit 1 + fi + upstream_repo="$2" + upstream_tag="$3" + git remote add upstream "https://github.com/${upstream_repo}.git" 2>/dev/null || true + git fetch --prune --no-tags upstream "+refs/tags/${upstream_tag}:refs/tags/${upstream_tag}" + shift 3 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done diff --git a/scripts/termux-create-checkpoint-pr.sh b/scripts/termux-create-checkpoint-pr.sh new file mode 100755 index 000000000000..ba07c289952d --- /dev/null +++ b/scripts/termux-create-checkpoint-pr.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/termux-release-paths.sh +source "${script_dir}/termux-release-paths.sh" + +source_branch="${SOURCE_BRANCH:-${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}}" +source_sha="${SOURCE_SHA:-${REQUESTED_SOURCE_SHA:-}}" +if [[ -z "${source_sha}" ]]; then + if [[ "${GITHUB_EVENT_NAME:-}" == "push" && "${source_branch}" == "${GITHUB_REF_NAME:-}" ]]; then + source_sha="${GITHUB_SHA}" + else + source_sha="$(git rev-parse "origin/${source_branch}")" + fi +fi + +if [[ -z "${DESTINATION_BRANCH:-}" ]]; then + echo "DESTINATION_BRANCH is required." >&2 + exit 1 +fi + +release_only_checkpoint_paths() { + printf '%s\n' "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +} + +resolve_source_version_conflicts() { + local path="$1" + local resolved_path + resolved_path="$(mktemp)" + + if ! awk ' + function normalize_versions(text) { + gsub(/version = "[^"]+"/, "version = \"\"", text) + return text + } + + BEGIN { + in_block = 0 + side = "" + ours = "" + theirs = "" + blocks = 0 + } + + /^<<<<<<< / { + if (in_block) { + exit 1 + } + in_block = 1 + side = "ours" + ours = "" + theirs = "" + blocks++ + next + } + + /^=======$/ && in_block { + side = "theirs" + next + } + + /^>>>>>>> / && in_block { + if (normalize_versions(ours) != normalize_versions(theirs)) { + exit 1 + } + printf "%s", theirs + in_block = 0 + side = "" + next + } + + { + if (!in_block) { + print + } else if (side == "ours") { + ours = ours $0 ORS + } else if (side == "theirs") { + theirs = theirs $0 ORS + } else { + exit 1 + } + } + + END { + if (in_block || blocks == 0) { + exit 1 + } + } + ' "${path}" > "${resolved_path}"; then + rm -f "${resolved_path}" + return 1 + fi + + cp "${resolved_path}" "${path}" + rm -f "${resolved_path}" +} + +short_sha="${source_sha:0:12}" +source_slug="${source_branch//\//_}" +dest_slug="${DESTINATION_BRANCH//\//_}" +checkpoint_branch="checkpoint/${dest_slug}_from_${source_slug}_${short_sha}" +pr_title="checkpoint: into ${DESTINATION_BRANCH} from ${source_branch} @ ${short_sha}" +merge_conflicted=false +conflict_summary="" + +existing_pr="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --head "${checkpoint_branch}" \ + --state all \ + --json number,state,mergedAt,url \ + --jq '[.[] | select(.state == "OPEN" or .mergedAt != null)] | .[0] // empty' +)" +if [[ -n "${existing_pr}" ]]; then + existing_url="$(jq -r '.url' <<< "${existing_pr}")" + existing_state="$(jq -r '.state' <<< "${existing_pr}")" + echo "Checkpoint PR already exists for ${checkpoint_branch}: ${existing_url} (${existing_state})." + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${existing_url}" >> "${GITHUB_OUTPUT}" + fi + exit 0 +fi + +git checkout -B "${checkpoint_branch}" "origin/${DESTINATION_BRANCH}" + +if ! git merge --no-ff --no-edit "${source_sha}"; then + mapfile -t conflicted_paths < <(git diff --name-only --diff-filter=U) + for conflicted_path in "${conflicted_paths[@]}"; do + if termux_is_checkpoint_release_only_path "${conflicted_path}"; then + echo "Auto-resolving release-only checkpoint conflict in ${conflicted_path} by keeping ${DESTINATION_BRANCH}." + if git cat-file -e "HEAD:${conflicted_path}" 2>/dev/null; then + git checkout --ours -- "${conflicted_path}" + git add "${conflicted_path}" + else + git rm -f --ignore-unmatch "${conflicted_path}" + fi + fi + done + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if ((${#remaining_conflicts[@]})); then + cargo_version_conflicts=true + for remaining_conflict in "${remaining_conflicts[@]}"; do + case "${remaining_conflict}" in + codex-rs/Cargo.toml|codex-rs/Cargo.lock) + ;; + *) + cargo_version_conflicts=false + ;; + esac + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + for remaining_conflict in "${remaining_conflicts[@]}"; do + if ! resolve_source_version_conflicts "${remaining_conflict}"; then + cargo_version_conflicts=false + break + fi + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + echo "Auto-resolving recurring Cargo version checkpoint conflicts by keeping ${source_branch} versions." + git add -- "${remaining_conflicts[@]}" + fi + fi + fi + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if [[ "${#remaining_conflicts[@]}" -eq 0 ]]; then + git commit --no-edit + else + merge_conflicted=true + conflict_summary="$( + printf '%s\n' "${remaining_conflicts[@]}" | awk '{ print "- `" $0 "`" }' + )" + echo "Automatic checkpoint merge failed; creating a manual-resolution PR instead." >&2 + if git rev-parse -q --verify MERGE_HEAD >/dev/null; then + git merge --abort + fi + git checkout -B "${checkpoint_branch}" "${source_sha}" + fi +fi + +if git cat-file -e "origin/${DESTINATION_BRANCH}:.github" 2>/dev/null; then + git restore --source="origin/${DESTINATION_BRANCH}" --staged --worktree -- .github + mapfile -t added_github_paths < <( + git diff --name-only --diff-filter=A "origin/${DESTINATION_BRANCH}" -- .github + ) + if ((${#added_github_paths[@]})); then + git rm -f --ignore-unmatch -- "${added_github_paths[@]}" + fi +else + git rm -r --ignore-unmatch .github +fi + +while IFS= read -r release_only_path; do + if git cat-file -e "origin/${DESTINATION_BRANCH}:${release_only_path}" 2>/dev/null; then + git restore --source="origin/${DESTINATION_BRANCH}" --staged --worktree -- "${release_only_path}" + else + git rm -f --ignore-unmatch -- "${release_only_path}" + fi +done < <(release_only_checkpoint_paths) + +if ! git diff --quiet || ! git diff --cached --quiet; then + if [[ -e .github || -L .github ]] || git ls-files --error-unmatch -- .github >/dev/null 2>&1; then + git add -A .github + fi + while IFS= read -r release_only_path; do + if [[ -e "${release_only_path}" || -L "${release_only_path}" ]] || git ls-files --error-unmatch -- "${release_only_path}" >/dev/null 2>&1; then + git add -A -- "${release_only_path}" + fi + done < <(release_only_checkpoint_paths) + if [[ "${merge_conflicted}" == "true" ]]; then + git commit -m "checkpoint: prepare ${source_branch} for ${DESTINATION_BRANCH}" + else + git commit --amend --no-edit + fi +fi + +if git diff --quiet "origin/${DESTINATION_BRANCH}" HEAD; then + echo "Checkpoint merge produced no destination changes after release-only files were restored." + exit 0 +fi + +git push --force-with-lease origin "${checkpoint_branch}" + +remaining="$( + git log --first-parent --pretty=format:%H "${source_sha}..origin/${source_branch}" | wc -w +)" + +body_path="${RUNNER_TEMP}/termux-checkpoint-pr.md" +{ + echo "## Termux release checkpoint" + echo + echo "- Source branch: \`${source_branch}\`" + echo "- Source hash: \`${source_sha}\`" + echo "- Destination branch: \`${DESTINATION_BRANCH}\`" + echo "- Remaining first-parent commits on source: ${remaining}" + echo + echo "This PR carries release-train conflict fixes and follow-up changes back into the reusable Termux patch branch." + if [[ "${merge_conflicted}" == "true" ]]; then + echo + echo "## Merge conflicts" + echo + echo "GitHub Actions could not create the checkpoint merge commit automatically, so this PR was created from the source branch state for manual conflict resolution." + echo + echo "Conflicted paths from the failed merge attempt:" + if [[ -n "${conflict_summary}" ]]; then + printf '%s\n' "${conflict_summary}" + else + echo "- Conflict details unavailable" + fi + fi + echo + echo "Release-only workflow files and metadata under \`.github\` were restored to the destination branch versions before opening this PR." +} > "${body_path}" + +pr_url="$( + gh pr create \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${DESTINATION_BRANCH}" \ + --head "${checkpoint_branch}" \ + --title "${pr_title}" \ + --body-file "${body_path}" +)" +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-reviewer "${REVIEWER}" || true +gh label create checkpoint --repo "${GITHUB_REPOSITORY}" --color c5def5 --description "Checkpoint merge" --force +gh label create termux-release --repo "${GITHUB_REPOSITORY}" --color 0e8a16 --description "Termux release automation" --force +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-label "checkpoint" --add-label "termux-release" + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${pr_url}" >> "${GITHUB_OUTPUT}" +fi diff --git a/scripts/termux-create-or-update-mirrored-release.sh b/scripts/termux-create-or-update-mirrored-release.sh new file mode 100755 index 000000000000..5787e894582b --- /dev/null +++ b/scripts/termux-create-or-update-mirrored-release.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +# Create a mirrored Termux release or repair an existing release that is missing +# the Android tarball. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" +: "${RUNNER_TEMP:?RUNNER_TEMP is required}" +: "${UPSTREAM_TAG:?UPSTREAM_TAG is required}" +: "${UPSTREAM_REPO:?UPSTREAM_REPO is required}" +: "${UPSTREAM_NAME?UPSTREAM_NAME is required}" +: "${TERMUX_TAG:?TERMUX_TAG is required}" +: "${UPSTREAM_PRERELEASE:?UPSTREAM_PRERELEASE is required}" +: "${UPSTREAM_HTML_URL?UPSTREAM_HTML_URL is required}" +: "${RELEASE_TRAIN?RELEASE_TRAIN is required}" +: "${RELEASE_EXISTS:?RELEASE_EXISTS is required}" +: "${PR_NUMBER:?PR_NUMBER is required}" +: "${HEAD_SHA:?HEAD_SHA is required}" +: "${RELEASE_SHA:?RELEASE_SHA is required}" + +promoted_dir="${PROMOTED_DIR:-promoted}" +asset_path="${promoted_dir}/codex-aarch64-linux-android.tar.gz" + +body_path="${RUNNER_TEMP}/release-body.md" +upstream_body_path="${RUNNER_TEMP}/upstream-release-body.md" +upstream_body_without_changelog_path="${RUNNER_TEMP}/upstream-release-body-without-changelog.md" +if gh release view "${UPSTREAM_TAG}" \ + --repo "${UPSTREAM_REPO}" \ + --json body \ + --jq '.body // ""' > "${upstream_body_path}"; then + awk ' + function heading_level(line, text) { + if (match(line, /^(#{1,6})[[:space:]]+(.+)$/, parts)) { + text = tolower(parts[2]) + sub(/[[:space:]]+#+[[:space:]]*$/, "", text) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", text) + if (text == "changelog" || text == "change log") { + return length(parts[1]) + } + } + return 0 + } + + { + if (!skip) { + level = heading_level($0) + if (level > 0) { + skip = 1 + skip_level = level + next + } + print + next + } + + if (match($0, /^(#{1,6})[[:space:]]+/, parts) && length(parts[1]) <= skip_level) { + skip = 0 + print + } + } + ' "${upstream_body_path}" > "${upstream_body_without_changelog_path}" +else + echo "::warning title=Upstream release notes unavailable::Could not read ${UPSTREAM_REPO} release ${UPSTREAM_TAG}." + : > "${upstream_body_without_changelog_path}" +fi + +{ + echo "Termux Android build for ${UPSTREAM_TAG}." + echo + echo "- Upstream release: ${UPSTREAM_HTML_URL}" + echo "- Release train: \`${RELEASE_TRAIN}\`" + echo "- Promoted PR: #${PR_NUMBER}" + echo "- Promoted PR head SHA: \`${HEAD_SHA}\`" +} > "${body_path}" +if [[ -s "${upstream_body_without_changelog_path}" ]]; then + { + echo + echo "## Upstream release notes" + echo + cat "${upstream_body_without_changelog_path}" + } >> "${body_path}" +fi + +release_title="${UPSTREAM_NAME}" +if [[ -z "${release_title}" || "${release_title}" == "null" ]]; then + release_title="${TERMUX_TAG}" +fi + +if [[ "${RELEASE_EXISTS}" == "true" ]]; then + gh release upload \ + "${TERMUX_TAG}" \ + "${asset_path}#codex-termux" \ + --repo "${GITHUB_REPOSITORY}" \ + --clobber + exit 0 +fi + +release_args=( + gh release create "${TERMUX_TAG}" + --repo "${GITHUB_REPOSITORY}" + --target "${RELEASE_SHA}" + --title "${release_title}" + --notes-file "${body_path}" +) +if [[ "${UPSTREAM_PRERELEASE}" == "true" ]]; then + release_args+=(--prerelease) +fi +release_args+=("${asset_path}#codex-termux") +"${release_args[@]}" diff --git a/scripts/termux-download-release-artifact.sh b/scripts/termux-download-release-artifact.sh new file mode 100755 index 000000000000..2acdba9025a0 --- /dev/null +++ b/scripts/termux-download-release-artifact.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${HEAD_SHA:-}" ]]; then + echo "HEAD_SHA is required." >&2 + exit 1 +fi + +if [[ -z "${PR_ARTIFACT_NAME:-}" ]]; then + echo "PR_ARTIFACT_NAME is required." >&2 + exit 1 +fi + +promoted_dir="${PROMOTED_DIR:-promoted}" + +find_run_id() { + local event="$1" + gh run list \ + --repo "${GITHUB_REPOSITORY}" \ + --workflow rust-release.yml \ + --event "${event}" \ + --status success \ + --commit "${HEAD_SHA}" \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId // empty' +} + +find_artifact_run_id() { + local artifact_name="$1" + + gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=${artifact_name}" \ + | jq -rs \ + --arg artifact_name "${artifact_name}" \ + --arg head_sha "${HEAD_SHA}" \ + ' + [ + .[].artifacts[] + | select(.name == $artifact_name) + | select(.expired == false) + | select(.workflow_run.head_sha == $head_sha) + ] + | sort_by(.created_at) + | reverse + | .[0].workflow_run.id // empty + ' +} + +artifact_name="${PR_ARTIFACT_NAME}" +run_id="$(find_artifact_run_id "${artifact_name}")" +mkdir -p "${promoted_dir}" +if [[ -n "${run_id}" ]] && gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}"; then + : +else + artifact_name="aarch64-linux-android" + run_id="$(find_run_id workflow_dispatch)" + if [[ -z "${run_id}" ]]; then + echo "No successful rust-release run found for ${HEAD_SHA}" >&2 + exit 1 + fi + gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}" +fi + +ls -la "${promoted_dir}" +if [[ -f "${promoted_dir}/SHA256SUMS" ]]; then + (cd "${promoted_dir}" && sha256sum -c SHA256SUMS) +fi +if [[ ! -f "${promoted_dir}/codex-aarch64-linux-android.tar.gz" ]]; then + echo "Expected ${promoted_dir}/codex-aarch64-linux-android.tar.gz in the downloaded artifact." >&2 + exit 1 +fi diff --git a/scripts/termux-find-release-pr.sh b/scripts/termux-find-release-pr.sh new file mode 100755 index 000000000000..3e35fa42eabe --- /dev/null +++ b/scripts/termux-find-release-pr.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -n "${INPUT_PR_NUMBER:-}" || -n "${INPUT_PR_HEAD_SHA:-}" ]]; then + if [[ -z "${INPUT_PR_NUMBER:-}" || -z "${INPUT_PR_HEAD_SHA:-}" ]]; then + echo "workflow_dispatch inputs pr_number and pr_head_sha must be provided together." >&2 + exit 1 + fi + + pr_number="${INPUT_PR_NUMBER}" + head_sha="${INPUT_PR_HEAD_SHA}" + head_ref="" +else + if [[ -z "${RELEASE_BRANCH:-}" || -z "${RELEASE_SHA:-}" ]]; then + echo "RELEASE_BRANCH and RELEASE_SHA are required." >&2 + exit 1 + fi + + pr_json="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${RELEASE_BRANCH}" \ + --state merged \ + --limit 100 \ + --json number,headRefOid,headRefName,mergeCommit \ + --jq "[.[] | select(.mergeCommit.oid == \"${RELEASE_SHA}\")] | sort_by(.number) | reverse | .[0] // empty" + )" + if [[ -z "${pr_json}" ]]; then + echo "Unable to find merged PR for ${RELEASE_SHA} into ${RELEASE_BRANCH}" >&2 + exit 1 + fi + + pr_number="$(jq -r '.number' <<< "${pr_json}")" + head_sha="$(jq -r '.headRefOid // .head.sha' <<< "${pr_json}")" + head_ref="$(jq -r '.headRefName // .head.ref' <<< "${pr_json}")" +fi + +artifact_name="termux-android-pr-${pr_number}-${head_sha}" +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "number=${pr_number}" + echo "head_sha=${head_sha}" + echo "head_ref=${head_ref}" + echo "artifact_name=${artifact_name}" + } >> "${GITHUB_OUTPUT}" +else + printf 'number=%s\nhead_sha=%s\nhead_ref=%s\nartifact_name=%s\n' \ + "${pr_number}" \ + "${head_sha}" \ + "${head_ref}" \ + "${artifact_name}" +fi diff --git a/scripts/termux-read-release-metadata.sh b/scripts/termux-read-release-metadata.sh new file mode 100755 index 000000000000..519a0635a368 --- /dev/null +++ b/scripts/termux-read-release-metadata.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# Parse .github/termux-release.json and emit workflow outputs for deploy or +# promote jobs. TERMUX_RELEASE_ACTION must be "deploy" or "promote". + +set -euo pipefail + +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +metadata=".github/termux-release.json" +action="${TERMUX_RELEASE_ACTION:-deploy}" +case "${action}" in + deploy) + missing_context="deployment" + ;; + promote) + missing_context="promotion" + ;; + *) + echo "TERMUX_RELEASE_ACTION must be deploy or promote." >&2 + exit 1 + ;; +esac + +if [[ ! -f "${metadata}" ]]; then + echo "No ${metadata}; this push is not a Termux release ${missing_context}." + echo "${action}=false" >> "${GITHUB_OUTPUT}" + exit 0 +fi + +upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" +upstream_name="$(jq -r '.upstream_name // .upstream_tag // empty' "${metadata}")" +termux_tag="$(jq -r '.termux_tag // empty' "${metadata}")" +upstream_version="${upstream_tag#rust-v}" +upstream_version="${upstream_version%-termux}" +upstream_prerelease=false +if [[ "${upstream_version}" == *-* ]]; then + upstream_prerelease=true +fi +upstream_html_url="$(jq -r '.upstream_html_url // ""' "${metadata}")" +upstream_repo="$(jq -r '.upstream_repo // "openai/codex"' "${metadata}")" +release_train="$(jq -r '.release_train // ""' "${metadata}")" +if [[ -z "${upstream_tag}" || -z "${termux_tag}" ]]; then + echo "Missing upstream_tag or termux_tag in ${metadata}" >&2 + exit 1 +fi + +release_state="$(TERMUX_TAG="${termux_tag}" "${script_dir}/termux-release-asset-state.sh")" +release_exists="$(awk -F= '$1 == "release_exists" { print $2 }' <<< "${release_state}")" +asset_exists="$(awk -F= '$1 == "asset_exists" { print $2 }' <<< "${release_state}")" + +if [[ "${action}" == "promote" ]]; then + if [[ "${asset_exists}" == "true" ]]; then + echo "${termux_tag} already exists with codex-aarch64-linux-android.tar.gz; skipping promotion." + echo "promote=false" >> "${GITHUB_OUTPUT}" + exit 0 + fi + if [[ "${release_exists}" == "true" ]]; then + echo "${termux_tag} exists but is missing codex-aarch64-linux-android.tar.gz; repairing promotion." + fi +fi + +{ + echo "${action}=true" + echo "upstream_tag=${upstream_tag}" + echo "upstream_name=${upstream_name}" + echo "termux_tag=${termux_tag}" + echo "upstream_prerelease=${upstream_prerelease}" + echo "upstream_html_url=${upstream_html_url}" + echo "upstream_repo=${upstream_repo}" + echo "release_train=${release_train}" + echo "release_exists=${release_exists}" + echo "asset_exists=${asset_exists}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-release-asset-state.sh b/scripts/termux-release-asset-state.sh new file mode 100755 index 000000000000..8694480b6a36 --- /dev/null +++ b/scripts/termux-release-asset-state.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +termux_tag="${1:-${TERMUX_TAG:-}}" +release_repo="${RELEASE_REPO:-${GITHUB_REPOSITORY:-}}" +asset_name="${ASSET_NAME:-codex-aarch64-linux-android.tar.gz}" + +if [[ -z "${termux_tag}" ]]; then + echo "TERMUX_TAG or tag argument is required." >&2 + exit 1 +fi + +if [[ -z "${release_repo}" ]]; then + echo "GITHUB_REPOSITORY or RELEASE_REPO is required." >&2 + exit 1 +fi + +release_exists=false +asset_exists=false + +if gh release view "${termux_tag}" --repo "${release_repo}" >/dev/null 2>&1; then + release_exists=true + release_asset_exists="$( + gh release view "${termux_tag}" \ + --repo "${release_repo}" \ + --json assets \ + | jq -r --arg asset_name "${asset_name}" \ + '.assets | map(.name) | any(. == $asset_name)' + )" + if [[ "${release_asset_exists}" == "true" ]]; then + asset_exists=true + fi +fi + +printf 'release_exists=%s\n' "${release_exists}" +printf 'asset_exists=%s\n' "${asset_exists}" diff --git a/scripts/termux-release-paths.sh b/scripts/termux-release-paths.sh new file mode 100755 index 000000000000..6990f323ec44 --- /dev/null +++ b/scripts/termux-release-paths.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# Sourceable path lists for files owned by the Termux release automation. + +set -euo pipefail + +readonly -a TERMUX_RELEASE_WORKFLOW_PATHS=( + .github/workflows/rust-release.yml + .github/workflows/shell-tool-mcp.yml + .github/workflows/termux-release-checkpoint.yml + .github/workflows/termux-release-deploy.yml + .github/workflows/termux-release-promote.yml +) + +readonly -a TERMUX_RELEASE_BRANCH_SCRIPT_PATHS=( + scripts/termux-configure-git.sh + scripts/termux-create-checkpoint-pr.sh + scripts/termux-create-or-update-mirrored-release.sh + scripts/termux-download-release-artifact.sh + scripts/termux-find-release-pr.sh + scripts/termux-read-release-metadata.sh + scripts/termux-release-asset-state.sh + scripts/termux-release-paths.sh + scripts/termux-resolve-release-ref.sh + scripts/termux-validate-gh-env.sh +) + +readonly -a TERMUX_RELEASE_AUTOMATION_PATHS=( + "${TERMUX_RELEASE_WORKFLOW_PATHS[@]}" + "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +) + +readonly -a TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS=( + "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" + .github/termux-release.json +) + +termux_path_in_list() { + local candidate="$1" + shift + local listed_path + + for listed_path in "$@"; do + [[ "${candidate}" != "${listed_path}" ]] || return 0 + done + return 1 +} + +termux_is_release_automation_path() { + termux_path_in_list "$1" "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" +} + +termux_is_checkpoint_release_only_path() { + termux_path_in_list "$1" "${TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS[@]}" +} diff --git a/scripts/termux-resolve-release-ref.sh b/scripts/termux-resolve-release-ref.sh new file mode 100755 index 000000000000..194773d45bed --- /dev/null +++ b/scripts/termux-resolve-release-ref.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Resolve the release branch and SHA for deploy/promote workflows. + +set -euo pipefail + +: "${GITHUB_EVENT_NAME:?GITHUB_EVENT_NAME is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" + +input_release_branch="${INPUT_RELEASE_BRANCH:-${REQUESTED_RELEASE_BRANCH:-}}" +input_release_sha="${INPUT_RELEASE_SHA:-${REQUESTED_RELEASE_SHA:-}}" + +if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then + if [[ -z "${input_release_branch}" ]]; then + echo "release_branch is required for workflow_dispatch." >&2 + exit 1 + fi + + release_branch="${input_release_branch}" + if [[ -n "${input_release_sha}" ]]; then + git checkout --detach "${input_release_sha}" + release_sha="${input_release_sha}" + else + release_sha="$(git rev-parse HEAD)" + fi +else + : "${GITHUB_REF_NAME:?GITHUB_REF_NAME is required}" + : "${GITHUB_SHA:?GITHUB_SHA is required}" + release_branch="${GITHUB_REF_NAME}" + release_sha="${GITHUB_SHA}" +fi + +{ + echo "branch=${release_branch}" + echo "sha=${release_sha}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-validate-gh-env.sh b/scripts/termux-validate-gh-env.sh new file mode 100755 index 000000000000..02581fdebceb --- /dev/null +++ b/scripts/termux-validate-gh-env.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Lightweight post-toolbox check for jobs that rely on authenticated gh calls. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +command -v gh +gh auth status --hostname github.com + +printf 'GITHUB_REPOSITORY=%s\n' "${GITHUB_REPOSITORY}" +printf 'REPO=%s\n' "${REPO:-}" +printf 'GH_REPO_URL=%s\n' "${GH_REPO_URL:-}" +printf 'GH_WORKFLOW_URL=%s\n' "${GH_WORKFLOW_URL:-}" From 289dd1edaba7c167895cc0a014d46f9be79e311d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 01:23:52 +0000 Subject: [PATCH 13/25] Prepare Termux rust-v0.133.0-alpha.3 --- .github/termux-release.json | 15 ++ codex-rs/Cargo.lock | 247 ++++++++++++++------------- codex-rs/Cargo.toml | 2 + codex-rs/arg0/Cargo.toml | 1 + codex-rs/arg0/src/lib.rs | 29 +++- codex-rs/core/Cargo.toml | 5 + codex-rs/core/src/installation_id.rs | 8 +- codex-rs/execpolicy/Cargo.toml | 1 + codex-rs/execpolicy/src/amend.rs | 23 ++- codex-rs/tui/Cargo.toml | 2 +- codex-rs/tui/src/lib.rs | 8 +- codex-rs/utils/file-lock/BUILD.bazel | 6 + codex-rs/utils/file-lock/Cargo.toml | 10 ++ codex-rs/utils/file-lock/src/lib.rs | 168 ++++++++++++++++++ justfile | 5 + scripts/termux-lock-audit.sh | 126 ++++++++++++++ 16 files changed, 518 insertions(+), 138 deletions(-) create mode 100644 .github/termux-release.json create mode 100644 codex-rs/utils/file-lock/BUILD.bazel create mode 100644 codex-rs/utils/file-lock/Cargo.toml create mode 100644 codex-rs/utils/file-lock/src/lib.rs create mode 100755 scripts/termux-lock-audit.sh diff --git a/.github/termux-release.json b/.github/termux-release.json new file mode 100644 index 000000000000..86ebe8919467 --- /dev/null +++ b/.github/termux-release.json @@ -0,0 +1,15 @@ +{ + "upstream_repo": "openai/codex", + "upstream_tag": "rust-v0.133.0-alpha.3", + "upstream_name": "0.133.0-alpha.3", + "upstream_html_url": "https://github.com/openai/codex/releases/tag/rust-v0.133.0-alpha.3", + "upstream_target": "main", + "upstream_release_id": "326321425", + "upstream_prerelease": true, + "release_train": "0.133.0", + "release_branch": "release/0.133.0", + "work_branch": "upstream/rust-v0.133.0", + "patch_branch": "wallentx/termux-target", + "patch_source_sha": "80124321fabd642ef157e775498b2a9dc0b24581", + "termux_tag": "rust-v0.133.0-alpha.3-termux" +} diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 823eb6d3c1a1..38395b2766f6 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -402,7 +402,7 @@ checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "app_test_support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -1778,7 +1778,7 @@ dependencies = [ [[package]] name = "codex-agent-graph-store" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-protocol", @@ -1793,7 +1793,7 @@ dependencies = [ [[package]] name = "codex-agent-identity" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -1812,7 +1812,7 @@ dependencies = [ [[package]] name = "codex-analytics" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-app-server-protocol", "codex-git-utils", @@ -1832,7 +1832,7 @@ dependencies = [ [[package]] name = "codex-ansi-escape" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "ansi-to-tui", "ratatui", @@ -1841,7 +1841,7 @@ dependencies = [ [[package]] name = "codex-api" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_matches", @@ -1875,7 +1875,7 @@ dependencies = [ [[package]] name = "codex-app-server" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "app_test_support", @@ -1960,7 +1960,7 @@ dependencies = [ [[package]] name = "codex-app-server-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-app-server", "codex-app-server-protocol", @@ -1987,7 +1987,7 @@ dependencies = [ [[package]] name = "codex-app-server-daemon" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-app-server-protocol", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "codex-app-server-protocol" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2035,7 +2035,7 @@ dependencies = [ [[package]] name = "codex-app-server-test-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2056,7 +2056,7 @@ dependencies = [ [[package]] name = "codex-app-server-transport" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "axum", @@ -2095,7 +2095,7 @@ dependencies = [ [[package]] name = "codex-apply-patch" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -2114,7 +2114,7 @@ dependencies = [ [[package]] name = "codex-arg0" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-apply-patch", @@ -2123,6 +2123,7 @@ dependencies = [ "codex-sandboxing", "codex-shell-escalation", "codex-utils-absolute-path", + "codex-utils-file-lock", "codex-utils-home-dir", "dotenvy", "tempfile", @@ -2131,7 +2132,7 @@ dependencies = [ [[package]] name = "codex-async-utils" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "pretty_assertions", @@ -2141,7 +2142,7 @@ dependencies = [ [[package]] name = "codex-aws-auth" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "aws-config", "aws-credential-types", @@ -2156,7 +2157,7 @@ dependencies = [ [[package]] name = "codex-backend-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-api", @@ -2173,7 +2174,7 @@ dependencies = [ [[package]] name = "codex-backend-openapi-models" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "serde", "serde_json", @@ -2182,7 +2183,7 @@ dependencies = [ [[package]] name = "codex-bwrap" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "cc", "libc", @@ -2191,7 +2192,7 @@ dependencies = [ [[package]] name = "codex-chatgpt" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2214,7 +2215,7 @@ dependencies = [ [[package]] name = "codex-cli" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -2279,7 +2280,7 @@ dependencies = [ [[package]] name = "codex-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "bytes", @@ -2310,7 +2311,7 @@ dependencies = [ [[package]] name = "codex-cloud-requirements" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "base64 0.22.1", @@ -2336,7 +2337,7 @@ dependencies = [ [[package]] name = "codex-cloud-tasks" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -2368,7 +2369,7 @@ dependencies = [ [[package]] name = "codex-cloud-tasks-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -2383,7 +2384,7 @@ dependencies = [ [[package]] name = "codex-cloud-tasks-mock-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "chrono", @@ -2393,7 +2394,7 @@ dependencies = [ [[package]] name = "codex-code-mode" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-channel", "async-trait", @@ -2410,11 +2411,11 @@ dependencies = [ [[package]] name = "codex-collaboration-mode-templates" -version = "0.0.0" +version = "0.132.0-alpha.1" [[package]] name = "codex-config" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -2461,7 +2462,7 @@ dependencies = [ [[package]] name = "codex-connectors" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-app-server-protocol", @@ -2477,7 +2478,7 @@ dependencies = [ [[package]] name = "codex-core" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "arc-swap", @@ -2531,6 +2532,7 @@ dependencies = [ "codex-utils-absolute-path", "codex-utils-cache", "codex-utils-cargo-bin", + "codex-utils-file-lock", "codex-utils-home-dir", "codex-utils-image", "codex-utils-output-truncation", @@ -2595,7 +2597,7 @@ dependencies = [ [[package]] name = "codex-core-api" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-analytics", "codex-app-server-protocol", @@ -2614,7 +2616,7 @@ dependencies = [ [[package]] name = "codex-core-plugins" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -2653,7 +2655,7 @@ dependencies = [ [[package]] name = "codex-core-skills" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-analytics", @@ -2684,7 +2686,7 @@ dependencies = [ [[package]] name = "codex-debug-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2696,7 +2698,7 @@ dependencies = [ [[package]] name = "codex-exec" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -2742,7 +2744,7 @@ dependencies = [ [[package]] name = "codex-exec-server" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "arc-swap", @@ -2783,11 +2785,12 @@ dependencies = [ [[package]] name = "codex-execpolicy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", "codex-utils-absolute-path", + "codex-utils-file-lock", "multimap", "pretty_assertions", "serde", @@ -2800,7 +2803,7 @@ dependencies = [ [[package]] name = "codex-execpolicy-legacy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "allocative", "anyhow", @@ -2820,7 +2823,7 @@ dependencies = [ [[package]] name = "codex-experimental-api-macros" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "proc-macro2", "quote", @@ -2829,7 +2832,7 @@ dependencies = [ [[package]] name = "codex-extension-api" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-protocol", @@ -2838,7 +2841,7 @@ dependencies = [ [[package]] name = "codex-external-agent-migration" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-hooks", "pretty_assertions", @@ -2850,7 +2853,7 @@ dependencies = [ [[package]] name = "codex-external-agent-sessions" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "chrono", "codex-app-server-protocol", @@ -2864,7 +2867,7 @@ dependencies = [ [[package]] name = "codex-features" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-otel", "codex-protocol", @@ -2877,7 +2880,7 @@ dependencies = [ [[package]] name = "codex-feedback" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-login", @@ -2890,7 +2893,7 @@ dependencies = [ [[package]] name = "codex-file-search" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -2906,7 +2909,7 @@ dependencies = [ [[package]] name = "codex-file-system" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-protocol", @@ -2916,7 +2919,7 @@ dependencies = [ [[package]] name = "codex-file-watcher" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "notify", "pretty_assertions", @@ -2927,7 +2930,7 @@ dependencies = [ [[package]] name = "codex-git-utils" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -2951,7 +2954,7 @@ dependencies = [ [[package]] name = "codex-goal-extension" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -2970,7 +2973,7 @@ dependencies = [ [[package]] name = "codex-guardian" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-core", @@ -2980,7 +2983,7 @@ dependencies = [ [[package]] name = "codex-hooks" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -3003,7 +3006,7 @@ dependencies = [ [[package]] name = "codex-install-context" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-absolute-path", "codex-utils-home-dir", @@ -3013,7 +3016,7 @@ dependencies = [ [[package]] name = "codex-keyring-store" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "keyring", "tracing", @@ -3021,7 +3024,7 @@ dependencies = [ [[package]] name = "codex-linux-sandbox" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "clap", "codex-core", @@ -3045,7 +3048,7 @@ dependencies = [ [[package]] name = "codex-lmstudio" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-core", "codex-model-provider-info", @@ -3059,7 +3062,7 @@ dependencies = [ [[package]] name = "codex-login" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3101,7 +3104,7 @@ dependencies = [ [[package]] name = "codex-mcp" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-channel", @@ -3133,7 +3136,7 @@ dependencies = [ [[package]] name = "codex-mcp-server" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-arg0", @@ -3165,7 +3168,7 @@ dependencies = [ [[package]] name = "codex-memories-extension" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-core", @@ -3186,7 +3189,7 @@ dependencies = [ [[package]] name = "codex-memories-mcp" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-utils-absolute-path", @@ -3203,7 +3206,7 @@ dependencies = [ [[package]] name = "codex-memories-read" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-protocol", "codex-shell-command", @@ -3217,7 +3220,7 @@ dependencies = [ [[package]] name = "codex-memories-write" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -3252,7 +3255,7 @@ dependencies = [ [[package]] name = "codex-message-history" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-config", "pretty_assertions", @@ -3265,7 +3268,7 @@ dependencies = [ [[package]] name = "codex-model-provider" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-agent-identity", @@ -3289,7 +3292,7 @@ dependencies = [ [[package]] name = "codex-model-provider-info" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-api", "codex-app-server-protocol", @@ -3306,7 +3309,7 @@ dependencies = [ [[package]] name = "codex-models-manager" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "chrono", @@ -3327,7 +3330,7 @@ dependencies = [ [[package]] name = "codex-network-proxy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3358,7 +3361,7 @@ dependencies = [ [[package]] name = "codex-ollama" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "assert_matches", "async-stream", @@ -3377,7 +3380,7 @@ dependencies = [ [[package]] name = "codex-otel" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "chrono", "codex-api", @@ -3409,7 +3412,7 @@ dependencies = [ [[package]] name = "codex-plugin" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-config", "codex-utils-absolute-path", @@ -3419,7 +3422,7 @@ dependencies = [ [[package]] name = "codex-process-hardening" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "libc", "pretty_assertions", @@ -3427,7 +3430,7 @@ dependencies = [ [[package]] name = "codex-protocol" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chardetng", @@ -3467,7 +3470,7 @@ dependencies = [ [[package]] name = "codex-realtime-webrtc" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "libwebrtc", "thiserror 2.0.18", @@ -3476,7 +3479,7 @@ dependencies = [ [[package]] name = "codex-response-debug-context" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "base64 0.22.1", "codex-api", @@ -3487,7 +3490,7 @@ dependencies = [ [[package]] name = "codex-responses-api-proxy" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -3504,7 +3507,7 @@ dependencies = [ [[package]] name = "codex-rmcp-client" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "axum", @@ -3542,7 +3545,7 @@ dependencies = [ [[package]] name = "codex-rollout" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3567,7 +3570,7 @@ dependencies = [ [[package]] name = "codex-rollout-trace" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-code-mode", @@ -3583,7 +3586,7 @@ dependencies = [ [[package]] name = "codex-sandboxing" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3604,7 +3607,7 @@ dependencies = [ [[package]] name = "codex-secrets" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "age", "anyhow", @@ -3625,7 +3628,7 @@ dependencies = [ [[package]] name = "codex-shell-command" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -3645,7 +3648,7 @@ dependencies = [ [[package]] name = "codex-shell-escalation" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -3666,7 +3669,7 @@ dependencies = [ [[package]] name = "codex-skills" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-absolute-path", "include_dir", @@ -3675,7 +3678,7 @@ dependencies = [ [[package]] name = "codex-state" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -3698,7 +3701,7 @@ dependencies = [ [[package]] name = "codex-stdio-to-uds" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-uds", @@ -3710,7 +3713,7 @@ dependencies = [ [[package]] name = "codex-terminal-detection" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "tracing", @@ -3718,7 +3721,7 @@ dependencies = [ [[package]] name = "codex-test-binary-support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-arg0", "tempfile", @@ -3726,7 +3729,7 @@ dependencies = [ [[package]] name = "codex-thread-manager-sample" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -3737,7 +3740,7 @@ dependencies = [ [[package]] name = "codex-thread-store" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "chrono", @@ -3758,7 +3761,7 @@ dependencies = [ [[package]] name = "codex-tools" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-trait", "codex-app-server-protocol", @@ -3779,7 +3782,7 @@ dependencies = [ [[package]] name = "codex-tui" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "arboard", @@ -3890,7 +3893,7 @@ dependencies = [ [[package]] name = "codex-uds" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "async-io", "pretty_assertions", @@ -3902,7 +3905,7 @@ dependencies = [ [[package]] name = "codex-utils-absolute-path" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "dirs", "dunce", @@ -3916,14 +3919,14 @@ dependencies = [ [[package]] name = "codex-utils-approval-presets" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-protocol", ] [[package]] name = "codex-utils-cache" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "lru 0.16.3", "sha1", @@ -3932,7 +3935,7 @@ dependencies = [ [[package]] name = "codex-utils-cargo-bin" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "assert_cmd", "runfiles", @@ -3941,7 +3944,7 @@ dependencies = [ [[package]] name = "codex-utils-cli" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "clap", "codex-protocol", @@ -3953,15 +3956,19 @@ dependencies = [ [[package]] name = "codex-utils-elapsed" -version = "0.0.0" +version = "0.132.0-alpha.1" + +[[package]] +name = "codex-utils-file-lock" +version = "0.132.0-alpha.1" [[package]] name = "codex-utils-fuzzy-match" -version = "0.0.0" +version = "0.132.0-alpha.1" [[package]] name = "codex-utils-home-dir" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-absolute-path", "dirs", @@ -3971,7 +3978,7 @@ dependencies = [ [[package]] name = "codex-utils-image" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "base64 0.22.1", "codex-utils-cache", @@ -3983,7 +3990,7 @@ dependencies = [ [[package]] name = "codex-utils-json-to-toml" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "serde_json", @@ -3992,7 +3999,7 @@ dependencies = [ [[package]] name = "codex-utils-oss" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-core", "codex-lmstudio", @@ -4002,7 +4009,7 @@ dependencies = [ [[package]] name = "codex-utils-output-truncation" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-protocol", "codex-utils-string", @@ -4011,7 +4018,7 @@ dependencies = [ [[package]] name = "codex-utils-path" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-utils-absolute-path", "dunce", @@ -4021,7 +4028,7 @@ dependencies = [ [[package]] name = "codex-utils-plugins" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-exec-server", "codex-login", @@ -4034,7 +4041,7 @@ dependencies = [ [[package]] name = "codex-utils-pty" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "filedescriptor", @@ -4050,7 +4057,7 @@ dependencies = [ [[package]] name = "codex-utils-readiness" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "assert_matches", "async-trait", @@ -4061,14 +4068,14 @@ dependencies = [ [[package]] name = "codex-utils-rustls-provider" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "rustls", ] [[package]] name = "codex-utils-sandbox-summary" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "codex-core", "codex-model-provider-info", @@ -4079,7 +4086,7 @@ dependencies = [ [[package]] name = "codex-utils-sleep-inhibitor" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "core-foundation 0.9.4", "libc", @@ -4089,14 +4096,14 @@ dependencies = [ [[package]] name = "codex-utils-stream-parser" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", ] [[package]] name = "codex-utils-string" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "regex-lite", @@ -4106,14 +4113,14 @@ dependencies = [ [[package]] name = "codex-utils-template" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", ] [[package]] name = "codex-v8-poc" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "pretty_assertions", "v8", @@ -4121,7 +4128,7 @@ dependencies = [ [[package]] name = "codex-windows-sandbox" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -4372,7 +4379,7 @@ dependencies = [ [[package]] name = "core_test_support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -5486,7 +5493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8571,7 +8578,7 @@ dependencies = [ [[package]] name = "mcp_test_support" -version = "0.0.0" +version = "0.132.0-alpha.1" dependencies = [ "anyhow", "codex-login", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 9dfb0bf3d316..ce3b943429e7 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -81,6 +81,7 @@ members = [ "v8-poc", "utils/absolute-path", "utils/cargo-bin", + "utils/file-lock", "git-utils", "utils/cache", "utils/image", @@ -219,6 +220,7 @@ codex-utils-cache = { path = "utils/cache" } codex-utils-cargo-bin = { path = "utils/cargo-bin" } codex-utils-cli = { path = "utils/cli" } codex-utils-elapsed = { path = "utils/elapsed" } +codex-utils-file-lock = { path = "utils/file-lock" } codex-utils-fuzzy-match = { path = "utils/fuzzy-match" } codex-utils-home-dir = { path = "utils/home-dir" } codex-utils-image = { path = "utils/image" } diff --git a/codex-rs/arg0/Cargo.toml b/codex-rs/arg0/Cargo.toml index 7ee21a770e49..7c2c17692020 100644 --- a/codex-rs/arg0/Cargo.toml +++ b/codex-rs/arg0/Cargo.toml @@ -20,6 +20,7 @@ codex-linux-sandbox = { workspace = true } codex-sandboxing = { workspace = true } codex-shell-escalation = { workspace = true } codex-utils-absolute-path = { workspace = true } +codex-utils-file-lock = { workspace = true } codex-utils-home-dir = { workspace = true } dotenvy = { workspace = true } tempfile = { workspace = true } diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index 2f6ae4653c65..5a558769a7e5 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -6,6 +6,8 @@ use std::path::PathBuf; use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1; use codex_exec_server::CODEX_FS_HELPER_ARG1; use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; +use codex_utils_file_lock::TryFileLockOutcome; +use codex_utils_file_lock::try_lock_exclusive_optional; use codex_utils_home_dir::find_codex_home; #[cfg(unix)] use std::os::unix::fs::symlink; @@ -33,12 +35,12 @@ pub struct Arg0DispatchPaths { /// Keeps the per-session PATH entry alive and locked for the process lifetime. pub struct Arg0PathEntryGuard { _temp_dir: TempDir, - _lock_file: File, + _lock_file: Option, paths: Arg0DispatchPaths, } impl Arg0PathEntryGuard { - fn new(temp_dir: TempDir, lock_file: File, paths: Arg0DispatchPaths) -> Self { + fn new(temp_dir: TempDir, lock_file: Option, paths: Arg0DispatchPaths) -> Self { Self { _temp_dir: temp_dir, _lock_file: lock_file, @@ -326,7 +328,13 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result Some(lock_file), + TryFileLockOutcome::Unsupported => None, + TryFileLockOutcome::WouldBlock => { + return Err(std::io::Error::from(std::io::ErrorKind::WouldBlock).into()); + } + }; for filename in &[ APPLY_PATCH_ARG0, @@ -445,10 +453,9 @@ fn try_lock_dir(dir: &Path) -> std::io::Result> { Err(err) => return Err(err), }; - match lock_file.try_lock() { - Ok(()) => Ok(Some(lock_file)), - Err(std::fs::TryLockError::WouldBlock) => Ok(None), - Err(err) => Err(err.into()), + match try_lock_exclusive_optional(&lock_file)? { + TryFileLockOutcome::Acquired => Ok(Some(lock_file)), + TryFileLockOutcome::WouldBlock | TryFileLockOutcome::Unsupported => Ok(None), } } @@ -562,7 +569,13 @@ mod tests { let dir = root.path().join("locked"); fs::create_dir(&dir)?; let lock_file = create_lock(&dir)?; - lock_file.try_lock()?; + match try_lock_exclusive_optional(&lock_file)? { + TryFileLockOutcome::Acquired => {} + TryFileLockOutcome::Unsupported => return Ok(()), + TryFileLockOutcome::WouldBlock => { + panic!("newly created lock file should not be locked"); + } + } janitor_cleanup(root.path())?; diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 8472918dcae9..fdf1d4b18a9e 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -63,6 +63,7 @@ codex-thread-store = { workspace = true } codex-tools = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-cache = { workspace = true } +codex-utils-file-lock = { workspace = true } codex-utils-image = { workspace = true } codex-utils-home-dir = { workspace = true } codex-utils-output-truncation = { workspace = true } @@ -125,6 +126,10 @@ openssl-sys = { workspace = true, features = ["vendored"] } [target.aarch64-unknown-linux-musl.dependencies] openssl-sys = { workspace = true, features = ["vendored"] } +# Build OpenSSL from source for Android builds. +[target.aarch64-linux-android.dependencies] +openssl-sys = { workspace = true, features = ["vendored"] } + [target.'cfg(unix)'.dependencies] codex-shell-escalation = { workspace = true } diff --git a/codex-rs/core/src/installation_id.rs b/codex-rs/core/src/installation_id.rs index a42e6b6d8353..a87c660f6132 100644 --- a/codex-rs/core/src/installation_id.rs +++ b/codex-rs/core/src/installation_id.rs @@ -11,6 +11,9 @@ use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::PermissionsExt; use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_file_lock::FileLockOutcome; +use codex_utils_file_lock::acquire_sibling_lock_dir; +use codex_utils_file_lock::lock_exclusive_optional; use tokio::fs; use uuid::Uuid; @@ -29,7 +32,10 @@ pub async fn resolve_installation_id(codex_home: &AbsolutePathBuf) -> Result None, + FileLockOutcome::Unsupported => Some(acquire_sibling_lock_dir(&path)?), + }; #[cfg(unix)] { diff --git a/codex-rs/execpolicy/Cargo.toml b/codex-rs/execpolicy/Cargo.toml index b22226a79e4b..b87af084d7d1 100644 --- a/codex-rs/execpolicy/Cargo.toml +++ b/codex-rs/execpolicy/Cargo.toml @@ -21,6 +21,7 @@ workspace = true anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] } codex-utils-absolute-path = { workspace = true } +codex-utils-file-lock = { workspace = true } multimap = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/codex-rs/execpolicy/src/amend.rs b/codex-rs/execpolicy/src/amend.rs index e25fd1bd9144..1ef36b1ff5d8 100644 --- a/codex-rs/execpolicy/src/amend.rs +++ b/codex-rs/execpolicy/src/amend.rs @@ -6,6 +6,10 @@ use std::io::Write; use std::path::Path; use std::path::PathBuf; +use codex_utils_file_lock::FileLockOutcome; +use codex_utils_file_lock::acquire_sibling_lock_dir; +use codex_utils_file_lock::lock_exclusive_optional; + use crate::decision::Decision; use crate::rule::NetworkRuleProtocol; use crate::rule::normalize_network_rule_host; @@ -154,10 +158,21 @@ fn append_locked_line(policy_path: &Path, line: &str) -> Result<(), AmendError> path: policy_path.to_path_buf(), source, })?; - file.lock().map_err(|source| AmendError::LockPolicyFile { - path: policy_path.to_path_buf(), - source, - })?; + let _lock_dir_guard = + match lock_exclusive_optional(&file).map_err(|source| AmendError::LockPolicyFile { + path: policy_path.to_path_buf(), + source, + })? { + FileLockOutcome::Acquired => None, + FileLockOutcome::Unsupported => { + Some(acquire_sibling_lock_dir(policy_path).map_err(|source| { + AmendError::LockPolicyFile { + path: policy_path.to_path_buf(), + source, + } + })?) + } + }; file.seek(SeekFrom::Start(0)) .map_err(|source| AmendError::SeekPolicyFile { diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index c213e92bae41..40624baba0d0 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -126,7 +126,7 @@ uuid = { workspace = true } codex-windows-sandbox = { workspace = true } tokio-util = { workspace = true, features = ["time"] } -[target.'cfg(not(target_os = "linux"))'.dependencies] +[target.'cfg(all(not(target_os = "linux"), not(target_os = "android")))'.dependencies] cpal = "0.15" [target.'cfg(unix)'.dependencies] diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 40cd121c7135..8a71fd738e1e 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -89,9 +89,9 @@ mod app_server_approval_conversions; mod app_server_session; mod approval_events; mod ascii_animation; -#[cfg(not(target_os = "linux"))] +#[cfg(not(any(target_os = "linux", target_os = "android")))] mod audio_device; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] #[allow(dead_code)] mod audio_device { use crate::app_event::RealtimeAudioDeviceKind; @@ -195,11 +195,11 @@ mod update_prompt; mod update_versions; mod updates; mod version; -#[cfg(not(target_os = "linux"))] +#[cfg(not(any(target_os = "linux", target_os = "android")))] mod voice; mod width; mod workspace_command; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] #[allow(dead_code)] mod voice { use crate::app_event_sender::AppEventSender; diff --git a/codex-rs/utils/file-lock/BUILD.bazel b/codex-rs/utils/file-lock/BUILD.bazel new file mode 100644 index 000000000000..face70c53518 --- /dev/null +++ b/codex-rs/utils/file-lock/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "file-lock", + crate_name = "codex_utils_file_lock", +) diff --git a/codex-rs/utils/file-lock/Cargo.toml b/codex-rs/utils/file-lock/Cargo.toml new file mode 100644 index 000000000000..5e3877fc1630 --- /dev/null +++ b/codex-rs/utils/file-lock/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "codex-utils-file-lock" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] diff --git a/codex-rs/utils/file-lock/src/lib.rs b/codex-rs/utils/file-lock/src/lib.rs new file mode 100644 index 000000000000..fdcfbdb81e75 --- /dev/null +++ b/codex-rs/utils/file-lock/src/lib.rs @@ -0,0 +1,168 @@ +use std::fs::File; +use std::fs::create_dir; +use std::fs::remove_dir; +use std::io; +use std::path::Path; +use std::path::PathBuf; +use std::thread; +use std::time::Duration; + +const LOCK_DIR_RETRY_SLEEP: Duration = Duration::from_millis(100); + +/// Result of acquiring a blocking advisory file lock. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileLockOutcome { + Acquired, + Unsupported, +} + +/// Result of attempting to acquire a non-blocking advisory file lock. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TryFileLockOutcome { + Acquired, + WouldBlock, + Unsupported, +} + +/// Result of attempting to acquire a non-blocking lock directory. +#[derive(Debug, PartialEq, Eq)] +pub enum TryLockDirOutcome { + Acquired(LockDirGuard), + WouldBlock, +} + +/// Guard for a sibling lock directory created with an atomic `mkdir`. +#[derive(Debug, PartialEq, Eq)] +pub struct LockDirGuard { + path: PathBuf, +} + +impl Drop for LockDirGuard { + fn drop(&mut self) { + let _ = remove_dir(&self.path); + } +} + +/// Acquires an exclusive advisory file lock, treating unsupported file locking +/// as a distinct outcome for platforms such as Termux. +pub fn lock_exclusive_optional(file: &File) -> io::Result { + match file.lock() { + Ok(()) => Ok(FileLockOutcome::Acquired), + Err(err) if err.kind() == io::ErrorKind::Unsupported => Ok(FileLockOutcome::Unsupported), + Err(err) => Err(err), + } +} + +/// Attempts to acquire an exclusive advisory file lock without blocking, +/// preserving `WouldBlock` and unsupported file locking as distinct outcomes. +pub fn try_lock_exclusive_optional(file: &File) -> io::Result { + match file.try_lock() { + Ok(()) => Ok(TryFileLockOutcome::Acquired), + Err(std::fs::TryLockError::WouldBlock) => Ok(TryFileLockOutcome::WouldBlock), + Err(std::fs::TryLockError::Error(err)) if err.kind() == io::ErrorKind::Unsupported => { + Ok(TryFileLockOutcome::Unsupported) + } + Err(std::fs::TryLockError::Error(err)) => Err(err), + } +} + +/// Returns the sibling directory path used as a fallback lock for `path`. +pub fn sibling_lock_dir(path: &Path) -> PathBuf { + let Some(file_name) = path.file_name() else { + return path.with_file_name(".lock"); + }; + + let mut lock_name = file_name.to_os_string(); + lock_name.push(".lock"); + path.with_file_name(lock_name) +} + +/// Acquires a sibling lock directory, blocking until it is available. +pub fn acquire_sibling_lock_dir(path: &Path) -> io::Result { + loop { + match try_acquire_sibling_lock_dir(path)? { + TryLockDirOutcome::Acquired(guard) => return Ok(guard), + TryLockDirOutcome::WouldBlock => thread::sleep(LOCK_DIR_RETRY_SLEEP), + } + } +} + +/// Attempts to acquire a sibling lock directory without blocking. +pub fn try_acquire_sibling_lock_dir(path: &Path) -> io::Result { + let lock_dir = sibling_lock_dir(path); + match create_dir(&lock_dir) { + Ok(()) => Ok(TryLockDirOutcome::Acquired(LockDirGuard { path: lock_dir })), + Err(err) if err.kind() == io::ErrorKind::AlreadyExists => Ok(TryLockDirOutcome::WouldBlock), + Err(err) => Err(err), + } +} + +/// Attempts to acquire a shared advisory file lock without blocking, +/// preserving `WouldBlock` and unsupported file locking as distinct outcomes. +pub fn try_lock_shared_optional(file: &File) -> io::Result { + match file.try_lock_shared() { + Ok(()) => Ok(TryFileLockOutcome::Acquired), + Err(std::fs::TryLockError::WouldBlock) => Ok(TryFileLockOutcome::WouldBlock), + Err(std::fs::TryLockError::Error(err)) if err.kind() == io::ErrorKind::Unsupported => { + Ok(TryFileLockOutcome::Unsupported) + } + Err(std::fs::TryLockError::Error(err)) => Err(err), + } +} + +#[cfg(test)] +mod tests { + use super::TryLockDirOutcome; + use super::sibling_lock_dir; + use super::try_acquire_sibling_lock_dir; + use std::fs; + use std::path::PathBuf; + use std::time::SystemTime; + + fn unique_temp_file_path(name: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("system clock should be after Unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "codex-file-lock-{name}-{}-{nanos}", + std::process::id() + )) + } + + #[test] + fn sibling_lock_dir_appends_lock_suffix() { + let path = PathBuf::from("/tmp/history.jsonl"); + + assert_eq!( + sibling_lock_dir(&path), + PathBuf::from("/tmp/history.jsonl.lock") + ); + } + + #[test] + fn try_acquire_sibling_lock_dir_is_exclusive_until_drop() { + let path = unique_temp_file_path("exclusive"); + let lock_dir = sibling_lock_dir(&path); + let _ = fs::remove_dir_all(&lock_dir); + + let guard = match try_acquire_sibling_lock_dir(&path).expect("acquire lock dir") { + TryLockDirOutcome::Acquired(guard) => guard, + TryLockDirOutcome::WouldBlock => panic!("first lock attempt should acquire"), + }; + assert!(lock_dir.is_dir()); + + assert!(matches!( + try_acquire_sibling_lock_dir(&path).expect("try acquire held lock dir"), + TryLockDirOutcome::WouldBlock + )); + + drop(guard); + assert!(!lock_dir.exists()); + + let reacquired = try_acquire_sibling_lock_dir(&path).expect("reacquire lock dir"); + assert!(matches!(reacquired, TryLockDirOutcome::Acquired(_))); + + let _ = fs::remove_dir_all(lock_dir); + } +} diff --git a/justfile b/justfile index ab2fbc63629a..569a04695216 100644 --- a/justfile +++ b/justfile @@ -116,6 +116,11 @@ argument-comment-lint *args: argument-comment-lint-from-source *args: {{ justfile_directory() }}/tools/argument-comment-lint/run.py "$@" +# Audit advisory file locks that may need Termux Unsupported handling. +[no-cd] +termux-lock-audit *args: + {{ justfile_directory() }}/scripts/termux-lock-audit.sh "$@" + # Tail logs from the state SQLite database log *args: if [ "${1:-}" = "--" ]; then shift; fi; cargo run -p codex-state --bin logs_client -- "$@" diff --git a/scripts/termux-lock-audit.sh b/scripts/termux-lock-audit.sh new file mode 100755 index 000000000000..ac40f8e57246 --- /dev/null +++ b/scripts/termux-lock-audit.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: scripts/termux-lock-audit.sh [--strict] + +Audits Rust advisory file-lock usage that may need Termux compatibility handling. + +By default this script prints findings and exits successfully. With --strict, it +exits non-zero when candidate file-lock calls are found in files that do not also +mention Unsupported/TryLockError handling. +USAGE +} + +strict=false +for arg in "$@"; do + case "$arg" in + --strict) + strict=true + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "unknown argument: $arg" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if ! command -v rg >/dev/null 2>&1; then + echo "error: rg is required for this audit" >&2 + exit 1 +fi + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +unsupported_pattern='std::fs::TryLockError|fs::TryLockError|TryLockError::|ErrorKind::Unsupported|std::io::ErrorKind::Unsupported' +lock_context_pattern='(\.lock\(\)|\.try_lock\(|\.try_lock_shared\(|TryLockError|ErrorKind::Unsupported)' +candidate_file_lock_pattern='\b([A-Za-z_][A-Za-z0-9_]*_file|file)\.(lock|try_lock|try_lock_shared)\(' +candidate_false_positive_pattern='poisoned' +helper_pattern='codex_utils_file_lock|FileLockOutcome|TryFileLockOutcome|lock_exclusive_optional|try_lock_exclusive_optional|try_lock_shared_optional' + +print_section() { + printf '\n== %s ==\n' "$1" +} + +print_section "Unsupported-aware lock handling" +echo "These files already mention Unsupported/TryLockError and are likely patched or intentionally reviewed:" + +unsupported_count=0 +while IFS= read -r file; do + if rg -q "$lock_context_pattern" "$file"; then + rg -n -H "$lock_context_pattern" "$file" + unsupported_count=$((unsupported_count + 1)) + fi +done < <(rg -l "$unsupported_pattern" codex-rs -g '*.rs' || true) + +if [ "$unsupported_count" -eq 0 ]; then + echo "No unsupported-aware lock handling found." +fi + +print_section "Optional file-lock helper usage" +echo "These files use the shared optional advisory file-lock helper:" + +helper_count=0 +while IFS= read -r file; do + rg -n -H "$helper_pattern" "$file" + helper_count=$((helper_count + 1)) +done < <(rg -l "$helper_pattern" codex-rs -g '*.rs' || true) + +if [ "$helper_count" -eq 0 ]; then + echo "No optional file-lock helper usage found." +fi + +print_section "Candidate file-lock calls for manual review" +echo "These receiver names may be std::fs::File advisory locks, but the file does not mention Unsupported/TryLockError handling." +echo "This section can include false positives, such as mutexes wrapping a file." + +review_count=0 +while IFS= read -r file; do + if ! rg -q "$unsupported_pattern" "$file"; then + matches="$(rg -n -H "$candidate_file_lock_pattern" "$file" | rg -v "$candidate_false_positive_pattern" || true)" + if [ -n "$matches" ]; then + echo "$matches" + review_count=$((review_count + 1)) + fi + fi +done < <(rg -l "$candidate_file_lock_pattern" codex-rs -g '*.rs' || true) + +if [ "$review_count" -eq 0 ]; then + echo "No unpatched candidate file-lock calls found." +fi + +print_section "Candidate file-lock calls already in unsupported-aware files" +echo "These are candidate file-lock calls in files that already mention Unsupported/TryLockError handling:" + +aware_candidate_count=0 +while IFS= read -r file; do + if rg -q "$unsupported_pattern" "$file"; then + matches="$(rg -n -H "$candidate_file_lock_pattern" "$file" | rg -v "$candidate_false_positive_pattern" || true)" + if [ -n "$matches" ]; then + echo "$matches" + aware_candidate_count=$((aware_candidate_count + 1)) + fi + fi +done < <(rg -l "$candidate_file_lock_pattern" codex-rs -g '*.rs' || true) + +if [ "$aware_candidate_count" -eq 0 ]; then + echo "No unsupported-aware candidate file-lock calls found." +fi + +print_section "Summary" +echo "unsupported-aware files: $unsupported_count" +echo "optional helper files: $helper_count" +echo "review candidate files: $review_count" +echo "unsupported-aware candidate files: $aware_candidate_count" + +if [ "$strict" = true ] && [ "$review_count" -gt 0 ]; then + echo "strict mode: manual-review candidate files were found" >&2 + exit 1 +fi From 9474e5cfc4494b0ba319352aa86ce436c59e65c8 Mon Sep 17 00:00:00 2001 From: Shijie Rao Date: Thu, 21 May 2026 08:27:49 -0700 Subject: [PATCH 14/25] ## New Features - Goals are now enabled by default, backed by dedicated storage, and track progress across active turns. (#23300, #23685, #23696, #23732) - `codex remote-control` now runs like a foreground command, waits for readiness, reports machine status, and keeps explicit daemon-style `start`/`stop` commands. (#22878) - Permission profiles gained list APIs, inheritance, managed `requirements.toml` support, runtime refresh behavior, and stronger Windows sandbox integration. (#22928, #23412, #22270, #23433, #22931, #23715) - Plugin discovery is easier to inspect, with marketplace-aware list output, installed versions, visible marketplace roots, and remote collection support. (#23372, #23584, #23727, #23730) - Extensions can observe more lifecycle events, including subagent start/stop, tool execution, turn metadata, and async approval/turn processing. (#22782, #22873, #23309, #23688, #23690, #23692) ## Bug Fixes - Fixed TUI startup choosing the wrong working directory when reusing a local app-server socket. (#23538) - Fixed plan-mode free-form answers so modified Enter keys, like Shift+Enter, no longer submit unexpectedly. (#23536) - Removed stale background terminal poll events after a process exits. (#23231) - Preserved raw code-mode exec output unless an explicit output token limit is requested. (#23564) - Made AGENTS instruction loading more reliable, including local global reads and warnings for invalid UTF-8 instead of silent drops. (#23343, #23232) - Fixed app-server startup/shutdown races, empty resume/fork paths, plugin upgrade failures, and realtime v1 websocket compatibility. (#23516, #23578, #23400, #23356, #23771) ## Documentation - Added clearer plugin-creator guidance for updating and reinstalling local personal plugins. (#23542) - Expanded app-server/API docs and schema coverage around managed permission profile requirements. (#23433, #23555) ## Chores - Added a canonical Codex package archive pipeline and moved installers, npm packages, DotSlash, and SDK runtimes toward that shared layout. (#23513, #23582, #23586, #23596, #23635, #23636, #23637, #23638, #23786) - Fixed Linux Python runtime wheel tags so glibc-based systems can install the runtime artifacts. (#21812) - Improved release and CI reliability with package-builder tests, prebuilt resource packaging, DotSlash zstd handling, platform-sharded Rust tests, and Codex Linux release runners. (#23760, #23759, #23752, #23358, #23761) ## Changelog Full Changelog: https://github.com/openai/codex/compare/rust-v0.132.0...rust-v0.133.0 - #23343 codex: route global AGENTS reads through LOCAL_FS @starr-openai - #22380 fix: default unknown tool schemas to empty schemas @celia-oai - #23309 Add tool lifecycle extension contributor @jif-oai - #23253 Reduce rust-ci-full Windows nextest timeout flakes @starr-openai - #22878 Improve `codex remote-control` CLI UX @owenlin0 - #21812 Publish Linux runtime wheels with glibc-compatible tags @aibrahim-oai - #22709 [codex] Trim unused TurnContextItem fields @pakrym-oai - #23353 Include plugin id in plugin MCP tool metadata @mzeng-openai - #22728 [codex] Move pending input into input queue @pakrym-oai - #23371 fix(tui): warn on unsupported iTerm2 pet versions @fcoury-oai - #23376 [codex-analytics] preserve user thread source for exec threads @marksteinbrick-oai - #23360 app-server: use profile ids in v2 permission params @bolinfest - #23384 [codex] Remove external websocket session resets @pakrym-oai - #22721 cleanup: Remove skill env var dependency prompting @xl-openai - #23389 Remove ToolSearch feature toggle @sayan-oai - #23080 [1 of 7] Add thread settings to UserInput @etraut-openai - #23081 [2 of 7] Remove UserInputWithTurnContext @etraut-openai - #23075 [3 of 7] Remove UserTurn @etraut-openai - #23396 [codex] Extract turn skill and plugin injections @pakrym-oai - #23356 fix(plugins): keep version upgrades additive @iceweasel-oai - #22508 [5 of 7] Replace OverrideTurnContext with ThreadSettings @etraut-openai - #22086 CI: Customize v8 building @cconger - #23390 Remove explicit connector tool undeferral @sayan-oai - #22928 core: expose permission profile picker metadata @viyatb-oai - #23352 Preserve context baselines for full-history agent forks @jif-oai - #23300 feat: dedicated goal DB @jif-oai - #22835 Remove ToolsConfig from tool planning @jif-oai - #22870 Add `body_after_prefix` auto-compact token limit scope @jif-oai - #23144 Defer v1 multi-agent tools behind tool search @jif-oai - #23409 [codex] Allow empty turn/start requests @pakrym-oai - #23388 [codex] Move hook request plumbing into hook runtime @pakrym-oai - #23405 [codex] Preserve steer input as user input @pakrym-oai - #22914 [2 of 4] tui: route app and skill enablement through app server @etraut-openai - #23397 [codex] Make contextual user fragments dyn-renderable @pakrym-oai - #23475 chore: namespace v1 sub-agent tools @jif-oai - #23493 Make `deny` canonical for filesystem permission entries @viyatb-oai - #22929 Harden CLI rate limit window labels @ase-openai - #22782 Add SubagentStart hook @abhinav-oai - #23513 build: add Codex package builder @bolinfest - #23369 Make local environment optional in EnvironmentManager @starr-openai - #23327 Refactor exec-server websocket pump @starr-openai - #23536 fix(tui): preserve modified enter in plan questions @fcoury-oai - #23400 Fix empty rollout path app-server handling @wiltzius-openai - #23551 Route local-only app-server gating through processors @starr-openai - #23372 Split plugin install discovery into list and request tools @mzeng-openai - #23516 fix: serialize unix app-server startup @efrazer-oai - #22169 [codex] Honor role-defined spawn service tiers @aibrahim-oai - #23555 Add CUA requirements subsection for locked computer use @adams-oai - #23538 Fix: TUI starting in wrong CWD @canvrno-oai - #23526 build: fetch rg for Codex packages @bolinfest - #23573 Remove unused ARC monitor path @mzeng-openai - #23576 test: fix multi-agent service tier assertion @bolinfest - #23541 build: default Codex package target and output @bolinfest - #23358 Fan out rust-ci-full nextest by platform @starr-openai - #23593 feat: expose codex-app-server version flag @bolinfest - #23412 feat: add permission profile list api @viyatb-oai - #23535 Move plugin and skill warmup into session startup @aibrahim-oai - #23231 Fix stale background terminal poll events @etraut-openai - #23564 [codex] Preserve raw code-mode exec output by default @aibrahim-oai - #23232 Warn on invalid UTF-8 in AGENTS.md files @etraut-openai - #23584 feat: Add vertical remote plugin collection support @xl-openai - #23586 build: package prebuilt Codex entrypoints @bolinfest - #23582 ci: build Codex package archives in release workflow @bolinfest - #23596 runtime: detect Codex package layout @bolinfest - #23500 add encryptedcontent to functioncalloutput @sayan-oai - #23633 Migrate exec-server remote registration to environments @richardopenai - #23451 Add timeout for remote compaction requests @jif-oai - #23667 feat: rename 1 @jif-oai - #23669 feat: rename 3 @jif-oai - #23668 feat: rename 2 @jif-oai - #23675 fix: main @jif-oai - #23685 feat: wire goal extension tools to the dedicated goal store @jif-oai - #23690 feat: async approval contrib @jif-oai - #23692 feat: async turn item process @jif-oai - #23688 feat: expose turn-start metadata to extensions @jif-oai - #23605 [codex] Hide deferred tools from code mode prompt @pakrym-oai - #23634 runtime: use install context for bundled bwrap @bolinfest - #23635 release: publish Codex package archive checksums @bolinfest - #23592 feat: Add btw alias for side slash command @anp-oai - #23696 feat: account active goal progress in the goal extension @jif-oai - #23176 [2 of 2] Start fresh TUI thread in background @etraut-openai - #23578 fix(app-server): speed up shutdown @fcoury-oai - #22896 windows-sandbox: add resolved permissions helper @bolinfest - #23502 Add thread/settings/update app-server API @etraut-openai - #23507 Sync TUI thread settings through app server @etraut-openai - #23666 feat: add turn_id and truncation_policy to extension tool calls @jif-oai - #23636 install: consume Codex package archives @bolinfest - #23717 [codex] Preserve failed goal accounting flushes @jif-oai - #23655 add standalone websearch api client @sayan-oai - #23724 Fix thread settings clippy failure @etraut-openai - #23637 npm: ship platform packages in Codex package layout @bolinfest - #23729 fix(config): resolve cloud requirements deny-read globs @viyatb-oai - #23638 dotslash: publish Codex entrypoints from package archives @bolinfest - #22918 windows-sandbox: send permission profiles to elevated runner @bolinfest - #23735 windows-sandbox: share bundled helper lookup @bolinfest - #18868 Add MITM hook config model @evawong-oai - #22270 feat(permissions): resolve permission profile inheritance @viyatb-oai - #23719 cli: add strict config to exec-server @bolinfest - #23542 [skills] Create a personal update flow for plugin creator @caseychow-oai - #21272 Support compact SessionStart hooks @abhinav-oai - #20659 Wire MITM hooks into runtime enforcement @evawong-oai - #23752 release: use DotSlash zstd for package archives @bolinfest - #22923 windows-sandbox: drive write roots from resolved permissions @bolinfest - #23761 chore: use Codex Linux runners for Rust releases @bolinfest - #23759 release: package prebuilt resource binaries @bolinfest - #23167 windows-sandbox: feed setup from resolved permissions @bolinfest - #22931 core: refresh active permission profiles at runtime @viyatb-oai - #22873 Add SubagentStop hook @abhinav-oai - #23727 feat(plugins): tabulate plugin list output @caseychow-oai - #23732 Make goals feature on by default and no longer experimental @etraut-openai - #23537 Honor client-resolved service tier defaults @shijie-oai - #23771 [codex] Fix realtime v1 websocket compatibility @guinness-oai - #23764 Remove Windows sandbox resource stamping @iceweasel-oai - #23730 [codex] List marketplaces considered by plugin discovery @caseychow-oai - #23760 ci: run Codex package builder tests @bolinfest - #23737 [codex] Add plugin id to MCP tool call items @mzeng-openai - #18240 Use named MITM permissions config @evawong-oai - #23774 [codex] Reject read-only fallback with approvals disabled @viyatb-oai - #23714 windows-sandbox: add profile-native elevated APIs @bolinfest - #23433 feat: support managed permission profiles in requirements.toml @viyatb-oai - #23715 core: pass permission profiles to Windows runner @bolinfest - #23786 sdk: launch packaged Codex runtimes @bolinfest --- codex-rs/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index ec38a87cff97..1808715c0839 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -117,7 +117,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.0.0" +version = "0.133.0" # Track the edition for all workspace crates in one place. Individual # crates can still override this value, but keeping it here means new # crates created with `cargo new -w ...` automatically inherit the 2024 From 5382ed7a2d473560b54edace538c7f03376fe832 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 19:01:37 +0000 Subject: [PATCH 15/25] Seed Termux release automation --- .github/workflows/rust-release.yml | 1808 +++++++---------- .github/workflows/shell-tool-mcp.yml | 461 +++++ .../workflows/termux-release-checkpoint.yml | 103 + .github/workflows/termux-release-deploy.yml | 243 +++ .github/workflows/termux-release-promote.yml | 135 ++ scripts/termux-configure-git.sh | 40 + scripts/termux-create-checkpoint-pr.sh | 275 +++ ...ermux-create-or-update-mirrored-release.sh | 111 + scripts/termux-download-release-artifact.sh | 78 + scripts/termux-find-release-pr.sh | 53 + scripts/termux-read-release-metadata.sh | 77 + scripts/termux-release-asset-state.sh | 37 + scripts/termux-release-paths.sh | 55 + scripts/termux-resolve-release-ref.sh | 36 + scripts/termux-validate-gh-env.sh | 16 + 15 files changed, 2445 insertions(+), 1083 deletions(-) create mode 100644 .github/workflows/shell-tool-mcp.yml create mode 100644 .github/workflows/termux-release-checkpoint.yml create mode 100644 .github/workflows/termux-release-deploy.yml create mode 100644 .github/workflows/termux-release-promote.yml create mode 100755 scripts/termux-configure-git.sh create mode 100755 scripts/termux-create-checkpoint-pr.sh create mode 100755 scripts/termux-create-or-update-mirrored-release.sh create mode 100755 scripts/termux-download-release-artifact.sh create mode 100755 scripts/termux-find-release-pr.sh create mode 100755 scripts/termux-read-release-metadata.sh create mode 100755 scripts/termux-release-asset-state.sh create mode 100755 scripts/termux-release-paths.sh create mode 100755 scripts/termux-resolve-release-ref.sh create mode 100755 scripts/termux-validate-gh-env.sh diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 4f10efa9dcf5..56d0977e6e67 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -4,46 +4,49 @@ # git tag -a rust-v0.1.0 -m "Release 0.1.0" # git push origin rust-v0.1.0 # ``` -# -# To use external macOS signing, manually dispatch `release_mode=build_unsigned`, -# sign the unsigned macOS artifacts in a secure enclave, upload the signed handoff -# archive as a GitHub Release asset, then manually dispatch -# `release_mode=promote_signed` with `unsigned_run_id` and `signed_macos_asset`. -# The signed handoff archive should contain target or artifact directories such -# as `aarch64-apple-darwin/` with signed binaries. name: rust-release on: - push: - tags: - - "rust-v*.*.*" + pull_request: + branches: + - "release/**" workflow_dispatch: inputs: - release_mode: - description: "build_unsigned creates unsigned macOS handoff artifacts; promote_signed finishes a release from signed macOS handoff artifacts." + source_ref: + description: "Branch, tag, or commit SHA to build (code only; keeps .github from dispatched ref)" required: false + type: string + default: "" + build_target: + description: "Target to build" + required: true type: choice - default: build_unsigned + default: all options: - - build_unsigned - - promote_signed - sign_macos: - description: "Deprecated compatibility input; use release_mode instead." - required: false - type: boolean - default: false - unsigned_run_id: - description: "For promote_signed: workflow run id from the build_unsigned run." - required: false - type: string - signed_macos_asset: - description: "For promote_signed: exact GitHub Release asset name containing signed macOS handoff artifacts." - required: false - type: string - signed_macos_sha256: - description: "For promote_signed: optional SHA-256 of signed_macos_asset." - required: false - type: string + - all + - aarch64-apple-darwin + - x86_64-apple-darwin + - x86_64-unknown-linux-musl + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-musl + - aarch64-unknown-linux-gnu + - aarch64-linux-android + - x86_64-pc-windows-msvc + - aarch64-pc-windows-msvc + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read concurrency: group: ${{ github.workflow }} @@ -51,72 +54,26 @@ concurrency: jobs: tag-check: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - uses: dtolnay/rust-toolchain@1.92 - name: Validate tag matches Cargo.toml version shell: bash - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - REQUESTED_SIGN_MACOS: ${{ inputs.sign_macos }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} run: | set -euo pipefail echo "::group::Tag validation" - case "${RELEASE_MODE}" in - signed) - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - echo "❌ Manual rust-release runs must use release_mode=build_unsigned or release_mode=promote_signed" - exit 1 - fi - ;; - build_unsigned) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=build_unsigned is only valid for manual runs" - exit 1 - fi - ;; - promote_signed) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=promote_signed is only valid for manual runs" - exit 1 - fi - if [[ ! "${UNSIGNED_RUN_ID}" =~ ^[0-9]+$ ]]; then - echo "❌ release_mode=promote_signed requires unsigned_run_id to be a workflow run id" - exit 1 - fi - if [[ -z "${SIGNED_MACOS_ASSET}" ]]; then - echo "❌ release_mode=promote_signed requires signed_macos_asset" - exit 1 - fi - if [[ "${SIGNED_MACOS_ASSET}" == */* || "${SIGNED_MACOS_ASSET}" == *"*"* || "${SIGNED_MACOS_ASSET}" == *"?"* || "${SIGNED_MACOS_ASSET}" == *"["* ]]; then - echo "❌ signed_macos_asset must be an exact release asset name, not a path or glob" - exit 1 - fi - if [[ "${UNSIGNED_RUN_ID}" == "${GITHUB_RUN_ID}" ]]; then - echo "❌ unsigned_run_id must refer to the earlier build_unsigned run, not this run" - exit 1 - fi - ;; - *) - echo "❌ Unknown release_mode '${RELEASE_MODE}'" - exit 1 - ;; - esac - - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${REQUESTED_SIGN_MACOS}" == "true" ]]; then - echo "::warning title=Deprecated sign_macos input ignored::Use release_mode=build_unsigned or release_mode=promote_signed instead." - fi - # 1. Must be a tag and match the regex [[ "${GITHUB_REF_TYPE}" == "tag" ]] \ || { echo "❌ Not a tag push"; exit 1; } - [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \ + [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?(-termux)?$ ]] \ || { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; } # 2. Extract versions @@ -131,115 +88,198 @@ jobs: echo "✅ Tag and Cargo.toml agree (${tag_ver})" echo "::endgroup::" - build: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} + select-build-matrix: + if: always() && (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') needs: tag-check - name: Build - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: ${{ matrix.runs_on || matrix.runner }} - # Release builds can take a long time, so leave some headroom to avoid - # having to restart the full workflow due to a timeout. - timeout-minutes: 90 - permissions: - contents: read - id-token: write + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.select.outputs.matrix }} + build_version: ${{ steps.select.outputs.build_version }} + steps: + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - id: select + shell: bash + env: + REPO_OWNER: ${{ github.repository_owner }} + EVENT_NAME: ${{ github.event_name }} + BUILD_TARGET: ${{ inputs.build_target }} + run: | + set -euo pipefail + + matrix='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04","target":"aarch64-linux-android"}, + {"runner":"windows-latest","target":"x86_64-pc-windows-msvc"}, + {"runner":"windows-11-arm","target":"aarch64-pc-windows-msvc"} + ]' + + if [[ "${REPO_OWNER}" != "openai" ]]; then + matrix="$(jq -c '[.[] | select(.runner | startswith("macos") | not)]' <<< "${matrix}")" + fi + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + matrix="$(jq -c '[.[] | select(.target == "aarch64-linux-android")]' <<< "${matrix}")" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" && -n "${BUILD_TARGET}" && "${BUILD_TARGET}" != "all" ]]; then + matrix="$(jq -c --arg build_target "${BUILD_TARGET}" '[.[] | select(.target == $build_target)]' <<< "${matrix}")" + fi + + if [[ "$(jq 'length' <<< "${matrix}")" -eq 0 ]]; then + echo "No build targets selected after applying owner/dispatch filters." >&2 + exit 1 + fi + + echo "matrix={\"include\":${matrix}}" >> "$GITHUB_OUTPUT" + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + metadata=".github/termux-release.json" + if [[ ! -f "${metadata}" ]]; then + echo "${metadata} is required for release train PR builds" >&2 + exit 1 + fi + upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" + if [[ -z "${upstream_tag}" || "${upstream_tag}" != rust-v* ]]; then + echo "Unable to determine upstream rust tag from ${metadata}" >&2 + exit 1 + fi + build_version="${upstream_tag#rust-v}" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then + latest_release_tag="$( + GH_TOKEN="${{ github.token }}" gh api repos/openai/codex/releases/latest --jq '.tag_name' + )" + + if [[ -z "${latest_release_tag}" || "${latest_release_tag}" != rust-v* ]]; then + echo "Unable to determine latest stable release tag from openai/codex" >&2 + exit 1 + fi + + latest_release_version="${latest_release_tag#rust-v}" + build_version="${latest_release_version}-dev" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + fi + + build: + if: >- + always() && + (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') && + needs.select-build-matrix.result == 'success' + needs: + - tag-check + - select-build-matrix + name: Build - ${{ matrix.runner }} - ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + permissions: write-all + timeout-minutes: 120 defaults: run: working-directory: codex-rs env: - # 2026-03-04: temporarily change releases to use thin LTO because - # Ubuntu ARM is timing out at 60 minutes. - CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' }} + CODEX_BWRAP_ENABLE_FFI: ${{ contains(matrix.target, 'unknown-linux') && '1' || '0' }} + CODEX_BUILD_VERSION: ${{ needs.select-build-matrix.outputs.build_version }} + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" strategy: fail-fast: false - matrix: - include: - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - # Release artifacts intentionally ship MUSL-linked Linux binaries. - - runner: codex-linux-x64-xl - target: x86_64-unknown-linux-musl - bundle: primary - artifact_name: x86_64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: codex-linux-x64-xl - target: x86_64-unknown-linux-musl - bundle: app-server - artifact_name: x86_64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: codex-linux-arm64 - target: aarch64-unknown-linux-musl - bundle: primary - artifact_name: aarch64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: codex-linux-arm64 - target: aarch64-unknown-linux-musl - bundle: app-server - artifact_name: aarch64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" + matrix: ${{ fromJson(needs.select-build-matrix.outputs.matrix) }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + with: + verbose: true + - name: Checkout selected source ref + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + uses: actions/checkout@v6 with: - persist-credentials: false - - name: Print runner specs (Linux) - if: ${{ runner.os == 'Linux' }} + ref: ${{ inputs.source_ref }} + path: __source__ + fetch-depth: 1 + - name: Overlay selected source ref (excluding .github) + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + shell: bash + working-directory: ${{ github.workspace }} + run: | + set -euo pipefail + echo "Using source ref: ${{ inputs.source_ref }}" + + # Keep workflow/actions from the dispatch ref while replacing all other files. + find . -mindepth 1 -maxdepth 1 \ + ! -name .git \ + ! -name .github \ + ! -name __source__ \ + -exec rm -rf -- {} + + tar --exclude='.github' -C __source__ -cf - . | tar -xf - + rm -rf __source__ + + - name: Apply selected build version + if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} shell: bash + working-directory: ${{ github.workspace }} run: | set -euo pipefail - cpu_model="$(lscpu | awk -F: '/Model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}')" - total_ram="$(awk '/MemTotal/ {printf "%.1f GiB\n", $2 / 1024 / 1024}' /proc/meminfo)" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(uname -a)" - echo "CPU model: ${cpu_model}" - echo "Logical CPUs: $(nproc)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . - - name: Print runner specs (macOS) - if: ${{ runner.os == 'macOS' }} + if [[ -z "${CODEX_BUILD_VERSION}" ]]; then + echo "CODEX_BUILD_VERSION is empty for workflow_dispatch" >&2 + exit 1 + fi + echo "Using build version: ${CODEX_BUILD_VERSION}" + BUILD_VERSION="${CODEX_BUILD_VERSION}" perl -0777 -i -pe \ + 's/(\[workspace\.package\][^\[]*?version = ")([^"]+)(")/${1}$ENV{BUILD_VERSION}$3/s' \ + codex-rs/Cargo.toml + + grep -n "^\[workspace.package\]\|^version = " codex-rs/Cargo.toml | head -n 4 + + - name: Select Android NDK + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail - total_ram="$(sysctl -n hw.memsize | awk '{printf "%.1f GiB\n", $1 / 1024 / 1024 / 1024}')" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(sw_vers -productName) $(sw_vers -productVersion)" - echo "Hardware model: $(sysctl -n hw.model)" - echo "CPU architecture: $(uname -m)" - echo "Logical CPUs: $(sysctl -n hw.logicalcpu)" - echo "Physical CPUs: $(sysctl -n hw.physicalcpu)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . + + fallback_version="29.0.13113456" + ndk_root="${ANDROID_HOME}/ndk" + selected_ndk="" + + if [[ -d "${ndk_root}" ]]; then + while IFS= read -r candidate; do + toolchain="${candidate}/toolchains/llvm/prebuilt/linux-x86_64" + if [[ -x "${toolchain}/bin/aarch64-linux-android24-clang" ]]; then + selected_ndk="${candidate}" + break + fi + done < <(find "${ndk_root}" -mindepth 1 -maxdepth 1 -type d | sort -Vr) + fi + + if [[ -z "${selected_ndk}" ]]; then + echo "No usable preinstalled Android NDK found; installing ${fallback_version}" + yes | sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses || true + sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" "ndk;${fallback_version}" + selected_ndk="${ndk_root}/${fallback_version}" + fi + + if [[ ! -x "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" ]]; then + echo "Selected Android NDK does not contain a usable aarch64 API 24 clang: ${selected_ndk}" >&2 + exit 1 + fi + + echo "Selected Android NDK: ${selected_ndk}" + "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" --version + echo "ANDROID_NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" + echo "NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" - name: Install Linux bwrap build dependencies - if: ${{ runner.os == 'Linux' }} + if: ${{ runner.os == 'Linux' && matrix.target != 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail @@ -254,9 +294,173 @@ jobs: sudo apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 fi - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@1.93 with: targets: ${{ matrix.target }} + - name: Set up sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + uses: mozilla-actions/sccache-action@main + - name: Enable sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + shell: bash + run: | + set -euo pipefail + echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_VERSION=android-release-${{ matrix.target }}" >> "$GITHUB_ENV" + echo "SCCACHE_CACHE_SIZE=5G" >> "$GITHUB_ENV" + - name: Ensure Rust target is installed + shell: bash + run: | + set -euo pipefail + rustup target add "${{ matrix.target }}" + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android build environment + shell: bash + run: | + set -euo pipefail + ndk="${ANDROID_NDK_HOME}" + target="${{ matrix.target }}" + + # Set up the Android toolchain + toolchain="${ndk}/toolchains/llvm/prebuilt/linux-x86_64" + + # Use API level 24 to follow termux-build convention + api_level="24" + + # Configure Cargo to use the NDK linker + cargo_target_var="CARGO_TARGET_${target^^}_LINKER" + cargo_target_var="${cargo_target_var//-/_}" + echo "${cargo_target_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + # Set CC and AR for the target + target_cc_var="CC_${target}" + target_cc_var="${target_cc_var//-/_}" + echo "${target_cc_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + target_cxx_var="CXX_${target}" + target_cxx_var="${target_cxx_var//-/_}" + echo "${target_cxx_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang++" >> "$GITHUB_ENV" + + target_ar_var="AR_${target}" + target_ar_var="${target_ar_var//-/_}" + echo "${target_ar_var}=${toolchain}/bin/llvm-ar" >> "$GITHUB_ENV" + + # Add toolchain to PATH + echo "${toolchain}/bin" >> "$GITHUB_PATH" + + tls_align_source="${RUNNER_TEMP}/codex-android-tls-align.S" + tls_align_object="${RUNNER_TEMP}/codex-android-tls-align.o" + cat > "${tls_align_source}" <<'EOF' + .section .tdata.codex_android_tls_align,"awT",%progbits + .p2align 6 + .globl __codex_android_tls_align + .hidden __codex_android_tls_align + .type __codex_android_tls_align, %object + __codex_android_tls_align: + .byte 0 + .size __codex_android_tls_align, 1 + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${tls_align_source}" -o "${tls_align_object}" + + v8_compat_source="${RUNNER_TEMP}/codex-android-v8-compat.c" + v8_compat_object="${RUNNER_TEMP}/codex-android-v8-compat.o" + cat > "${v8_compat_source}" <<'EOF' + #include + #include + + extern int posix_memalign(void **memptr, size_t alignment, size_t size); + extern double strtod(const char *nptr, char **endptr); + extern float strtof(const char *nptr, char **endptr); + + void *aligned_alloc(size_t alignment, size_t size) { + void *ptr = 0; + if (posix_memalign(&ptr, alignment, size) != 0) { + return 0; + } + return ptr; + } + + double strtod_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtod(nptr, endptr); + } + + float strtof_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtof(nptr, endptr); + } + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${v8_compat_source}" -o "${v8_compat_object}" + + builtins_archive="$( + find "${toolchain}/lib/clang" \ + -path "*/lib/linux/libclang_rt.builtins-aarch64-android.a" \ + -print -quit + )" + if [[ -z "${builtins_archive}" ]]; then + echo "Could not find Android compiler-rt builtins archive under ${toolchain}/lib/clang" >&2 + exit 1 + fi + builtins_dir="$(dirname "${builtins_archive}")" + + # Provide compatibility symbols and C++ ABI needed by the Android rusty_v8 archive. + rustflags_var="CARGO_TARGET_${target^^}_RUSTFLAGS" + rustflags_var="${rustflags_var//-/_}" + echo "${rustflags_var}=-C link-arg=${tls_align_object} -C link-arg=${v8_compat_object} -C link-arg=-Wl,-u,__codex_android_tls_align -C link-arg=-L${builtins_dir} -C link-arg=-lclang_rt.builtins-aarch64-android -C link-arg=-lc++abi -C link-arg=-Wl,-z,max-page-size=65536" >> "$GITHUB_ENV" + + + # Configure for vendored OpenSSL build + echo "CARGO_BUILD_TARGET_APPLIES_TO_HOST=false" >> "$GITHUB_ENV" + echo "CARGO_BUILD_TARGET=${{ matrix.target }}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android rusty_v8 artifact overrides + env: + GH_TOKEN: ${{ github.token }} + TARGET: ${{ matrix.target }} + shell: bash + run: | + set -euo pipefail + binding_dir="${RUNNER_TEMP}/rusty_v8" + archive="${binding_dir}/librusty_v8_release_${TARGET}.a.gz" + binding_path="${binding_dir}/src_binding_release_${TARGET}.rs" + checksums_path="${binding_dir}/rusty_v8_release_${TARGET}.sha256" + artifact_repository="wallentx/codex-termux" + release_tag="rusty-v8-v147.4.0" + mkdir -p "${binding_dir}" + echo "Downloading Android rusty_v8 artifacts from ${artifact_repository}@${release_tag}" + gh release download "${release_tag}" \ + --repo "${artifact_repository}" \ + --dir "${binding_dir}" \ + --pattern "librusty_v8_release_${TARGET}.a.gz" \ + --pattern "src_binding_release_${TARGET}.rs" \ + --pattern "rusty_v8_release_${TARGET}.sha256" + (cd "${binding_dir}" && sha256sum -c "$(basename "${checksums_path}")") + echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV" + echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Install termux-elf-cleaner + shell: bash + run: | + set -euo pipefail + version="v3.0.1" + src_dir="${RUNNER_TEMP}/termux-elf-cleaner-src" + build_dir="${RUNNER_TEMP}/termux-elf-cleaner-build" + bin_dir="${RUNNER_TEMP}/termux-elf-cleaner-bin" + + rm -rf "${src_dir}" "${build_dir}" "${bin_dir}" + mkdir -p "${src_dir}" "${build_dir}" "${bin_dir}" + + curl -fsSL "https://github.com/termux/termux-elf-cleaner/archive/refs/tags/${version}.tar.gz" \ + | tar -xzf - --strip-components=1 -C "${src_dir}" + + cmake -S "${src_dir}" -B "${build_dir}" -DCMAKE_BUILD_TYPE=Release + cmake --build "${build_dir}" --parallel + + install "${build_dir}/termux-elf-cleaner" "${bin_dir}/termux-elf-cleaner" + echo "${bin_dir}" >> "$GITHUB_PATH" - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Use hermetic Cargo home (musl) @@ -269,12 +473,25 @@ jobs: echo "${cargo_home}/bin" >> "$GITHUB_PATH" : > "${cargo_home}/config.toml" + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ${{ github.workspace }}/.cargo-home/bin/ + ${{ github.workspace }}/.cargo-home/registry/index/ + ${{ github.workspace }}/.cargo-home/registry/cache/ + ${{ github.workspace }}/.cargo-home/git/db/ + ${{ github.workspace }}/codex-rs/target/ + key: cargo-${{ matrix.runner }}-${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }} + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install Zig - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + uses: mlugg/setup-zig@v2 with: version: 0.14.0 - use-cache: false - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install musl build tools @@ -310,12 +527,6 @@ jobs: shell: bash run: | set -euo pipefail - # Avoid problematic aws-lc jitter entropy code path on musl builders. - echo "AWS_LC_SYS_NO_JITTER_ENTROPY=1" >> "$GITHUB_ENV" - target_no_jitter="AWS_LC_SYS_NO_JITTER_ENTROPY_${{ matrix.target }}" - target_no_jitter="${target_no_jitter//-/_}" - echo "${target_no_jitter}=1" >> "$GITHUB_ENV" - # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. echo "RUSTFLAGS=" >> "$GITHUB_ENV" echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" @@ -340,94 +551,70 @@ jobs: echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" - - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }} - name: Configure musl rusty_v8 artifact overrides and verify checksums - uses: ./.github/actions/setup-rusty-v8-musl - with: - target: ${{ matrix.target }} - - - if: ${{ contains(matrix.target, 'linux') }} - name: Build bwrap and export digest + - name: Cargo build shell: bash run: | set -euo pipefail - target="${{ matrix.target }}" - cargo build --target "$target" --release --timings --bin bwrap - bwrap_path="target/${target}/release/bwrap" - if [[ ! -f "$bwrap_path" ]]; then - echo "bwrap binary ${bwrap_path} not found" - exit 1 + if [[ "${{ matrix.target }}" == 'aarch64-linux-android' ]]; then + export CARGO_BUILD_JOBS=4 + export CARGO_PROFILE_RELEASE_LTO=thin + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 + cargo build --target ${{ matrix.target }} --release --bin codex + exit 0 fi - digest="$(sha256sum "$bwrap_path" | awk '{print $1}')" - echo "CODEX_BWRAP_SHA256=${digest}" >> "$GITHUB_ENV" - echo "Built bwrap ${bwrap_path} with sha256:${digest}" - - - name: Cargo build + if [[ "${{ contains(matrix.target, 'windows') }}" == 'true' ]]; then + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy --bin codex-windows-sandbox-setup --bin codex-command-runner + else + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy + fi + - name: sccache stats (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash - run: | - build_args=() - for binary in ${{ matrix.binaries }}; do - build_args+=(--bin "$binary") - done - echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}" - cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}" - - - name: Upload Cargo timings - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }} - path: codex-rs/target/**/cargo-timings/cargo-timing.html - if-no-files-found: warn + run: ${SCCACHE_PATH:-sccache} --show-stats || true - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Stage unsigned macOS artifacts + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Normalize Android ELF for Termux shell: bash run: | set -euo pipefail - - target="${{ matrix.target }}" - release_dir="target/${target}/release" - dest="unsigned-dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - unsigned_name="${binary}-${target}-unsigned" - unsigned_path="${dest}/${unsigned_name}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 + for binary in codex codex-responses-api-proxy; do + binary_path="target/${{ matrix.target }}/release/${binary}" + if [[ -f "${binary_path}" ]]; then + termux-elf-cleaner --api-level 24 "${binary_path}" + chmod +x "${binary_path}" + # if [[ "${binary}" == "codex" ]]; then + # # Patch PT_TLS (0x7) to PT_NULL (0x0) at offset 400 to bypass Bionic alignment checks. + # printf '\x00\x00\x00\x00' | dd of="${binary_path}" bs=1 seek=400 count=4 conv=notrunc + # fi fi - - cp "${binary_path}" "${unsigned_path}" - tar -C "$dest" -czf "${unsigned_path}.tar.gz" "${unsigned_name}" - zstd -T0 -19 --rm "${unsigned_path}" done - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Upload unsigned macOS artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }}-unsigned - path: codex-rs/unsigned-dist/${{ matrix.target }}/* - if-no-files-found: error - - - if: ${{ contains(matrix.target, 'linux') }} + - if: ${{ contains(matrix.target, 'linux') && !contains(matrix.target, 'android') && github.repository_owner == 'openai' }} name: Cosign Linux artifacts uses: ./.github/actions/linux-code-sign with: target: ${{ matrix.target }} artifacts-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release - binaries: ${{ matrix.binaries }} - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS == 'true' }} + - if: ${{ contains(matrix.target, 'windows') && github.repository_owner == 'openai' }} + name: Sign Windows binaries with Azure Trusted Signing + uses: ./.github/actions/windows-code-sign + with: + target: ${{ matrix.target }} + client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID }} + endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} + account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} + + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (binaries) uses: ./.github/actions/macos-code-sign with: target: ${{ matrix.target }} - binaries: ${{ matrix.binaries }} sign-binaries: "true" sign-dmg: "false" apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }} @@ -436,7 +623,7 @@ jobs: apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: Build macOS dmg shell: bash run: | @@ -451,17 +638,23 @@ jobs: # The previous "MacOS code signing (binaries)" step signs + notarizes the # built artifacts in `${release_dir}`. This step packages *those same* # signed binaries into a dmg. + codex_binary_path="${release_dir}/codex" + proxy_binary_path="${release_dir}/codex-responses-api-proxy" + rm -rf "$dmg_root" mkdir -p "$dmg_root" - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 - fi - ditto "${binary_path}" "${dmg_root}/${binary}" - done + if [[ ! -f "$codex_binary_path" ]]; then + echo "Binary $codex_binary_path not found" + exit 1 + fi + if [[ ! -f "$proxy_binary_path" ]]; then + echo "Binary $proxy_binary_path not found" + exit 1 + fi + + ditto "$codex_binary_path" "${dmg_root}/codex" + ditto "$proxy_binary_path" "${dmg_root}/codex-responses-api-proxy" rm -f "$dmg_path" hdiutil create \ @@ -476,7 +669,7 @@ jobs: exit 1 fi - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (dmg) uses: ./.github/actions/macos-code-sign with: @@ -490,113 +683,65 @@ jobs: apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - name: Stage artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | dest="dist/${{ matrix.target }}" mkdir -p "$dest" - for binary in ${{ matrix.binaries }}; do - cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" - if [[ "${{ matrix.target }}" == *linux* ]]; then - cp "target/${{ matrix.target }}/release/${binary}.sigstore" \ - "$dest/${binary}-${{ matrix.target }}.sigstore" + if [[ "${{ matrix.runner }}" == windows* ]]; then + cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe" + else + # For Android, we want the binary to be named just 'codex' in the archive. + if [[ "${{ matrix.target }}" == "aarch64-linux-android" ]]; then + cp target/${{ matrix.target }}/release/codex "$dest/codex" + else + cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" fi - done + if [[ -f target/${{ matrix.target }}/release/codex-responses-api-proxy ]]; then + cp target/${{ matrix.target }}/release/codex-responses-api-proxy "$dest/codex-responses-api-proxy-${{ matrix.target }}" + fi + fi - if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.bundle }}" == "primary" ]]; then - bundle_root="${RUNNER_TEMP}/codex-${{ matrix.target }}-bundle" - rm -rf "$bundle_root" - mkdir -p "$bundle_root/codex-resources" - cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex" - cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap" - chmod 0755 "$bundle_root/codex" "$bundle_root/codex-resources/bwrap" - tar -C "$bundle_root" -cf - codex codex-resources/bwrap | - zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst" + if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.target }}" != *android* ]]; then + cp target/${{ matrix.target }}/release/codex.sigstore "$dest/codex-${{ matrix.target }}.sigstore" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.sigstore "$dest/codex-responses-api-proxy-${{ matrix.target }}.sigstore" fi - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then + if [[ "${{ matrix.target }}" == *apple-darwin ]]; then cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" fi - - name: Build Codex package archive - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "target/${TARGET}/release" \ - --archive-dir "dist/${TARGET}" - - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - aarch64-unknown-linux-musl) - platform_tag="manylinux_2_17_aarch64" - ;; - x86_64-unknown-linux-musl) - platform_tag="manylinux_2_17_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - # Do not install into the runner's system Python; macOS runners mark - # the Homebrew Python as externally managed under PEP 668. - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - stage_runtime_args=( - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" - stage-runtime - "$stage_dir" - "dist/${{ matrix.target }}/codex-package-${{ matrix.target }}.tar.gz" - --codex-version "${GITHUB_REF_NAME}" - --platform-tag "$platform_tag" - ) - python3 "${stage_runtime_args[@]}" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error + - if: ${{ matrix.runner == 'windows-11-arm' }} + name: Install zstd + shell: powershell + run: choco install -y zstandard - name: Compress artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | # Path that contains the uncompressed binaries for the current # ${{ matrix.target }} dest="dist/${{ matrix.target }}" + repo_root=$PWD + + # We want to ship the raw Windows executables in the GitHub Release + # in addition to the compressed archives. Keep the originals for + # Windows targets; remove them elsewhere to limit the number of + # artifacts that end up in the GitHub Release. + keep_originals=false + if [[ "${{ matrix.runner }}" == windows* ]]; then + keep_originals=true + fi # For compatibility with environments that lack the `zstd` tool we - # additionally create a `.tar.gz` alongside every binary we publish. - # The end result is: + # additionally create a `.tar.gz` for all platforms and `.zip` for + # Windows alongside every single binary that we publish. The end result is: # codex-.zst (existing) # codex-.tar.gz (new) + # codex-.zip (only for Windows) # 1. Produce a .tar.gz for every file in the directory *before* we # run `zstd --rm`, because that flag deletes the original files. @@ -604,7 +749,7 @@ jobs: base="$(basename "$f")" # Skip files that are already archives (shouldn't happen, but be # safe). - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then + if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then continue fi @@ -614,344 +759,239 @@ jobs: fi # Create per-binary tar.gz - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - - # Also create .zst and remove the uncompressed binaries to keep - # non-Windows artifact directories small. - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - with: - name: ${{ matrix.artifact_name }} - # Upload the per-binary .zst files, .tar.gz equivalents, and any - # prebuilt archives staged above. - path: | - codex-rs/dist/${{ matrix.target }}/* - - stage-signed-macos: - if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode == 'promote_signed' }} - needs: tag-check - name: Stage signed macOS handoff - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: macos-15-xlarge - timeout-minutes: 30 - permissions: - contents: read - defaults: - run: - working-directory: codex-rs + # For Android, we want the archive to have the target suffix even though the binary is just 'codex'. + archive_name="${base}" + if [[ "${{ matrix.target }}" == "aarch64-linux-android" && "${base}" == "codex" ]]; then + archive_name="codex-${{ matrix.target }}" + fi + tar -C "$dest" -czf "$dest/${archive_name}.tar.gz" "$base" + + # Create zip archive for Windows binaries + # Must run from inside the dest dir so 7z won't + # embed the directory path inside the zip. + if [[ "${{ matrix.runner }}" == windows* ]]; then + if [[ "$base" == "codex-${{ matrix.target }}.exe" ]]; then + # Bundle the sandbox helper binaries into the main codex zip so + # WinGet installs include the required helpers next to codex.exe. + # Fall back to the single-binary zip if the helpers are missing + # to avoid breaking releases. + bundle_dir="$(mktemp -d)" + runner_src="$dest/codex-command-runner-${{ matrix.target }}.exe" + setup_src="$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + if [[ -f "$runner_src" && -f "$setup_src" ]]; then + cp "$dest/$base" "$bundle_dir/$base" + cp "$runner_src" "$bundle_dir/codex-command-runner.exe" + cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" + # Use an absolute path so bundle zips land in the real dist + # dir even when 7z runs from a temp directory. + (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) + else + echo "warning: missing sandbox binaries; falling back to single-binary zip" + echo "warning: expected $runner_src and $setup_src" + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + rm -rf "$bundle_dir" + else + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + fi - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" + # Also create .zst (existing behaviour) *and* remove the original + # uncompressed binary to keep the directory small. + zstd_args=(-T0 -19) + if [[ "${keep_originals}" == false ]]; then + zstd_args+=(--rm) + fi - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + if [[ "${archive_name}" != "${base}" ]]; then + zstd "${zstd_args[@]}" "$dest/$base" -o "$dest/${archive_name}.zst" + else + zstd "${zstd_args[@]}" "$dest/$base" + fi + done - - name: Download signed macOS handoff + - name: Add Termux release metadata + if: ${{ (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && matrix.target == 'aarch64-linux-android' }} shell: bash - env: - GH_TOKEN: ${{ github.token }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - SIGNED_MACOS_SHA256: ${{ inputs.signed_macos_sha256 }} run: | set -euo pipefail - - download_dir="${RUNNER_TEMP}/signed-macos-download" - handoff_dir="${RUNNER_TEMP}/signed-macos-handoff" - rm -rf "$download_dir" "$handoff_dir" - mkdir -p "$download_dir" "$handoff_dir" - - gh release download "$GITHUB_REF_NAME" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern "$SIGNED_MACOS_ASSET" \ - --dir "$download_dir" - - asset_count="$(find "$download_dir" -maxdepth 1 -type f | wc -l | tr -d '[:space:]')" - if [[ "$asset_count" != "1" ]]; then - echo "Expected exactly one signed macOS handoff asset named ${SIGNED_MACOS_ASSET}; found ${asset_count}" - find "$download_dir" -maxdepth 1 -type f -print - exit 1 - fi - - asset_path="$(find "$download_dir" -maxdepth 1 -type f -print -quit)" - if [[ -n "${SIGNED_MACOS_SHA256}" ]]; then - expected_sha="$(printf '%s' "$SIGNED_MACOS_SHA256" | tr '[:upper:]' '[:lower:]')" - actual_sha="$(shasum -a 256 "$asset_path" | awk '{print $1}')" - if [[ "$actual_sha" != "$expected_sha" ]]; then - echo "signed_macos_sha256 mismatch for ${SIGNED_MACOS_ASSET}" - echo "expected: ${expected_sha}" - echo "actual: ${actual_sha}" - exit 1 - fi + if [[ ! -f "${GITHUB_WORKSPACE}/.github/termux-release.json" ]]; then + echo "No Termux release metadata found; skipping metadata attachment." + exit 0 fi + dest="dist/${{ matrix.target }}" + cp "${GITHUB_WORKSPACE}/.github/termux-release.json" "${dest}/termux-release.json" + ( + cd "${dest}" + sha256sum ./* > SHA256SUMS + ) - asset_name="$(basename "$asset_path")" - case "$asset_name" in - *.tar.zst) - zstd -dc "$asset_path" | tar -C "$handoff_dir" -xf - - ;; - *.tar.gz|*.tgz) - tar -C "$handoff_dir" -xzf "$asset_path" - ;; - *.zip) - ditto -x -k "$asset_path" "$handoff_dir" - ;; - *) - echo "Unsupported signed macOS handoff archive format: ${asset_name}" - exit 1 - ;; - esac - - echo "SIGNED_MACOS_HANDOFF_DIR=$handoff_dir" >> "$GITHUB_ENV" + - id: upload-artifact + uses: actions/upload-artifact@v6 + with: + name: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + # Upload the per-binary .zst files as well as the new .tar.gz + # equivalents we generated in the previous step. + path: | + codex-rs/dist/${{ matrix.target }}/* - - name: Stage signed macOS artifacts + - name: Publish Termux artifact notification + if: ${{ github.event_name == 'pull_request' && startsWith(github.base_ref, 'release/') && matrix.target == 'aarch64-linux-android' }} shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_ID: ${{ steps.upload-artifact.outputs.artifact-id }} + ARTIFACT_NAME: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + PR_NUMBER: ${{ github.event.pull_request.number }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | set -euo pipefail - - target="${{ matrix.target }}" - artifact_name="${{ matrix.artifact_name }}" - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${target}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - fi - if [[ ! -d "$source_dir" ]]; then - echo "Signed macOS handoff is missing ${artifact_name}/" - echo "Expected either:" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${target}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - find "$SIGNED_MACOS_HANDOFF_DIR" -maxdepth 3 -type f -print + if [[ -z "${ARTIFACT_ID}" ]]; then + echo "upload-artifact did not return an artifact id" >&2 exit 1 fi - dest="dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - source_path="${source_dir}/${binary}" - if [[ ! -f "$source_path" ]]; then - source_path="${source_dir}/${binary}-${target}" - fi - if [[ ! -f "$source_path" ]]; then - echo "Signed macOS handoff is missing ${binary} for ${artifact_name}" - exit 1 - fi - - release_path="${dest}/${binary}-${target}" - ditto "$source_path" "$release_path" - chmod 0755 "$release_path" - codesign --verify --strict --verbose=2 "$release_path" - done - - # DMG staging is disabled for signed promotion because we no longer - # distribute DMGs from this release path. Keep the branch here so the - # handoff can opt back in by flipping matrix.build_dmg if needed. - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - dmg_name="codex-${target}.dmg" - dmg_source="${source_dir}/${dmg_name}" - if [[ ! -f "$dmg_source" ]]; then - echo "Signed macOS handoff is missing ${dmg_name} for ${artifact_name}" - exit 1 - fi - - codesign --verify --strict --verbose=2 "$dmg_source" - xcrun stapler validate "$dmg_source" - cp "$dmg_source" "$dest/$dmg_name" + marker="" + artifact_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts/${ARTIFACT_ID}" + run_url="${GH_WORKFLOW_URL:-https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + body_path="${RUNNER_TEMP}/termux-artifact-comment.md" + { + echo "${marker}" + echo "Termux Android artifact ready for testing:" + echo + echo "- Artifact: [${ARTIFACT_NAME}](${artifact_url})" + echo "- Workflow run: [${GITHUB_RUN_ID}](${run_url})" + echo + echo "You must be signed in to GitHub with repository access to download Actions artifacts." + } > "${body_path}" + + existing_comment_id="$( + gh pr view "${PR_NUMBER}" \ + --repo "${GITHUB_REPOSITORY}" \ + --json comments \ + --jq ".comments | map(select(.author.is_bot == true and (.body | contains(\"${marker}\")))) | .[-1].id // \"\"" + )" + if [[ -n "${existing_comment_id}" ]]; then + gh api graphql \ + -f query=' + mutation($id: ID!, $body: String!) { + updateIssueComment(input: {id: $id, body: $body}) { + issueComment { + id + } + } + } + ' \ + -f id="${existing_comment_id}" \ + -f body="$(cat "${body_path}")" \ + >/dev/null + else + gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body-file "${body_path}" fi - - name: Build Codex package archive - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "dist/${TARGET}" \ - --archive-dir "dist/${TARGET}" \ - --target-suffixed-entrypoint - - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - python3 \ - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \ - stage-runtime \ - "$stage_dir" \ - "dist/${{ matrix.target }}/codex-package-${{ matrix.target }}.tar.gz" \ - --codex-version "${GITHUB_REF_NAME}" \ - --platform-tag "$platform_tag" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error - - - name: Compress artifacts - shell: bash - run: | - set -euo pipefail - - dest="dist/${{ matrix.target }}" - for f in "$dest"/*; do - base="$(basename "$f")" - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then - continue + gh label create binary-ready \ + --repo "${GITHUB_REPOSITORY}" \ + --color 0e8a16 \ + --description "Android binary is ready for testing" \ + --force + gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --add-label binary-ready + + if [[ -n "${SLACK_WEBHOOK_URL:-}" ]]; then + slack_payload_path="${RUNNER_TEMP}/termux-artifact-slack.json" + pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}" + jq -n \ + --arg artifact_name "${ARTIFACT_NAME}" \ + --arg artifact_url "${artifact_url}" \ + --arg pr_number "${PR_NUMBER}" \ + --arg pr_url "${pr_url}" \ + --arg run_id "${GITHUB_RUN_ID}" \ + --arg run_url "${run_url}" \ + '{ + text: ("Termux Android artifact ready for testing: " + $artifact_url), + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: ("*Termux Android artifact ready for testing*\n" + $artifact_url) + } + }, + { + type: "section", + fields: [ + { + type: "mrkdwn", + text: ("*Artifact:*\n<" + $artifact_url + "|" + $artifact_name + ">") + }, + { + type: "mrkdwn", + text: ("*Pull request:*\n<" + $pr_url + "|#" + $pr_number + ">") + }, + { + type: "mrkdwn", + text: ("*Workflow run:*\n<" + $run_url + "|" + $run_id + ">") + } + ] + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: "You must be signed in to GitHub with repository access to download Actions artifacts." + } + ] + } + ] + }' > "${slack_payload_path}" + + if ! curl -fsS \ + -X POST \ + -H "Content-type: application/json" \ + --data @"${slack_payload_path}" \ + "${SLACK_WEBHOOK_URL}"; then + echo "::warning::Failed to send Slack artifact notification" fi + fi - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }} - path: | - codex-rs/dist/${{ matrix.target }}/* - - build-windows: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - needs: tag-check - uses: ./.github/workflows/rust-release-windows.yml - with: - release-lto: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }} - secrets: inherit - - argument-comment-lint-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: argument-comment-lint release assets + shell-tool-mcp: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'openai' + name: shell-tool-mcp needs: tag-check - uses: ./.github/workflows/rust-release-argument-comment-lint.yml + permissions: + contents: read + id-token: write + uses: ./.github/workflows/shell-tool-mcp.yml with: + release-tag: ${{ github.ref_name }} publish: true - - zsh-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: zsh release assets - needs: tag-check - uses: ./.github/workflows/rust-release-zsh.yml + secrets: inherit release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') needs: - - tag-check - build - - stage-signed-macos - - build-windows - - argument-comment-lint-release-assets - - zsh-release-assets - if: >- - ${{ - always() && - needs.tag-check.result == 'success' && - ( - ( - github.event_name == 'workflow_dispatch' && - inputs.release_mode == 'promote_signed' && - needs.stage-signed-macos.result == 'success' && - needs.build.result == 'skipped' && - needs.build-windows.result == 'skipped' && - needs.argument-comment-lint-release-assets.result == 'skipped' && - needs.zsh-release-assets.result == 'skipped' - ) || - ( - (github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed') && - needs.build.result == 'success' && - needs.stage-signed-macos.result == 'skipped' && - needs.build-windows.result == 'success' && - needs.argument-comment-lint-release-assets.result == 'success' && - needs.zsh-release-assets.result == 'success' - ) - ) - }} + - shell-tool-mcp name: release runs-on: ubuntu-latest permissions: contents: write actions: read - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode == 'promote_signed' }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} outputs: version: ${{ steps.release_name.outputs.name }} tag: ${{ github.ref_name }} - sign_macos: ${{ steps.release_mode.outputs.sign_macos }} should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }} npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }} - should_publish_python_runtime: ${{ steps.python_runtime_publish_settings.outputs.should_publish }} steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + uses: actions/checkout@v6 - - name: Define release mode - id: release_mode - run: | - echo "release_mode=${RELEASE_MODE}" >> "$GITHUB_OUTPUT" - echo "sign_macos=${SIGN_MACOS}" >> "$GITHUB_OUTPUT" + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main - name: Generate release notes from tag commit message id: release_notes @@ -973,159 +1013,21 @@ jobs: echo "path=${notes_path}" >> "${GITHUB_OUTPUT}" - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@v7 with: path: dist - - name: Validate unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - run_summary="$(gh run view "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --json conclusion,event,headBranch,headSha,status,workflowName,url \ - --jq '[.workflowName, .event, .headBranch, .headSha, .status, .conclusion, .url] | @tsv')" - IFS=$'\t' read -r workflow_name event head_branch head_sha status conclusion run_url <<< "$run_summary" - expected_head_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")" - - if [[ "$workflow_name" != "$GITHUB_WORKFLOW" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is for workflow '${workflow_name}', expected '${GITHUB_WORKFLOW}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$event" != "workflow_dispatch" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} was triggered by '${event}', expected 'workflow_dispatch'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_branch" != "$GITHUB_REF_NAME" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used ref '${head_branch}', expected '${GITHUB_REF_NAME}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_sha" != "$expected_head_sha" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used head SHA '${head_sha}', expected '${expected_head_sha}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is ${status}/${conclusion}, expected completed/success" - echo "Run URL: ${run_url}" - exit 1 - fi - - - name: Download artifacts from unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - gh run download "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --dir dist - - - name: Remove unsigned macOS staging artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - run: | - set -euo pipefail - find dist -mindepth 1 -maxdepth 1 -type d \ - -name '*-apple-darwin*-unsigned' \ - -exec rm -rf {} + - - - name: Re-upload promoted Linux x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-unknown-linux-musl - path: dist/x86_64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Linux arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-unknown-linux-musl - path: dist/aarch64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Windows x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-pc-windows-msvc - path: dist/x86_64-pc-windows-msvc/* - if-no-files-found: error - - - name: Re-upload promoted Windows arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-pc-windows-msvc - path: dist/aarch64-pc-windows-msvc/* - if-no-files-found: error - - name: List run: ls -R dist/ - - name: Prune artifacts excluded from unsigned macOS release - if: ${{ env.SIGN_MACOS == 'false' }} - run: | - find dist -mindepth 1 -maxdepth 1 -type d \ - ! -name '*-apple-darwin*-unsigned' \ - ! -name 'aarch64-unknown-linux-musl' \ - ! -name 'aarch64-unknown-linux-musl-app-server' \ - ! -name 'x86_64-unknown-linux-musl' \ - ! -name 'x86_64-unknown-linux-musl-app-server' \ - ! -name 'aarch64-pc-windows-msvc' \ - ! -name 'x86_64-pc-windows-msvc' \ - -exec rm -rf {} + - - if ! find dist -type f -name '*-apple-darwin*-unsigned*' | grep -q .; then - echo "No unsigned macOS artifacts found in downloaded workflow artifacts." - exit 1 - fi - + # This is a temporary fix: we should modify shell-tool-mcp.yml so these + # files do not end up in dist/ in the first place. - name: Delete entries from dist/ that should not go in the release run: | - rm -rf dist/windows-binaries* - # cargo-timing.html appears under multiple target-specific directories. - # If included in files: dist/**, release upload races on duplicate - # asset names and can fail with 404s. - find dist -type f -name 'cargo-timing.html' -delete - find dist -type d -empty -delete + rm -rf dist/shell-tool-mcp* ls -R dist/ - - name: Add Codex package checksum manifest - run: | - set -euo pipefail - - manifest="dist/codex-package_SHA256SUMS" - tmp_manifest="$(mktemp)" - find dist -type f \ - \( -name 'codex-package-*.tar.gz' -o -name 'codex-app-server-package-*.tar.gz' \) \ - -print | - sort | - while IFS= read -r archive; do - sha256sum "$archive" | - awk -v name="$(basename "$archive")" '{ print $1 " " name }' - done > "$tmp_manifest" - - if [[ ! -s "$tmp_manifest" ]]; then - echo "No Codex package archives found for checksum manifest" - exit 1 - fi - - mv "$tmp_manifest" "$manifest" - cat "$manifest" - - name: Add config schema release asset run: | cp codex-rs/core/config.schema.json dist/config-schema.json @@ -1146,12 +1048,6 @@ jobs: set -euo pipefail version="${VERSION}" - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - echo "npm_tag=" >> "$GITHUB_OUTPUT" - exit 0 - fi - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "should_publish=true" >> "$GITHUB_OUTPUT" echo "npm_tag=" >> "$GITHUB_OUTPUT" @@ -1163,130 +1059,55 @@ jobs: echo "npm_tag=" >> "$GITHUB_OUTPUT" fi - - name: Determine Python runtime publish settings - id: python_runtime_publish_settings - env: - VERSION: ${{ steps.release_name.outputs.name }} - run: | - set -euo pipefail - version="${VERSION}" - - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - else - echo "should_publish=false" >> "$GITHUB_OUTPUT" - fi - - name: Setup pnpm - if: ${{ env.SIGN_MACOS == 'true' }} - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + uses: pnpm/action-setup@v4 with: run_install: false - name: Setup Node.js for npm packaging - if: ${{ env.SIGN_MACOS == 'true' }} - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: node-version: 22 - name: Install dependencies - if: ${{ env.SIGN_MACOS == 'true' }} run: pnpm install --frozen-lockfile + # stage_npm_packages.py requires DotSlash when staging releases. + - uses: facebook/install-dotslash@v2 - name: Stage npm packages - if: ${{ env.SIGN_MACOS == 'true' }} + if: github.repository_owner == 'openai' env: GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ steps.release_name.outputs.name }} run: | - workflow_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" ./scripts/stage_npm_packages.py \ - --release-version "$RELEASE_VERSION" \ - --workflow-url "$workflow_url" \ + --release-version "${{ steps.release_name.outputs.name }}" \ --package codex \ --package codex-responses-api-proxy \ --package codex-sdk - - name: Stage installer scripts - if: ${{ env.SIGN_MACOS == 'true' }} - run: | - cp scripts/install/install.sh dist/install.sh - cp scripts/install/install.ps1 dist/install.ps1 - - name: Create GitHub Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@v2 with: name: ${{ steps.release_name.outputs.name }} tag_name: ${{ github.ref_name }} body_path: ${{ steps.release_notes.outputs.path }} files: dist/** - overwrite_files: true - make_latest: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} # Mark as prerelease only when the version has a suffix after x.y.z # (e.g. -alpha, -beta). Otherwise publish a normal release. prerelease: ${{ contains(steps.release_name.outputs.name, '-') }} - - name: Clean up signed promotion handoff assets - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" --jq '.id')" - gh api --paginate "repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets" \ - --jq '.[] | [.id, .name] | @tsv' | - while IFS=$'\t' read -r asset_id asset_name; do - if [[ -z "$asset_id" || -z "$asset_name" ]]; then - continue - fi - - delete_asset=false - if [[ "$asset_name" == *unsigned* || "$asset_name" == "$SIGNED_MACOS_ASSET" ]]; then - delete_asset=true - fi - - if [[ "$delete_asset" == "true" ]]; then - echo "Deleting release asset ${asset_name}" - gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" - fi - done - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 + - if: github.repository_owner == 'openai' + uses: facebook/dotslash-publish-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag: ${{ github.ref_name }} config: .github/dotslash-config.json - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-zsh-config.json - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-argument-comment-lint-config.json - - name: Trigger developers.openai.com deploy # Only trigger the deploy if the release is not a pre-release. # The deploy is used to update the developers.openai.com website with the new config schema json file. - if: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} + if: ${{ !contains(steps.release_name.outputs.name, '-') && github.repository_owner == 'openai' }} continue-on-error: true env: DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }} @@ -1301,15 +1122,7 @@ jobs: # npm docs: https://docs.npmjs.com/trusted-publishers publish-npm: # Publish to npm for stable releases and alpha pre-releases with numeric suffixes. - # promote_signed intentionally skips build jobs that are ancestors of release; - # include the !cancelled() status function so Actions does not apply its implicit - # success() check to the whole dependency chain before evaluating release outputs. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_npm == 'true' - }} + if: ${{ needs.release.outputs.should_publish_npm == 'true' && github.repository_owner == 'openai' }} name: publish-npm needs: release runs-on: ubuntu-latest @@ -1319,37 +1132,36 @@ jobs: steps: - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: - # Node 24 bundles npm >= 11.5.1, which trusted publishing requires. - node-version: 24 + node-version: 22 registry-url: "https://registry.npmjs.org" scope: "@openai" + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + - name: Download npm tarballs from release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} run: | set -euo pipefail - version="$RELEASE_VERSION" - tag="$RELEASE_TAG" + version="${{ needs.release.outputs.version }}" + tag="${{ needs.release.outputs.tag }}" mkdir -p dist/npm - patterns=( - "codex-npm-${version}.tgz" - "codex-npm-linux-*-${version}.tgz" - "codex-npm-darwin-*-${version}.tgz" - "codex-npm-win32-*-${version}.tgz" - "codex-responses-api-proxy-npm-${version}.tgz" - "codex-sdk-npm-${version}.tgz" - ) - for pattern in "${patterns[@]}"; do - gh release download "$tag" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "$pattern" \ - --dir dist/npm - done + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-responses-api-proxy-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-sdk-npm-${version}.tgz" \ + --dir dist/npm # No NODE_AUTH_TOKEN needed because we use OIDC. - name: Publish to npm @@ -1358,193 +1170,23 @@ jobs: NPM_TAG: ${{ needs.release.outputs.npm_tag }} run: | set -euo pipefail - prefix="" + tag_args=() if [[ -n "${NPM_TAG}" ]]; then - prefix="${NPM_TAG}-" + tag_args+=(--tag "${NPM_TAG}") fi - root_tarball="dist/npm/codex-npm-${VERSION}.tgz" - sdk_tarball="dist/npm/codex-sdk-npm-${VERSION}.tgz" - # Keep this list in sync with CODEX_PLATFORM_PACKAGES in - # codex-cli/scripts/build_npm_package.py. The root wrapper advances - # @openai/codex@latest as soon as it publishes, so every platform - # package it aliases must already exist in the registry first. - platform_tarballs=( - "dist/npm/codex-npm-linux-x64-${VERSION}.tgz" - "dist/npm/codex-npm-linux-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-x64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-x64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-arm64-${VERSION}.tgz" - ) - - for required_tarball in "${platform_tarballs[@]}" "${root_tarball}"; do - if [[ ! -f "${required_tarball}" ]]; then - echo "Missing npm tarball: ${required_tarball}" - exit 1 - fi - done - - shopt -s nullglob - other_tarballs=() - for tarball in dist/npm/*-"${VERSION}".tgz; do - if [[ "${tarball}" == "${root_tarball}" || "${tarball}" == "${sdk_tarball}" ]]; then - continue - fi - - is_platform_tarball=false - for platform_tarball in "${platform_tarballs[@]}"; do - if [[ "${tarball}" == "${platform_tarball}" ]]; then - is_platform_tarball=true - break - fi - done - if [[ "${is_platform_tarball}" == true ]]; then - continue - fi - - other_tarballs+=("${tarball}") - done - - # Publish the platform packages before the root CLI wrapper. The root - # wrapper advances @openai/codex@latest, so it should only publish - # after the optional dependency versions it references exist. tarballs=( - "${platform_tarballs[@]}" - "${other_tarballs[@]}" - "${root_tarball}" + "codex-npm-${VERSION}.tgz" + "codex-responses-api-proxy-npm-${VERSION}.tgz" + "codex-sdk-npm-${VERSION}.tgz" ) - if [[ -f "${sdk_tarball}" ]]; then - tarballs+=("${sdk_tarball}") - fi for tarball in "${tarballs[@]}"; do - filename="$(basename "${tarball}")" - tag="" - - case "${filename}" in - codex-npm-linux-*-"${VERSION}".tgz|codex-npm-darwin-*-"${VERSION}".tgz|codex-npm-win32-*-"${VERSION}".tgz) - platform="${filename#codex-npm-}" - platform="${platform%-${VERSION}.tgz}" - tag="${prefix}${platform}" - ;; - codex-npm-"${VERSION}".tgz|codex-responses-api-proxy-npm-"${VERSION}".tgz|codex-sdk-npm-"${VERSION}".tgz) - tag="${NPM_TAG}" - ;; - *) - echo "Unexpected npm tarball: ${filename}" - exit 1 - ;; - esac - - publish_cmd=(npm publish "${GITHUB_WORKSPACE}/${tarball}") - if [[ -n "${tag}" ]]; then - publish_cmd+=(--tag "${tag}") - fi - - echo "+ ${publish_cmd[*]}" - set +e - publish_output="$("${publish_cmd[@]}" 2>&1)" - publish_status=$? - set -e - - echo "${publish_output}" - if [[ ${publish_status} -eq 0 ]]; then - continue - fi - - if grep -qiE "previously published|cannot publish over|version already exists" <<< "${publish_output}"; then - echo "Skipping already-published package version for ${filename}" - continue - fi - - exit "${publish_status}" + npm publish "${GITHUB_WORKSPACE}/dist/npm/${tarball}" "${tag_args[@]}" done - # Publish the platform-specific Python runtime wheels using PyPI trusted publishing. - # PyPI project configuration must trust this workflow and job. Keep this - # non-blocking while the Python runtime publishing path is new; failures still - # need release follow-up, but should not invalidate the Rust release itself. - publish-python-runtime: - # Publish to PyPI for stable releases and alpha pre-releases with numeric suffixes. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_python_runtime == 'true' - }} - name: publish-python-runtime - needs: release - runs-on: ubuntu-latest - continue-on-error: true - environment: pypi - permissions: - id-token: write # Required for PyPI trusted publishing. - contents: read - - steps: - - name: Download Python runtime wheels from release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} - run: | - set -euo pipefail - python_version="$RELEASE_VERSION" - python_version="${python_version/-alpha./a}" - python_version="${python_version/-beta./b}" - python_version="${python_version/-rc./rc}" - - mkdir -p dist/python-runtime - gh release download "$RELEASE_TAG" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "openai_codex_cli_bin-${python_version}-*.whl" \ - --dir dist/python-runtime - ls -lh dist/python-runtime - - - name: Publish Python runtime wheels to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - with: - packages-dir: dist/python-runtime - skip-existing: true - - winget: - name: winget - needs: release - # Only publish stable/mainline releases to WinGet; pre-releases include a - # '-' in the semver string (e.g., 1.2.3-alpha.1). - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' && - !contains(needs.release.outputs.version, '-') - }} - # This job only invokes a GitHub Action to open/update the winget-pkgs PR; - # it does not execute Windows-only tooling, so Linux is sufficient. - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Publish to WinGet - uses: vedantmgoyal9/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5 - with: - identifier: OpenAI.Codex - version: ${{ needs.release.outputs.version }} - release-tag: ${{ needs.release.outputs.tag }} - fork-user: openai-oss-forks - installers-regex: '^codex-(?:x86_64|aarch64)-pc-windows-msvc\.exe\.zip$' - token: ${{ secrets.WINGET_PUBLISH_PAT }} - update-branch: name: Update latest-alpha-cli branch - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' - }} permissions: contents: write needs: release diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml new file mode 100644 index 000000000000..66a76aa4d938 --- /dev/null +++ b/.github/workflows/shell-tool-mcp.yml @@ -0,0 +1,461 @@ +name: shell-tool-mcp + +on: + workflow_call: + inputs: + release-version: + description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v. + required: false + type: string + release-tag: + description: Tag name to use when downloading release artifacts (defaults to rust-v). + required: false + type: string + publish: + description: Whether to publish to npm when the version is releasable. + required: false + default: true + type: boolean + +env: + NODE_VERSION: 22 + +jobs: + metadata: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.compute.outputs.version }} + release_tag: ${{ steps.compute.outputs.release_tag }} + should_publish: ${{ steps.compute.outputs.should_publish }} + npm_tag: ${{ steps.compute.outputs.npm_tag }} + steps: + - name: Compute version and tags + id: compute + run: | + set -euo pipefail + + version="${{ inputs.release-version }}" + release_tag="${{ inputs.release-tag }}" + + if [[ -z "$version" ]]; then + if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then + version="${release_tag#rust-v}" + elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then + version="${GITHUB_REF_NAME#rust-v}" + release_tag="${GITHUB_REF_NAME}" + else + echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag." + exit 1 + fi + fi + + if [[ -z "$release_tag" ]]; then + release_tag="rust-v${version}" + fi + + npm_tag="" + should_publish="false" + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + should_publish="true" + elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then + should_publish="true" + npm_tag="alpha" + fi + + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" + echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT" + echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT" + + matrix-setup: + runs-on: ubuntu-latest + outputs: + rust-binaries: ${{ steps.compute.outputs.rust-binaries }} + bash-linux: ${{ steps.compute.outputs.bash-linux }} + bash-darwin: ${{ steps.compute.outputs.bash-darwin }} + steps: + - name: Compute matrices + id: compute + shell: bash + run: | + set -euo pipefail + is_openai="${{ github.repository_owner == 'openai' }}" + + # rust-binaries: always include x86_64-musl; conditionally include paid/fork runners + if [[ "$is_openai" == "true" ]]; then + rust='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","install_musl":true} + ]' + else + rust='[{"runner":"macos-latest","target":"aarch64-apple-darwin"}]' + fi + rust=$(echo "$rust" | jq -c '. + [{"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","install_musl":true}]') + echo "rust-binaries={\"include\":$rust}" >> "$GITHUB_OUTPUT" + + # bash-linux: always include x86_64 variants; add arm64 on openai + bash_linux='[ + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-24.04","image":"ubuntu:24.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-22.04","image":"ubuntu:22.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-12","image":"debian:12"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-11","image":"debian:11"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]' + if [[ "$is_openai" == "true" ]]; then + bash_linux=$(echo "$bash_linux" | jq -c '. + [ + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-24.04","image":"arm64v8/ubuntu:24.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-22.04","image":"arm64v8/ubuntu:22.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-20.04","image":"arm64v8/ubuntu:20.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-12","image":"arm64v8/debian:12"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-11","image":"arm64v8/debian:11"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]') + fi + echo "bash-linux={\"include\":$(echo "$bash_linux" | jq -c)}" >> "$GITHUB_OUTPUT" + + # bash-darwin: always include macos-14; add macos-15-xlarge on openai + bash_darwin='[{"runner":"macos-14","target":"aarch64-apple-darwin","variant":"macos-14"}]' + if [[ "$is_openai" == "true" ]]; then + bash_darwin=$(echo "$bash_darwin" | jq -c '. + [ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin","variant":"macos-15"} + ]') + fi + echo "bash-darwin={\"include\":$(echo "$bash_darwin" | jq -c)}" >> "$GITHUB_OUTPUT" + + rust-binaries: + name: Build Rust - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + defaults: + run: + working-directory: codex-rs + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.rust-binaries) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install UBSan runtime (musl) + if: ${{ matrix.install_musl }} + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -y + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 + fi + + - uses: dtolnay/rust-toolchain@1.93 + with: + targets: ${{ matrix.target }} + + - if: ${{ matrix.install_musl }} + name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.14.0 + + - if: ${{ matrix.install_musl }} + name: Install musl build dependencies + env: + TARGET: ${{ matrix.target }} + run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh" + + - if: ${{ matrix.install_musl }} + name: Configure rustc UBSan wrapper (musl host) + shell: bash + run: | + set -euo pipefail + ubsan="" + if command -v ldconfig >/dev/null 2>&1; then + ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')" + fi + wrapper_root="${RUNNER_TEMP:-/tmp}" + wrapper="${wrapper_root}/rustc-ubsan-wrapper" + cat > "${wrapper}" <> "$GITHUB_ENV" + echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV" + + - if: ${{ matrix.install_musl }} + name: Clear sanitizer flags (musl) + shell: bash + run: | + set -euo pipefail + # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. + echo "RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV" + # Override any runner-level Cargo config rustflags as well. + echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + + sanitize_flags() { + local input="$1" + input="${input//-fsanitize=undefined/}" + input="${input//-fno-sanitize-recover=undefined/}" + input="${input//-fno-sanitize-trap=undefined/}" + echo "$input" + } + + cflags="$(sanitize_flags "${CFLAGS-}")" + cxxflags="$(sanitize_flags "${CXXFLAGS-}")" + echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" + echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + + - name: Build exec server binaries + run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper + + - name: Stage exec server binaries + run: | + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}" + mkdir -p "$dest" + cp "target/${{ matrix.target }}/release/codex-exec-mcp-server" "$dest/" + cp "target/${{ matrix.target }}/release/codex-execve-wrapper" "$dest/" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-rust-${{ matrix.target }} + path: artifacts/** + if-no-files-found: error + + bash-linux: + name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + container: + image: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-linux) }} + steps: + - name: Install build prerequisites + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext + elif command -v dnf >/dev/null 2>&1; then + dnf install -y git gcc gcc-c++ make bison autoconf gettext + elif command -v yum >/dev/null 2>&1; then + yum install -y git gcc gcc-c++ make bison autoconf gettext + else + echo "Unsupported package manager in container" + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + bash-darwin: + name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-darwin) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + package: + name: Package npm module + needs: + - metadata + - rust-binaries + - bash-linux + - bash-darwin + runs-on: ubuntu-latest + env: + PACKAGE_VERSION: ${{ needs.metadata.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install JavaScript dependencies + run: pnpm install --frozen-lockfile + + - name: Build (shell-tool-mcp) + run: pnpm --filter @openai/codex-shell-tool-mcp run build + + - name: Download build artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts + + - name: Assemble staging directory + id: staging + shell: bash + run: | + set -euo pipefail + staging="${STAGING_DIR}" + mkdir -p "$staging" "$staging/vendor" + cp shell-tool-mcp/README.md "$staging/" + cp shell-tool-mcp/package.json "$staging/" + cp -R shell-tool-mcp/bin "$staging/" + + found_vendor="false" + shopt -s nullglob + for vendor_dir in artifacts/*/vendor; do + rsync -av "$vendor_dir/" "$staging/vendor/" + found_vendor="true" + done + if [[ "$found_vendor" == "false" ]]; then + echo "No vendor payloads were downloaded." + exit 1 + fi + + node - <<'NODE' + import fs from "node:fs"; + import path from "node:path"; + + const stagingDir = process.env.STAGING_DIR; + const version = process.env.PACKAGE_VERSION; + const pkgPath = path.join(stagingDir, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + pkg.version = version; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + NODE + + echo "dir=$staging" >> "$GITHUB_OUTPUT" + env: + STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp + + - name: Ensure binaries are executable + run: | + set -euo pipefail + staging="${{ steps.staging.outputs.dir }}" + chmod +x \ + "$staging"/vendor/*/codex-exec-mcp-server \ + "$staging"/vendor/*/codex-execve-wrapper \ + "$staging"/vendor/*/bash/*/bash + + - name: Create npm tarball + shell: bash + run: | + set -euo pipefail + mkdir -p dist/npm + staging="${{ steps.staging.outputs.dir }}" + pack_info=$(cd "$staging" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm") + filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);') + mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz" + + - uses: actions/upload-artifact@v6 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz + if-no-files-found: error + + publish: + name: Publish npm package + needs: + - metadata + - package + if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }} + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: https://registry.npmjs.org + scope: "@openai" + + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + + - name: Download npm tarball + uses: actions/download-artifact@v7 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm + + - name: Publish to npm + if: github.repository == 'openai/codex' + env: + NPM_TAG: ${{ needs.metadata.outputs.npm_tag }} + VERSION: ${{ needs.metadata.outputs.version }} + shell: bash + run: | + set -euo pipefail + tag_args=() + if [[ -n "${NPM_TAG}" ]]; then + tag_args+=(--tag "${NPM_TAG}") + fi + npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}" diff --git a/.github/workflows/termux-release-checkpoint.yml b/.github/workflows/termux-release-checkpoint.yml new file mode 100644 index 000000000000..0347e6f1b3d6 --- /dev/null +++ b/.github/workflows/termux-release-checkpoint.yml @@ -0,0 +1,103 @@ +name: termux-release-checkpoint + +on: + workflow_dispatch: + inputs: + source_branch: + description: "Release branch to checkpoint from, for example release/0.123.0" + required: false + type: string + default: "" + source_sha: + description: "Specific source commit SHA to checkpoint; defaults to the source branch tip" + required: false + type: string + default: "" + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + type: string + default: "wallentx/termux-target" + reviewer: + description: "GitHub username to request as reviewer" + required: false + type: string + default: "wallentx" + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-checkpoint-${{ inputs.source_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + checkpoint: + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + env: + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + REQUESTED_SOURCE_BRANCH: ${{ inputs.source_branch }} + REQUESTED_SOURCE_SHA: ${{ inputs.source_sha }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ inputs.source_branch || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + source_branch="${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${source_branch}" + + - name: Create checkpoint PR + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" diff --git a/.github/workflows/termux-release-deploy.yml b/.github/workflows/termux-release-deploy.yml new file mode 100644 index 000000000000..2071611dacc4 --- /dev/null +++ b/.github/workflows/termux-release-deploy.yml @@ -0,0 +1,243 @@ +name: termux-release-deploy + +on: + push: + branches: + - "release/**" + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to deploy, for example release/0.124.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to deploy. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged release PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged release PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + default: "wallentx/termux-target" + type: string + reviewer: + description: "GitHub username to request as reviewer on the checkpoint PR" + required: false + default: "wallentx" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-deploy-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + deploy: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' || !startsWith(github.event.head_commit.message, 'Seed Termux release automation') }} + permissions: + actions: read + contents: write + deployments: write + issues: write + pull-requests: write + env: + REQUESTED_RELEASE_BRANCH: ${{ inputs.release_branch }} + REQUESTED_RELEASE_SHA: ${{ inputs.release_sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + release_branch="${REQUESTED_RELEASE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${release_branch}" + + - name: Resolve release ref + id: release-ref + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: deploy + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Create deployment + if: steps.metadata.outputs.deploy == 'true' + id: deployment + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + deployment_id="$( + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments" \ + -f ref="${RELEASE_SHA}" \ + -f environment="termux-release" \ + -F auto_merge=false \ + -F required_contexts[] \ + -f description="Termux release deployment for ${TERMUX_TAG}" \ + --jq '.id' + )" + echo "id=${deployment_id}" >> "$GITHUB_OUTPUT" + + - name: Mark deployment in progress + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="in_progress" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Promoting Termux release artifact and preparing checkpoint PR" \ + >/dev/null + + - name: Locate merged pull request + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" + + - name: Ensure checkpoint PR + if: steps.metadata.outputs.deploy == 'true' + id: checkpoint + env: + SOURCE_BRANCH: ${{ steps.release-ref.outputs.branch }} + SOURCE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" + + - name: Mark deployment success + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="success" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment completed for ${TERMUX_TAG}" \ + >/dev/null + + - name: Mark deployment failure + if: failure() && steps.deployment.outputs.id != '' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="failure" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment failed" \ + >/dev/null diff --git a/.github/workflows/termux-release-promote.yml b/.github/workflows/termux-release-promote.yml new file mode 100644 index 000000000000..22c277c21a70 --- /dev/null +++ b/.github/workflows/termux-release-promote.yml @@ -0,0 +1,135 @@ +name: termux-release-promote + +on: + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to promote, for example release/0.122.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to promote. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-promote-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + promote: + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: write + pull-requests: read + env: + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Resolve release ref + id: release-ref + env: + INPUT_RELEASE_BRANCH: ${{ inputs.release_branch }} + INPUT_RELEASE_SHA: ${{ inputs.release_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: promote + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Locate merged pull request + if: steps.metadata.outputs.promote == 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.promote == 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.promote == 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" diff --git a/scripts/termux-configure-git.sh b/scripts/termux-configure-git.sh new file mode 100755 index 000000000000..fae3abcbab48 --- /dev/null +++ b/scripts/termux-configure-git.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Configure the explicit bot identity used by release automation and fetch any +# refs requested by the calling workflow. + +set -euo pipefail + +git config user.name "github-actions[bot]" +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + +while (($#)); do + case "$1" in + --origin) + shift + origin_refs=() + while (($#)) && [[ "$1" != --* ]]; do + origin_refs+=("$1") + shift + done + if ((${#origin_refs[@]})); then + git fetch --prune origin "${origin_refs[@]}" + fi + ;; + --upstream-tag) + if (($# < 3)); then + echo "--upstream-tag requires ." >&2 + exit 1 + fi + upstream_repo="$2" + upstream_tag="$3" + git remote add upstream "https://github.com/${upstream_repo}.git" 2>/dev/null || true + git fetch --prune --no-tags upstream "+refs/tags/${upstream_tag}:refs/tags/${upstream_tag}" + shift 3 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done diff --git a/scripts/termux-create-checkpoint-pr.sh b/scripts/termux-create-checkpoint-pr.sh new file mode 100755 index 000000000000..f315a5b91822 --- /dev/null +++ b/scripts/termux-create-checkpoint-pr.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/termux-release-paths.sh +source "${script_dir}/termux-release-paths.sh" + +source_branch="${SOURCE_BRANCH:-${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}}" +source_sha="${SOURCE_SHA:-${REQUESTED_SOURCE_SHA:-}}" +if [[ -z "${source_sha}" ]]; then + if [[ "${GITHUB_EVENT_NAME:-}" == "push" && "${source_branch}" == "${GITHUB_REF_NAME:-}" ]]; then + source_sha="${GITHUB_SHA}" + else + source_sha="$(git rev-parse "origin/${source_branch}")" + fi +fi + +if [[ -z "${DESTINATION_BRANCH:-}" ]]; then + echo "DESTINATION_BRANCH is required." >&2 + exit 1 +fi + +release_only_checkpoint_paths() { + printf '%s\n' "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +} + +resolve_source_version_conflicts() { + local path="$1" + local resolved_path + resolved_path="$(mktemp)" + + if ! awk ' + function normalize_versions(text) { + gsub(/version = "[^"]+"/, "version = \"\"", text) + return text + } + + BEGIN { + in_block = 0 + side = "" + ours = "" + theirs = "" + blocks = 0 + } + + /^<<<<<<< / { + if (in_block) { + exit 1 + } + in_block = 1 + side = "ours" + ours = "" + theirs = "" + blocks++ + next + } + + /^=======$/ && in_block { + side = "theirs" + next + } + + /^>>>>>>> / && in_block { + if (normalize_versions(ours) != normalize_versions(theirs)) { + exit 1 + } + printf "%s", theirs + in_block = 0 + side = "" + next + } + + { + if (!in_block) { + print + } else if (side == "ours") { + ours = ours $0 ORS + } else if (side == "theirs") { + theirs = theirs $0 ORS + } else { + exit 1 + } + } + + END { + if (in_block || blocks == 0) { + exit 1 + } + } + ' "${path}" > "${resolved_path}"; then + rm -f "${resolved_path}" + return 1 + fi + + cp "${resolved_path}" "${path}" + rm -f "${resolved_path}" +} + +short_sha="${source_sha:0:12}" +source_slug="${source_branch//\//_}" +dest_slug="${DESTINATION_BRANCH//\//_}" +checkpoint_branch="checkpoint/${dest_slug}_from_${source_slug}_${short_sha}" +pr_title="checkpoint: into ${DESTINATION_BRANCH} from ${source_branch} @ ${short_sha}" +merge_conflicted=false +conflict_summary="" + +existing_pr="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --head "${checkpoint_branch}" \ + --state all \ + --json number,state,mergedAt,url \ + --jq '[.[] | select(.state == "OPEN" or .mergedAt != null)] | .[0] // empty' +)" +if [[ -n "${existing_pr}" ]]; then + existing_url="$(jq -r '.url' <<< "${existing_pr}")" + existing_state="$(jq -r '.state' <<< "${existing_pr}")" + echo "Checkpoint PR already exists for ${checkpoint_branch}: ${existing_url} (${existing_state})." + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${existing_url}" >> "${GITHUB_OUTPUT}" + fi + exit 0 +fi + +git checkout -B "${checkpoint_branch}" "origin/${DESTINATION_BRANCH}" + +if ! git merge --no-ff --no-edit "${source_sha}"; then + mapfile -t conflicted_paths < <(git diff --name-only --diff-filter=U) + for conflicted_path in "${conflicted_paths[@]}"; do + if termux_is_checkpoint_release_only_path "${conflicted_path}"; then + echo "Auto-resolving release-only checkpoint conflict in ${conflicted_path} by keeping ${DESTINATION_BRANCH}." + if git cat-file -e "HEAD:${conflicted_path}" 2>/dev/null; then + git checkout --ours -- "${conflicted_path}" + git add "${conflicted_path}" + else + git rm -f --ignore-unmatch "${conflicted_path}" + fi + fi + done + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if ((${#remaining_conflicts[@]})); then + cargo_version_conflicts=true + for remaining_conflict in "${remaining_conflicts[@]}"; do + case "${remaining_conflict}" in + codex-rs/Cargo.toml|codex-rs/Cargo.lock) + ;; + *) + cargo_version_conflicts=false + ;; + esac + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + for remaining_conflict in "${remaining_conflicts[@]}"; do + if ! resolve_source_version_conflicts "${remaining_conflict}"; then + cargo_version_conflicts=false + break + fi + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + echo "Auto-resolving recurring Cargo version checkpoint conflicts by keeping ${source_branch} versions." + git add -- "${remaining_conflicts[@]}" + fi + fi + fi + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if [[ "${#remaining_conflicts[@]}" -eq 0 ]]; then + git commit --no-edit + else + merge_conflicted=true + conflict_summary="$( + printf '%s\n' "${remaining_conflicts[@]}" | awk '{ print "- `" $0 "`" }' + )" + echo "Automatic checkpoint merge failed; creating a manual-resolution PR instead." >&2 + if git rev-parse -q --verify MERGE_HEAD >/dev/null; then + git merge --abort + fi + git checkout -B "${checkpoint_branch}" "${source_sha}" + fi +fi + +if git cat-file -e "origin/${DESTINATION_BRANCH}:.github" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- .github + mapfile -t added_github_paths < <( + git diff --name-only --diff-filter=A "origin/${DESTINATION_BRANCH}" -- .github + ) + if ((${#added_github_paths[@]})); then + git rm -f --ignore-unmatch -- "${added_github_paths[@]}" + fi +else + git rm -r --ignore-unmatch .github +fi + +while IFS= read -r release_only_path; do + if git cat-file -e "origin/${DESTINATION_BRANCH}:${release_only_path}" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- "${release_only_path}" + else + git rm -f --ignore-unmatch -- "${release_only_path}" + fi +done < <(release_only_checkpoint_paths) + +if ! git diff --quiet || ! git diff --cached --quiet; then + if [[ -e .github || -L .github ]] || git ls-files --error-unmatch -- .github >/dev/null 2>&1; then + git add -A .github + fi + while IFS= read -r release_only_path; do + if [[ -e "${release_only_path}" || -L "${release_only_path}" ]] || git ls-files --error-unmatch -- "${release_only_path}" >/dev/null 2>&1; then + git add -A -- "${release_only_path}" + fi + done < <(release_only_checkpoint_paths) + if [[ "${merge_conflicted}" == "true" ]]; then + git commit -m "checkpoint: prepare ${source_branch} for ${DESTINATION_BRANCH}" + else + git commit --amend --no-edit + fi +fi + +if git diff --quiet "origin/${DESTINATION_BRANCH}" HEAD; then + echo "Checkpoint merge produced no destination changes after release-only files were restored." + exit 0 +fi + +git push --force-with-lease origin "${checkpoint_branch}" + +remaining="$( + git log --first-parent --pretty=format:%H "${source_sha}..origin/${source_branch}" | wc -w +)" + +body_path="${RUNNER_TEMP}/termux-checkpoint-pr.md" +{ + echo "## Termux release checkpoint" + echo + echo "- Source branch: \`${source_branch}\`" + echo "- Source hash: \`${source_sha}\`" + echo "- Destination branch: \`${DESTINATION_BRANCH}\`" + echo "- Remaining first-parent commits on source: ${remaining}" + echo + echo "This PR carries release-train conflict fixes and follow-up changes back into the reusable Termux patch branch." + if [[ "${merge_conflicted}" == "true" ]]; then + echo + echo "## Merge conflicts" + echo + echo "GitHub Actions could not create the checkpoint merge commit automatically, so this PR was created from the source branch state for manual conflict resolution." + echo + echo "Conflicted paths from the failed merge attempt:" + if [[ -n "${conflict_summary}" ]]; then + printf '%s\n' "${conflict_summary}" + else + echo "- Conflict details unavailable" + fi + fi + echo + echo "Release-only workflow files and metadata under \`.github\` were restored to the destination branch versions before opening this PR." +} > "${body_path}" + +pr_url="$( + gh pr create \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${DESTINATION_BRANCH}" \ + --head "${checkpoint_branch}" \ + --title "${pr_title}" \ + --body-file "${body_path}" +)" +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-reviewer "${REVIEWER}" || true +gh label create checkpoint --repo "${GITHUB_REPOSITORY}" --color c5def5 --description "Checkpoint merge" --force +gh label create termux-release --repo "${GITHUB_REPOSITORY}" --color 0e8a16 --description "Termux release automation" --force +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-label "checkpoint" --add-label "termux-release" + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${pr_url}" >> "${GITHUB_OUTPUT}" +fi diff --git a/scripts/termux-create-or-update-mirrored-release.sh b/scripts/termux-create-or-update-mirrored-release.sh new file mode 100755 index 000000000000..5787e894582b --- /dev/null +++ b/scripts/termux-create-or-update-mirrored-release.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +# Create a mirrored Termux release or repair an existing release that is missing +# the Android tarball. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" +: "${RUNNER_TEMP:?RUNNER_TEMP is required}" +: "${UPSTREAM_TAG:?UPSTREAM_TAG is required}" +: "${UPSTREAM_REPO:?UPSTREAM_REPO is required}" +: "${UPSTREAM_NAME?UPSTREAM_NAME is required}" +: "${TERMUX_TAG:?TERMUX_TAG is required}" +: "${UPSTREAM_PRERELEASE:?UPSTREAM_PRERELEASE is required}" +: "${UPSTREAM_HTML_URL?UPSTREAM_HTML_URL is required}" +: "${RELEASE_TRAIN?RELEASE_TRAIN is required}" +: "${RELEASE_EXISTS:?RELEASE_EXISTS is required}" +: "${PR_NUMBER:?PR_NUMBER is required}" +: "${HEAD_SHA:?HEAD_SHA is required}" +: "${RELEASE_SHA:?RELEASE_SHA is required}" + +promoted_dir="${PROMOTED_DIR:-promoted}" +asset_path="${promoted_dir}/codex-aarch64-linux-android.tar.gz" + +body_path="${RUNNER_TEMP}/release-body.md" +upstream_body_path="${RUNNER_TEMP}/upstream-release-body.md" +upstream_body_without_changelog_path="${RUNNER_TEMP}/upstream-release-body-without-changelog.md" +if gh release view "${UPSTREAM_TAG}" \ + --repo "${UPSTREAM_REPO}" \ + --json body \ + --jq '.body // ""' > "${upstream_body_path}"; then + awk ' + function heading_level(line, text) { + if (match(line, /^(#{1,6})[[:space:]]+(.+)$/, parts)) { + text = tolower(parts[2]) + sub(/[[:space:]]+#+[[:space:]]*$/, "", text) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", text) + if (text == "changelog" || text == "change log") { + return length(parts[1]) + } + } + return 0 + } + + { + if (!skip) { + level = heading_level($0) + if (level > 0) { + skip = 1 + skip_level = level + next + } + print + next + } + + if (match($0, /^(#{1,6})[[:space:]]+/, parts) && length(parts[1]) <= skip_level) { + skip = 0 + print + } + } + ' "${upstream_body_path}" > "${upstream_body_without_changelog_path}" +else + echo "::warning title=Upstream release notes unavailable::Could not read ${UPSTREAM_REPO} release ${UPSTREAM_TAG}." + : > "${upstream_body_without_changelog_path}" +fi + +{ + echo "Termux Android build for ${UPSTREAM_TAG}." + echo + echo "- Upstream release: ${UPSTREAM_HTML_URL}" + echo "- Release train: \`${RELEASE_TRAIN}\`" + echo "- Promoted PR: #${PR_NUMBER}" + echo "- Promoted PR head SHA: \`${HEAD_SHA}\`" +} > "${body_path}" +if [[ -s "${upstream_body_without_changelog_path}" ]]; then + { + echo + echo "## Upstream release notes" + echo + cat "${upstream_body_without_changelog_path}" + } >> "${body_path}" +fi + +release_title="${UPSTREAM_NAME}" +if [[ -z "${release_title}" || "${release_title}" == "null" ]]; then + release_title="${TERMUX_TAG}" +fi + +if [[ "${RELEASE_EXISTS}" == "true" ]]; then + gh release upload \ + "${TERMUX_TAG}" \ + "${asset_path}#codex-termux" \ + --repo "${GITHUB_REPOSITORY}" \ + --clobber + exit 0 +fi + +release_args=( + gh release create "${TERMUX_TAG}" + --repo "${GITHUB_REPOSITORY}" + --target "${RELEASE_SHA}" + --title "${release_title}" + --notes-file "${body_path}" +) +if [[ "${UPSTREAM_PRERELEASE}" == "true" ]]; then + release_args+=(--prerelease) +fi +release_args+=("${asset_path}#codex-termux") +"${release_args[@]}" diff --git a/scripts/termux-download-release-artifact.sh b/scripts/termux-download-release-artifact.sh new file mode 100755 index 000000000000..2acdba9025a0 --- /dev/null +++ b/scripts/termux-download-release-artifact.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${HEAD_SHA:-}" ]]; then + echo "HEAD_SHA is required." >&2 + exit 1 +fi + +if [[ -z "${PR_ARTIFACT_NAME:-}" ]]; then + echo "PR_ARTIFACT_NAME is required." >&2 + exit 1 +fi + +promoted_dir="${PROMOTED_DIR:-promoted}" + +find_run_id() { + local event="$1" + gh run list \ + --repo "${GITHUB_REPOSITORY}" \ + --workflow rust-release.yml \ + --event "${event}" \ + --status success \ + --commit "${HEAD_SHA}" \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId // empty' +} + +find_artifact_run_id() { + local artifact_name="$1" + + gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=${artifact_name}" \ + | jq -rs \ + --arg artifact_name "${artifact_name}" \ + --arg head_sha "${HEAD_SHA}" \ + ' + [ + .[].artifacts[] + | select(.name == $artifact_name) + | select(.expired == false) + | select(.workflow_run.head_sha == $head_sha) + ] + | sort_by(.created_at) + | reverse + | .[0].workflow_run.id // empty + ' +} + +artifact_name="${PR_ARTIFACT_NAME}" +run_id="$(find_artifact_run_id "${artifact_name}")" +mkdir -p "${promoted_dir}" +if [[ -n "${run_id}" ]] && gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}"; then + : +else + artifact_name="aarch64-linux-android" + run_id="$(find_run_id workflow_dispatch)" + if [[ -z "${run_id}" ]]; then + echo "No successful rust-release run found for ${HEAD_SHA}" >&2 + exit 1 + fi + gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}" +fi + +ls -la "${promoted_dir}" +if [[ -f "${promoted_dir}/SHA256SUMS" ]]; then + (cd "${promoted_dir}" && sha256sum -c SHA256SUMS) +fi +if [[ ! -f "${promoted_dir}/codex-aarch64-linux-android.tar.gz" ]]; then + echo "Expected ${promoted_dir}/codex-aarch64-linux-android.tar.gz in the downloaded artifact." >&2 + exit 1 +fi diff --git a/scripts/termux-find-release-pr.sh b/scripts/termux-find-release-pr.sh new file mode 100755 index 000000000000..3e35fa42eabe --- /dev/null +++ b/scripts/termux-find-release-pr.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -n "${INPUT_PR_NUMBER:-}" || -n "${INPUT_PR_HEAD_SHA:-}" ]]; then + if [[ -z "${INPUT_PR_NUMBER:-}" || -z "${INPUT_PR_HEAD_SHA:-}" ]]; then + echo "workflow_dispatch inputs pr_number and pr_head_sha must be provided together." >&2 + exit 1 + fi + + pr_number="${INPUT_PR_NUMBER}" + head_sha="${INPUT_PR_HEAD_SHA}" + head_ref="" +else + if [[ -z "${RELEASE_BRANCH:-}" || -z "${RELEASE_SHA:-}" ]]; then + echo "RELEASE_BRANCH and RELEASE_SHA are required." >&2 + exit 1 + fi + + pr_json="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${RELEASE_BRANCH}" \ + --state merged \ + --limit 100 \ + --json number,headRefOid,headRefName,mergeCommit \ + --jq "[.[] | select(.mergeCommit.oid == \"${RELEASE_SHA}\")] | sort_by(.number) | reverse | .[0] // empty" + )" + if [[ -z "${pr_json}" ]]; then + echo "Unable to find merged PR for ${RELEASE_SHA} into ${RELEASE_BRANCH}" >&2 + exit 1 + fi + + pr_number="$(jq -r '.number' <<< "${pr_json}")" + head_sha="$(jq -r '.headRefOid // .head.sha' <<< "${pr_json}")" + head_ref="$(jq -r '.headRefName // .head.ref' <<< "${pr_json}")" +fi + +artifact_name="termux-android-pr-${pr_number}-${head_sha}" +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "number=${pr_number}" + echo "head_sha=${head_sha}" + echo "head_ref=${head_ref}" + echo "artifact_name=${artifact_name}" + } >> "${GITHUB_OUTPUT}" +else + printf 'number=%s\nhead_sha=%s\nhead_ref=%s\nartifact_name=%s\n' \ + "${pr_number}" \ + "${head_sha}" \ + "${head_ref}" \ + "${artifact_name}" +fi diff --git a/scripts/termux-read-release-metadata.sh b/scripts/termux-read-release-metadata.sh new file mode 100755 index 000000000000..519a0635a368 --- /dev/null +++ b/scripts/termux-read-release-metadata.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# Parse .github/termux-release.json and emit workflow outputs for deploy or +# promote jobs. TERMUX_RELEASE_ACTION must be "deploy" or "promote". + +set -euo pipefail + +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +metadata=".github/termux-release.json" +action="${TERMUX_RELEASE_ACTION:-deploy}" +case "${action}" in + deploy) + missing_context="deployment" + ;; + promote) + missing_context="promotion" + ;; + *) + echo "TERMUX_RELEASE_ACTION must be deploy or promote." >&2 + exit 1 + ;; +esac + +if [[ ! -f "${metadata}" ]]; then + echo "No ${metadata}; this push is not a Termux release ${missing_context}." + echo "${action}=false" >> "${GITHUB_OUTPUT}" + exit 0 +fi + +upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" +upstream_name="$(jq -r '.upstream_name // .upstream_tag // empty' "${metadata}")" +termux_tag="$(jq -r '.termux_tag // empty' "${metadata}")" +upstream_version="${upstream_tag#rust-v}" +upstream_version="${upstream_version%-termux}" +upstream_prerelease=false +if [[ "${upstream_version}" == *-* ]]; then + upstream_prerelease=true +fi +upstream_html_url="$(jq -r '.upstream_html_url // ""' "${metadata}")" +upstream_repo="$(jq -r '.upstream_repo // "openai/codex"' "${metadata}")" +release_train="$(jq -r '.release_train // ""' "${metadata}")" +if [[ -z "${upstream_tag}" || -z "${termux_tag}" ]]; then + echo "Missing upstream_tag or termux_tag in ${metadata}" >&2 + exit 1 +fi + +release_state="$(TERMUX_TAG="${termux_tag}" "${script_dir}/termux-release-asset-state.sh")" +release_exists="$(awk -F= '$1 == "release_exists" { print $2 }' <<< "${release_state}")" +asset_exists="$(awk -F= '$1 == "asset_exists" { print $2 }' <<< "${release_state}")" + +if [[ "${action}" == "promote" ]]; then + if [[ "${asset_exists}" == "true" ]]; then + echo "${termux_tag} already exists with codex-aarch64-linux-android.tar.gz; skipping promotion." + echo "promote=false" >> "${GITHUB_OUTPUT}" + exit 0 + fi + if [[ "${release_exists}" == "true" ]]; then + echo "${termux_tag} exists but is missing codex-aarch64-linux-android.tar.gz; repairing promotion." + fi +fi + +{ + echo "${action}=true" + echo "upstream_tag=${upstream_tag}" + echo "upstream_name=${upstream_name}" + echo "termux_tag=${termux_tag}" + echo "upstream_prerelease=${upstream_prerelease}" + echo "upstream_html_url=${upstream_html_url}" + echo "upstream_repo=${upstream_repo}" + echo "release_train=${release_train}" + echo "release_exists=${release_exists}" + echo "asset_exists=${asset_exists}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-release-asset-state.sh b/scripts/termux-release-asset-state.sh new file mode 100755 index 000000000000..8694480b6a36 --- /dev/null +++ b/scripts/termux-release-asset-state.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +termux_tag="${1:-${TERMUX_TAG:-}}" +release_repo="${RELEASE_REPO:-${GITHUB_REPOSITORY:-}}" +asset_name="${ASSET_NAME:-codex-aarch64-linux-android.tar.gz}" + +if [[ -z "${termux_tag}" ]]; then + echo "TERMUX_TAG or tag argument is required." >&2 + exit 1 +fi + +if [[ -z "${release_repo}" ]]; then + echo "GITHUB_REPOSITORY or RELEASE_REPO is required." >&2 + exit 1 +fi + +release_exists=false +asset_exists=false + +if gh release view "${termux_tag}" --repo "${release_repo}" >/dev/null 2>&1; then + release_exists=true + release_asset_exists="$( + gh release view "${termux_tag}" \ + --repo "${release_repo}" \ + --json assets \ + | jq -r --arg asset_name "${asset_name}" \ + '.assets | map(.name) | any(. == $asset_name)' + )" + if [[ "${release_asset_exists}" == "true" ]]; then + asset_exists=true + fi +fi + +printf 'release_exists=%s\n' "${release_exists}" +printf 'asset_exists=%s\n' "${asset_exists}" diff --git a/scripts/termux-release-paths.sh b/scripts/termux-release-paths.sh new file mode 100755 index 000000000000..6990f323ec44 --- /dev/null +++ b/scripts/termux-release-paths.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# Sourceable path lists for files owned by the Termux release automation. + +set -euo pipefail + +readonly -a TERMUX_RELEASE_WORKFLOW_PATHS=( + .github/workflows/rust-release.yml + .github/workflows/shell-tool-mcp.yml + .github/workflows/termux-release-checkpoint.yml + .github/workflows/termux-release-deploy.yml + .github/workflows/termux-release-promote.yml +) + +readonly -a TERMUX_RELEASE_BRANCH_SCRIPT_PATHS=( + scripts/termux-configure-git.sh + scripts/termux-create-checkpoint-pr.sh + scripts/termux-create-or-update-mirrored-release.sh + scripts/termux-download-release-artifact.sh + scripts/termux-find-release-pr.sh + scripts/termux-read-release-metadata.sh + scripts/termux-release-asset-state.sh + scripts/termux-release-paths.sh + scripts/termux-resolve-release-ref.sh + scripts/termux-validate-gh-env.sh +) + +readonly -a TERMUX_RELEASE_AUTOMATION_PATHS=( + "${TERMUX_RELEASE_WORKFLOW_PATHS[@]}" + "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +) + +readonly -a TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS=( + "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" + .github/termux-release.json +) + +termux_path_in_list() { + local candidate="$1" + shift + local listed_path + + for listed_path in "$@"; do + [[ "${candidate}" != "${listed_path}" ]] || return 0 + done + return 1 +} + +termux_is_release_automation_path() { + termux_path_in_list "$1" "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" +} + +termux_is_checkpoint_release_only_path() { + termux_path_in_list "$1" "${TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS[@]}" +} diff --git a/scripts/termux-resolve-release-ref.sh b/scripts/termux-resolve-release-ref.sh new file mode 100755 index 000000000000..194773d45bed --- /dev/null +++ b/scripts/termux-resolve-release-ref.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Resolve the release branch and SHA for deploy/promote workflows. + +set -euo pipefail + +: "${GITHUB_EVENT_NAME:?GITHUB_EVENT_NAME is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" + +input_release_branch="${INPUT_RELEASE_BRANCH:-${REQUESTED_RELEASE_BRANCH:-}}" +input_release_sha="${INPUT_RELEASE_SHA:-${REQUESTED_RELEASE_SHA:-}}" + +if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then + if [[ -z "${input_release_branch}" ]]; then + echo "release_branch is required for workflow_dispatch." >&2 + exit 1 + fi + + release_branch="${input_release_branch}" + if [[ -n "${input_release_sha}" ]]; then + git checkout --detach "${input_release_sha}" + release_sha="${input_release_sha}" + else + release_sha="$(git rev-parse HEAD)" + fi +else + : "${GITHUB_REF_NAME:?GITHUB_REF_NAME is required}" + : "${GITHUB_SHA:?GITHUB_SHA is required}" + release_branch="${GITHUB_REF_NAME}" + release_sha="${GITHUB_SHA}" +fi + +{ + echo "branch=${release_branch}" + echo "sha=${release_sha}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-validate-gh-env.sh b/scripts/termux-validate-gh-env.sh new file mode 100755 index 000000000000..02581fdebceb --- /dev/null +++ b/scripts/termux-validate-gh-env.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Lightweight post-toolbox check for jobs that rely on authenticated gh calls. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +command -v gh +gh auth status --hostname github.com + +printf 'GITHUB_REPOSITORY=%s\n' "${GITHUB_REPOSITORY}" +printf 'REPO=%s\n' "${REPO:-}" +printf 'GH_REPO_URL=%s\n' "${GH_REPO_URL:-}" +printf 'GH_WORKFLOW_URL=%s\n' "${GH_WORKFLOW_URL:-}" From 225559739cc92c147a3a66311e41d885d3c8a98d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 19:01:41 +0000 Subject: [PATCH 16/25] Prepare Termux rust-v0.133.0 --- .github/termux-release.json | 15 + .github/workflows/rust-release.yml | 1798 +++++++---------- .github/workflows/shell-tool-mcp.yml | 461 +++++ .../workflows/termux-release-checkpoint.yml | 103 + .github/workflows/termux-release-deploy.yml | 243 +++ .github/workflows/termux-release-promote.yml | 135 ++ codex-rs/Cargo.toml | 2 +- scripts/termux-configure-git.sh | 40 + scripts/termux-create-checkpoint-pr.sh | 275 +++ ...ermux-create-or-update-mirrored-release.sh | 111 + scripts/termux-download-release-artifact.sh | 78 + scripts/termux-find-release-pr.sh | 53 + scripts/termux-read-release-metadata.sh | 77 + scripts/termux-release-asset-state.sh | 37 + scripts/termux-release-paths.sh | 55 + scripts/termux-resolve-release-ref.sh | 36 + scripts/termux-validate-gh-env.sh | 16 + 17 files changed, 2460 insertions(+), 1075 deletions(-) create mode 100644 .github/termux-release.json create mode 100644 .github/workflows/shell-tool-mcp.yml create mode 100644 .github/workflows/termux-release-checkpoint.yml create mode 100644 .github/workflows/termux-release-deploy.yml create mode 100644 .github/workflows/termux-release-promote.yml create mode 100755 scripts/termux-configure-git.sh create mode 100755 scripts/termux-create-checkpoint-pr.sh create mode 100755 scripts/termux-create-or-update-mirrored-release.sh create mode 100755 scripts/termux-download-release-artifact.sh create mode 100755 scripts/termux-find-release-pr.sh create mode 100755 scripts/termux-read-release-metadata.sh create mode 100755 scripts/termux-release-asset-state.sh create mode 100755 scripts/termux-release-paths.sh create mode 100755 scripts/termux-resolve-release-ref.sh create mode 100755 scripts/termux-validate-gh-env.sh diff --git a/.github/termux-release.json b/.github/termux-release.json new file mode 100644 index 000000000000..ee27af17fe2f --- /dev/null +++ b/.github/termux-release.json @@ -0,0 +1,15 @@ +{ + "upstream_repo": "openai/codex", + "upstream_tag": "rust-v0.133.0", + "upstream_name": "0.133.0", + "upstream_html_url": "https://github.com/openai/codex/releases/tag/rust-v0.133.0", + "upstream_target": "main", + "upstream_release_id": "327109656", + "upstream_prerelease": false, + "release_train": "0.133.0", + "release_branch": "release/0.133.0", + "work_branch": "upstream/rust-v0.133.0", + "patch_branch": "wallentx/termux-target", + "patch_source_sha": "e9d18f72507bc0f66e761990996e27a6c1a9fdb1", + "termux_tag": "rust-v0.133.0-termux" +} diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index c55337ecfe6e..56d0977e6e67 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -4,46 +4,49 @@ # git tag -a rust-v0.1.0 -m "Release 0.1.0" # git push origin rust-v0.1.0 # ``` -# -# To use external macOS signing, manually dispatch `release_mode=build_unsigned`, -# sign the unsigned macOS artifacts in a secure enclave, upload the signed handoff -# archive as a GitHub Release asset, then manually dispatch -# `release_mode=promote_signed` with `unsigned_run_id` and `signed_macos_asset`. -# The signed handoff archive should contain target or artifact directories such -# as `aarch64-apple-darwin/` with signed binaries. name: rust-release on: - push: - tags: - - "rust-v*.*.*" + pull_request: + branches: + - "release/**" workflow_dispatch: inputs: - release_mode: - description: "build_unsigned creates unsigned macOS handoff artifacts; promote_signed finishes a release from signed macOS handoff artifacts." + source_ref: + description: "Branch, tag, or commit SHA to build (code only; keeps .github from dispatched ref)" required: false + type: string + default: "" + build_target: + description: "Target to build" + required: true type: choice - default: build_unsigned + default: all options: - - build_unsigned - - promote_signed - sign_macos: - description: "Deprecated compatibility input; use release_mode instead." - required: false - type: boolean - default: false - unsigned_run_id: - description: "For promote_signed: workflow run id from the build_unsigned run." - required: false - type: string - signed_macos_asset: - description: "For promote_signed: exact GitHub Release asset name containing signed macOS handoff artifacts." - required: false - type: string - signed_macos_sha256: - description: "For promote_signed: optional SHA-256 of signed_macos_asset." - required: false - type: string + - all + - aarch64-apple-darwin + - x86_64-apple-darwin + - x86_64-unknown-linux-musl + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-musl + - aarch64-unknown-linux-gnu + - aarch64-linux-android + - x86_64-pc-windows-msvc + - aarch64-pc-windows-msvc + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read concurrency: group: ${{ github.workflow }} @@ -51,72 +54,26 @@ concurrency: jobs: tag-check: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - uses: dtolnay/rust-toolchain@1.92 - name: Validate tag matches Cargo.toml version shell: bash - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - REQUESTED_SIGN_MACOS: ${{ inputs.sign_macos }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} run: | set -euo pipefail echo "::group::Tag validation" - case "${RELEASE_MODE}" in - signed) - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - echo "❌ Manual rust-release runs must use release_mode=build_unsigned or release_mode=promote_signed" - exit 1 - fi - ;; - build_unsigned) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=build_unsigned is only valid for manual runs" - exit 1 - fi - ;; - promote_signed) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=promote_signed is only valid for manual runs" - exit 1 - fi - if [[ ! "${UNSIGNED_RUN_ID}" =~ ^[0-9]+$ ]]; then - echo "❌ release_mode=promote_signed requires unsigned_run_id to be a workflow run id" - exit 1 - fi - if [[ -z "${SIGNED_MACOS_ASSET}" ]]; then - echo "❌ release_mode=promote_signed requires signed_macos_asset" - exit 1 - fi - if [[ "${SIGNED_MACOS_ASSET}" == */* || "${SIGNED_MACOS_ASSET}" == *"*"* || "${SIGNED_MACOS_ASSET}" == *"?"* || "${SIGNED_MACOS_ASSET}" == *"["* ]]; then - echo "❌ signed_macos_asset must be an exact release asset name, not a path or glob" - exit 1 - fi - if [[ "${UNSIGNED_RUN_ID}" == "${GITHUB_RUN_ID}" ]]; then - echo "❌ unsigned_run_id must refer to the earlier build_unsigned run, not this run" - exit 1 - fi - ;; - *) - echo "❌ Unknown release_mode '${RELEASE_MODE}'" - exit 1 - ;; - esac - - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${REQUESTED_SIGN_MACOS}" == "true" ]]; then - echo "::warning title=Deprecated sign_macos input ignored::Use release_mode=build_unsigned or release_mode=promote_signed instead." - fi - # 1. Must be a tag and match the regex [[ "${GITHUB_REF_TYPE}" == "tag" ]] \ || { echo "❌ Not a tag push"; exit 1; } - [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \ + [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?(-termux)?$ ]] \ || { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; } # 2. Extract versions @@ -131,115 +88,198 @@ jobs: echo "✅ Tag and Cargo.toml agree (${tag_ver})" echo "::endgroup::" - build: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} + select-build-matrix: + if: always() && (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') needs: tag-check - name: Build - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: ${{ matrix.runs_on || matrix.runner }} - # Release builds can take a long time, so leave some headroom to avoid - # having to restart the full workflow due to a timeout. - timeout-minutes: 90 - permissions: - contents: read - id-token: write + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.select.outputs.matrix }} + build_version: ${{ steps.select.outputs.build_version }} + steps: + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - id: select + shell: bash + env: + REPO_OWNER: ${{ github.repository_owner }} + EVENT_NAME: ${{ github.event_name }} + BUILD_TARGET: ${{ inputs.build_target }} + run: | + set -euo pipefail + + matrix='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04","target":"aarch64-linux-android"}, + {"runner":"windows-latest","target":"x86_64-pc-windows-msvc"}, + {"runner":"windows-11-arm","target":"aarch64-pc-windows-msvc"} + ]' + + if [[ "${REPO_OWNER}" != "openai" ]]; then + matrix="$(jq -c '[.[] | select(.runner | startswith("macos") | not)]' <<< "${matrix}")" + fi + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + matrix="$(jq -c '[.[] | select(.target == "aarch64-linux-android")]' <<< "${matrix}")" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" && -n "${BUILD_TARGET}" && "${BUILD_TARGET}" != "all" ]]; then + matrix="$(jq -c --arg build_target "${BUILD_TARGET}" '[.[] | select(.target == $build_target)]' <<< "${matrix}")" + fi + + if [[ "$(jq 'length' <<< "${matrix}")" -eq 0 ]]; then + echo "No build targets selected after applying owner/dispatch filters." >&2 + exit 1 + fi + + echo "matrix={\"include\":${matrix}}" >> "$GITHUB_OUTPUT" + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + metadata=".github/termux-release.json" + if [[ ! -f "${metadata}" ]]; then + echo "${metadata} is required for release train PR builds" >&2 + exit 1 + fi + upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" + if [[ -z "${upstream_tag}" || "${upstream_tag}" != rust-v* ]]; then + echo "Unable to determine upstream rust tag from ${metadata}" >&2 + exit 1 + fi + build_version="${upstream_tag#rust-v}" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then + latest_release_tag="$( + GH_TOKEN="${{ github.token }}" gh api repos/openai/codex/releases/latest --jq '.tag_name' + )" + + if [[ -z "${latest_release_tag}" || "${latest_release_tag}" != rust-v* ]]; then + echo "Unable to determine latest stable release tag from openai/codex" >&2 + exit 1 + fi + + latest_release_version="${latest_release_tag#rust-v}" + build_version="${latest_release_version}-dev" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + fi + + build: + if: >- + always() && + (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') && + needs.select-build-matrix.result == 'success' + needs: + - tag-check + - select-build-matrix + name: Build - ${{ matrix.runner }} - ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + permissions: write-all + timeout-minutes: 120 defaults: run: working-directory: codex-rs env: - # 2026-03-04: temporarily change releases to use thin LTO because - # Ubuntu ARM is timing out at 60 minutes. - CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' }} + CODEX_BWRAP_ENABLE_FFI: ${{ contains(matrix.target, 'unknown-linux') && '1' || '0' }} + CODEX_BUILD_VERSION: ${{ needs.select-build-matrix.outputs.build_version }} + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" strategy: fail-fast: false - matrix: - include: - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - # Release artifacts intentionally ship MUSL-linked Linux binaries. - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - bundle: primary - artifact_name: x86_64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - bundle: app-server - artifact_name: x86_64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - bundle: primary - artifact_name: aarch64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - bundle: app-server - artifact_name: aarch64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" + matrix: ${{ fromJson(needs.select-build-matrix.outputs.matrix) }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + with: + verbose: true + - name: Checkout selected source ref + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + uses: actions/checkout@v6 with: - persist-credentials: false - - name: Print runner specs (Linux) - if: ${{ runner.os == 'Linux' }} + ref: ${{ inputs.source_ref }} + path: __source__ + fetch-depth: 1 + - name: Overlay selected source ref (excluding .github) + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + shell: bash + working-directory: ${{ github.workspace }} + run: | + set -euo pipefail + echo "Using source ref: ${{ inputs.source_ref }}" + + # Keep workflow/actions from the dispatch ref while replacing all other files. + find . -mindepth 1 -maxdepth 1 \ + ! -name .git \ + ! -name .github \ + ! -name __source__ \ + -exec rm -rf -- {} + + tar --exclude='.github' -C __source__ -cf - . | tar -xf - + rm -rf __source__ + + - name: Apply selected build version + if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} shell: bash + working-directory: ${{ github.workspace }} run: | set -euo pipefail - cpu_model="$(lscpu | awk -F: '/Model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}')" - total_ram="$(awk '/MemTotal/ {printf "%.1f GiB\n", $2 / 1024 / 1024}' /proc/meminfo)" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(uname -a)" - echo "CPU model: ${cpu_model}" - echo "Logical CPUs: $(nproc)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . - - name: Print runner specs (macOS) - if: ${{ runner.os == 'macOS' }} + if [[ -z "${CODEX_BUILD_VERSION}" ]]; then + echo "CODEX_BUILD_VERSION is empty for workflow_dispatch" >&2 + exit 1 + fi + echo "Using build version: ${CODEX_BUILD_VERSION}" + BUILD_VERSION="${CODEX_BUILD_VERSION}" perl -0777 -i -pe \ + 's/(\[workspace\.package\][^\[]*?version = ")([^"]+)(")/${1}$ENV{BUILD_VERSION}$3/s' \ + codex-rs/Cargo.toml + + grep -n "^\[workspace.package\]\|^version = " codex-rs/Cargo.toml | head -n 4 + + - name: Select Android NDK + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail - total_ram="$(sysctl -n hw.memsize | awk '{printf "%.1f GiB\n", $1 / 1024 / 1024 / 1024}')" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(sw_vers -productName) $(sw_vers -productVersion)" - echo "Hardware model: $(sysctl -n hw.model)" - echo "CPU architecture: $(uname -m)" - echo "Logical CPUs: $(sysctl -n hw.logicalcpu)" - echo "Physical CPUs: $(sysctl -n hw.physicalcpu)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . + + fallback_version="29.0.13113456" + ndk_root="${ANDROID_HOME}/ndk" + selected_ndk="" + + if [[ -d "${ndk_root}" ]]; then + while IFS= read -r candidate; do + toolchain="${candidate}/toolchains/llvm/prebuilt/linux-x86_64" + if [[ -x "${toolchain}/bin/aarch64-linux-android24-clang" ]]; then + selected_ndk="${candidate}" + break + fi + done < <(find "${ndk_root}" -mindepth 1 -maxdepth 1 -type d | sort -Vr) + fi + + if [[ -z "${selected_ndk}" ]]; then + echo "No usable preinstalled Android NDK found; installing ${fallback_version}" + yes | sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses || true + sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" "ndk;${fallback_version}" + selected_ndk="${ndk_root}/${fallback_version}" + fi + + if [[ ! -x "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" ]]; then + echo "Selected Android NDK does not contain a usable aarch64 API 24 clang: ${selected_ndk}" >&2 + exit 1 + fi + + echo "Selected Android NDK: ${selected_ndk}" + "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" --version + echo "ANDROID_NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" + echo "NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" - name: Install Linux bwrap build dependencies - if: ${{ runner.os == 'Linux' }} + if: ${{ runner.os == 'Linux' && matrix.target != 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail @@ -254,9 +294,173 @@ jobs: sudo apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 fi - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@1.93 with: targets: ${{ matrix.target }} + - name: Set up sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + uses: mozilla-actions/sccache-action@main + - name: Enable sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + shell: bash + run: | + set -euo pipefail + echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_VERSION=android-release-${{ matrix.target }}" >> "$GITHUB_ENV" + echo "SCCACHE_CACHE_SIZE=5G" >> "$GITHUB_ENV" + - name: Ensure Rust target is installed + shell: bash + run: | + set -euo pipefail + rustup target add "${{ matrix.target }}" + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android build environment + shell: bash + run: | + set -euo pipefail + ndk="${ANDROID_NDK_HOME}" + target="${{ matrix.target }}" + + # Set up the Android toolchain + toolchain="${ndk}/toolchains/llvm/prebuilt/linux-x86_64" + + # Use API level 24 to follow termux-build convention + api_level="24" + + # Configure Cargo to use the NDK linker + cargo_target_var="CARGO_TARGET_${target^^}_LINKER" + cargo_target_var="${cargo_target_var//-/_}" + echo "${cargo_target_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + # Set CC and AR for the target + target_cc_var="CC_${target}" + target_cc_var="${target_cc_var//-/_}" + echo "${target_cc_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + target_cxx_var="CXX_${target}" + target_cxx_var="${target_cxx_var//-/_}" + echo "${target_cxx_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang++" >> "$GITHUB_ENV" + + target_ar_var="AR_${target}" + target_ar_var="${target_ar_var//-/_}" + echo "${target_ar_var}=${toolchain}/bin/llvm-ar" >> "$GITHUB_ENV" + + # Add toolchain to PATH + echo "${toolchain}/bin" >> "$GITHUB_PATH" + + tls_align_source="${RUNNER_TEMP}/codex-android-tls-align.S" + tls_align_object="${RUNNER_TEMP}/codex-android-tls-align.o" + cat > "${tls_align_source}" <<'EOF' + .section .tdata.codex_android_tls_align,"awT",%progbits + .p2align 6 + .globl __codex_android_tls_align + .hidden __codex_android_tls_align + .type __codex_android_tls_align, %object + __codex_android_tls_align: + .byte 0 + .size __codex_android_tls_align, 1 + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${tls_align_source}" -o "${tls_align_object}" + + v8_compat_source="${RUNNER_TEMP}/codex-android-v8-compat.c" + v8_compat_object="${RUNNER_TEMP}/codex-android-v8-compat.o" + cat > "${v8_compat_source}" <<'EOF' + #include + #include + + extern int posix_memalign(void **memptr, size_t alignment, size_t size); + extern double strtod(const char *nptr, char **endptr); + extern float strtof(const char *nptr, char **endptr); + + void *aligned_alloc(size_t alignment, size_t size) { + void *ptr = 0; + if (posix_memalign(&ptr, alignment, size) != 0) { + return 0; + } + return ptr; + } + + double strtod_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtod(nptr, endptr); + } + + float strtof_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtof(nptr, endptr); + } + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${v8_compat_source}" -o "${v8_compat_object}" + + builtins_archive="$( + find "${toolchain}/lib/clang" \ + -path "*/lib/linux/libclang_rt.builtins-aarch64-android.a" \ + -print -quit + )" + if [[ -z "${builtins_archive}" ]]; then + echo "Could not find Android compiler-rt builtins archive under ${toolchain}/lib/clang" >&2 + exit 1 + fi + builtins_dir="$(dirname "${builtins_archive}")" + + # Provide compatibility symbols and C++ ABI needed by the Android rusty_v8 archive. + rustflags_var="CARGO_TARGET_${target^^}_RUSTFLAGS" + rustflags_var="${rustflags_var//-/_}" + echo "${rustflags_var}=-C link-arg=${tls_align_object} -C link-arg=${v8_compat_object} -C link-arg=-Wl,-u,__codex_android_tls_align -C link-arg=-L${builtins_dir} -C link-arg=-lclang_rt.builtins-aarch64-android -C link-arg=-lc++abi -C link-arg=-Wl,-z,max-page-size=65536" >> "$GITHUB_ENV" + + + # Configure for vendored OpenSSL build + echo "CARGO_BUILD_TARGET_APPLIES_TO_HOST=false" >> "$GITHUB_ENV" + echo "CARGO_BUILD_TARGET=${{ matrix.target }}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android rusty_v8 artifact overrides + env: + GH_TOKEN: ${{ github.token }} + TARGET: ${{ matrix.target }} + shell: bash + run: | + set -euo pipefail + binding_dir="${RUNNER_TEMP}/rusty_v8" + archive="${binding_dir}/librusty_v8_release_${TARGET}.a.gz" + binding_path="${binding_dir}/src_binding_release_${TARGET}.rs" + checksums_path="${binding_dir}/rusty_v8_release_${TARGET}.sha256" + artifact_repository="wallentx/codex-termux" + release_tag="rusty-v8-v147.4.0" + mkdir -p "${binding_dir}" + echo "Downloading Android rusty_v8 artifacts from ${artifact_repository}@${release_tag}" + gh release download "${release_tag}" \ + --repo "${artifact_repository}" \ + --dir "${binding_dir}" \ + --pattern "librusty_v8_release_${TARGET}.a.gz" \ + --pattern "src_binding_release_${TARGET}.rs" \ + --pattern "rusty_v8_release_${TARGET}.sha256" + (cd "${binding_dir}" && sha256sum -c "$(basename "${checksums_path}")") + echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV" + echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Install termux-elf-cleaner + shell: bash + run: | + set -euo pipefail + version="v3.0.1" + src_dir="${RUNNER_TEMP}/termux-elf-cleaner-src" + build_dir="${RUNNER_TEMP}/termux-elf-cleaner-build" + bin_dir="${RUNNER_TEMP}/termux-elf-cleaner-bin" + + rm -rf "${src_dir}" "${build_dir}" "${bin_dir}" + mkdir -p "${src_dir}" "${build_dir}" "${bin_dir}" + + curl -fsSL "https://github.com/termux/termux-elf-cleaner/archive/refs/tags/${version}.tar.gz" \ + | tar -xzf - --strip-components=1 -C "${src_dir}" + + cmake -S "${src_dir}" -B "${build_dir}" -DCMAKE_BUILD_TYPE=Release + cmake --build "${build_dir}" --parallel + + install "${build_dir}/termux-elf-cleaner" "${bin_dir}/termux-elf-cleaner" + echo "${bin_dir}" >> "$GITHUB_PATH" - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Use hermetic Cargo home (musl) @@ -269,12 +473,25 @@ jobs: echo "${cargo_home}/bin" >> "$GITHUB_PATH" : > "${cargo_home}/config.toml" + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ${{ github.workspace }}/.cargo-home/bin/ + ${{ github.workspace }}/.cargo-home/registry/index/ + ${{ github.workspace }}/.cargo-home/registry/cache/ + ${{ github.workspace }}/.cargo-home/git/db/ + ${{ github.workspace }}/codex-rs/target/ + key: cargo-${{ matrix.runner }}-${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }} + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install Zig - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + uses: mlugg/setup-zig@v2 with: version: 0.14.0 - use-cache: false - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install musl build tools @@ -310,12 +527,6 @@ jobs: shell: bash run: | set -euo pipefail - # Avoid problematic aws-lc jitter entropy code path on musl builders. - echo "AWS_LC_SYS_NO_JITTER_ENTROPY=1" >> "$GITHUB_ENV" - target_no_jitter="AWS_LC_SYS_NO_JITTER_ENTROPY_${{ matrix.target }}" - target_no_jitter="${target_no_jitter//-/_}" - echo "${target_no_jitter}=1" >> "$GITHUB_ENV" - # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. echo "RUSTFLAGS=" >> "$GITHUB_ENV" echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" @@ -340,94 +551,70 @@ jobs: echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" - - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }} - name: Configure musl rusty_v8 artifact overrides and verify checksums - uses: ./.github/actions/setup-rusty-v8-musl - with: - target: ${{ matrix.target }} - - - if: ${{ contains(matrix.target, 'linux') && matrix.bundle == 'primary' }} - name: Build bwrap and export digest + - name: Cargo build shell: bash run: | set -euo pipefail - target="${{ matrix.target }}" - cargo build --target "$target" --release --timings --bin bwrap - bwrap_path="target/${target}/release/bwrap" - if [[ ! -f "$bwrap_path" ]]; then - echo "bwrap binary ${bwrap_path} not found" - exit 1 + if [[ "${{ matrix.target }}" == 'aarch64-linux-android' ]]; then + export CARGO_BUILD_JOBS=4 + export CARGO_PROFILE_RELEASE_LTO=thin + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 + cargo build --target ${{ matrix.target }} --release --bin codex + exit 0 fi - digest="$(sha256sum "$bwrap_path" | awk '{print $1}')" - echo "CODEX_BWRAP_SHA256=${digest}" >> "$GITHUB_ENV" - echo "Built bwrap ${bwrap_path} with sha256:${digest}" - - - name: Cargo build + if [[ "${{ contains(matrix.target, 'windows') }}" == 'true' ]]; then + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy --bin codex-windows-sandbox-setup --bin codex-command-runner + else + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy + fi + - name: sccache stats (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash - run: | - build_args=() - for binary in ${{ matrix.binaries }}; do - build_args+=(--bin "$binary") - done - echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}" - cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}" + run: ${SCCACHE_PATH:-sccache} --show-stats || true - - name: Upload Cargo timings - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }} - path: codex-rs/target/**/cargo-timings/cargo-timing.html - if-no-files-found: warn - - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Stage unsigned macOS artifacts + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Normalize Android ELF for Termux shell: bash run: | set -euo pipefail - - target="${{ matrix.target }}" - release_dir="target/${target}/release" - dest="unsigned-dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - unsigned_name="${binary}-${target}-unsigned" - unsigned_path="${dest}/${unsigned_name}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 + for binary in codex codex-responses-api-proxy; do + binary_path="target/${{ matrix.target }}/release/${binary}" + if [[ -f "${binary_path}" ]]; then + termux-elf-cleaner --api-level 24 "${binary_path}" + chmod +x "${binary_path}" + # if [[ "${binary}" == "codex" ]]; then + # # Patch PT_TLS (0x7) to PT_NULL (0x0) at offset 400 to bypass Bionic alignment checks. + # printf '\x00\x00\x00\x00' | dd of="${binary_path}" bs=1 seek=400 count=4 conv=notrunc + # fi fi - - cp "${binary_path}" "${unsigned_path}" - tar -C "$dest" -czf "${unsigned_path}.tar.gz" "${unsigned_name}" - zstd -T0 -19 --rm "${unsigned_path}" done - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Upload unsigned macOS artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }}-unsigned - path: codex-rs/unsigned-dist/${{ matrix.target }}/* - if-no-files-found: error - - - if: ${{ contains(matrix.target, 'linux') }} + - if: ${{ contains(matrix.target, 'linux') && !contains(matrix.target, 'android') && github.repository_owner == 'openai' }} name: Cosign Linux artifacts uses: ./.github/actions/linux-code-sign with: target: ${{ matrix.target }} artifacts-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release - binaries: ${{ matrix.binaries }} - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS == 'true' }} + - if: ${{ contains(matrix.target, 'windows') && github.repository_owner == 'openai' }} + name: Sign Windows binaries with Azure Trusted Signing + uses: ./.github/actions/windows-code-sign + with: + target: ${{ matrix.target }} + client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID }} + endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} + account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} + + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (binaries) uses: ./.github/actions/macos-code-sign with: target: ${{ matrix.target }} - binaries: ${{ matrix.binaries }} sign-binaries: "true" sign-dmg: "false" apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }} @@ -436,7 +623,7 @@ jobs: apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: Build macOS dmg shell: bash run: | @@ -451,17 +638,23 @@ jobs: # The previous "MacOS code signing (binaries)" step signs + notarizes the # built artifacts in `${release_dir}`. This step packages *those same* # signed binaries into a dmg. + codex_binary_path="${release_dir}/codex" + proxy_binary_path="${release_dir}/codex-responses-api-proxy" + rm -rf "$dmg_root" mkdir -p "$dmg_root" - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 - fi - ditto "${binary_path}" "${dmg_root}/${binary}" - done + if [[ ! -f "$codex_binary_path" ]]; then + echo "Binary $codex_binary_path not found" + exit 1 + fi + if [[ ! -f "$proxy_binary_path" ]]; then + echo "Binary $proxy_binary_path not found" + exit 1 + fi + + ditto "$codex_binary_path" "${dmg_root}/codex" + ditto "$proxy_binary_path" "${dmg_root}/codex-responses-api-proxy" rm -f "$dmg_path" hdiutil create \ @@ -476,7 +669,7 @@ jobs: exit 1 fi - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (dmg) uses: ./.github/actions/macos-code-sign with: @@ -490,121 +683,65 @@ jobs: apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - name: Stage artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | dest="dist/${{ matrix.target }}" mkdir -p "$dest" - for binary in ${{ matrix.binaries }}; do - cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" - if [[ "${{ matrix.target }}" == *linux* ]]; then - cp "target/${{ matrix.target }}/release/${binary}.sigstore" \ - "$dest/${binary}-${{ matrix.target }}.sigstore" + if [[ "${{ matrix.runner }}" == windows* ]]; then + cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe" + else + # For Android, we want the binary to be named just 'codex' in the archive. + if [[ "${{ matrix.target }}" == "aarch64-linux-android" ]]; then + cp target/${{ matrix.target }}/release/codex "$dest/codex" + else + cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" + fi + if [[ -f target/${{ matrix.target }}/release/codex-responses-api-proxy ]]; then + cp target/${{ matrix.target }}/release/codex-responses-api-proxy "$dest/codex-responses-api-proxy-${{ matrix.target }}" fi - done - - if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.bundle }}" == "primary" ]]; then - bundle_root="${RUNNER_TEMP}/codex-${{ matrix.target }}-bundle" - rm -rf "$bundle_root" - mkdir -p "$bundle_root/codex-resources" - cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex" - cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap" - chmod 0755 "$bundle_root/codex" "$bundle_root/codex-resources/bwrap" - tar -C "$bundle_root" -cf - codex codex-resources/bwrap | - zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst" fi - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" + if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.target }}" != *android* ]]; then + cp target/${{ matrix.target }}/release/codex.sigstore "$dest/codex-${{ matrix.target }}.sigstore" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.sigstore "$dest/codex-responses-api-proxy-${{ matrix.target }}.sigstore" fi - - name: Build Codex package archive - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "target/${TARGET}/release" \ - --archive-dir "dist/${TARGET}" - - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - aarch64-unknown-linux-musl) - platform_tag="manylinux_2_17_aarch64" - ;; - x86_64-unknown-linux-musl) - platform_tag="manylinux_2_17_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - # Do not install into the runner's system Python; macOS runners mark - # the Homebrew Python as externally managed under PEP 668. - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - stage_runtime_args=( - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" - stage-runtime - "$stage_dir" - "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/codex" - --codex-version "${GITHUB_REF_NAME}" - --platform-tag "$platform_tag" - ) - if [[ "${{ matrix.target }}" == *linux* ]]; then - # Keep bwrap in the runtime wheel so Linux sandbox fallback behavior - # matches the standalone release bundle on hosts without system bwrap. - stage_runtime_args+=( - --resource-binary - "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/bwrap" - ) + if [[ "${{ matrix.target }}" == *apple-darwin ]]; then + cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" fi - python3 "${stage_runtime_args[@]}" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error + - if: ${{ matrix.runner == 'windows-11-arm' }} + name: Install zstd + shell: powershell + run: choco install -y zstandard - name: Compress artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | # Path that contains the uncompressed binaries for the current # ${{ matrix.target }} dest="dist/${{ matrix.target }}" + repo_root=$PWD + + # We want to ship the raw Windows executables in the GitHub Release + # in addition to the compressed archives. Keep the originals for + # Windows targets; remove them elsewhere to limit the number of + # artifacts that end up in the GitHub Release. + keep_originals=false + if [[ "${{ matrix.runner }}" == windows* ]]; then + keep_originals=true + fi # For compatibility with environments that lack the `zstd` tool we - # additionally create a `.tar.gz` alongside every binary we publish. - # The end result is: + # additionally create a `.tar.gz` for all platforms and `.zip` for + # Windows alongside every single binary that we publish. The end result is: # codex-.zst (existing) # codex-.tar.gz (new) + # codex-.zip (only for Windows) # 1. Produce a .tar.gz for every file in the directory *before* we # run `zstd --rm`, because that flag deletes the original files. @@ -612,7 +749,7 @@ jobs: base="$(basename "$f")" # Skip files that are already archives (shouldn't happen, but be # safe). - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then + if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then continue fi @@ -622,344 +759,239 @@ jobs: fi # Create per-binary tar.gz - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - - # Also create .zst and remove the uncompressed binaries to keep - # non-Windows artifact directories small. - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - with: - name: ${{ matrix.artifact_name }} - # Upload the per-binary .zst files, .tar.gz equivalents, and any - # prebuilt archives staged above. - path: | - codex-rs/dist/${{ matrix.target }}/* - - stage-signed-macos: - if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode == 'promote_signed' }} - needs: tag-check - name: Stage signed macOS handoff - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: macos-15-xlarge - timeout-minutes: 30 - permissions: - contents: read - defaults: - run: - working-directory: codex-rs + # For Android, we want the archive to have the target suffix even though the binary is just 'codex'. + archive_name="${base}" + if [[ "${{ matrix.target }}" == "aarch64-linux-android" && "${base}" == "codex" ]]; then + archive_name="codex-${{ matrix.target }}" + fi + tar -C "$dest" -czf "$dest/${archive_name}.tar.gz" "$base" + + # Create zip archive for Windows binaries + # Must run from inside the dest dir so 7z won't + # embed the directory path inside the zip. + if [[ "${{ matrix.runner }}" == windows* ]]; then + if [[ "$base" == "codex-${{ matrix.target }}.exe" ]]; then + # Bundle the sandbox helper binaries into the main codex zip so + # WinGet installs include the required helpers next to codex.exe. + # Fall back to the single-binary zip if the helpers are missing + # to avoid breaking releases. + bundle_dir="$(mktemp -d)" + runner_src="$dest/codex-command-runner-${{ matrix.target }}.exe" + setup_src="$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + if [[ -f "$runner_src" && -f "$setup_src" ]]; then + cp "$dest/$base" "$bundle_dir/$base" + cp "$runner_src" "$bundle_dir/codex-command-runner.exe" + cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" + # Use an absolute path so bundle zips land in the real dist + # dir even when 7z runs from a temp directory. + (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) + else + echo "warning: missing sandbox binaries; falling back to single-binary zip" + echo "warning: expected $runner_src and $setup_src" + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + rm -rf "$bundle_dir" + else + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + fi - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" + # Also create .zst (existing behaviour) *and* remove the original + # uncompressed binary to keep the directory small. + zstd_args=(-T0 -19) + if [[ "${keep_originals}" == false ]]; then + zstd_args+=(--rm) + fi - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + if [[ "${archive_name}" != "${base}" ]]; then + zstd "${zstd_args[@]}" "$dest/$base" -o "$dest/${archive_name}.zst" + else + zstd "${zstd_args[@]}" "$dest/$base" + fi + done - - name: Download signed macOS handoff + - name: Add Termux release metadata + if: ${{ (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && matrix.target == 'aarch64-linux-android' }} shell: bash - env: - GH_TOKEN: ${{ github.token }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - SIGNED_MACOS_SHA256: ${{ inputs.signed_macos_sha256 }} run: | set -euo pipefail - - download_dir="${RUNNER_TEMP}/signed-macos-download" - handoff_dir="${RUNNER_TEMP}/signed-macos-handoff" - rm -rf "$download_dir" "$handoff_dir" - mkdir -p "$download_dir" "$handoff_dir" - - gh release download "$GITHUB_REF_NAME" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern "$SIGNED_MACOS_ASSET" \ - --dir "$download_dir" - - asset_count="$(find "$download_dir" -maxdepth 1 -type f | wc -l | tr -d '[:space:]')" - if [[ "$asset_count" != "1" ]]; then - echo "Expected exactly one signed macOS handoff asset named ${SIGNED_MACOS_ASSET}; found ${asset_count}" - find "$download_dir" -maxdepth 1 -type f -print - exit 1 - fi - - asset_path="$(find "$download_dir" -maxdepth 1 -type f -print -quit)" - if [[ -n "${SIGNED_MACOS_SHA256}" ]]; then - expected_sha="$(printf '%s' "$SIGNED_MACOS_SHA256" | tr '[:upper:]' '[:lower:]')" - actual_sha="$(shasum -a 256 "$asset_path" | awk '{print $1}')" - if [[ "$actual_sha" != "$expected_sha" ]]; then - echo "signed_macos_sha256 mismatch for ${SIGNED_MACOS_ASSET}" - echo "expected: ${expected_sha}" - echo "actual: ${actual_sha}" - exit 1 - fi + if [[ ! -f "${GITHUB_WORKSPACE}/.github/termux-release.json" ]]; then + echo "No Termux release metadata found; skipping metadata attachment." + exit 0 fi + dest="dist/${{ matrix.target }}" + cp "${GITHUB_WORKSPACE}/.github/termux-release.json" "${dest}/termux-release.json" + ( + cd "${dest}" + sha256sum ./* > SHA256SUMS + ) - asset_name="$(basename "$asset_path")" - case "$asset_name" in - *.tar.zst) - zstd -dc "$asset_path" | tar -C "$handoff_dir" -xf - - ;; - *.tar.gz|*.tgz) - tar -C "$handoff_dir" -xzf "$asset_path" - ;; - *.zip) - ditto -x -k "$asset_path" "$handoff_dir" - ;; - *) - echo "Unsupported signed macOS handoff archive format: ${asset_name}" - exit 1 - ;; - esac - - echo "SIGNED_MACOS_HANDOFF_DIR=$handoff_dir" >> "$GITHUB_ENV" + - id: upload-artifact + uses: actions/upload-artifact@v6 + with: + name: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + # Upload the per-binary .zst files as well as the new .tar.gz + # equivalents we generated in the previous step. + path: | + codex-rs/dist/${{ matrix.target }}/* - - name: Stage signed macOS artifacts + - name: Publish Termux artifact notification + if: ${{ github.event_name == 'pull_request' && startsWith(github.base_ref, 'release/') && matrix.target == 'aarch64-linux-android' }} shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_ID: ${{ steps.upload-artifact.outputs.artifact-id }} + ARTIFACT_NAME: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + PR_NUMBER: ${{ github.event.pull_request.number }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | set -euo pipefail - - target="${{ matrix.target }}" - artifact_name="${{ matrix.artifact_name }}" - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${target}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - fi - if [[ ! -d "$source_dir" ]]; then - echo "Signed macOS handoff is missing ${artifact_name}/" - echo "Expected either:" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${target}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - find "$SIGNED_MACOS_HANDOFF_DIR" -maxdepth 3 -type f -print + if [[ -z "${ARTIFACT_ID}" ]]; then + echo "upload-artifact did not return an artifact id" >&2 exit 1 fi - dest="dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - source_path="${source_dir}/${binary}" - if [[ ! -f "$source_path" ]]; then - source_path="${source_dir}/${binary}-${target}" - fi - if [[ ! -f "$source_path" ]]; then - echo "Signed macOS handoff is missing ${binary} for ${artifact_name}" - exit 1 - fi - - release_path="${dest}/${binary}-${target}" - ditto "$source_path" "$release_path" - chmod 0755 "$release_path" - codesign --verify --strict --verbose=2 "$release_path" - done - - # DMG staging is disabled for signed promotion because we no longer - # distribute DMGs from this release path. Keep the branch here so the - # handoff can opt back in by flipping matrix.build_dmg if needed. - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - dmg_name="codex-${target}.dmg" - dmg_source="${source_dir}/${dmg_name}" - if [[ ! -f "$dmg_source" ]]; then - echo "Signed macOS handoff is missing ${dmg_name} for ${artifact_name}" - exit 1 - fi - - codesign --verify --strict --verbose=2 "$dmg_source" - xcrun stapler validate "$dmg_source" - cp "$dmg_source" "$dest/$dmg_name" + marker="" + artifact_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts/${ARTIFACT_ID}" + run_url="${GH_WORKFLOW_URL:-https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + body_path="${RUNNER_TEMP}/termux-artifact-comment.md" + { + echo "${marker}" + echo "Termux Android artifact ready for testing:" + echo + echo "- Artifact: [${ARTIFACT_NAME}](${artifact_url})" + echo "- Workflow run: [${GITHUB_RUN_ID}](${run_url})" + echo + echo "You must be signed in to GitHub with repository access to download Actions artifacts." + } > "${body_path}" + + existing_comment_id="$( + gh pr view "${PR_NUMBER}" \ + --repo "${GITHUB_REPOSITORY}" \ + --json comments \ + --jq ".comments | map(select(.author.is_bot == true and (.body | contains(\"${marker}\")))) | .[-1].id // \"\"" + )" + if [[ -n "${existing_comment_id}" ]]; then + gh api graphql \ + -f query=' + mutation($id: ID!, $body: String!) { + updateIssueComment(input: {id: $id, body: $body}) { + issueComment { + id + } + } + } + ' \ + -f id="${existing_comment_id}" \ + -f body="$(cat "${body_path}")" \ + >/dev/null + else + gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body-file "${body_path}" fi - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - python3 \ - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \ - stage-runtime \ - "$stage_dir" \ - "${GITHUB_WORKSPACE}/codex-rs/dist/${{ matrix.target }}/codex-${{ matrix.target }}" \ - --codex-version "${GITHUB_REF_NAME}" \ - --platform-tag "$platform_tag" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - - name: Build Codex package archive - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "dist/${TARGET}" \ - --archive-dir "dist/${TARGET}" \ - --target-suffixed-entrypoint - - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error - - - name: Compress artifacts - shell: bash - run: | - set -euo pipefail - - dest="dist/${{ matrix.target }}" - for f in "$dest"/*; do - base="$(basename "$f")" - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then - continue + gh label create binary-ready \ + --repo "${GITHUB_REPOSITORY}" \ + --color 0e8a16 \ + --description "Android binary is ready for testing" \ + --force + gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --add-label binary-ready + + if [[ -n "${SLACK_WEBHOOK_URL:-}" ]]; then + slack_payload_path="${RUNNER_TEMP}/termux-artifact-slack.json" + pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}" + jq -n \ + --arg artifact_name "${ARTIFACT_NAME}" \ + --arg artifact_url "${artifact_url}" \ + --arg pr_number "${PR_NUMBER}" \ + --arg pr_url "${pr_url}" \ + --arg run_id "${GITHUB_RUN_ID}" \ + --arg run_url "${run_url}" \ + '{ + text: ("Termux Android artifact ready for testing: " + $artifact_url), + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: ("*Termux Android artifact ready for testing*\n" + $artifact_url) + } + }, + { + type: "section", + fields: [ + { + type: "mrkdwn", + text: ("*Artifact:*\n<" + $artifact_url + "|" + $artifact_name + ">") + }, + { + type: "mrkdwn", + text: ("*Pull request:*\n<" + $pr_url + "|#" + $pr_number + ">") + }, + { + type: "mrkdwn", + text: ("*Workflow run:*\n<" + $run_url + "|" + $run_id + ">") + } + ] + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: "You must be signed in to GitHub with repository access to download Actions artifacts." + } + ] + } + ] + }' > "${slack_payload_path}" + + if ! curl -fsS \ + -X POST \ + -H "Content-type: application/json" \ + --data @"${slack_payload_path}" \ + "${SLACK_WEBHOOK_URL}"; then + echo "::warning::Failed to send Slack artifact notification" fi + fi - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }} - path: | - codex-rs/dist/${{ matrix.target }}/* - - build-windows: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - needs: tag-check - uses: ./.github/workflows/rust-release-windows.yml - with: - release-lto: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }} - secrets: inherit - - argument-comment-lint-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: argument-comment-lint release assets + shell-tool-mcp: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'openai' + name: shell-tool-mcp needs: tag-check - uses: ./.github/workflows/rust-release-argument-comment-lint.yml + permissions: + contents: read + id-token: write + uses: ./.github/workflows/shell-tool-mcp.yml with: + release-tag: ${{ github.ref_name }} publish: true - - zsh-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: zsh release assets - needs: tag-check - uses: ./.github/workflows/rust-release-zsh.yml + secrets: inherit release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') needs: - - tag-check - build - - stage-signed-macos - - build-windows - - argument-comment-lint-release-assets - - zsh-release-assets - if: >- - ${{ - always() && - needs.tag-check.result == 'success' && - ( - ( - github.event_name == 'workflow_dispatch' && - inputs.release_mode == 'promote_signed' && - needs.stage-signed-macos.result == 'success' && - needs.build.result == 'skipped' && - needs.build-windows.result == 'skipped' && - needs.argument-comment-lint-release-assets.result == 'skipped' && - needs.zsh-release-assets.result == 'skipped' - ) || - ( - (github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed') && - needs.build.result == 'success' && - needs.stage-signed-macos.result == 'skipped' && - needs.build-windows.result == 'success' && - needs.argument-comment-lint-release-assets.result == 'success' && - needs.zsh-release-assets.result == 'success' - ) - ) - }} + - shell-tool-mcp name: release runs-on: ubuntu-latest permissions: contents: write actions: read - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode == 'promote_signed' }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} outputs: version: ${{ steps.release_name.outputs.name }} tag: ${{ github.ref_name }} - sign_macos: ${{ steps.release_mode.outputs.sign_macos }} should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }} npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }} - should_publish_python_runtime: ${{ steps.python_runtime_publish_settings.outputs.should_publish }} steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + uses: actions/checkout@v6 - - name: Define release mode - id: release_mode - run: | - echo "release_mode=${RELEASE_MODE}" >> "$GITHUB_OUTPUT" - echo "sign_macos=${SIGN_MACOS}" >> "$GITHUB_OUTPUT" + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main - name: Generate release notes from tag commit message id: release_notes @@ -981,138 +1013,18 @@ jobs: echo "path=${notes_path}" >> "${GITHUB_OUTPUT}" - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@v7 with: path: dist - - name: Validate unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - run_summary="$(gh run view "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --json conclusion,event,headBranch,headSha,status,workflowName,url \ - --jq '[.workflowName, .event, .headBranch, .headSha, .status, .conclusion, .url] | @tsv')" - IFS=$'\t' read -r workflow_name event head_branch head_sha status conclusion run_url <<< "$run_summary" - expected_head_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")" - - if [[ "$workflow_name" != "$GITHUB_WORKFLOW" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is for workflow '${workflow_name}', expected '${GITHUB_WORKFLOW}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$event" != "workflow_dispatch" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} was triggered by '${event}', expected 'workflow_dispatch'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_branch" != "$GITHUB_REF_NAME" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used ref '${head_branch}', expected '${GITHUB_REF_NAME}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_sha" != "$expected_head_sha" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used head SHA '${head_sha}', expected '${expected_head_sha}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is ${status}/${conclusion}, expected completed/success" - echo "Run URL: ${run_url}" - exit 1 - fi - - - name: Download artifacts from unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - gh run download "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --dir dist - - - name: Remove unsigned macOS staging artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - run: | - set -euo pipefail - find dist -mindepth 1 -maxdepth 1 -type d \ - -name '*-apple-darwin*-unsigned' \ - -exec rm -rf {} + - - - name: Re-upload promoted Linux x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-unknown-linux-musl - path: dist/x86_64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Linux arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-unknown-linux-musl - path: dist/aarch64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Windows x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-pc-windows-msvc - path: dist/x86_64-pc-windows-msvc/* - if-no-files-found: error - - - name: Re-upload promoted Windows arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-pc-windows-msvc - path: dist/aarch64-pc-windows-msvc/* - if-no-files-found: error - - name: List run: ls -R dist/ - - name: Prune artifacts excluded from unsigned macOS release - if: ${{ env.SIGN_MACOS == 'false' }} - run: | - find dist -mindepth 1 -maxdepth 1 -type d \ - ! -name '*-apple-darwin*-unsigned' \ - ! -name 'aarch64-unknown-linux-musl' \ - ! -name 'aarch64-unknown-linux-musl-app-server' \ - ! -name 'x86_64-unknown-linux-musl' \ - ! -name 'x86_64-unknown-linux-musl-app-server' \ - ! -name 'aarch64-pc-windows-msvc' \ - ! -name 'x86_64-pc-windows-msvc' \ - -exec rm -rf {} + - - if ! find dist -type f -name '*-apple-darwin*-unsigned*' | grep -q .; then - echo "No unsigned macOS artifacts found in downloaded workflow artifacts." - exit 1 - fi - + # This is a temporary fix: we should modify shell-tool-mcp.yml so these + # files do not end up in dist/ in the first place. - name: Delete entries from dist/ that should not go in the release run: | - rm -rf dist/windows-binaries* - # cargo-timing.html appears under multiple target-specific directories. - # If included in files: dist/**, release upload races on duplicate - # asset names and can fail with 404s. - find dist -type f -name 'cargo-timing.html' -delete - # Keep package-builder sidecar archives as workflow artifacts only - # until distribution channels are ready to consume them. - find dist -type f \ - \( -name 'codex-package-*' -o -name 'codex-app-server-package-*' \) \ - -delete - find dist -type d -empty -delete + rm -rf dist/shell-tool-mcp* ls -R dist/ @@ -1136,12 +1048,6 @@ jobs: set -euo pipefail version="${VERSION}" - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - echo "npm_tag=" >> "$GITHUB_OUTPUT" - exit 0 - fi - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "should_publish=true" >> "$GITHUB_OUTPUT" echo "npm_tag=" >> "$GITHUB_OUTPUT" @@ -1153,132 +1059,55 @@ jobs: echo "npm_tag=" >> "$GITHUB_OUTPUT" fi - - name: Determine Python runtime publish settings - id: python_runtime_publish_settings - env: - VERSION: ${{ steps.release_name.outputs.name }} - run: | - set -euo pipefail - version="${VERSION}" - - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - else - echo "should_publish=false" >> "$GITHUB_OUTPUT" - fi - - name: Setup pnpm - if: ${{ env.SIGN_MACOS == 'true' }} - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + uses: pnpm/action-setup@v4 with: run_install: false - name: Setup Node.js for npm packaging - if: ${{ env.SIGN_MACOS == 'true' }} - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: node-version: 22 - name: Install dependencies - if: ${{ env.SIGN_MACOS == 'true' }} run: pnpm install --frozen-lockfile # stage_npm_packages.py requires DotSlash when staging releases. - - uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2 + - uses: facebook/install-dotslash@v2 - name: Stage npm packages - if: ${{ env.SIGN_MACOS == 'true' }} + if: github.repository_owner == 'openai' env: GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ steps.release_name.outputs.name }} run: | - workflow_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" ./scripts/stage_npm_packages.py \ - --release-version "$RELEASE_VERSION" \ - --workflow-url "$workflow_url" \ + --release-version "${{ steps.release_name.outputs.name }}" \ --package codex \ --package codex-responses-api-proxy \ --package codex-sdk - - name: Stage installer scripts - if: ${{ env.SIGN_MACOS == 'true' }} - run: | - cp scripts/install/install.sh dist/install.sh - cp scripts/install/install.ps1 dist/install.ps1 - - name: Create GitHub Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@v2 with: name: ${{ steps.release_name.outputs.name }} tag_name: ${{ github.ref_name }} body_path: ${{ steps.release_notes.outputs.path }} files: dist/** - overwrite_files: true - make_latest: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} # Mark as prerelease only when the version has a suffix after x.y.z # (e.g. -alpha, -beta). Otherwise publish a normal release. prerelease: ${{ contains(steps.release_name.outputs.name, '-') }} - - name: Clean up signed promotion handoff assets - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" --jq '.id')" - gh api --paginate "repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets" \ - --jq '.[] | [.id, .name] | @tsv' | - while IFS=$'\t' read -r asset_id asset_name; do - if [[ -z "$asset_id" || -z "$asset_name" ]]; then - continue - fi - - delete_asset=false - if [[ "$asset_name" == *unsigned* || "$asset_name" == "$SIGNED_MACOS_ASSET" ]]; then - delete_asset=true - fi - - if [[ "$delete_asset" == "true" ]]; then - echo "Deleting release asset ${asset_name}" - gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" - fi - done - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 + - if: github.repository_owner == 'openai' + uses: facebook/dotslash-publish-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag: ${{ github.ref_name }} config: .github/dotslash-config.json - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-zsh-config.json - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-argument-comment-lint-config.json - - name: Trigger developers.openai.com deploy # Only trigger the deploy if the release is not a pre-release. # The deploy is used to update the developers.openai.com website with the new config schema json file. - if: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} + if: ${{ !contains(steps.release_name.outputs.name, '-') && github.repository_owner == 'openai' }} continue-on-error: true env: DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }} @@ -1293,15 +1122,7 @@ jobs: # npm docs: https://docs.npmjs.com/trusted-publishers publish-npm: # Publish to npm for stable releases and alpha pre-releases with numeric suffixes. - # promote_signed intentionally skips build jobs that are ancestors of release; - # include the !cancelled() status function so Actions does not apply its implicit - # success() check to the whole dependency chain before evaluating release outputs. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_npm == 'true' - }} + if: ${{ needs.release.outputs.should_publish_npm == 'true' && github.repository_owner == 'openai' }} name: publish-npm needs: release runs-on: ubuntu-latest @@ -1311,37 +1132,36 @@ jobs: steps: - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: - # Node 24 bundles npm >= 11.5.1, which trusted publishing requires. - node-version: 24 + node-version: 22 registry-url: "https://registry.npmjs.org" scope: "@openai" + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + - name: Download npm tarballs from release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} run: | set -euo pipefail - version="$RELEASE_VERSION" - tag="$RELEASE_TAG" + version="${{ needs.release.outputs.version }}" + tag="${{ needs.release.outputs.tag }}" mkdir -p dist/npm - patterns=( - "codex-npm-${version}.tgz" - "codex-npm-linux-*-${version}.tgz" - "codex-npm-darwin-*-${version}.tgz" - "codex-npm-win32-*-${version}.tgz" - "codex-responses-api-proxy-npm-${version}.tgz" - "codex-sdk-npm-${version}.tgz" - ) - for pattern in "${patterns[@]}"; do - gh release download "$tag" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "$pattern" \ - --dir dist/npm - done + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-responses-api-proxy-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-sdk-npm-${version}.tgz" \ + --dir dist/npm # No NODE_AUTH_TOKEN needed because we use OIDC. - name: Publish to npm @@ -1350,193 +1170,23 @@ jobs: NPM_TAG: ${{ needs.release.outputs.npm_tag }} run: | set -euo pipefail - prefix="" + tag_args=() if [[ -n "${NPM_TAG}" ]]; then - prefix="${NPM_TAG}-" + tag_args+=(--tag "${NPM_TAG}") fi - root_tarball="dist/npm/codex-npm-${VERSION}.tgz" - sdk_tarball="dist/npm/codex-sdk-npm-${VERSION}.tgz" - # Keep this list in sync with CODEX_PLATFORM_PACKAGES in - # codex-cli/scripts/build_npm_package.py. The root wrapper advances - # @openai/codex@latest as soon as it publishes, so every platform - # package it aliases must already exist in the registry first. - platform_tarballs=( - "dist/npm/codex-npm-linux-x64-${VERSION}.tgz" - "dist/npm/codex-npm-linux-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-x64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-x64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-arm64-${VERSION}.tgz" - ) - - for required_tarball in "${platform_tarballs[@]}" "${root_tarball}"; do - if [[ ! -f "${required_tarball}" ]]; then - echo "Missing npm tarball: ${required_tarball}" - exit 1 - fi - done - - shopt -s nullglob - other_tarballs=() - for tarball in dist/npm/*-"${VERSION}".tgz; do - if [[ "${tarball}" == "${root_tarball}" || "${tarball}" == "${sdk_tarball}" ]]; then - continue - fi - - is_platform_tarball=false - for platform_tarball in "${platform_tarballs[@]}"; do - if [[ "${tarball}" == "${platform_tarball}" ]]; then - is_platform_tarball=true - break - fi - done - if [[ "${is_platform_tarball}" == true ]]; then - continue - fi - - other_tarballs+=("${tarball}") - done - - # Publish the platform packages before the root CLI wrapper. The root - # wrapper advances @openai/codex@latest, so it should only publish - # after the optional dependency versions it references exist. tarballs=( - "${platform_tarballs[@]}" - "${other_tarballs[@]}" - "${root_tarball}" + "codex-npm-${VERSION}.tgz" + "codex-responses-api-proxy-npm-${VERSION}.tgz" + "codex-sdk-npm-${VERSION}.tgz" ) - if [[ -f "${sdk_tarball}" ]]; then - tarballs+=("${sdk_tarball}") - fi for tarball in "${tarballs[@]}"; do - filename="$(basename "${tarball}")" - tag="" - - case "${filename}" in - codex-npm-linux-*-"${VERSION}".tgz|codex-npm-darwin-*-"${VERSION}".tgz|codex-npm-win32-*-"${VERSION}".tgz) - platform="${filename#codex-npm-}" - platform="${platform%-${VERSION}.tgz}" - tag="${prefix}${platform}" - ;; - codex-npm-"${VERSION}".tgz|codex-responses-api-proxy-npm-"${VERSION}".tgz|codex-sdk-npm-"${VERSION}".tgz) - tag="${NPM_TAG}" - ;; - *) - echo "Unexpected npm tarball: ${filename}" - exit 1 - ;; - esac - - publish_cmd=(npm publish "${GITHUB_WORKSPACE}/${tarball}") - if [[ -n "${tag}" ]]; then - publish_cmd+=(--tag "${tag}") - fi - - echo "+ ${publish_cmd[*]}" - set +e - publish_output="$("${publish_cmd[@]}" 2>&1)" - publish_status=$? - set -e - - echo "${publish_output}" - if [[ ${publish_status} -eq 0 ]]; then - continue - fi - - if grep -qiE "previously published|cannot publish over|version already exists" <<< "${publish_output}"; then - echo "Skipping already-published package version for ${filename}" - continue - fi - - exit "${publish_status}" + npm publish "${GITHUB_WORKSPACE}/dist/npm/${tarball}" "${tag_args[@]}" done - # Publish the platform-specific Python runtime wheels using PyPI trusted publishing. - # PyPI project configuration must trust this workflow and job. Keep this - # non-blocking while the Python runtime publishing path is new; failures still - # need release follow-up, but should not invalidate the Rust release itself. - publish-python-runtime: - # Publish to PyPI for stable releases and alpha pre-releases with numeric suffixes. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_python_runtime == 'true' - }} - name: publish-python-runtime - needs: release - runs-on: ubuntu-latest - continue-on-error: true - environment: pypi - permissions: - id-token: write # Required for PyPI trusted publishing. - contents: read - - steps: - - name: Download Python runtime wheels from release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} - run: | - set -euo pipefail - python_version="$RELEASE_VERSION" - python_version="${python_version/-alpha./a}" - python_version="${python_version/-beta./b}" - python_version="${python_version/-rc./rc}" - - mkdir -p dist/python-runtime - gh release download "$RELEASE_TAG" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "openai_codex_cli_bin-${python_version}-*.whl" \ - --dir dist/python-runtime - ls -lh dist/python-runtime - - - name: Publish Python runtime wheels to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - with: - packages-dir: dist/python-runtime - skip-existing: true - - winget: - name: winget - needs: release - # Only publish stable/mainline releases to WinGet; pre-releases include a - # '-' in the semver string (e.g., 1.2.3-alpha.1). - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' && - !contains(needs.release.outputs.version, '-') - }} - # This job only invokes a GitHub Action to open/update the winget-pkgs PR; - # it does not execute Windows-only tooling, so Linux is sufficient. - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Publish to WinGet - uses: vedantmgoyal9/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5 - with: - identifier: OpenAI.Codex - version: ${{ needs.release.outputs.version }} - release-tag: ${{ needs.release.outputs.tag }} - fork-user: openai-oss-forks - installers-regex: '^codex-(?:x86_64|aarch64)-pc-windows-msvc\.exe\.zip$' - token: ${{ secrets.WINGET_PUBLISH_PAT }} - update-branch: name: Update latest-alpha-cli branch - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' - }} permissions: contents: write needs: release diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml new file mode 100644 index 000000000000..66a76aa4d938 --- /dev/null +++ b/.github/workflows/shell-tool-mcp.yml @@ -0,0 +1,461 @@ +name: shell-tool-mcp + +on: + workflow_call: + inputs: + release-version: + description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v. + required: false + type: string + release-tag: + description: Tag name to use when downloading release artifacts (defaults to rust-v). + required: false + type: string + publish: + description: Whether to publish to npm when the version is releasable. + required: false + default: true + type: boolean + +env: + NODE_VERSION: 22 + +jobs: + metadata: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.compute.outputs.version }} + release_tag: ${{ steps.compute.outputs.release_tag }} + should_publish: ${{ steps.compute.outputs.should_publish }} + npm_tag: ${{ steps.compute.outputs.npm_tag }} + steps: + - name: Compute version and tags + id: compute + run: | + set -euo pipefail + + version="${{ inputs.release-version }}" + release_tag="${{ inputs.release-tag }}" + + if [[ -z "$version" ]]; then + if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then + version="${release_tag#rust-v}" + elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then + version="${GITHUB_REF_NAME#rust-v}" + release_tag="${GITHUB_REF_NAME}" + else + echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag." + exit 1 + fi + fi + + if [[ -z "$release_tag" ]]; then + release_tag="rust-v${version}" + fi + + npm_tag="" + should_publish="false" + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + should_publish="true" + elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then + should_publish="true" + npm_tag="alpha" + fi + + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" + echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT" + echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT" + + matrix-setup: + runs-on: ubuntu-latest + outputs: + rust-binaries: ${{ steps.compute.outputs.rust-binaries }} + bash-linux: ${{ steps.compute.outputs.bash-linux }} + bash-darwin: ${{ steps.compute.outputs.bash-darwin }} + steps: + - name: Compute matrices + id: compute + shell: bash + run: | + set -euo pipefail + is_openai="${{ github.repository_owner == 'openai' }}" + + # rust-binaries: always include x86_64-musl; conditionally include paid/fork runners + if [[ "$is_openai" == "true" ]]; then + rust='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","install_musl":true} + ]' + else + rust='[{"runner":"macos-latest","target":"aarch64-apple-darwin"}]' + fi + rust=$(echo "$rust" | jq -c '. + [{"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","install_musl":true}]') + echo "rust-binaries={\"include\":$rust}" >> "$GITHUB_OUTPUT" + + # bash-linux: always include x86_64 variants; add arm64 on openai + bash_linux='[ + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-24.04","image":"ubuntu:24.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-22.04","image":"ubuntu:22.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-12","image":"debian:12"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-11","image":"debian:11"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]' + if [[ "$is_openai" == "true" ]]; then + bash_linux=$(echo "$bash_linux" | jq -c '. + [ + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-24.04","image":"arm64v8/ubuntu:24.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-22.04","image":"arm64v8/ubuntu:22.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-20.04","image":"arm64v8/ubuntu:20.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-12","image":"arm64v8/debian:12"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-11","image":"arm64v8/debian:11"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]') + fi + echo "bash-linux={\"include\":$(echo "$bash_linux" | jq -c)}" >> "$GITHUB_OUTPUT" + + # bash-darwin: always include macos-14; add macos-15-xlarge on openai + bash_darwin='[{"runner":"macos-14","target":"aarch64-apple-darwin","variant":"macos-14"}]' + if [[ "$is_openai" == "true" ]]; then + bash_darwin=$(echo "$bash_darwin" | jq -c '. + [ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin","variant":"macos-15"} + ]') + fi + echo "bash-darwin={\"include\":$(echo "$bash_darwin" | jq -c)}" >> "$GITHUB_OUTPUT" + + rust-binaries: + name: Build Rust - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + defaults: + run: + working-directory: codex-rs + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.rust-binaries) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install UBSan runtime (musl) + if: ${{ matrix.install_musl }} + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -y + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 + fi + + - uses: dtolnay/rust-toolchain@1.93 + with: + targets: ${{ matrix.target }} + + - if: ${{ matrix.install_musl }} + name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.14.0 + + - if: ${{ matrix.install_musl }} + name: Install musl build dependencies + env: + TARGET: ${{ matrix.target }} + run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh" + + - if: ${{ matrix.install_musl }} + name: Configure rustc UBSan wrapper (musl host) + shell: bash + run: | + set -euo pipefail + ubsan="" + if command -v ldconfig >/dev/null 2>&1; then + ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')" + fi + wrapper_root="${RUNNER_TEMP:-/tmp}" + wrapper="${wrapper_root}/rustc-ubsan-wrapper" + cat > "${wrapper}" <> "$GITHUB_ENV" + echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV" + + - if: ${{ matrix.install_musl }} + name: Clear sanitizer flags (musl) + shell: bash + run: | + set -euo pipefail + # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. + echo "RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV" + # Override any runner-level Cargo config rustflags as well. + echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + + sanitize_flags() { + local input="$1" + input="${input//-fsanitize=undefined/}" + input="${input//-fno-sanitize-recover=undefined/}" + input="${input//-fno-sanitize-trap=undefined/}" + echo "$input" + } + + cflags="$(sanitize_flags "${CFLAGS-}")" + cxxflags="$(sanitize_flags "${CXXFLAGS-}")" + echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" + echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + + - name: Build exec server binaries + run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper + + - name: Stage exec server binaries + run: | + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}" + mkdir -p "$dest" + cp "target/${{ matrix.target }}/release/codex-exec-mcp-server" "$dest/" + cp "target/${{ matrix.target }}/release/codex-execve-wrapper" "$dest/" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-rust-${{ matrix.target }} + path: artifacts/** + if-no-files-found: error + + bash-linux: + name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + container: + image: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-linux) }} + steps: + - name: Install build prerequisites + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext + elif command -v dnf >/dev/null 2>&1; then + dnf install -y git gcc gcc-c++ make bison autoconf gettext + elif command -v yum >/dev/null 2>&1; then + yum install -y git gcc gcc-c++ make bison autoconf gettext + else + echo "Unsupported package manager in container" + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + bash-darwin: + name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-darwin) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + package: + name: Package npm module + needs: + - metadata + - rust-binaries + - bash-linux + - bash-darwin + runs-on: ubuntu-latest + env: + PACKAGE_VERSION: ${{ needs.metadata.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install JavaScript dependencies + run: pnpm install --frozen-lockfile + + - name: Build (shell-tool-mcp) + run: pnpm --filter @openai/codex-shell-tool-mcp run build + + - name: Download build artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts + + - name: Assemble staging directory + id: staging + shell: bash + run: | + set -euo pipefail + staging="${STAGING_DIR}" + mkdir -p "$staging" "$staging/vendor" + cp shell-tool-mcp/README.md "$staging/" + cp shell-tool-mcp/package.json "$staging/" + cp -R shell-tool-mcp/bin "$staging/" + + found_vendor="false" + shopt -s nullglob + for vendor_dir in artifacts/*/vendor; do + rsync -av "$vendor_dir/" "$staging/vendor/" + found_vendor="true" + done + if [[ "$found_vendor" == "false" ]]; then + echo "No vendor payloads were downloaded." + exit 1 + fi + + node - <<'NODE' + import fs from "node:fs"; + import path from "node:path"; + + const stagingDir = process.env.STAGING_DIR; + const version = process.env.PACKAGE_VERSION; + const pkgPath = path.join(stagingDir, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + pkg.version = version; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + NODE + + echo "dir=$staging" >> "$GITHUB_OUTPUT" + env: + STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp + + - name: Ensure binaries are executable + run: | + set -euo pipefail + staging="${{ steps.staging.outputs.dir }}" + chmod +x \ + "$staging"/vendor/*/codex-exec-mcp-server \ + "$staging"/vendor/*/codex-execve-wrapper \ + "$staging"/vendor/*/bash/*/bash + + - name: Create npm tarball + shell: bash + run: | + set -euo pipefail + mkdir -p dist/npm + staging="${{ steps.staging.outputs.dir }}" + pack_info=$(cd "$staging" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm") + filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);') + mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz" + + - uses: actions/upload-artifact@v6 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz + if-no-files-found: error + + publish: + name: Publish npm package + needs: + - metadata + - package + if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }} + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: https://registry.npmjs.org + scope: "@openai" + + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + + - name: Download npm tarball + uses: actions/download-artifact@v7 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm + + - name: Publish to npm + if: github.repository == 'openai/codex' + env: + NPM_TAG: ${{ needs.metadata.outputs.npm_tag }} + VERSION: ${{ needs.metadata.outputs.version }} + shell: bash + run: | + set -euo pipefail + tag_args=() + if [[ -n "${NPM_TAG}" ]]; then + tag_args+=(--tag "${NPM_TAG}") + fi + npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}" diff --git a/.github/workflows/termux-release-checkpoint.yml b/.github/workflows/termux-release-checkpoint.yml new file mode 100644 index 000000000000..0347e6f1b3d6 --- /dev/null +++ b/.github/workflows/termux-release-checkpoint.yml @@ -0,0 +1,103 @@ +name: termux-release-checkpoint + +on: + workflow_dispatch: + inputs: + source_branch: + description: "Release branch to checkpoint from, for example release/0.123.0" + required: false + type: string + default: "" + source_sha: + description: "Specific source commit SHA to checkpoint; defaults to the source branch tip" + required: false + type: string + default: "" + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + type: string + default: "wallentx/termux-target" + reviewer: + description: "GitHub username to request as reviewer" + required: false + type: string + default: "wallentx" + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-checkpoint-${{ inputs.source_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + checkpoint: + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + env: + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + REQUESTED_SOURCE_BRANCH: ${{ inputs.source_branch }} + REQUESTED_SOURCE_SHA: ${{ inputs.source_sha }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ inputs.source_branch || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + source_branch="${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${source_branch}" + + - name: Create checkpoint PR + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" diff --git a/.github/workflows/termux-release-deploy.yml b/.github/workflows/termux-release-deploy.yml new file mode 100644 index 000000000000..2071611dacc4 --- /dev/null +++ b/.github/workflows/termux-release-deploy.yml @@ -0,0 +1,243 @@ +name: termux-release-deploy + +on: + push: + branches: + - "release/**" + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to deploy, for example release/0.124.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to deploy. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged release PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged release PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + default: "wallentx/termux-target" + type: string + reviewer: + description: "GitHub username to request as reviewer on the checkpoint PR" + required: false + default: "wallentx" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-deploy-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + deploy: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' || !startsWith(github.event.head_commit.message, 'Seed Termux release automation') }} + permissions: + actions: read + contents: write + deployments: write + issues: write + pull-requests: write + env: + REQUESTED_RELEASE_BRANCH: ${{ inputs.release_branch }} + REQUESTED_RELEASE_SHA: ${{ inputs.release_sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + release_branch="${REQUESTED_RELEASE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${release_branch}" + + - name: Resolve release ref + id: release-ref + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: deploy + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Create deployment + if: steps.metadata.outputs.deploy == 'true' + id: deployment + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + deployment_id="$( + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments" \ + -f ref="${RELEASE_SHA}" \ + -f environment="termux-release" \ + -F auto_merge=false \ + -F required_contexts[] \ + -f description="Termux release deployment for ${TERMUX_TAG}" \ + --jq '.id' + )" + echo "id=${deployment_id}" >> "$GITHUB_OUTPUT" + + - name: Mark deployment in progress + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="in_progress" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Promoting Termux release artifact and preparing checkpoint PR" \ + >/dev/null + + - name: Locate merged pull request + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" + + - name: Ensure checkpoint PR + if: steps.metadata.outputs.deploy == 'true' + id: checkpoint + env: + SOURCE_BRANCH: ${{ steps.release-ref.outputs.branch }} + SOURCE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" + + - name: Mark deployment success + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="success" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment completed for ${TERMUX_TAG}" \ + >/dev/null + + - name: Mark deployment failure + if: failure() && steps.deployment.outputs.id != '' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="failure" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment failed" \ + >/dev/null diff --git a/.github/workflows/termux-release-promote.yml b/.github/workflows/termux-release-promote.yml new file mode 100644 index 000000000000..22c277c21a70 --- /dev/null +++ b/.github/workflows/termux-release-promote.yml @@ -0,0 +1,135 @@ +name: termux-release-promote + +on: + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to promote, for example release/0.122.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to promote. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-promote-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + promote: + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: write + pull-requests: read + env: + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Resolve release ref + id: release-ref + env: + INPUT_RELEASE_BRANCH: ${{ inputs.release_branch }} + INPUT_RELEASE_SHA: ${{ inputs.release_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: promote + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Locate merged pull request + if: steps.metadata.outputs.promote == 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.promote == 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.promote == 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index ce3b943429e7..739f710f6111 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -118,7 +118,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.133.0-alpha.3" +version = "0.133.0" # Track the edition for all workspace crates in one place. Individual # crates can still override this value, but keeping it here means new # crates created with `cargo new -w ...` automatically inherit the 2024 diff --git a/scripts/termux-configure-git.sh b/scripts/termux-configure-git.sh new file mode 100755 index 000000000000..fae3abcbab48 --- /dev/null +++ b/scripts/termux-configure-git.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Configure the explicit bot identity used by release automation and fetch any +# refs requested by the calling workflow. + +set -euo pipefail + +git config user.name "github-actions[bot]" +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + +while (($#)); do + case "$1" in + --origin) + shift + origin_refs=() + while (($#)) && [[ "$1" != --* ]]; do + origin_refs+=("$1") + shift + done + if ((${#origin_refs[@]})); then + git fetch --prune origin "${origin_refs[@]}" + fi + ;; + --upstream-tag) + if (($# < 3)); then + echo "--upstream-tag requires ." >&2 + exit 1 + fi + upstream_repo="$2" + upstream_tag="$3" + git remote add upstream "https://github.com/${upstream_repo}.git" 2>/dev/null || true + git fetch --prune --no-tags upstream "+refs/tags/${upstream_tag}:refs/tags/${upstream_tag}" + shift 3 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done diff --git a/scripts/termux-create-checkpoint-pr.sh b/scripts/termux-create-checkpoint-pr.sh new file mode 100755 index 000000000000..f315a5b91822 --- /dev/null +++ b/scripts/termux-create-checkpoint-pr.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/termux-release-paths.sh +source "${script_dir}/termux-release-paths.sh" + +source_branch="${SOURCE_BRANCH:-${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}}" +source_sha="${SOURCE_SHA:-${REQUESTED_SOURCE_SHA:-}}" +if [[ -z "${source_sha}" ]]; then + if [[ "${GITHUB_EVENT_NAME:-}" == "push" && "${source_branch}" == "${GITHUB_REF_NAME:-}" ]]; then + source_sha="${GITHUB_SHA}" + else + source_sha="$(git rev-parse "origin/${source_branch}")" + fi +fi + +if [[ -z "${DESTINATION_BRANCH:-}" ]]; then + echo "DESTINATION_BRANCH is required." >&2 + exit 1 +fi + +release_only_checkpoint_paths() { + printf '%s\n' "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +} + +resolve_source_version_conflicts() { + local path="$1" + local resolved_path + resolved_path="$(mktemp)" + + if ! awk ' + function normalize_versions(text) { + gsub(/version = "[^"]+"/, "version = \"\"", text) + return text + } + + BEGIN { + in_block = 0 + side = "" + ours = "" + theirs = "" + blocks = 0 + } + + /^<<<<<<< / { + if (in_block) { + exit 1 + } + in_block = 1 + side = "ours" + ours = "" + theirs = "" + blocks++ + next + } + + /^=======$/ && in_block { + side = "theirs" + next + } + + /^>>>>>>> / && in_block { + if (normalize_versions(ours) != normalize_versions(theirs)) { + exit 1 + } + printf "%s", theirs + in_block = 0 + side = "" + next + } + + { + if (!in_block) { + print + } else if (side == "ours") { + ours = ours $0 ORS + } else if (side == "theirs") { + theirs = theirs $0 ORS + } else { + exit 1 + } + } + + END { + if (in_block || blocks == 0) { + exit 1 + } + } + ' "${path}" > "${resolved_path}"; then + rm -f "${resolved_path}" + return 1 + fi + + cp "${resolved_path}" "${path}" + rm -f "${resolved_path}" +} + +short_sha="${source_sha:0:12}" +source_slug="${source_branch//\//_}" +dest_slug="${DESTINATION_BRANCH//\//_}" +checkpoint_branch="checkpoint/${dest_slug}_from_${source_slug}_${short_sha}" +pr_title="checkpoint: into ${DESTINATION_BRANCH} from ${source_branch} @ ${short_sha}" +merge_conflicted=false +conflict_summary="" + +existing_pr="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --head "${checkpoint_branch}" \ + --state all \ + --json number,state,mergedAt,url \ + --jq '[.[] | select(.state == "OPEN" or .mergedAt != null)] | .[0] // empty' +)" +if [[ -n "${existing_pr}" ]]; then + existing_url="$(jq -r '.url' <<< "${existing_pr}")" + existing_state="$(jq -r '.state' <<< "${existing_pr}")" + echo "Checkpoint PR already exists for ${checkpoint_branch}: ${existing_url} (${existing_state})." + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${existing_url}" >> "${GITHUB_OUTPUT}" + fi + exit 0 +fi + +git checkout -B "${checkpoint_branch}" "origin/${DESTINATION_BRANCH}" + +if ! git merge --no-ff --no-edit "${source_sha}"; then + mapfile -t conflicted_paths < <(git diff --name-only --diff-filter=U) + for conflicted_path in "${conflicted_paths[@]}"; do + if termux_is_checkpoint_release_only_path "${conflicted_path}"; then + echo "Auto-resolving release-only checkpoint conflict in ${conflicted_path} by keeping ${DESTINATION_BRANCH}." + if git cat-file -e "HEAD:${conflicted_path}" 2>/dev/null; then + git checkout --ours -- "${conflicted_path}" + git add "${conflicted_path}" + else + git rm -f --ignore-unmatch "${conflicted_path}" + fi + fi + done + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if ((${#remaining_conflicts[@]})); then + cargo_version_conflicts=true + for remaining_conflict in "${remaining_conflicts[@]}"; do + case "${remaining_conflict}" in + codex-rs/Cargo.toml|codex-rs/Cargo.lock) + ;; + *) + cargo_version_conflicts=false + ;; + esac + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + for remaining_conflict in "${remaining_conflicts[@]}"; do + if ! resolve_source_version_conflicts "${remaining_conflict}"; then + cargo_version_conflicts=false + break + fi + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + echo "Auto-resolving recurring Cargo version checkpoint conflicts by keeping ${source_branch} versions." + git add -- "${remaining_conflicts[@]}" + fi + fi + fi + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if [[ "${#remaining_conflicts[@]}" -eq 0 ]]; then + git commit --no-edit + else + merge_conflicted=true + conflict_summary="$( + printf '%s\n' "${remaining_conflicts[@]}" | awk '{ print "- `" $0 "`" }' + )" + echo "Automatic checkpoint merge failed; creating a manual-resolution PR instead." >&2 + if git rev-parse -q --verify MERGE_HEAD >/dev/null; then + git merge --abort + fi + git checkout -B "${checkpoint_branch}" "${source_sha}" + fi +fi + +if git cat-file -e "origin/${DESTINATION_BRANCH}:.github" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- .github + mapfile -t added_github_paths < <( + git diff --name-only --diff-filter=A "origin/${DESTINATION_BRANCH}" -- .github + ) + if ((${#added_github_paths[@]})); then + git rm -f --ignore-unmatch -- "${added_github_paths[@]}" + fi +else + git rm -r --ignore-unmatch .github +fi + +while IFS= read -r release_only_path; do + if git cat-file -e "origin/${DESTINATION_BRANCH}:${release_only_path}" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- "${release_only_path}" + else + git rm -f --ignore-unmatch -- "${release_only_path}" + fi +done < <(release_only_checkpoint_paths) + +if ! git diff --quiet || ! git diff --cached --quiet; then + if [[ -e .github || -L .github ]] || git ls-files --error-unmatch -- .github >/dev/null 2>&1; then + git add -A .github + fi + while IFS= read -r release_only_path; do + if [[ -e "${release_only_path}" || -L "${release_only_path}" ]] || git ls-files --error-unmatch -- "${release_only_path}" >/dev/null 2>&1; then + git add -A -- "${release_only_path}" + fi + done < <(release_only_checkpoint_paths) + if [[ "${merge_conflicted}" == "true" ]]; then + git commit -m "checkpoint: prepare ${source_branch} for ${DESTINATION_BRANCH}" + else + git commit --amend --no-edit + fi +fi + +if git diff --quiet "origin/${DESTINATION_BRANCH}" HEAD; then + echo "Checkpoint merge produced no destination changes after release-only files were restored." + exit 0 +fi + +git push --force-with-lease origin "${checkpoint_branch}" + +remaining="$( + git log --first-parent --pretty=format:%H "${source_sha}..origin/${source_branch}" | wc -w +)" + +body_path="${RUNNER_TEMP}/termux-checkpoint-pr.md" +{ + echo "## Termux release checkpoint" + echo + echo "- Source branch: \`${source_branch}\`" + echo "- Source hash: \`${source_sha}\`" + echo "- Destination branch: \`${DESTINATION_BRANCH}\`" + echo "- Remaining first-parent commits on source: ${remaining}" + echo + echo "This PR carries release-train conflict fixes and follow-up changes back into the reusable Termux patch branch." + if [[ "${merge_conflicted}" == "true" ]]; then + echo + echo "## Merge conflicts" + echo + echo "GitHub Actions could not create the checkpoint merge commit automatically, so this PR was created from the source branch state for manual conflict resolution." + echo + echo "Conflicted paths from the failed merge attempt:" + if [[ -n "${conflict_summary}" ]]; then + printf '%s\n' "${conflict_summary}" + else + echo "- Conflict details unavailable" + fi + fi + echo + echo "Release-only workflow files and metadata under \`.github\` were restored to the destination branch versions before opening this PR." +} > "${body_path}" + +pr_url="$( + gh pr create \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${DESTINATION_BRANCH}" \ + --head "${checkpoint_branch}" \ + --title "${pr_title}" \ + --body-file "${body_path}" +)" +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-reviewer "${REVIEWER}" || true +gh label create checkpoint --repo "${GITHUB_REPOSITORY}" --color c5def5 --description "Checkpoint merge" --force +gh label create termux-release --repo "${GITHUB_REPOSITORY}" --color 0e8a16 --description "Termux release automation" --force +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-label "checkpoint" --add-label "termux-release" + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${pr_url}" >> "${GITHUB_OUTPUT}" +fi diff --git a/scripts/termux-create-or-update-mirrored-release.sh b/scripts/termux-create-or-update-mirrored-release.sh new file mode 100755 index 000000000000..5787e894582b --- /dev/null +++ b/scripts/termux-create-or-update-mirrored-release.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +# Create a mirrored Termux release or repair an existing release that is missing +# the Android tarball. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" +: "${RUNNER_TEMP:?RUNNER_TEMP is required}" +: "${UPSTREAM_TAG:?UPSTREAM_TAG is required}" +: "${UPSTREAM_REPO:?UPSTREAM_REPO is required}" +: "${UPSTREAM_NAME?UPSTREAM_NAME is required}" +: "${TERMUX_TAG:?TERMUX_TAG is required}" +: "${UPSTREAM_PRERELEASE:?UPSTREAM_PRERELEASE is required}" +: "${UPSTREAM_HTML_URL?UPSTREAM_HTML_URL is required}" +: "${RELEASE_TRAIN?RELEASE_TRAIN is required}" +: "${RELEASE_EXISTS:?RELEASE_EXISTS is required}" +: "${PR_NUMBER:?PR_NUMBER is required}" +: "${HEAD_SHA:?HEAD_SHA is required}" +: "${RELEASE_SHA:?RELEASE_SHA is required}" + +promoted_dir="${PROMOTED_DIR:-promoted}" +asset_path="${promoted_dir}/codex-aarch64-linux-android.tar.gz" + +body_path="${RUNNER_TEMP}/release-body.md" +upstream_body_path="${RUNNER_TEMP}/upstream-release-body.md" +upstream_body_without_changelog_path="${RUNNER_TEMP}/upstream-release-body-without-changelog.md" +if gh release view "${UPSTREAM_TAG}" \ + --repo "${UPSTREAM_REPO}" \ + --json body \ + --jq '.body // ""' > "${upstream_body_path}"; then + awk ' + function heading_level(line, text) { + if (match(line, /^(#{1,6})[[:space:]]+(.+)$/, parts)) { + text = tolower(parts[2]) + sub(/[[:space:]]+#+[[:space:]]*$/, "", text) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", text) + if (text == "changelog" || text == "change log") { + return length(parts[1]) + } + } + return 0 + } + + { + if (!skip) { + level = heading_level($0) + if (level > 0) { + skip = 1 + skip_level = level + next + } + print + next + } + + if (match($0, /^(#{1,6})[[:space:]]+/, parts) && length(parts[1]) <= skip_level) { + skip = 0 + print + } + } + ' "${upstream_body_path}" > "${upstream_body_without_changelog_path}" +else + echo "::warning title=Upstream release notes unavailable::Could not read ${UPSTREAM_REPO} release ${UPSTREAM_TAG}." + : > "${upstream_body_without_changelog_path}" +fi + +{ + echo "Termux Android build for ${UPSTREAM_TAG}." + echo + echo "- Upstream release: ${UPSTREAM_HTML_URL}" + echo "- Release train: \`${RELEASE_TRAIN}\`" + echo "- Promoted PR: #${PR_NUMBER}" + echo "- Promoted PR head SHA: \`${HEAD_SHA}\`" +} > "${body_path}" +if [[ -s "${upstream_body_without_changelog_path}" ]]; then + { + echo + echo "## Upstream release notes" + echo + cat "${upstream_body_without_changelog_path}" + } >> "${body_path}" +fi + +release_title="${UPSTREAM_NAME}" +if [[ -z "${release_title}" || "${release_title}" == "null" ]]; then + release_title="${TERMUX_TAG}" +fi + +if [[ "${RELEASE_EXISTS}" == "true" ]]; then + gh release upload \ + "${TERMUX_TAG}" \ + "${asset_path}#codex-termux" \ + --repo "${GITHUB_REPOSITORY}" \ + --clobber + exit 0 +fi + +release_args=( + gh release create "${TERMUX_TAG}" + --repo "${GITHUB_REPOSITORY}" + --target "${RELEASE_SHA}" + --title "${release_title}" + --notes-file "${body_path}" +) +if [[ "${UPSTREAM_PRERELEASE}" == "true" ]]; then + release_args+=(--prerelease) +fi +release_args+=("${asset_path}#codex-termux") +"${release_args[@]}" diff --git a/scripts/termux-download-release-artifact.sh b/scripts/termux-download-release-artifact.sh new file mode 100755 index 000000000000..2acdba9025a0 --- /dev/null +++ b/scripts/termux-download-release-artifact.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${HEAD_SHA:-}" ]]; then + echo "HEAD_SHA is required." >&2 + exit 1 +fi + +if [[ -z "${PR_ARTIFACT_NAME:-}" ]]; then + echo "PR_ARTIFACT_NAME is required." >&2 + exit 1 +fi + +promoted_dir="${PROMOTED_DIR:-promoted}" + +find_run_id() { + local event="$1" + gh run list \ + --repo "${GITHUB_REPOSITORY}" \ + --workflow rust-release.yml \ + --event "${event}" \ + --status success \ + --commit "${HEAD_SHA}" \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId // empty' +} + +find_artifact_run_id() { + local artifact_name="$1" + + gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=${artifact_name}" \ + | jq -rs \ + --arg artifact_name "${artifact_name}" \ + --arg head_sha "${HEAD_SHA}" \ + ' + [ + .[].artifacts[] + | select(.name == $artifact_name) + | select(.expired == false) + | select(.workflow_run.head_sha == $head_sha) + ] + | sort_by(.created_at) + | reverse + | .[0].workflow_run.id // empty + ' +} + +artifact_name="${PR_ARTIFACT_NAME}" +run_id="$(find_artifact_run_id "${artifact_name}")" +mkdir -p "${promoted_dir}" +if [[ -n "${run_id}" ]] && gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}"; then + : +else + artifact_name="aarch64-linux-android" + run_id="$(find_run_id workflow_dispatch)" + if [[ -z "${run_id}" ]]; then + echo "No successful rust-release run found for ${HEAD_SHA}" >&2 + exit 1 + fi + gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}" +fi + +ls -la "${promoted_dir}" +if [[ -f "${promoted_dir}/SHA256SUMS" ]]; then + (cd "${promoted_dir}" && sha256sum -c SHA256SUMS) +fi +if [[ ! -f "${promoted_dir}/codex-aarch64-linux-android.tar.gz" ]]; then + echo "Expected ${promoted_dir}/codex-aarch64-linux-android.tar.gz in the downloaded artifact." >&2 + exit 1 +fi diff --git a/scripts/termux-find-release-pr.sh b/scripts/termux-find-release-pr.sh new file mode 100755 index 000000000000..3e35fa42eabe --- /dev/null +++ b/scripts/termux-find-release-pr.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -n "${INPUT_PR_NUMBER:-}" || -n "${INPUT_PR_HEAD_SHA:-}" ]]; then + if [[ -z "${INPUT_PR_NUMBER:-}" || -z "${INPUT_PR_HEAD_SHA:-}" ]]; then + echo "workflow_dispatch inputs pr_number and pr_head_sha must be provided together." >&2 + exit 1 + fi + + pr_number="${INPUT_PR_NUMBER}" + head_sha="${INPUT_PR_HEAD_SHA}" + head_ref="" +else + if [[ -z "${RELEASE_BRANCH:-}" || -z "${RELEASE_SHA:-}" ]]; then + echo "RELEASE_BRANCH and RELEASE_SHA are required." >&2 + exit 1 + fi + + pr_json="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${RELEASE_BRANCH}" \ + --state merged \ + --limit 100 \ + --json number,headRefOid,headRefName,mergeCommit \ + --jq "[.[] | select(.mergeCommit.oid == \"${RELEASE_SHA}\")] | sort_by(.number) | reverse | .[0] // empty" + )" + if [[ -z "${pr_json}" ]]; then + echo "Unable to find merged PR for ${RELEASE_SHA} into ${RELEASE_BRANCH}" >&2 + exit 1 + fi + + pr_number="$(jq -r '.number' <<< "${pr_json}")" + head_sha="$(jq -r '.headRefOid // .head.sha' <<< "${pr_json}")" + head_ref="$(jq -r '.headRefName // .head.ref' <<< "${pr_json}")" +fi + +artifact_name="termux-android-pr-${pr_number}-${head_sha}" +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "number=${pr_number}" + echo "head_sha=${head_sha}" + echo "head_ref=${head_ref}" + echo "artifact_name=${artifact_name}" + } >> "${GITHUB_OUTPUT}" +else + printf 'number=%s\nhead_sha=%s\nhead_ref=%s\nartifact_name=%s\n' \ + "${pr_number}" \ + "${head_sha}" \ + "${head_ref}" \ + "${artifact_name}" +fi diff --git a/scripts/termux-read-release-metadata.sh b/scripts/termux-read-release-metadata.sh new file mode 100755 index 000000000000..519a0635a368 --- /dev/null +++ b/scripts/termux-read-release-metadata.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# Parse .github/termux-release.json and emit workflow outputs for deploy or +# promote jobs. TERMUX_RELEASE_ACTION must be "deploy" or "promote". + +set -euo pipefail + +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +metadata=".github/termux-release.json" +action="${TERMUX_RELEASE_ACTION:-deploy}" +case "${action}" in + deploy) + missing_context="deployment" + ;; + promote) + missing_context="promotion" + ;; + *) + echo "TERMUX_RELEASE_ACTION must be deploy or promote." >&2 + exit 1 + ;; +esac + +if [[ ! -f "${metadata}" ]]; then + echo "No ${metadata}; this push is not a Termux release ${missing_context}." + echo "${action}=false" >> "${GITHUB_OUTPUT}" + exit 0 +fi + +upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" +upstream_name="$(jq -r '.upstream_name // .upstream_tag // empty' "${metadata}")" +termux_tag="$(jq -r '.termux_tag // empty' "${metadata}")" +upstream_version="${upstream_tag#rust-v}" +upstream_version="${upstream_version%-termux}" +upstream_prerelease=false +if [[ "${upstream_version}" == *-* ]]; then + upstream_prerelease=true +fi +upstream_html_url="$(jq -r '.upstream_html_url // ""' "${metadata}")" +upstream_repo="$(jq -r '.upstream_repo // "openai/codex"' "${metadata}")" +release_train="$(jq -r '.release_train // ""' "${metadata}")" +if [[ -z "${upstream_tag}" || -z "${termux_tag}" ]]; then + echo "Missing upstream_tag or termux_tag in ${metadata}" >&2 + exit 1 +fi + +release_state="$(TERMUX_TAG="${termux_tag}" "${script_dir}/termux-release-asset-state.sh")" +release_exists="$(awk -F= '$1 == "release_exists" { print $2 }' <<< "${release_state}")" +asset_exists="$(awk -F= '$1 == "asset_exists" { print $2 }' <<< "${release_state}")" + +if [[ "${action}" == "promote" ]]; then + if [[ "${asset_exists}" == "true" ]]; then + echo "${termux_tag} already exists with codex-aarch64-linux-android.tar.gz; skipping promotion." + echo "promote=false" >> "${GITHUB_OUTPUT}" + exit 0 + fi + if [[ "${release_exists}" == "true" ]]; then + echo "${termux_tag} exists but is missing codex-aarch64-linux-android.tar.gz; repairing promotion." + fi +fi + +{ + echo "${action}=true" + echo "upstream_tag=${upstream_tag}" + echo "upstream_name=${upstream_name}" + echo "termux_tag=${termux_tag}" + echo "upstream_prerelease=${upstream_prerelease}" + echo "upstream_html_url=${upstream_html_url}" + echo "upstream_repo=${upstream_repo}" + echo "release_train=${release_train}" + echo "release_exists=${release_exists}" + echo "asset_exists=${asset_exists}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-release-asset-state.sh b/scripts/termux-release-asset-state.sh new file mode 100755 index 000000000000..8694480b6a36 --- /dev/null +++ b/scripts/termux-release-asset-state.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +termux_tag="${1:-${TERMUX_TAG:-}}" +release_repo="${RELEASE_REPO:-${GITHUB_REPOSITORY:-}}" +asset_name="${ASSET_NAME:-codex-aarch64-linux-android.tar.gz}" + +if [[ -z "${termux_tag}" ]]; then + echo "TERMUX_TAG or tag argument is required." >&2 + exit 1 +fi + +if [[ -z "${release_repo}" ]]; then + echo "GITHUB_REPOSITORY or RELEASE_REPO is required." >&2 + exit 1 +fi + +release_exists=false +asset_exists=false + +if gh release view "${termux_tag}" --repo "${release_repo}" >/dev/null 2>&1; then + release_exists=true + release_asset_exists="$( + gh release view "${termux_tag}" \ + --repo "${release_repo}" \ + --json assets \ + | jq -r --arg asset_name "${asset_name}" \ + '.assets | map(.name) | any(. == $asset_name)' + )" + if [[ "${release_asset_exists}" == "true" ]]; then + asset_exists=true + fi +fi + +printf 'release_exists=%s\n' "${release_exists}" +printf 'asset_exists=%s\n' "${asset_exists}" diff --git a/scripts/termux-release-paths.sh b/scripts/termux-release-paths.sh new file mode 100755 index 000000000000..6990f323ec44 --- /dev/null +++ b/scripts/termux-release-paths.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# Sourceable path lists for files owned by the Termux release automation. + +set -euo pipefail + +readonly -a TERMUX_RELEASE_WORKFLOW_PATHS=( + .github/workflows/rust-release.yml + .github/workflows/shell-tool-mcp.yml + .github/workflows/termux-release-checkpoint.yml + .github/workflows/termux-release-deploy.yml + .github/workflows/termux-release-promote.yml +) + +readonly -a TERMUX_RELEASE_BRANCH_SCRIPT_PATHS=( + scripts/termux-configure-git.sh + scripts/termux-create-checkpoint-pr.sh + scripts/termux-create-or-update-mirrored-release.sh + scripts/termux-download-release-artifact.sh + scripts/termux-find-release-pr.sh + scripts/termux-read-release-metadata.sh + scripts/termux-release-asset-state.sh + scripts/termux-release-paths.sh + scripts/termux-resolve-release-ref.sh + scripts/termux-validate-gh-env.sh +) + +readonly -a TERMUX_RELEASE_AUTOMATION_PATHS=( + "${TERMUX_RELEASE_WORKFLOW_PATHS[@]}" + "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +) + +readonly -a TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS=( + "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" + .github/termux-release.json +) + +termux_path_in_list() { + local candidate="$1" + shift + local listed_path + + for listed_path in "$@"; do + [[ "${candidate}" != "${listed_path}" ]] || return 0 + done + return 1 +} + +termux_is_release_automation_path() { + termux_path_in_list "$1" "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" +} + +termux_is_checkpoint_release_only_path() { + termux_path_in_list "$1" "${TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS[@]}" +} diff --git a/scripts/termux-resolve-release-ref.sh b/scripts/termux-resolve-release-ref.sh new file mode 100755 index 000000000000..194773d45bed --- /dev/null +++ b/scripts/termux-resolve-release-ref.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Resolve the release branch and SHA for deploy/promote workflows. + +set -euo pipefail + +: "${GITHUB_EVENT_NAME:?GITHUB_EVENT_NAME is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" + +input_release_branch="${INPUT_RELEASE_BRANCH:-${REQUESTED_RELEASE_BRANCH:-}}" +input_release_sha="${INPUT_RELEASE_SHA:-${REQUESTED_RELEASE_SHA:-}}" + +if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then + if [[ -z "${input_release_branch}" ]]; then + echo "release_branch is required for workflow_dispatch." >&2 + exit 1 + fi + + release_branch="${input_release_branch}" + if [[ -n "${input_release_sha}" ]]; then + git checkout --detach "${input_release_sha}" + release_sha="${input_release_sha}" + else + release_sha="$(git rev-parse HEAD)" + fi +else + : "${GITHUB_REF_NAME:?GITHUB_REF_NAME is required}" + : "${GITHUB_SHA:?GITHUB_SHA is required}" + release_branch="${GITHUB_REF_NAME}" + release_sha="${GITHUB_SHA}" +fi + +{ + echo "branch=${release_branch}" + echo "sha=${release_sha}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-validate-gh-env.sh b/scripts/termux-validate-gh-env.sh new file mode 100755 index 000000000000..02581fdebceb --- /dev/null +++ b/scripts/termux-validate-gh-env.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Lightweight post-toolbox check for jobs that rely on authenticated gh calls. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +command -v gh +gh auth status --hostname github.com + +printf 'GITHUB_REPOSITORY=%s\n' "${GITHUB_REPOSITORY}" +printf 'REPO=%s\n' "${REPO:-}" +printf 'GH_REPO_URL=%s\n' "${GH_REPO_URL:-}" +printf 'GH_WORKFLOW_URL=%s\n' "${GH_WORKFLOW_URL:-}" From 811db957e7e52556dbeaa25d54caa715ea9b1b4f Mon Sep 17 00:00:00 2001 From: Shijie Rao Date: Fri, 22 May 2026 13:41:30 -0700 Subject: [PATCH 17/25] Release 0.134.0-alpha.2 --- codex-rs/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index df169504f081..daa3d1fdcf39 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -117,7 +117,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.0.0" +version = "0.134.0-alpha.2" # Track the edition for all workspace crates in one place. Individual # crates can still override this value, but keeping it here means new # crates created with `cargo new -w ...` automatically inherit the 2024 From 52ce382a40975a9883ebbdfdc5a57b16cbfa8583 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 21:45:31 +0000 Subject: [PATCH 18/25] Seed Termux release automation --- .github/workflows/rust-release.yml | 1807 +++++++---------- .github/workflows/shell-tool-mcp.yml | 461 +++++ .../workflows/termux-release-checkpoint.yml | 103 + .github/workflows/termux-release-deploy.yml | 243 +++ .github/workflows/termux-release-promote.yml | 135 ++ scripts/termux-configure-git.sh | 40 + scripts/termux-create-checkpoint-pr.sh | 275 +++ ...ermux-create-or-update-mirrored-release.sh | 111 + scripts/termux-download-release-artifact.sh | 78 + scripts/termux-find-release-pr.sh | 53 + scripts/termux-read-release-metadata.sh | 77 + scripts/termux-release-asset-state.sh | 37 + scripts/termux-release-paths.sh | 55 + scripts/termux-resolve-release-ref.sh | 36 + scripts/termux-validate-gh-env.sh | 16 + 15 files changed, 2445 insertions(+), 1082 deletions(-) create mode 100644 .github/workflows/shell-tool-mcp.yml create mode 100644 .github/workflows/termux-release-checkpoint.yml create mode 100644 .github/workflows/termux-release-deploy.yml create mode 100644 .github/workflows/termux-release-promote.yml create mode 100755 scripts/termux-configure-git.sh create mode 100755 scripts/termux-create-checkpoint-pr.sh create mode 100755 scripts/termux-create-or-update-mirrored-release.sh create mode 100755 scripts/termux-download-release-artifact.sh create mode 100755 scripts/termux-find-release-pr.sh create mode 100755 scripts/termux-read-release-metadata.sh create mode 100755 scripts/termux-release-asset-state.sh create mode 100755 scripts/termux-release-paths.sh create mode 100755 scripts/termux-resolve-release-ref.sh create mode 100755 scripts/termux-validate-gh-env.sh diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 2cd11e66fe0f..56d0977e6e67 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -4,46 +4,49 @@ # git tag -a rust-v0.1.0 -m "Release 0.1.0" # git push origin rust-v0.1.0 # ``` -# -# To use external macOS signing, manually dispatch `release_mode=build_unsigned`, -# sign the unsigned macOS artifacts in a secure enclave, upload the signed handoff -# archive as a GitHub Release asset, then manually dispatch -# `release_mode=promote_signed` with `unsigned_run_id` and `signed_macos_asset`. -# The signed handoff archive should contain target or artifact directories such -# as `aarch64-apple-darwin/` with signed binaries. name: rust-release on: - push: - tags: - - "rust-v*.*.*" + pull_request: + branches: + - "release/**" workflow_dispatch: inputs: - release_mode: - description: "build_unsigned creates unsigned macOS handoff artifacts; promote_signed finishes a release from signed macOS handoff artifacts." + source_ref: + description: "Branch, tag, or commit SHA to build (code only; keeps .github from dispatched ref)" required: false + type: string + default: "" + build_target: + description: "Target to build" + required: true type: choice - default: build_unsigned + default: all options: - - build_unsigned - - promote_signed - sign_macos: - description: "Deprecated compatibility input; use release_mode instead." - required: false - type: boolean - default: false - unsigned_run_id: - description: "For promote_signed: workflow run id from the build_unsigned run." - required: false - type: string - signed_macos_asset: - description: "For promote_signed: exact GitHub Release asset name containing signed macOS handoff artifacts." - required: false - type: string - signed_macos_sha256: - description: "For promote_signed: optional SHA-256 of signed_macos_asset." - required: false - type: string + - all + - aarch64-apple-darwin + - x86_64-apple-darwin + - x86_64-unknown-linux-musl + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-musl + - aarch64-unknown-linux-gnu + - aarch64-linux-android + - x86_64-pc-windows-msvc + - aarch64-pc-windows-msvc + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read concurrency: group: ${{ github.workflow }} @@ -51,72 +54,26 @@ concurrency: jobs: tag-check: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - uses: dtolnay/rust-toolchain@1.92 - name: Validate tag matches Cargo.toml version shell: bash - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - REQUESTED_SIGN_MACOS: ${{ inputs.sign_macos }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} run: | set -euo pipefail echo "::group::Tag validation" - case "${RELEASE_MODE}" in - signed) - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - echo "❌ Manual rust-release runs must use release_mode=build_unsigned or release_mode=promote_signed" - exit 1 - fi - ;; - build_unsigned) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=build_unsigned is only valid for manual runs" - exit 1 - fi - ;; - promote_signed) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=promote_signed is only valid for manual runs" - exit 1 - fi - if [[ ! "${UNSIGNED_RUN_ID}" =~ ^[0-9]+$ ]]; then - echo "❌ release_mode=promote_signed requires unsigned_run_id to be a workflow run id" - exit 1 - fi - if [[ -z "${SIGNED_MACOS_ASSET}" ]]; then - echo "❌ release_mode=promote_signed requires signed_macos_asset" - exit 1 - fi - if [[ "${SIGNED_MACOS_ASSET}" == */* || "${SIGNED_MACOS_ASSET}" == *"*"* || "${SIGNED_MACOS_ASSET}" == *"?"* || "${SIGNED_MACOS_ASSET}" == *"["* ]]; then - echo "❌ signed_macos_asset must be an exact release asset name, not a path or glob" - exit 1 - fi - if [[ "${UNSIGNED_RUN_ID}" == "${GITHUB_RUN_ID}" ]]; then - echo "❌ unsigned_run_id must refer to the earlier build_unsigned run, not this run" - exit 1 - fi - ;; - *) - echo "❌ Unknown release_mode '${RELEASE_MODE}'" - exit 1 - ;; - esac - - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${REQUESTED_SIGN_MACOS}" == "true" ]]; then - echo "::warning title=Deprecated sign_macos input ignored::Use release_mode=build_unsigned or release_mode=promote_signed instead." - fi - # 1. Must be a tag and match the regex [[ "${GITHUB_REF_TYPE}" == "tag" ]] \ || { echo "❌ Not a tag push"; exit 1; } - [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \ + [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?(-termux)?$ ]] \ || { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; } # 2. Extract versions @@ -131,115 +88,198 @@ jobs: echo "✅ Tag and Cargo.toml agree (${tag_ver})" echo "::endgroup::" - build: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} + select-build-matrix: + if: always() && (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') needs: tag-check - name: Build - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: ${{ matrix.runs_on || matrix.runner }} - # Release builds can take a long time, so leave some headroom to avoid - # having to restart the full workflow due to a timeout. - timeout-minutes: 90 - permissions: - contents: read - id-token: write + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.select.outputs.matrix }} + build_version: ${{ steps.select.outputs.build_version }} + steps: + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - id: select + shell: bash + env: + REPO_OWNER: ${{ github.repository_owner }} + EVENT_NAME: ${{ github.event_name }} + BUILD_TARGET: ${{ inputs.build_target }} + run: | + set -euo pipefail + + matrix='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04","target":"aarch64-linux-android"}, + {"runner":"windows-latest","target":"x86_64-pc-windows-msvc"}, + {"runner":"windows-11-arm","target":"aarch64-pc-windows-msvc"} + ]' + + if [[ "${REPO_OWNER}" != "openai" ]]; then + matrix="$(jq -c '[.[] | select(.runner | startswith("macos") | not)]' <<< "${matrix}")" + fi + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + matrix="$(jq -c '[.[] | select(.target == "aarch64-linux-android")]' <<< "${matrix}")" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" && -n "${BUILD_TARGET}" && "${BUILD_TARGET}" != "all" ]]; then + matrix="$(jq -c --arg build_target "${BUILD_TARGET}" '[.[] | select(.target == $build_target)]' <<< "${matrix}")" + fi + + if [[ "$(jq 'length' <<< "${matrix}")" -eq 0 ]]; then + echo "No build targets selected after applying owner/dispatch filters." >&2 + exit 1 + fi + + echo "matrix={\"include\":${matrix}}" >> "$GITHUB_OUTPUT" + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + metadata=".github/termux-release.json" + if [[ ! -f "${metadata}" ]]; then + echo "${metadata} is required for release train PR builds" >&2 + exit 1 + fi + upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" + if [[ -z "${upstream_tag}" || "${upstream_tag}" != rust-v* ]]; then + echo "Unable to determine upstream rust tag from ${metadata}" >&2 + exit 1 + fi + build_version="${upstream_tag#rust-v}" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then + latest_release_tag="$( + GH_TOKEN="${{ github.token }}" gh api repos/openai/codex/releases/latest --jq '.tag_name' + )" + + if [[ -z "${latest_release_tag}" || "${latest_release_tag}" != rust-v* ]]; then + echo "Unable to determine latest stable release tag from openai/codex" >&2 + exit 1 + fi + + latest_release_version="${latest_release_tag#rust-v}" + build_version="${latest_release_version}-dev" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + fi + + build: + if: >- + always() && + (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') && + needs.select-build-matrix.result == 'success' + needs: + - tag-check + - select-build-matrix + name: Build - ${{ matrix.runner }} - ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + permissions: write-all + timeout-minutes: 120 defaults: run: working-directory: codex-rs env: - # 2026-03-04: temporarily change releases to use thin LTO because - # Ubuntu ARM is timing out at 60 minutes. - CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' }} + CODEX_BWRAP_ENABLE_FFI: ${{ contains(matrix.target, 'unknown-linux') && '1' || '0' }} + CODEX_BUILD_VERSION: ${{ needs.select-build-matrix.outputs.build_version }} + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" strategy: fail-fast: false - matrix: - include: - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - # Release artifacts intentionally ship MUSL-linked Linux binaries. - - runner: codex-linux-x64-xl - target: x86_64-unknown-linux-musl - bundle: primary - artifact_name: x86_64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: codex-linux-x64-xl - target: x86_64-unknown-linux-musl - bundle: app-server - artifact_name: x86_64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: codex-linux-arm64 - target: aarch64-unknown-linux-musl - bundle: primary - artifact_name: aarch64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: codex-linux-arm64 - target: aarch64-unknown-linux-musl - bundle: app-server - artifact_name: aarch64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" + matrix: ${{ fromJson(needs.select-build-matrix.outputs.matrix) }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + with: + verbose: true + - name: Checkout selected source ref + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + uses: actions/checkout@v6 with: - persist-credentials: false - - name: Print runner specs (Linux) - if: ${{ runner.os == 'Linux' }} + ref: ${{ inputs.source_ref }} + path: __source__ + fetch-depth: 1 + - name: Overlay selected source ref (excluding .github) + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + shell: bash + working-directory: ${{ github.workspace }} + run: | + set -euo pipefail + echo "Using source ref: ${{ inputs.source_ref }}" + + # Keep workflow/actions from the dispatch ref while replacing all other files. + find . -mindepth 1 -maxdepth 1 \ + ! -name .git \ + ! -name .github \ + ! -name __source__ \ + -exec rm -rf -- {} + + tar --exclude='.github' -C __source__ -cf - . | tar -xf - + rm -rf __source__ + + - name: Apply selected build version + if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} shell: bash + working-directory: ${{ github.workspace }} run: | set -euo pipefail - cpu_model="$(lscpu | awk -F: '/Model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}')" - total_ram="$(awk '/MemTotal/ {printf "%.1f GiB\n", $2 / 1024 / 1024}' /proc/meminfo)" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(uname -a)" - echo "CPU model: ${cpu_model}" - echo "Logical CPUs: $(nproc)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . - - name: Print runner specs (macOS) - if: ${{ runner.os == 'macOS' }} + if [[ -z "${CODEX_BUILD_VERSION}" ]]; then + echo "CODEX_BUILD_VERSION is empty for workflow_dispatch" >&2 + exit 1 + fi + echo "Using build version: ${CODEX_BUILD_VERSION}" + BUILD_VERSION="${CODEX_BUILD_VERSION}" perl -0777 -i -pe \ + 's/(\[workspace\.package\][^\[]*?version = ")([^"]+)(")/${1}$ENV{BUILD_VERSION}$3/s' \ + codex-rs/Cargo.toml + + grep -n "^\[workspace.package\]\|^version = " codex-rs/Cargo.toml | head -n 4 + + - name: Select Android NDK + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail - total_ram="$(sysctl -n hw.memsize | awk '{printf "%.1f GiB\n", $1 / 1024 / 1024 / 1024}')" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(sw_vers -productName) $(sw_vers -productVersion)" - echo "Hardware model: $(sysctl -n hw.model)" - echo "CPU architecture: $(uname -m)" - echo "Logical CPUs: $(sysctl -n hw.logicalcpu)" - echo "Physical CPUs: $(sysctl -n hw.physicalcpu)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . + + fallback_version="29.0.13113456" + ndk_root="${ANDROID_HOME}/ndk" + selected_ndk="" + + if [[ -d "${ndk_root}" ]]; then + while IFS= read -r candidate; do + toolchain="${candidate}/toolchains/llvm/prebuilt/linux-x86_64" + if [[ -x "${toolchain}/bin/aarch64-linux-android24-clang" ]]; then + selected_ndk="${candidate}" + break + fi + done < <(find "${ndk_root}" -mindepth 1 -maxdepth 1 -type d | sort -Vr) + fi + + if [[ -z "${selected_ndk}" ]]; then + echo "No usable preinstalled Android NDK found; installing ${fallback_version}" + yes | sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses || true + sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" "ndk;${fallback_version}" + selected_ndk="${ndk_root}/${fallback_version}" + fi + + if [[ ! -x "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" ]]; then + echo "Selected Android NDK does not contain a usable aarch64 API 24 clang: ${selected_ndk}" >&2 + exit 1 + fi + + echo "Selected Android NDK: ${selected_ndk}" + "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" --version + echo "ANDROID_NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" + echo "NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" - name: Install Linux bwrap build dependencies - if: ${{ runner.os == 'Linux' }} + if: ${{ runner.os == 'Linux' && matrix.target != 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail @@ -254,9 +294,173 @@ jobs: sudo apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 fi - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@1.93 with: targets: ${{ matrix.target }} + - name: Set up sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + uses: mozilla-actions/sccache-action@main + - name: Enable sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + shell: bash + run: | + set -euo pipefail + echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_VERSION=android-release-${{ matrix.target }}" >> "$GITHUB_ENV" + echo "SCCACHE_CACHE_SIZE=5G" >> "$GITHUB_ENV" + - name: Ensure Rust target is installed + shell: bash + run: | + set -euo pipefail + rustup target add "${{ matrix.target }}" + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android build environment + shell: bash + run: | + set -euo pipefail + ndk="${ANDROID_NDK_HOME}" + target="${{ matrix.target }}" + + # Set up the Android toolchain + toolchain="${ndk}/toolchains/llvm/prebuilt/linux-x86_64" + + # Use API level 24 to follow termux-build convention + api_level="24" + + # Configure Cargo to use the NDK linker + cargo_target_var="CARGO_TARGET_${target^^}_LINKER" + cargo_target_var="${cargo_target_var//-/_}" + echo "${cargo_target_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + # Set CC and AR for the target + target_cc_var="CC_${target}" + target_cc_var="${target_cc_var//-/_}" + echo "${target_cc_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + target_cxx_var="CXX_${target}" + target_cxx_var="${target_cxx_var//-/_}" + echo "${target_cxx_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang++" >> "$GITHUB_ENV" + + target_ar_var="AR_${target}" + target_ar_var="${target_ar_var//-/_}" + echo "${target_ar_var}=${toolchain}/bin/llvm-ar" >> "$GITHUB_ENV" + + # Add toolchain to PATH + echo "${toolchain}/bin" >> "$GITHUB_PATH" + + tls_align_source="${RUNNER_TEMP}/codex-android-tls-align.S" + tls_align_object="${RUNNER_TEMP}/codex-android-tls-align.o" + cat > "${tls_align_source}" <<'EOF' + .section .tdata.codex_android_tls_align,"awT",%progbits + .p2align 6 + .globl __codex_android_tls_align + .hidden __codex_android_tls_align + .type __codex_android_tls_align, %object + __codex_android_tls_align: + .byte 0 + .size __codex_android_tls_align, 1 + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${tls_align_source}" -o "${tls_align_object}" + + v8_compat_source="${RUNNER_TEMP}/codex-android-v8-compat.c" + v8_compat_object="${RUNNER_TEMP}/codex-android-v8-compat.o" + cat > "${v8_compat_source}" <<'EOF' + #include + #include + + extern int posix_memalign(void **memptr, size_t alignment, size_t size); + extern double strtod(const char *nptr, char **endptr); + extern float strtof(const char *nptr, char **endptr); + + void *aligned_alloc(size_t alignment, size_t size) { + void *ptr = 0; + if (posix_memalign(&ptr, alignment, size) != 0) { + return 0; + } + return ptr; + } + + double strtod_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtod(nptr, endptr); + } + + float strtof_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtof(nptr, endptr); + } + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${v8_compat_source}" -o "${v8_compat_object}" + + builtins_archive="$( + find "${toolchain}/lib/clang" \ + -path "*/lib/linux/libclang_rt.builtins-aarch64-android.a" \ + -print -quit + )" + if [[ -z "${builtins_archive}" ]]; then + echo "Could not find Android compiler-rt builtins archive under ${toolchain}/lib/clang" >&2 + exit 1 + fi + builtins_dir="$(dirname "${builtins_archive}")" + + # Provide compatibility symbols and C++ ABI needed by the Android rusty_v8 archive. + rustflags_var="CARGO_TARGET_${target^^}_RUSTFLAGS" + rustflags_var="${rustflags_var//-/_}" + echo "${rustflags_var}=-C link-arg=${tls_align_object} -C link-arg=${v8_compat_object} -C link-arg=-Wl,-u,__codex_android_tls_align -C link-arg=-L${builtins_dir} -C link-arg=-lclang_rt.builtins-aarch64-android -C link-arg=-lc++abi -C link-arg=-Wl,-z,max-page-size=65536" >> "$GITHUB_ENV" + + + # Configure for vendored OpenSSL build + echo "CARGO_BUILD_TARGET_APPLIES_TO_HOST=false" >> "$GITHUB_ENV" + echo "CARGO_BUILD_TARGET=${{ matrix.target }}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android rusty_v8 artifact overrides + env: + GH_TOKEN: ${{ github.token }} + TARGET: ${{ matrix.target }} + shell: bash + run: | + set -euo pipefail + binding_dir="${RUNNER_TEMP}/rusty_v8" + archive="${binding_dir}/librusty_v8_release_${TARGET}.a.gz" + binding_path="${binding_dir}/src_binding_release_${TARGET}.rs" + checksums_path="${binding_dir}/rusty_v8_release_${TARGET}.sha256" + artifact_repository="wallentx/codex-termux" + release_tag="rusty-v8-v147.4.0" + mkdir -p "${binding_dir}" + echo "Downloading Android rusty_v8 artifacts from ${artifact_repository}@${release_tag}" + gh release download "${release_tag}" \ + --repo "${artifact_repository}" \ + --dir "${binding_dir}" \ + --pattern "librusty_v8_release_${TARGET}.a.gz" \ + --pattern "src_binding_release_${TARGET}.rs" \ + --pattern "rusty_v8_release_${TARGET}.sha256" + (cd "${binding_dir}" && sha256sum -c "$(basename "${checksums_path}")") + echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV" + echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Install termux-elf-cleaner + shell: bash + run: | + set -euo pipefail + version="v3.0.1" + src_dir="${RUNNER_TEMP}/termux-elf-cleaner-src" + build_dir="${RUNNER_TEMP}/termux-elf-cleaner-build" + bin_dir="${RUNNER_TEMP}/termux-elf-cleaner-bin" + + rm -rf "${src_dir}" "${build_dir}" "${bin_dir}" + mkdir -p "${src_dir}" "${build_dir}" "${bin_dir}" + + curl -fsSL "https://github.com/termux/termux-elf-cleaner/archive/refs/tags/${version}.tar.gz" \ + | tar -xzf - --strip-components=1 -C "${src_dir}" + + cmake -S "${src_dir}" -B "${build_dir}" -DCMAKE_BUILD_TYPE=Release + cmake --build "${build_dir}" --parallel + + install "${build_dir}/termux-elf-cleaner" "${bin_dir}/termux-elf-cleaner" + echo "${bin_dir}" >> "$GITHUB_PATH" - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Use hermetic Cargo home (musl) @@ -269,12 +473,25 @@ jobs: echo "${cargo_home}/bin" >> "$GITHUB_PATH" : > "${cargo_home}/config.toml" + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ${{ github.workspace }}/.cargo-home/bin/ + ${{ github.workspace }}/.cargo-home/registry/index/ + ${{ github.workspace }}/.cargo-home/registry/cache/ + ${{ github.workspace }}/.cargo-home/git/db/ + ${{ github.workspace }}/codex-rs/target/ + key: cargo-${{ matrix.runner }}-${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }} + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install Zig - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + uses: mlugg/setup-zig@v2 with: version: 0.14.0 - use-cache: false - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install musl build tools @@ -310,12 +527,6 @@ jobs: shell: bash run: | set -euo pipefail - # Avoid problematic aws-lc jitter entropy code path on musl builders. - echo "AWS_LC_SYS_NO_JITTER_ENTROPY=1" >> "$GITHUB_ENV" - target_no_jitter="AWS_LC_SYS_NO_JITTER_ENTROPY_${{ matrix.target }}" - target_no_jitter="${target_no_jitter//-/_}" - echo "${target_no_jitter}=1" >> "$GITHUB_ENV" - # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. echo "RUSTFLAGS=" >> "$GITHUB_ENV" echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" @@ -340,93 +551,70 @@ jobs: echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" - - name: Configure rusty_v8 artifact overrides and verify checksums - uses: ./.github/actions/setup-rusty-v8 - with: - target: ${{ matrix.target }} - - - if: ${{ contains(matrix.target, 'linux') }} - name: Build bwrap and export digest + - name: Cargo build shell: bash run: | set -euo pipefail - target="${{ matrix.target }}" - cargo build --target "$target" --release --timings --bin bwrap - bwrap_path="target/${target}/release/bwrap" - if [[ ! -f "$bwrap_path" ]]; then - echo "bwrap binary ${bwrap_path} not found" - exit 1 + if [[ "${{ matrix.target }}" == 'aarch64-linux-android' ]]; then + export CARGO_BUILD_JOBS=4 + export CARGO_PROFILE_RELEASE_LTO=thin + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 + cargo build --target ${{ matrix.target }} --release --bin codex + exit 0 fi - digest="$(sha256sum "$bwrap_path" | awk '{print $1}')" - echo "CODEX_BWRAP_SHA256=${digest}" >> "$GITHUB_ENV" - echo "Built bwrap ${bwrap_path} with sha256:${digest}" - - - name: Cargo build + if [[ "${{ contains(matrix.target, 'windows') }}" == 'true' ]]; then + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy --bin codex-windows-sandbox-setup --bin codex-command-runner + else + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy + fi + - name: sccache stats (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash - run: | - build_args=() - for binary in ${{ matrix.binaries }}; do - build_args+=(--bin "$binary") - done - echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}" - cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}" - - - name: Upload Cargo timings - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }} - path: codex-rs/target/**/cargo-timings/cargo-timing.html - if-no-files-found: warn + run: ${SCCACHE_PATH:-sccache} --show-stats || true - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Stage unsigned macOS artifacts + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Normalize Android ELF for Termux shell: bash run: | set -euo pipefail - - target="${{ matrix.target }}" - release_dir="target/${target}/release" - dest="unsigned-dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - unsigned_name="${binary}-${target}-unsigned" - unsigned_path="${dest}/${unsigned_name}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 + for binary in codex codex-responses-api-proxy; do + binary_path="target/${{ matrix.target }}/release/${binary}" + if [[ -f "${binary_path}" ]]; then + termux-elf-cleaner --api-level 24 "${binary_path}" + chmod +x "${binary_path}" + # if [[ "${binary}" == "codex" ]]; then + # # Patch PT_TLS (0x7) to PT_NULL (0x0) at offset 400 to bypass Bionic alignment checks. + # printf '\x00\x00\x00\x00' | dd of="${binary_path}" bs=1 seek=400 count=4 conv=notrunc + # fi fi - - cp "${binary_path}" "${unsigned_path}" - tar -C "$dest" -czf "${unsigned_path}.tar.gz" "${unsigned_name}" - zstd -T0 -19 --rm "${unsigned_path}" done - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Upload unsigned macOS artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }}-unsigned - path: codex-rs/unsigned-dist/${{ matrix.target }}/* - if-no-files-found: error - - - if: ${{ contains(matrix.target, 'linux') }} + - if: ${{ contains(matrix.target, 'linux') && !contains(matrix.target, 'android') && github.repository_owner == 'openai' }} name: Cosign Linux artifacts uses: ./.github/actions/linux-code-sign with: target: ${{ matrix.target }} artifacts-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release - binaries: ${{ matrix.binaries }} - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS == 'true' }} + - if: ${{ contains(matrix.target, 'windows') && github.repository_owner == 'openai' }} + name: Sign Windows binaries with Azure Trusted Signing + uses: ./.github/actions/windows-code-sign + with: + target: ${{ matrix.target }} + client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID }} + endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} + account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} + + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (binaries) uses: ./.github/actions/macos-code-sign with: target: ${{ matrix.target }} - binaries: ${{ matrix.binaries }} sign-binaries: "true" sign-dmg: "false" apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }} @@ -435,7 +623,7 @@ jobs: apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: Build macOS dmg shell: bash run: | @@ -450,17 +638,23 @@ jobs: # The previous "MacOS code signing (binaries)" step signs + notarizes the # built artifacts in `${release_dir}`. This step packages *those same* # signed binaries into a dmg. + codex_binary_path="${release_dir}/codex" + proxy_binary_path="${release_dir}/codex-responses-api-proxy" + rm -rf "$dmg_root" mkdir -p "$dmg_root" - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 - fi - ditto "${binary_path}" "${dmg_root}/${binary}" - done + if [[ ! -f "$codex_binary_path" ]]; then + echo "Binary $codex_binary_path not found" + exit 1 + fi + if [[ ! -f "$proxy_binary_path" ]]; then + echo "Binary $proxy_binary_path not found" + exit 1 + fi + + ditto "$codex_binary_path" "${dmg_root}/codex" + ditto "$proxy_binary_path" "${dmg_root}/codex-responses-api-proxy" rm -f "$dmg_path" hdiutil create \ @@ -475,7 +669,7 @@ jobs: exit 1 fi - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (dmg) uses: ./.github/actions/macos-code-sign with: @@ -489,113 +683,65 @@ jobs: apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - name: Stage artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | dest="dist/${{ matrix.target }}" mkdir -p "$dest" - for binary in ${{ matrix.binaries }}; do - cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" - if [[ "${{ matrix.target }}" == *linux* ]]; then - cp "target/${{ matrix.target }}/release/${binary}.sigstore" \ - "$dest/${binary}-${{ matrix.target }}.sigstore" + if [[ "${{ matrix.runner }}" == windows* ]]; then + cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe" + else + # For Android, we want the binary to be named just 'codex' in the archive. + if [[ "${{ matrix.target }}" == "aarch64-linux-android" ]]; then + cp target/${{ matrix.target }}/release/codex "$dest/codex" + else + cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" fi - done + if [[ -f target/${{ matrix.target }}/release/codex-responses-api-proxy ]]; then + cp target/${{ matrix.target }}/release/codex-responses-api-proxy "$dest/codex-responses-api-proxy-${{ matrix.target }}" + fi + fi - if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.bundle }}" == "primary" ]]; then - bundle_root="${RUNNER_TEMP}/codex-${{ matrix.target }}-bundle" - rm -rf "$bundle_root" - mkdir -p "$bundle_root/codex-resources" - cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex" - cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap" - chmod 0755 "$bundle_root/codex" "$bundle_root/codex-resources/bwrap" - tar -C "$bundle_root" -cf - codex codex-resources/bwrap | - zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst" + if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.target }}" != *android* ]]; then + cp target/${{ matrix.target }}/release/codex.sigstore "$dest/codex-${{ matrix.target }}.sigstore" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.sigstore "$dest/codex-responses-api-proxy-${{ matrix.target }}.sigstore" fi - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then + if [[ "${{ matrix.target }}" == *apple-darwin ]]; then cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" fi - - name: Build Codex package archive - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "target/${TARGET}/release" \ - --archive-dir "dist/${TARGET}" - - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - aarch64-unknown-linux-musl) - platform_tag="manylinux_2_17_aarch64" - ;; - x86_64-unknown-linux-musl) - platform_tag="manylinux_2_17_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - # Do not install into the runner's system Python; macOS runners mark - # the Homebrew Python as externally managed under PEP 668. - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - stage_runtime_args=( - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" - stage-runtime - "$stage_dir" - "dist/${{ matrix.target }}/codex-package-${{ matrix.target }}.tar.gz" - --codex-version "${GITHUB_REF_NAME}" - --platform-tag "$platform_tag" - ) - python3 "${stage_runtime_args[@]}" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error + - if: ${{ matrix.runner == 'windows-11-arm' }} + name: Install zstd + shell: powershell + run: choco install -y zstandard - name: Compress artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | # Path that contains the uncompressed binaries for the current # ${{ matrix.target }} dest="dist/${{ matrix.target }}" + repo_root=$PWD + + # We want to ship the raw Windows executables in the GitHub Release + # in addition to the compressed archives. Keep the originals for + # Windows targets; remove them elsewhere to limit the number of + # artifacts that end up in the GitHub Release. + keep_originals=false + if [[ "${{ matrix.runner }}" == windows* ]]; then + keep_originals=true + fi # For compatibility with environments that lack the `zstd` tool we - # additionally create a `.tar.gz` alongside every binary we publish. - # The end result is: + # additionally create a `.tar.gz` for all platforms and `.zip` for + # Windows alongside every single binary that we publish. The end result is: # codex-.zst (existing) # codex-.tar.gz (new) + # codex-.zip (only for Windows) # 1. Produce a .tar.gz for every file in the directory *before* we # run `zstd --rm`, because that flag deletes the original files. @@ -603,7 +749,7 @@ jobs: base="$(basename "$f")" # Skip files that are already archives (shouldn't happen, but be # safe). - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then + if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then continue fi @@ -613,344 +759,239 @@ jobs: fi # Create per-binary tar.gz - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - - # Also create .zst and remove the uncompressed binaries to keep - # non-Windows artifact directories small. - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - with: - name: ${{ matrix.artifact_name }} - # Upload the per-binary .zst files, .tar.gz equivalents, and any - # prebuilt archives staged above. - path: | - codex-rs/dist/${{ matrix.target }}/* - - stage-signed-macos: - if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode == 'promote_signed' }} - needs: tag-check - name: Stage signed macOS handoff - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: macos-15-xlarge - timeout-minutes: 30 - permissions: - contents: read - defaults: - run: - working-directory: codex-rs + # For Android, we want the archive to have the target suffix even though the binary is just 'codex'. + archive_name="${base}" + if [[ "${{ matrix.target }}" == "aarch64-linux-android" && "${base}" == "codex" ]]; then + archive_name="codex-${{ matrix.target }}" + fi + tar -C "$dest" -czf "$dest/${archive_name}.tar.gz" "$base" + + # Create zip archive for Windows binaries + # Must run from inside the dest dir so 7z won't + # embed the directory path inside the zip. + if [[ "${{ matrix.runner }}" == windows* ]]; then + if [[ "$base" == "codex-${{ matrix.target }}.exe" ]]; then + # Bundle the sandbox helper binaries into the main codex zip so + # WinGet installs include the required helpers next to codex.exe. + # Fall back to the single-binary zip if the helpers are missing + # to avoid breaking releases. + bundle_dir="$(mktemp -d)" + runner_src="$dest/codex-command-runner-${{ matrix.target }}.exe" + setup_src="$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + if [[ -f "$runner_src" && -f "$setup_src" ]]; then + cp "$dest/$base" "$bundle_dir/$base" + cp "$runner_src" "$bundle_dir/codex-command-runner.exe" + cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" + # Use an absolute path so bundle zips land in the real dist + # dir even when 7z runs from a temp directory. + (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) + else + echo "warning: missing sandbox binaries; falling back to single-binary zip" + echo "warning: expected $runner_src and $setup_src" + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + rm -rf "$bundle_dir" + else + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + fi - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" + # Also create .zst (existing behaviour) *and* remove the original + # uncompressed binary to keep the directory small. + zstd_args=(-T0 -19) + if [[ "${keep_originals}" == false ]]; then + zstd_args+=(--rm) + fi - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + if [[ "${archive_name}" != "${base}" ]]; then + zstd "${zstd_args[@]}" "$dest/$base" -o "$dest/${archive_name}.zst" + else + zstd "${zstd_args[@]}" "$dest/$base" + fi + done - - name: Download signed macOS handoff + - name: Add Termux release metadata + if: ${{ (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && matrix.target == 'aarch64-linux-android' }} shell: bash - env: - GH_TOKEN: ${{ github.token }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - SIGNED_MACOS_SHA256: ${{ inputs.signed_macos_sha256 }} run: | set -euo pipefail - - download_dir="${RUNNER_TEMP}/signed-macos-download" - handoff_dir="${RUNNER_TEMP}/signed-macos-handoff" - rm -rf "$download_dir" "$handoff_dir" - mkdir -p "$download_dir" "$handoff_dir" - - gh release download "$GITHUB_REF_NAME" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern "$SIGNED_MACOS_ASSET" \ - --dir "$download_dir" - - asset_count="$(find "$download_dir" -maxdepth 1 -type f | wc -l | tr -d '[:space:]')" - if [[ "$asset_count" != "1" ]]; then - echo "Expected exactly one signed macOS handoff asset named ${SIGNED_MACOS_ASSET}; found ${asset_count}" - find "$download_dir" -maxdepth 1 -type f -print - exit 1 - fi - - asset_path="$(find "$download_dir" -maxdepth 1 -type f -print -quit)" - if [[ -n "${SIGNED_MACOS_SHA256}" ]]; then - expected_sha="$(printf '%s' "$SIGNED_MACOS_SHA256" | tr '[:upper:]' '[:lower:]')" - actual_sha="$(shasum -a 256 "$asset_path" | awk '{print $1}')" - if [[ "$actual_sha" != "$expected_sha" ]]; then - echo "signed_macos_sha256 mismatch for ${SIGNED_MACOS_ASSET}" - echo "expected: ${expected_sha}" - echo "actual: ${actual_sha}" - exit 1 - fi + if [[ ! -f "${GITHUB_WORKSPACE}/.github/termux-release.json" ]]; then + echo "No Termux release metadata found; skipping metadata attachment." + exit 0 fi + dest="dist/${{ matrix.target }}" + cp "${GITHUB_WORKSPACE}/.github/termux-release.json" "${dest}/termux-release.json" + ( + cd "${dest}" + sha256sum ./* > SHA256SUMS + ) - asset_name="$(basename "$asset_path")" - case "$asset_name" in - *.tar.zst) - zstd -dc "$asset_path" | tar -C "$handoff_dir" -xf - - ;; - *.tar.gz|*.tgz) - tar -C "$handoff_dir" -xzf "$asset_path" - ;; - *.zip) - ditto -x -k "$asset_path" "$handoff_dir" - ;; - *) - echo "Unsupported signed macOS handoff archive format: ${asset_name}" - exit 1 - ;; - esac - - echo "SIGNED_MACOS_HANDOFF_DIR=$handoff_dir" >> "$GITHUB_ENV" + - id: upload-artifact + uses: actions/upload-artifact@v6 + with: + name: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + # Upload the per-binary .zst files as well as the new .tar.gz + # equivalents we generated in the previous step. + path: | + codex-rs/dist/${{ matrix.target }}/* - - name: Stage signed macOS artifacts + - name: Publish Termux artifact notification + if: ${{ github.event_name == 'pull_request' && startsWith(github.base_ref, 'release/') && matrix.target == 'aarch64-linux-android' }} shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_ID: ${{ steps.upload-artifact.outputs.artifact-id }} + ARTIFACT_NAME: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + PR_NUMBER: ${{ github.event.pull_request.number }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | set -euo pipefail - - target="${{ matrix.target }}" - artifact_name="${{ matrix.artifact_name }}" - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${target}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - fi - if [[ ! -d "$source_dir" ]]; then - echo "Signed macOS handoff is missing ${artifact_name}/" - echo "Expected either:" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${target}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - find "$SIGNED_MACOS_HANDOFF_DIR" -maxdepth 3 -type f -print + if [[ -z "${ARTIFACT_ID}" ]]; then + echo "upload-artifact did not return an artifact id" >&2 exit 1 fi - dest="dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - source_path="${source_dir}/${binary}" - if [[ ! -f "$source_path" ]]; then - source_path="${source_dir}/${binary}-${target}" - fi - if [[ ! -f "$source_path" ]]; then - echo "Signed macOS handoff is missing ${binary} for ${artifact_name}" - exit 1 - fi - - release_path="${dest}/${binary}-${target}" - ditto "$source_path" "$release_path" - chmod 0755 "$release_path" - codesign --verify --strict --verbose=2 "$release_path" - done - - # DMG staging is disabled for signed promotion because we no longer - # distribute DMGs from this release path. Keep the branch here so the - # handoff can opt back in by flipping matrix.build_dmg if needed. - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - dmg_name="codex-${target}.dmg" - dmg_source="${source_dir}/${dmg_name}" - if [[ ! -f "$dmg_source" ]]; then - echo "Signed macOS handoff is missing ${dmg_name} for ${artifact_name}" - exit 1 - fi - - codesign --verify --strict --verbose=2 "$dmg_source" - xcrun stapler validate "$dmg_source" - cp "$dmg_source" "$dest/$dmg_name" + marker="" + artifact_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts/${ARTIFACT_ID}" + run_url="${GH_WORKFLOW_URL:-https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + body_path="${RUNNER_TEMP}/termux-artifact-comment.md" + { + echo "${marker}" + echo "Termux Android artifact ready for testing:" + echo + echo "- Artifact: [${ARTIFACT_NAME}](${artifact_url})" + echo "- Workflow run: [${GITHUB_RUN_ID}](${run_url})" + echo + echo "You must be signed in to GitHub with repository access to download Actions artifacts." + } > "${body_path}" + + existing_comment_id="$( + gh pr view "${PR_NUMBER}" \ + --repo "${GITHUB_REPOSITORY}" \ + --json comments \ + --jq ".comments | map(select(.author.is_bot == true and (.body | contains(\"${marker}\")))) | .[-1].id // \"\"" + )" + if [[ -n "${existing_comment_id}" ]]; then + gh api graphql \ + -f query=' + mutation($id: ID!, $body: String!) { + updateIssueComment(input: {id: $id, body: $body}) { + issueComment { + id + } + } + } + ' \ + -f id="${existing_comment_id}" \ + -f body="$(cat "${body_path}")" \ + >/dev/null + else + gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body-file "${body_path}" fi - - name: Build Codex package archive - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "dist/${TARGET}" \ - --archive-dir "dist/${TARGET}" \ - --target-suffixed-entrypoint - - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - python3 \ - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \ - stage-runtime \ - "$stage_dir" \ - "dist/${{ matrix.target }}/codex-package-${{ matrix.target }}.tar.gz" \ - --codex-version "${GITHUB_REF_NAME}" \ - --platform-tag "$platform_tag" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error - - - name: Compress artifacts - shell: bash - run: | - set -euo pipefail - - dest="dist/${{ matrix.target }}" - for f in "$dest"/*; do - base="$(basename "$f")" - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then - continue + gh label create binary-ready \ + --repo "${GITHUB_REPOSITORY}" \ + --color 0e8a16 \ + --description "Android binary is ready for testing" \ + --force + gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --add-label binary-ready + + if [[ -n "${SLACK_WEBHOOK_URL:-}" ]]; then + slack_payload_path="${RUNNER_TEMP}/termux-artifact-slack.json" + pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}" + jq -n \ + --arg artifact_name "${ARTIFACT_NAME}" \ + --arg artifact_url "${artifact_url}" \ + --arg pr_number "${PR_NUMBER}" \ + --arg pr_url "${pr_url}" \ + --arg run_id "${GITHUB_RUN_ID}" \ + --arg run_url "${run_url}" \ + '{ + text: ("Termux Android artifact ready for testing: " + $artifact_url), + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: ("*Termux Android artifact ready for testing*\n" + $artifact_url) + } + }, + { + type: "section", + fields: [ + { + type: "mrkdwn", + text: ("*Artifact:*\n<" + $artifact_url + "|" + $artifact_name + ">") + }, + { + type: "mrkdwn", + text: ("*Pull request:*\n<" + $pr_url + "|#" + $pr_number + ">") + }, + { + type: "mrkdwn", + text: ("*Workflow run:*\n<" + $run_url + "|" + $run_id + ">") + } + ] + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: "You must be signed in to GitHub with repository access to download Actions artifacts." + } + ] + } + ] + }' > "${slack_payload_path}" + + if ! curl -fsS \ + -X POST \ + -H "Content-type: application/json" \ + --data @"${slack_payload_path}" \ + "${SLACK_WEBHOOK_URL}"; then + echo "::warning::Failed to send Slack artifact notification" fi + fi - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }} - path: | - codex-rs/dist/${{ matrix.target }}/* - - build-windows: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - needs: tag-check - uses: ./.github/workflows/rust-release-windows.yml - with: - release-lto: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }} - secrets: inherit - - argument-comment-lint-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: argument-comment-lint release assets + shell-tool-mcp: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'openai' + name: shell-tool-mcp needs: tag-check - uses: ./.github/workflows/rust-release-argument-comment-lint.yml + permissions: + contents: read + id-token: write + uses: ./.github/workflows/shell-tool-mcp.yml with: + release-tag: ${{ github.ref_name }} publish: true - - zsh-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: zsh release assets - needs: tag-check - uses: ./.github/workflows/rust-release-zsh.yml + secrets: inherit release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') needs: - - tag-check - build - - stage-signed-macos - - build-windows - - argument-comment-lint-release-assets - - zsh-release-assets - if: >- - ${{ - always() && - needs.tag-check.result == 'success' && - ( - ( - github.event_name == 'workflow_dispatch' && - inputs.release_mode == 'promote_signed' && - needs.stage-signed-macos.result == 'success' && - needs.build.result == 'skipped' && - needs.build-windows.result == 'skipped' && - needs.argument-comment-lint-release-assets.result == 'skipped' && - needs.zsh-release-assets.result == 'skipped' - ) || - ( - (github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed') && - needs.build.result == 'success' && - needs.stage-signed-macos.result == 'skipped' && - needs.build-windows.result == 'success' && - needs.argument-comment-lint-release-assets.result == 'success' && - needs.zsh-release-assets.result == 'success' - ) - ) - }} + - shell-tool-mcp name: release runs-on: ubuntu-latest permissions: contents: write actions: read - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode == 'promote_signed' }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} outputs: version: ${{ steps.release_name.outputs.name }} tag: ${{ github.ref_name }} - sign_macos: ${{ steps.release_mode.outputs.sign_macos }} should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }} npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }} - should_publish_python_runtime: ${{ steps.python_runtime_publish_settings.outputs.should_publish }} steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + uses: actions/checkout@v6 - - name: Define release mode - id: release_mode - run: | - echo "release_mode=${RELEASE_MODE}" >> "$GITHUB_OUTPUT" - echo "sign_macos=${SIGN_MACOS}" >> "$GITHUB_OUTPUT" + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main - name: Generate release notes from tag commit message id: release_notes @@ -972,159 +1013,21 @@ jobs: echo "path=${notes_path}" >> "${GITHUB_OUTPUT}" - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@v7 with: path: dist - - name: Validate unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - run_summary="$(gh run view "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --json conclusion,event,headBranch,headSha,status,workflowName,url \ - --jq '[.workflowName, .event, .headBranch, .headSha, .status, .conclusion, .url] | @tsv')" - IFS=$'\t' read -r workflow_name event head_branch head_sha status conclusion run_url <<< "$run_summary" - expected_head_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")" - - if [[ "$workflow_name" != "$GITHUB_WORKFLOW" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is for workflow '${workflow_name}', expected '${GITHUB_WORKFLOW}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$event" != "workflow_dispatch" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} was triggered by '${event}', expected 'workflow_dispatch'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_branch" != "$GITHUB_REF_NAME" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used ref '${head_branch}', expected '${GITHUB_REF_NAME}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_sha" != "$expected_head_sha" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used head SHA '${head_sha}', expected '${expected_head_sha}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is ${status}/${conclusion}, expected completed/success" - echo "Run URL: ${run_url}" - exit 1 - fi - - - name: Download artifacts from unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - gh run download "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --dir dist - - - name: Remove unsigned macOS staging artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - run: | - set -euo pipefail - find dist -mindepth 1 -maxdepth 1 -type d \ - -name '*-apple-darwin*-unsigned' \ - -exec rm -rf {} + - - - name: Re-upload promoted Linux x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-unknown-linux-musl - path: dist/x86_64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Linux arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-unknown-linux-musl - path: dist/aarch64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Windows x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-pc-windows-msvc - path: dist/x86_64-pc-windows-msvc/* - if-no-files-found: error - - - name: Re-upload promoted Windows arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-pc-windows-msvc - path: dist/aarch64-pc-windows-msvc/* - if-no-files-found: error - - name: List run: ls -R dist/ - - name: Prune artifacts excluded from unsigned macOS release - if: ${{ env.SIGN_MACOS == 'false' }} - run: | - find dist -mindepth 1 -maxdepth 1 -type d \ - ! -name '*-apple-darwin*-unsigned' \ - ! -name 'aarch64-unknown-linux-musl' \ - ! -name 'aarch64-unknown-linux-musl-app-server' \ - ! -name 'x86_64-unknown-linux-musl' \ - ! -name 'x86_64-unknown-linux-musl-app-server' \ - ! -name 'aarch64-pc-windows-msvc' \ - ! -name 'x86_64-pc-windows-msvc' \ - -exec rm -rf {} + - - if ! find dist -type f -name '*-apple-darwin*-unsigned*' | grep -q .; then - echo "No unsigned macOS artifacts found in downloaded workflow artifacts." - exit 1 - fi - + # This is a temporary fix: we should modify shell-tool-mcp.yml so these + # files do not end up in dist/ in the first place. - name: Delete entries from dist/ that should not go in the release run: | - rm -rf dist/windows-binaries* - # cargo-timing.html appears under multiple target-specific directories. - # If included in files: dist/**, release upload races on duplicate - # asset names and can fail with 404s. - find dist -type f -name 'cargo-timing.html' -delete - find dist -type d -empty -delete + rm -rf dist/shell-tool-mcp* ls -R dist/ - - name: Add Codex package checksum manifest - run: | - set -euo pipefail - - manifest="dist/codex-package_SHA256SUMS" - tmp_manifest="$(mktemp)" - find dist -type f \ - \( -name 'codex-package-*.tar.gz' -o -name 'codex-app-server-package-*.tar.gz' \) \ - -print | - sort | - while IFS= read -r archive; do - sha256sum "$archive" | - awk -v name="$(basename "$archive")" '{ print $1 " " name }' - done > "$tmp_manifest" - - if [[ ! -s "$tmp_manifest" ]]; then - echo "No Codex package archives found for checksum manifest" - exit 1 - fi - - mv "$tmp_manifest" "$manifest" - cat "$manifest" - - name: Add config schema release asset run: | cp codex-rs/core/config.schema.json dist/config-schema.json @@ -1145,12 +1048,6 @@ jobs: set -euo pipefail version="${VERSION}" - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - echo "npm_tag=" >> "$GITHUB_OUTPUT" - exit 0 - fi - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "should_publish=true" >> "$GITHUB_OUTPUT" echo "npm_tag=" >> "$GITHUB_OUTPUT" @@ -1162,130 +1059,55 @@ jobs: echo "npm_tag=" >> "$GITHUB_OUTPUT" fi - - name: Determine Python runtime publish settings - id: python_runtime_publish_settings - env: - VERSION: ${{ steps.release_name.outputs.name }} - run: | - set -euo pipefail - version="${VERSION}" - - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - else - echo "should_publish=false" >> "$GITHUB_OUTPUT" - fi - - name: Setup pnpm - if: ${{ env.SIGN_MACOS == 'true' }} - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + uses: pnpm/action-setup@v4 with: run_install: false - name: Setup Node.js for npm packaging - if: ${{ env.SIGN_MACOS == 'true' }} - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: node-version: 22 - name: Install dependencies - if: ${{ env.SIGN_MACOS == 'true' }} run: pnpm install --frozen-lockfile + # stage_npm_packages.py requires DotSlash when staging releases. + - uses: facebook/install-dotslash@v2 - name: Stage npm packages - if: ${{ env.SIGN_MACOS == 'true' }} + if: github.repository_owner == 'openai' env: GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ steps.release_name.outputs.name }} run: | - workflow_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" ./scripts/stage_npm_packages.py \ - --release-version "$RELEASE_VERSION" \ - --workflow-url "$workflow_url" \ + --release-version "${{ steps.release_name.outputs.name }}" \ --package codex \ --package codex-responses-api-proxy \ --package codex-sdk - - name: Stage installer scripts - if: ${{ env.SIGN_MACOS == 'true' }} - run: | - cp scripts/install/install.sh dist/install.sh - cp scripts/install/install.ps1 dist/install.ps1 - - name: Create GitHub Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@v2 with: name: ${{ steps.release_name.outputs.name }} tag_name: ${{ github.ref_name }} body_path: ${{ steps.release_notes.outputs.path }} files: dist/** - overwrite_files: true - make_latest: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} # Mark as prerelease only when the version has a suffix after x.y.z # (e.g. -alpha, -beta). Otherwise publish a normal release. prerelease: ${{ contains(steps.release_name.outputs.name, '-') }} - - name: Clean up signed promotion handoff assets - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" --jq '.id')" - gh api --paginate "repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets" \ - --jq '.[] | [.id, .name] | @tsv' | - while IFS=$'\t' read -r asset_id asset_name; do - if [[ -z "$asset_id" || -z "$asset_name" ]]; then - continue - fi - - delete_asset=false - if [[ "$asset_name" == *unsigned* || "$asset_name" == "$SIGNED_MACOS_ASSET" ]]; then - delete_asset=true - fi - - if [[ "$delete_asset" == "true" ]]; then - echo "Deleting release asset ${asset_name}" - gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" - fi - done - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 + - if: github.repository_owner == 'openai' + uses: facebook/dotslash-publish-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag: ${{ github.ref_name }} config: .github/dotslash-config.json - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-zsh-config.json - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-argument-comment-lint-config.json - - name: Trigger developers.openai.com deploy # Only trigger the deploy if the release is not a pre-release. # The deploy is used to update the developers.openai.com website with the new config schema json file. - if: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} + if: ${{ !contains(steps.release_name.outputs.name, '-') && github.repository_owner == 'openai' }} continue-on-error: true env: DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }} @@ -1300,15 +1122,7 @@ jobs: # npm docs: https://docs.npmjs.com/trusted-publishers publish-npm: # Publish to npm for stable releases and alpha pre-releases with numeric suffixes. - # promote_signed intentionally skips build jobs that are ancestors of release; - # include the !cancelled() status function so Actions does not apply its implicit - # success() check to the whole dependency chain before evaluating release outputs. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_npm == 'true' - }} + if: ${{ needs.release.outputs.should_publish_npm == 'true' && github.repository_owner == 'openai' }} name: publish-npm needs: release runs-on: ubuntu-latest @@ -1318,37 +1132,36 @@ jobs: steps: - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: - # Node 24 bundles npm >= 11.5.1, which trusted publishing requires. - node-version: 24 + node-version: 22 registry-url: "https://registry.npmjs.org" scope: "@openai" + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + - name: Download npm tarballs from release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} run: | set -euo pipefail - version="$RELEASE_VERSION" - tag="$RELEASE_TAG" + version="${{ needs.release.outputs.version }}" + tag="${{ needs.release.outputs.tag }}" mkdir -p dist/npm - patterns=( - "codex-npm-${version}.tgz" - "codex-npm-linux-*-${version}.tgz" - "codex-npm-darwin-*-${version}.tgz" - "codex-npm-win32-*-${version}.tgz" - "codex-responses-api-proxy-npm-${version}.tgz" - "codex-sdk-npm-${version}.tgz" - ) - for pattern in "${patterns[@]}"; do - gh release download "$tag" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "$pattern" \ - --dir dist/npm - done + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-responses-api-proxy-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-sdk-npm-${version}.tgz" \ + --dir dist/npm # No NODE_AUTH_TOKEN needed because we use OIDC. - name: Publish to npm @@ -1357,193 +1170,23 @@ jobs: NPM_TAG: ${{ needs.release.outputs.npm_tag }} run: | set -euo pipefail - prefix="" + tag_args=() if [[ -n "${NPM_TAG}" ]]; then - prefix="${NPM_TAG}-" + tag_args+=(--tag "${NPM_TAG}") fi - root_tarball="dist/npm/codex-npm-${VERSION}.tgz" - sdk_tarball="dist/npm/codex-sdk-npm-${VERSION}.tgz" - # Keep this list in sync with CODEX_PLATFORM_PACKAGES in - # codex-cli/scripts/build_npm_package.py. The root wrapper advances - # @openai/codex@latest as soon as it publishes, so every platform - # package it aliases must already exist in the registry first. - platform_tarballs=( - "dist/npm/codex-npm-linux-x64-${VERSION}.tgz" - "dist/npm/codex-npm-linux-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-x64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-x64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-arm64-${VERSION}.tgz" - ) - - for required_tarball in "${platform_tarballs[@]}" "${root_tarball}"; do - if [[ ! -f "${required_tarball}" ]]; then - echo "Missing npm tarball: ${required_tarball}" - exit 1 - fi - done - - shopt -s nullglob - other_tarballs=() - for tarball in dist/npm/*-"${VERSION}".tgz; do - if [[ "${tarball}" == "${root_tarball}" || "${tarball}" == "${sdk_tarball}" ]]; then - continue - fi - - is_platform_tarball=false - for platform_tarball in "${platform_tarballs[@]}"; do - if [[ "${tarball}" == "${platform_tarball}" ]]; then - is_platform_tarball=true - break - fi - done - if [[ "${is_platform_tarball}" == true ]]; then - continue - fi - - other_tarballs+=("${tarball}") - done - - # Publish the platform packages before the root CLI wrapper. The root - # wrapper advances @openai/codex@latest, so it should only publish - # after the optional dependency versions it references exist. tarballs=( - "${platform_tarballs[@]}" - "${other_tarballs[@]}" - "${root_tarball}" + "codex-npm-${VERSION}.tgz" + "codex-responses-api-proxy-npm-${VERSION}.tgz" + "codex-sdk-npm-${VERSION}.tgz" ) - if [[ -f "${sdk_tarball}" ]]; then - tarballs+=("${sdk_tarball}") - fi for tarball in "${tarballs[@]}"; do - filename="$(basename "${tarball}")" - tag="" - - case "${filename}" in - codex-npm-linux-*-"${VERSION}".tgz|codex-npm-darwin-*-"${VERSION}".tgz|codex-npm-win32-*-"${VERSION}".tgz) - platform="${filename#codex-npm-}" - platform="${platform%-${VERSION}.tgz}" - tag="${prefix}${platform}" - ;; - codex-npm-"${VERSION}".tgz|codex-responses-api-proxy-npm-"${VERSION}".tgz|codex-sdk-npm-"${VERSION}".tgz) - tag="${NPM_TAG}" - ;; - *) - echo "Unexpected npm tarball: ${filename}" - exit 1 - ;; - esac - - publish_cmd=(npm publish "${GITHUB_WORKSPACE}/${tarball}") - if [[ -n "${tag}" ]]; then - publish_cmd+=(--tag "${tag}") - fi - - echo "+ ${publish_cmd[*]}" - set +e - publish_output="$("${publish_cmd[@]}" 2>&1)" - publish_status=$? - set -e - - echo "${publish_output}" - if [[ ${publish_status} -eq 0 ]]; then - continue - fi - - if grep -qiE "previously published|cannot publish over|version already exists" <<< "${publish_output}"; then - echo "Skipping already-published package version for ${filename}" - continue - fi - - exit "${publish_status}" + npm publish "${GITHUB_WORKSPACE}/dist/npm/${tarball}" "${tag_args[@]}" done - # Publish the platform-specific Python runtime wheels using PyPI trusted publishing. - # PyPI project configuration must trust this workflow and job. Keep this - # non-blocking while the Python runtime publishing path is new; failures still - # need release follow-up, but should not invalidate the Rust release itself. - publish-python-runtime: - # Publish to PyPI for stable releases and alpha pre-releases with numeric suffixes. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_python_runtime == 'true' - }} - name: publish-python-runtime - needs: release - runs-on: ubuntu-latest - continue-on-error: true - environment: pypi - permissions: - id-token: write # Required for PyPI trusted publishing. - contents: read - - steps: - - name: Download Python runtime wheels from release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} - run: | - set -euo pipefail - python_version="$RELEASE_VERSION" - python_version="${python_version/-alpha./a}" - python_version="${python_version/-beta./b}" - python_version="${python_version/-rc./rc}" - - mkdir -p dist/python-runtime - gh release download "$RELEASE_TAG" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "openai_codex_cli_bin-${python_version}-*.whl" \ - --dir dist/python-runtime - ls -lh dist/python-runtime - - - name: Publish Python runtime wheels to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - with: - packages-dir: dist/python-runtime - skip-existing: true - - winget: - name: winget - needs: release - # Only publish stable/mainline releases to WinGet; pre-releases include a - # '-' in the semver string (e.g., 1.2.3-alpha.1). - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' && - !contains(needs.release.outputs.version, '-') - }} - # This job only invokes a GitHub Action to open/update the winget-pkgs PR; - # it does not execute Windows-only tooling, so Linux is sufficient. - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Publish to WinGet - uses: vedantmgoyal9/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5 - with: - identifier: OpenAI.Codex - version: ${{ needs.release.outputs.version }} - release-tag: ${{ needs.release.outputs.tag }} - fork-user: openai-oss-forks - installers-regex: '^codex-(?:x86_64|aarch64)-pc-windows-msvc\.exe\.zip$' - token: ${{ secrets.WINGET_PUBLISH_PAT }} - update-branch: name: Update latest-alpha-cli branch - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' - }} permissions: contents: write needs: release diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml new file mode 100644 index 000000000000..66a76aa4d938 --- /dev/null +++ b/.github/workflows/shell-tool-mcp.yml @@ -0,0 +1,461 @@ +name: shell-tool-mcp + +on: + workflow_call: + inputs: + release-version: + description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v. + required: false + type: string + release-tag: + description: Tag name to use when downloading release artifacts (defaults to rust-v). + required: false + type: string + publish: + description: Whether to publish to npm when the version is releasable. + required: false + default: true + type: boolean + +env: + NODE_VERSION: 22 + +jobs: + metadata: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.compute.outputs.version }} + release_tag: ${{ steps.compute.outputs.release_tag }} + should_publish: ${{ steps.compute.outputs.should_publish }} + npm_tag: ${{ steps.compute.outputs.npm_tag }} + steps: + - name: Compute version and tags + id: compute + run: | + set -euo pipefail + + version="${{ inputs.release-version }}" + release_tag="${{ inputs.release-tag }}" + + if [[ -z "$version" ]]; then + if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then + version="${release_tag#rust-v}" + elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then + version="${GITHUB_REF_NAME#rust-v}" + release_tag="${GITHUB_REF_NAME}" + else + echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag." + exit 1 + fi + fi + + if [[ -z "$release_tag" ]]; then + release_tag="rust-v${version}" + fi + + npm_tag="" + should_publish="false" + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + should_publish="true" + elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then + should_publish="true" + npm_tag="alpha" + fi + + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" + echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT" + echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT" + + matrix-setup: + runs-on: ubuntu-latest + outputs: + rust-binaries: ${{ steps.compute.outputs.rust-binaries }} + bash-linux: ${{ steps.compute.outputs.bash-linux }} + bash-darwin: ${{ steps.compute.outputs.bash-darwin }} + steps: + - name: Compute matrices + id: compute + shell: bash + run: | + set -euo pipefail + is_openai="${{ github.repository_owner == 'openai' }}" + + # rust-binaries: always include x86_64-musl; conditionally include paid/fork runners + if [[ "$is_openai" == "true" ]]; then + rust='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","install_musl":true} + ]' + else + rust='[{"runner":"macos-latest","target":"aarch64-apple-darwin"}]' + fi + rust=$(echo "$rust" | jq -c '. + [{"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","install_musl":true}]') + echo "rust-binaries={\"include\":$rust}" >> "$GITHUB_OUTPUT" + + # bash-linux: always include x86_64 variants; add arm64 on openai + bash_linux='[ + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-24.04","image":"ubuntu:24.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-22.04","image":"ubuntu:22.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-12","image":"debian:12"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-11","image":"debian:11"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]' + if [[ "$is_openai" == "true" ]]; then + bash_linux=$(echo "$bash_linux" | jq -c '. + [ + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-24.04","image":"arm64v8/ubuntu:24.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-22.04","image":"arm64v8/ubuntu:22.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-20.04","image":"arm64v8/ubuntu:20.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-12","image":"arm64v8/debian:12"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-11","image":"arm64v8/debian:11"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]') + fi + echo "bash-linux={\"include\":$(echo "$bash_linux" | jq -c)}" >> "$GITHUB_OUTPUT" + + # bash-darwin: always include macos-14; add macos-15-xlarge on openai + bash_darwin='[{"runner":"macos-14","target":"aarch64-apple-darwin","variant":"macos-14"}]' + if [[ "$is_openai" == "true" ]]; then + bash_darwin=$(echo "$bash_darwin" | jq -c '. + [ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin","variant":"macos-15"} + ]') + fi + echo "bash-darwin={\"include\":$(echo "$bash_darwin" | jq -c)}" >> "$GITHUB_OUTPUT" + + rust-binaries: + name: Build Rust - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + defaults: + run: + working-directory: codex-rs + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.rust-binaries) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install UBSan runtime (musl) + if: ${{ matrix.install_musl }} + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -y + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 + fi + + - uses: dtolnay/rust-toolchain@1.93 + with: + targets: ${{ matrix.target }} + + - if: ${{ matrix.install_musl }} + name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.14.0 + + - if: ${{ matrix.install_musl }} + name: Install musl build dependencies + env: + TARGET: ${{ matrix.target }} + run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh" + + - if: ${{ matrix.install_musl }} + name: Configure rustc UBSan wrapper (musl host) + shell: bash + run: | + set -euo pipefail + ubsan="" + if command -v ldconfig >/dev/null 2>&1; then + ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')" + fi + wrapper_root="${RUNNER_TEMP:-/tmp}" + wrapper="${wrapper_root}/rustc-ubsan-wrapper" + cat > "${wrapper}" <> "$GITHUB_ENV" + echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV" + + - if: ${{ matrix.install_musl }} + name: Clear sanitizer flags (musl) + shell: bash + run: | + set -euo pipefail + # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. + echo "RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV" + # Override any runner-level Cargo config rustflags as well. + echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + + sanitize_flags() { + local input="$1" + input="${input//-fsanitize=undefined/}" + input="${input//-fno-sanitize-recover=undefined/}" + input="${input//-fno-sanitize-trap=undefined/}" + echo "$input" + } + + cflags="$(sanitize_flags "${CFLAGS-}")" + cxxflags="$(sanitize_flags "${CXXFLAGS-}")" + echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" + echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + + - name: Build exec server binaries + run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper + + - name: Stage exec server binaries + run: | + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}" + mkdir -p "$dest" + cp "target/${{ matrix.target }}/release/codex-exec-mcp-server" "$dest/" + cp "target/${{ matrix.target }}/release/codex-execve-wrapper" "$dest/" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-rust-${{ matrix.target }} + path: artifacts/** + if-no-files-found: error + + bash-linux: + name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + container: + image: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-linux) }} + steps: + - name: Install build prerequisites + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext + elif command -v dnf >/dev/null 2>&1; then + dnf install -y git gcc gcc-c++ make bison autoconf gettext + elif command -v yum >/dev/null 2>&1; then + yum install -y git gcc gcc-c++ make bison autoconf gettext + else + echo "Unsupported package manager in container" + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + bash-darwin: + name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-darwin) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + package: + name: Package npm module + needs: + - metadata + - rust-binaries + - bash-linux + - bash-darwin + runs-on: ubuntu-latest + env: + PACKAGE_VERSION: ${{ needs.metadata.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install JavaScript dependencies + run: pnpm install --frozen-lockfile + + - name: Build (shell-tool-mcp) + run: pnpm --filter @openai/codex-shell-tool-mcp run build + + - name: Download build artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts + + - name: Assemble staging directory + id: staging + shell: bash + run: | + set -euo pipefail + staging="${STAGING_DIR}" + mkdir -p "$staging" "$staging/vendor" + cp shell-tool-mcp/README.md "$staging/" + cp shell-tool-mcp/package.json "$staging/" + cp -R shell-tool-mcp/bin "$staging/" + + found_vendor="false" + shopt -s nullglob + for vendor_dir in artifacts/*/vendor; do + rsync -av "$vendor_dir/" "$staging/vendor/" + found_vendor="true" + done + if [[ "$found_vendor" == "false" ]]; then + echo "No vendor payloads were downloaded." + exit 1 + fi + + node - <<'NODE' + import fs from "node:fs"; + import path from "node:path"; + + const stagingDir = process.env.STAGING_DIR; + const version = process.env.PACKAGE_VERSION; + const pkgPath = path.join(stagingDir, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + pkg.version = version; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + NODE + + echo "dir=$staging" >> "$GITHUB_OUTPUT" + env: + STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp + + - name: Ensure binaries are executable + run: | + set -euo pipefail + staging="${{ steps.staging.outputs.dir }}" + chmod +x \ + "$staging"/vendor/*/codex-exec-mcp-server \ + "$staging"/vendor/*/codex-execve-wrapper \ + "$staging"/vendor/*/bash/*/bash + + - name: Create npm tarball + shell: bash + run: | + set -euo pipefail + mkdir -p dist/npm + staging="${{ steps.staging.outputs.dir }}" + pack_info=$(cd "$staging" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm") + filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);') + mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz" + + - uses: actions/upload-artifact@v6 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz + if-no-files-found: error + + publish: + name: Publish npm package + needs: + - metadata + - package + if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }} + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: https://registry.npmjs.org + scope: "@openai" + + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + + - name: Download npm tarball + uses: actions/download-artifact@v7 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm + + - name: Publish to npm + if: github.repository == 'openai/codex' + env: + NPM_TAG: ${{ needs.metadata.outputs.npm_tag }} + VERSION: ${{ needs.metadata.outputs.version }} + shell: bash + run: | + set -euo pipefail + tag_args=() + if [[ -n "${NPM_TAG}" ]]; then + tag_args+=(--tag "${NPM_TAG}") + fi + npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}" diff --git a/.github/workflows/termux-release-checkpoint.yml b/.github/workflows/termux-release-checkpoint.yml new file mode 100644 index 000000000000..0347e6f1b3d6 --- /dev/null +++ b/.github/workflows/termux-release-checkpoint.yml @@ -0,0 +1,103 @@ +name: termux-release-checkpoint + +on: + workflow_dispatch: + inputs: + source_branch: + description: "Release branch to checkpoint from, for example release/0.123.0" + required: false + type: string + default: "" + source_sha: + description: "Specific source commit SHA to checkpoint; defaults to the source branch tip" + required: false + type: string + default: "" + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + type: string + default: "wallentx/termux-target" + reviewer: + description: "GitHub username to request as reviewer" + required: false + type: string + default: "wallentx" + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-checkpoint-${{ inputs.source_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + checkpoint: + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + env: + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + REQUESTED_SOURCE_BRANCH: ${{ inputs.source_branch }} + REQUESTED_SOURCE_SHA: ${{ inputs.source_sha }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ inputs.source_branch || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + source_branch="${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${source_branch}" + + - name: Create checkpoint PR + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" diff --git a/.github/workflows/termux-release-deploy.yml b/.github/workflows/termux-release-deploy.yml new file mode 100644 index 000000000000..2071611dacc4 --- /dev/null +++ b/.github/workflows/termux-release-deploy.yml @@ -0,0 +1,243 @@ +name: termux-release-deploy + +on: + push: + branches: + - "release/**" + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to deploy, for example release/0.124.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to deploy. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged release PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged release PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + default: "wallentx/termux-target" + type: string + reviewer: + description: "GitHub username to request as reviewer on the checkpoint PR" + required: false + default: "wallentx" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-deploy-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + deploy: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' || !startsWith(github.event.head_commit.message, 'Seed Termux release automation') }} + permissions: + actions: read + contents: write + deployments: write + issues: write + pull-requests: write + env: + REQUESTED_RELEASE_BRANCH: ${{ inputs.release_branch }} + REQUESTED_RELEASE_SHA: ${{ inputs.release_sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + release_branch="${REQUESTED_RELEASE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${release_branch}" + + - name: Resolve release ref + id: release-ref + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: deploy + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Create deployment + if: steps.metadata.outputs.deploy == 'true' + id: deployment + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + deployment_id="$( + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments" \ + -f ref="${RELEASE_SHA}" \ + -f environment="termux-release" \ + -F auto_merge=false \ + -F required_contexts[] \ + -f description="Termux release deployment for ${TERMUX_TAG}" \ + --jq '.id' + )" + echo "id=${deployment_id}" >> "$GITHUB_OUTPUT" + + - name: Mark deployment in progress + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="in_progress" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Promoting Termux release artifact and preparing checkpoint PR" \ + >/dev/null + + - name: Locate merged pull request + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" + + - name: Ensure checkpoint PR + if: steps.metadata.outputs.deploy == 'true' + id: checkpoint + env: + SOURCE_BRANCH: ${{ steps.release-ref.outputs.branch }} + SOURCE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" + + - name: Mark deployment success + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="success" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment completed for ${TERMUX_TAG}" \ + >/dev/null + + - name: Mark deployment failure + if: failure() && steps.deployment.outputs.id != '' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="failure" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment failed" \ + >/dev/null diff --git a/.github/workflows/termux-release-promote.yml b/.github/workflows/termux-release-promote.yml new file mode 100644 index 000000000000..22c277c21a70 --- /dev/null +++ b/.github/workflows/termux-release-promote.yml @@ -0,0 +1,135 @@ +name: termux-release-promote + +on: + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to promote, for example release/0.122.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to promote. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-promote-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + promote: + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: write + pull-requests: read + env: + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Resolve release ref + id: release-ref + env: + INPUT_RELEASE_BRANCH: ${{ inputs.release_branch }} + INPUT_RELEASE_SHA: ${{ inputs.release_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: promote + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Locate merged pull request + if: steps.metadata.outputs.promote == 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.promote == 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.promote == 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" diff --git a/scripts/termux-configure-git.sh b/scripts/termux-configure-git.sh new file mode 100755 index 000000000000..fae3abcbab48 --- /dev/null +++ b/scripts/termux-configure-git.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Configure the explicit bot identity used by release automation and fetch any +# refs requested by the calling workflow. + +set -euo pipefail + +git config user.name "github-actions[bot]" +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + +while (($#)); do + case "$1" in + --origin) + shift + origin_refs=() + while (($#)) && [[ "$1" != --* ]]; do + origin_refs+=("$1") + shift + done + if ((${#origin_refs[@]})); then + git fetch --prune origin "${origin_refs[@]}" + fi + ;; + --upstream-tag) + if (($# < 3)); then + echo "--upstream-tag requires ." >&2 + exit 1 + fi + upstream_repo="$2" + upstream_tag="$3" + git remote add upstream "https://github.com/${upstream_repo}.git" 2>/dev/null || true + git fetch --prune --no-tags upstream "+refs/tags/${upstream_tag}:refs/tags/${upstream_tag}" + shift 3 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done diff --git a/scripts/termux-create-checkpoint-pr.sh b/scripts/termux-create-checkpoint-pr.sh new file mode 100755 index 000000000000..f315a5b91822 --- /dev/null +++ b/scripts/termux-create-checkpoint-pr.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/termux-release-paths.sh +source "${script_dir}/termux-release-paths.sh" + +source_branch="${SOURCE_BRANCH:-${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}}" +source_sha="${SOURCE_SHA:-${REQUESTED_SOURCE_SHA:-}}" +if [[ -z "${source_sha}" ]]; then + if [[ "${GITHUB_EVENT_NAME:-}" == "push" && "${source_branch}" == "${GITHUB_REF_NAME:-}" ]]; then + source_sha="${GITHUB_SHA}" + else + source_sha="$(git rev-parse "origin/${source_branch}")" + fi +fi + +if [[ -z "${DESTINATION_BRANCH:-}" ]]; then + echo "DESTINATION_BRANCH is required." >&2 + exit 1 +fi + +release_only_checkpoint_paths() { + printf '%s\n' "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +} + +resolve_source_version_conflicts() { + local path="$1" + local resolved_path + resolved_path="$(mktemp)" + + if ! awk ' + function normalize_versions(text) { + gsub(/version = "[^"]+"/, "version = \"\"", text) + return text + } + + BEGIN { + in_block = 0 + side = "" + ours = "" + theirs = "" + blocks = 0 + } + + /^<<<<<<< / { + if (in_block) { + exit 1 + } + in_block = 1 + side = "ours" + ours = "" + theirs = "" + blocks++ + next + } + + /^=======$/ && in_block { + side = "theirs" + next + } + + /^>>>>>>> / && in_block { + if (normalize_versions(ours) != normalize_versions(theirs)) { + exit 1 + } + printf "%s", theirs + in_block = 0 + side = "" + next + } + + { + if (!in_block) { + print + } else if (side == "ours") { + ours = ours $0 ORS + } else if (side == "theirs") { + theirs = theirs $0 ORS + } else { + exit 1 + } + } + + END { + if (in_block || blocks == 0) { + exit 1 + } + } + ' "${path}" > "${resolved_path}"; then + rm -f "${resolved_path}" + return 1 + fi + + cp "${resolved_path}" "${path}" + rm -f "${resolved_path}" +} + +short_sha="${source_sha:0:12}" +source_slug="${source_branch//\//_}" +dest_slug="${DESTINATION_BRANCH//\//_}" +checkpoint_branch="checkpoint/${dest_slug}_from_${source_slug}_${short_sha}" +pr_title="checkpoint: into ${DESTINATION_BRANCH} from ${source_branch} @ ${short_sha}" +merge_conflicted=false +conflict_summary="" + +existing_pr="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --head "${checkpoint_branch}" \ + --state all \ + --json number,state,mergedAt,url \ + --jq '[.[] | select(.state == "OPEN" or .mergedAt != null)] | .[0] // empty' +)" +if [[ -n "${existing_pr}" ]]; then + existing_url="$(jq -r '.url' <<< "${existing_pr}")" + existing_state="$(jq -r '.state' <<< "${existing_pr}")" + echo "Checkpoint PR already exists for ${checkpoint_branch}: ${existing_url} (${existing_state})." + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${existing_url}" >> "${GITHUB_OUTPUT}" + fi + exit 0 +fi + +git checkout -B "${checkpoint_branch}" "origin/${DESTINATION_BRANCH}" + +if ! git merge --no-ff --no-edit "${source_sha}"; then + mapfile -t conflicted_paths < <(git diff --name-only --diff-filter=U) + for conflicted_path in "${conflicted_paths[@]}"; do + if termux_is_checkpoint_release_only_path "${conflicted_path}"; then + echo "Auto-resolving release-only checkpoint conflict in ${conflicted_path} by keeping ${DESTINATION_BRANCH}." + if git cat-file -e "HEAD:${conflicted_path}" 2>/dev/null; then + git checkout --ours -- "${conflicted_path}" + git add "${conflicted_path}" + else + git rm -f --ignore-unmatch "${conflicted_path}" + fi + fi + done + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if ((${#remaining_conflicts[@]})); then + cargo_version_conflicts=true + for remaining_conflict in "${remaining_conflicts[@]}"; do + case "${remaining_conflict}" in + codex-rs/Cargo.toml|codex-rs/Cargo.lock) + ;; + *) + cargo_version_conflicts=false + ;; + esac + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + for remaining_conflict in "${remaining_conflicts[@]}"; do + if ! resolve_source_version_conflicts "${remaining_conflict}"; then + cargo_version_conflicts=false + break + fi + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + echo "Auto-resolving recurring Cargo version checkpoint conflicts by keeping ${source_branch} versions." + git add -- "${remaining_conflicts[@]}" + fi + fi + fi + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if [[ "${#remaining_conflicts[@]}" -eq 0 ]]; then + git commit --no-edit + else + merge_conflicted=true + conflict_summary="$( + printf '%s\n' "${remaining_conflicts[@]}" | awk '{ print "- `" $0 "`" }' + )" + echo "Automatic checkpoint merge failed; creating a manual-resolution PR instead." >&2 + if git rev-parse -q --verify MERGE_HEAD >/dev/null; then + git merge --abort + fi + git checkout -B "${checkpoint_branch}" "${source_sha}" + fi +fi + +if git cat-file -e "origin/${DESTINATION_BRANCH}:.github" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- .github + mapfile -t added_github_paths < <( + git diff --name-only --diff-filter=A "origin/${DESTINATION_BRANCH}" -- .github + ) + if ((${#added_github_paths[@]})); then + git rm -f --ignore-unmatch -- "${added_github_paths[@]}" + fi +else + git rm -r --ignore-unmatch .github +fi + +while IFS= read -r release_only_path; do + if git cat-file -e "origin/${DESTINATION_BRANCH}:${release_only_path}" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- "${release_only_path}" + else + git rm -f --ignore-unmatch -- "${release_only_path}" + fi +done < <(release_only_checkpoint_paths) + +if ! git diff --quiet || ! git diff --cached --quiet; then + if [[ -e .github || -L .github ]] || git ls-files --error-unmatch -- .github >/dev/null 2>&1; then + git add -A .github + fi + while IFS= read -r release_only_path; do + if [[ -e "${release_only_path}" || -L "${release_only_path}" ]] || git ls-files --error-unmatch -- "${release_only_path}" >/dev/null 2>&1; then + git add -A -- "${release_only_path}" + fi + done < <(release_only_checkpoint_paths) + if [[ "${merge_conflicted}" == "true" ]]; then + git commit -m "checkpoint: prepare ${source_branch} for ${DESTINATION_BRANCH}" + else + git commit --amend --no-edit + fi +fi + +if git diff --quiet "origin/${DESTINATION_BRANCH}" HEAD; then + echo "Checkpoint merge produced no destination changes after release-only files were restored." + exit 0 +fi + +git push --force-with-lease origin "${checkpoint_branch}" + +remaining="$( + git log --first-parent --pretty=format:%H "${source_sha}..origin/${source_branch}" | wc -w +)" + +body_path="${RUNNER_TEMP}/termux-checkpoint-pr.md" +{ + echo "## Termux release checkpoint" + echo + echo "- Source branch: \`${source_branch}\`" + echo "- Source hash: \`${source_sha}\`" + echo "- Destination branch: \`${DESTINATION_BRANCH}\`" + echo "- Remaining first-parent commits on source: ${remaining}" + echo + echo "This PR carries release-train conflict fixes and follow-up changes back into the reusable Termux patch branch." + if [[ "${merge_conflicted}" == "true" ]]; then + echo + echo "## Merge conflicts" + echo + echo "GitHub Actions could not create the checkpoint merge commit automatically, so this PR was created from the source branch state for manual conflict resolution." + echo + echo "Conflicted paths from the failed merge attempt:" + if [[ -n "${conflict_summary}" ]]; then + printf '%s\n' "${conflict_summary}" + else + echo "- Conflict details unavailable" + fi + fi + echo + echo "Release-only workflow files and metadata under \`.github\` were restored to the destination branch versions before opening this PR." +} > "${body_path}" + +pr_url="$( + gh pr create \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${DESTINATION_BRANCH}" \ + --head "${checkpoint_branch}" \ + --title "${pr_title}" \ + --body-file "${body_path}" +)" +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-reviewer "${REVIEWER}" || true +gh label create checkpoint --repo "${GITHUB_REPOSITORY}" --color c5def5 --description "Checkpoint merge" --force +gh label create termux-release --repo "${GITHUB_REPOSITORY}" --color 0e8a16 --description "Termux release automation" --force +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-label "checkpoint" --add-label "termux-release" + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${pr_url}" >> "${GITHUB_OUTPUT}" +fi diff --git a/scripts/termux-create-or-update-mirrored-release.sh b/scripts/termux-create-or-update-mirrored-release.sh new file mode 100755 index 000000000000..5787e894582b --- /dev/null +++ b/scripts/termux-create-or-update-mirrored-release.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +# Create a mirrored Termux release or repair an existing release that is missing +# the Android tarball. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" +: "${RUNNER_TEMP:?RUNNER_TEMP is required}" +: "${UPSTREAM_TAG:?UPSTREAM_TAG is required}" +: "${UPSTREAM_REPO:?UPSTREAM_REPO is required}" +: "${UPSTREAM_NAME?UPSTREAM_NAME is required}" +: "${TERMUX_TAG:?TERMUX_TAG is required}" +: "${UPSTREAM_PRERELEASE:?UPSTREAM_PRERELEASE is required}" +: "${UPSTREAM_HTML_URL?UPSTREAM_HTML_URL is required}" +: "${RELEASE_TRAIN?RELEASE_TRAIN is required}" +: "${RELEASE_EXISTS:?RELEASE_EXISTS is required}" +: "${PR_NUMBER:?PR_NUMBER is required}" +: "${HEAD_SHA:?HEAD_SHA is required}" +: "${RELEASE_SHA:?RELEASE_SHA is required}" + +promoted_dir="${PROMOTED_DIR:-promoted}" +asset_path="${promoted_dir}/codex-aarch64-linux-android.tar.gz" + +body_path="${RUNNER_TEMP}/release-body.md" +upstream_body_path="${RUNNER_TEMP}/upstream-release-body.md" +upstream_body_without_changelog_path="${RUNNER_TEMP}/upstream-release-body-without-changelog.md" +if gh release view "${UPSTREAM_TAG}" \ + --repo "${UPSTREAM_REPO}" \ + --json body \ + --jq '.body // ""' > "${upstream_body_path}"; then + awk ' + function heading_level(line, text) { + if (match(line, /^(#{1,6})[[:space:]]+(.+)$/, parts)) { + text = tolower(parts[2]) + sub(/[[:space:]]+#+[[:space:]]*$/, "", text) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", text) + if (text == "changelog" || text == "change log") { + return length(parts[1]) + } + } + return 0 + } + + { + if (!skip) { + level = heading_level($0) + if (level > 0) { + skip = 1 + skip_level = level + next + } + print + next + } + + if (match($0, /^(#{1,6})[[:space:]]+/, parts) && length(parts[1]) <= skip_level) { + skip = 0 + print + } + } + ' "${upstream_body_path}" > "${upstream_body_without_changelog_path}" +else + echo "::warning title=Upstream release notes unavailable::Could not read ${UPSTREAM_REPO} release ${UPSTREAM_TAG}." + : > "${upstream_body_without_changelog_path}" +fi + +{ + echo "Termux Android build for ${UPSTREAM_TAG}." + echo + echo "- Upstream release: ${UPSTREAM_HTML_URL}" + echo "- Release train: \`${RELEASE_TRAIN}\`" + echo "- Promoted PR: #${PR_NUMBER}" + echo "- Promoted PR head SHA: \`${HEAD_SHA}\`" +} > "${body_path}" +if [[ -s "${upstream_body_without_changelog_path}" ]]; then + { + echo + echo "## Upstream release notes" + echo + cat "${upstream_body_without_changelog_path}" + } >> "${body_path}" +fi + +release_title="${UPSTREAM_NAME}" +if [[ -z "${release_title}" || "${release_title}" == "null" ]]; then + release_title="${TERMUX_TAG}" +fi + +if [[ "${RELEASE_EXISTS}" == "true" ]]; then + gh release upload \ + "${TERMUX_TAG}" \ + "${asset_path}#codex-termux" \ + --repo "${GITHUB_REPOSITORY}" \ + --clobber + exit 0 +fi + +release_args=( + gh release create "${TERMUX_TAG}" + --repo "${GITHUB_REPOSITORY}" + --target "${RELEASE_SHA}" + --title "${release_title}" + --notes-file "${body_path}" +) +if [[ "${UPSTREAM_PRERELEASE}" == "true" ]]; then + release_args+=(--prerelease) +fi +release_args+=("${asset_path}#codex-termux") +"${release_args[@]}" diff --git a/scripts/termux-download-release-artifact.sh b/scripts/termux-download-release-artifact.sh new file mode 100755 index 000000000000..2acdba9025a0 --- /dev/null +++ b/scripts/termux-download-release-artifact.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${HEAD_SHA:-}" ]]; then + echo "HEAD_SHA is required." >&2 + exit 1 +fi + +if [[ -z "${PR_ARTIFACT_NAME:-}" ]]; then + echo "PR_ARTIFACT_NAME is required." >&2 + exit 1 +fi + +promoted_dir="${PROMOTED_DIR:-promoted}" + +find_run_id() { + local event="$1" + gh run list \ + --repo "${GITHUB_REPOSITORY}" \ + --workflow rust-release.yml \ + --event "${event}" \ + --status success \ + --commit "${HEAD_SHA}" \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId // empty' +} + +find_artifact_run_id() { + local artifact_name="$1" + + gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=${artifact_name}" \ + | jq -rs \ + --arg artifact_name "${artifact_name}" \ + --arg head_sha "${HEAD_SHA}" \ + ' + [ + .[].artifacts[] + | select(.name == $artifact_name) + | select(.expired == false) + | select(.workflow_run.head_sha == $head_sha) + ] + | sort_by(.created_at) + | reverse + | .[0].workflow_run.id // empty + ' +} + +artifact_name="${PR_ARTIFACT_NAME}" +run_id="$(find_artifact_run_id "${artifact_name}")" +mkdir -p "${promoted_dir}" +if [[ -n "${run_id}" ]] && gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}"; then + : +else + artifact_name="aarch64-linux-android" + run_id="$(find_run_id workflow_dispatch)" + if [[ -z "${run_id}" ]]; then + echo "No successful rust-release run found for ${HEAD_SHA}" >&2 + exit 1 + fi + gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}" +fi + +ls -la "${promoted_dir}" +if [[ -f "${promoted_dir}/SHA256SUMS" ]]; then + (cd "${promoted_dir}" && sha256sum -c SHA256SUMS) +fi +if [[ ! -f "${promoted_dir}/codex-aarch64-linux-android.tar.gz" ]]; then + echo "Expected ${promoted_dir}/codex-aarch64-linux-android.tar.gz in the downloaded artifact." >&2 + exit 1 +fi diff --git a/scripts/termux-find-release-pr.sh b/scripts/termux-find-release-pr.sh new file mode 100755 index 000000000000..3e35fa42eabe --- /dev/null +++ b/scripts/termux-find-release-pr.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -n "${INPUT_PR_NUMBER:-}" || -n "${INPUT_PR_HEAD_SHA:-}" ]]; then + if [[ -z "${INPUT_PR_NUMBER:-}" || -z "${INPUT_PR_HEAD_SHA:-}" ]]; then + echo "workflow_dispatch inputs pr_number and pr_head_sha must be provided together." >&2 + exit 1 + fi + + pr_number="${INPUT_PR_NUMBER}" + head_sha="${INPUT_PR_HEAD_SHA}" + head_ref="" +else + if [[ -z "${RELEASE_BRANCH:-}" || -z "${RELEASE_SHA:-}" ]]; then + echo "RELEASE_BRANCH and RELEASE_SHA are required." >&2 + exit 1 + fi + + pr_json="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${RELEASE_BRANCH}" \ + --state merged \ + --limit 100 \ + --json number,headRefOid,headRefName,mergeCommit \ + --jq "[.[] | select(.mergeCommit.oid == \"${RELEASE_SHA}\")] | sort_by(.number) | reverse | .[0] // empty" + )" + if [[ -z "${pr_json}" ]]; then + echo "Unable to find merged PR for ${RELEASE_SHA} into ${RELEASE_BRANCH}" >&2 + exit 1 + fi + + pr_number="$(jq -r '.number' <<< "${pr_json}")" + head_sha="$(jq -r '.headRefOid // .head.sha' <<< "${pr_json}")" + head_ref="$(jq -r '.headRefName // .head.ref' <<< "${pr_json}")" +fi + +artifact_name="termux-android-pr-${pr_number}-${head_sha}" +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "number=${pr_number}" + echo "head_sha=${head_sha}" + echo "head_ref=${head_ref}" + echo "artifact_name=${artifact_name}" + } >> "${GITHUB_OUTPUT}" +else + printf 'number=%s\nhead_sha=%s\nhead_ref=%s\nartifact_name=%s\n' \ + "${pr_number}" \ + "${head_sha}" \ + "${head_ref}" \ + "${artifact_name}" +fi diff --git a/scripts/termux-read-release-metadata.sh b/scripts/termux-read-release-metadata.sh new file mode 100755 index 000000000000..519a0635a368 --- /dev/null +++ b/scripts/termux-read-release-metadata.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# Parse .github/termux-release.json and emit workflow outputs for deploy or +# promote jobs. TERMUX_RELEASE_ACTION must be "deploy" or "promote". + +set -euo pipefail + +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +metadata=".github/termux-release.json" +action="${TERMUX_RELEASE_ACTION:-deploy}" +case "${action}" in + deploy) + missing_context="deployment" + ;; + promote) + missing_context="promotion" + ;; + *) + echo "TERMUX_RELEASE_ACTION must be deploy or promote." >&2 + exit 1 + ;; +esac + +if [[ ! -f "${metadata}" ]]; then + echo "No ${metadata}; this push is not a Termux release ${missing_context}." + echo "${action}=false" >> "${GITHUB_OUTPUT}" + exit 0 +fi + +upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" +upstream_name="$(jq -r '.upstream_name // .upstream_tag // empty' "${metadata}")" +termux_tag="$(jq -r '.termux_tag // empty' "${metadata}")" +upstream_version="${upstream_tag#rust-v}" +upstream_version="${upstream_version%-termux}" +upstream_prerelease=false +if [[ "${upstream_version}" == *-* ]]; then + upstream_prerelease=true +fi +upstream_html_url="$(jq -r '.upstream_html_url // ""' "${metadata}")" +upstream_repo="$(jq -r '.upstream_repo // "openai/codex"' "${metadata}")" +release_train="$(jq -r '.release_train // ""' "${metadata}")" +if [[ -z "${upstream_tag}" || -z "${termux_tag}" ]]; then + echo "Missing upstream_tag or termux_tag in ${metadata}" >&2 + exit 1 +fi + +release_state="$(TERMUX_TAG="${termux_tag}" "${script_dir}/termux-release-asset-state.sh")" +release_exists="$(awk -F= '$1 == "release_exists" { print $2 }' <<< "${release_state}")" +asset_exists="$(awk -F= '$1 == "asset_exists" { print $2 }' <<< "${release_state}")" + +if [[ "${action}" == "promote" ]]; then + if [[ "${asset_exists}" == "true" ]]; then + echo "${termux_tag} already exists with codex-aarch64-linux-android.tar.gz; skipping promotion." + echo "promote=false" >> "${GITHUB_OUTPUT}" + exit 0 + fi + if [[ "${release_exists}" == "true" ]]; then + echo "${termux_tag} exists but is missing codex-aarch64-linux-android.tar.gz; repairing promotion." + fi +fi + +{ + echo "${action}=true" + echo "upstream_tag=${upstream_tag}" + echo "upstream_name=${upstream_name}" + echo "termux_tag=${termux_tag}" + echo "upstream_prerelease=${upstream_prerelease}" + echo "upstream_html_url=${upstream_html_url}" + echo "upstream_repo=${upstream_repo}" + echo "release_train=${release_train}" + echo "release_exists=${release_exists}" + echo "asset_exists=${asset_exists}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-release-asset-state.sh b/scripts/termux-release-asset-state.sh new file mode 100755 index 000000000000..8694480b6a36 --- /dev/null +++ b/scripts/termux-release-asset-state.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +termux_tag="${1:-${TERMUX_TAG:-}}" +release_repo="${RELEASE_REPO:-${GITHUB_REPOSITORY:-}}" +asset_name="${ASSET_NAME:-codex-aarch64-linux-android.tar.gz}" + +if [[ -z "${termux_tag}" ]]; then + echo "TERMUX_TAG or tag argument is required." >&2 + exit 1 +fi + +if [[ -z "${release_repo}" ]]; then + echo "GITHUB_REPOSITORY or RELEASE_REPO is required." >&2 + exit 1 +fi + +release_exists=false +asset_exists=false + +if gh release view "${termux_tag}" --repo "${release_repo}" >/dev/null 2>&1; then + release_exists=true + release_asset_exists="$( + gh release view "${termux_tag}" \ + --repo "${release_repo}" \ + --json assets \ + | jq -r --arg asset_name "${asset_name}" \ + '.assets | map(.name) | any(. == $asset_name)' + )" + if [[ "${release_asset_exists}" == "true" ]]; then + asset_exists=true + fi +fi + +printf 'release_exists=%s\n' "${release_exists}" +printf 'asset_exists=%s\n' "${asset_exists}" diff --git a/scripts/termux-release-paths.sh b/scripts/termux-release-paths.sh new file mode 100755 index 000000000000..6990f323ec44 --- /dev/null +++ b/scripts/termux-release-paths.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# Sourceable path lists for files owned by the Termux release automation. + +set -euo pipefail + +readonly -a TERMUX_RELEASE_WORKFLOW_PATHS=( + .github/workflows/rust-release.yml + .github/workflows/shell-tool-mcp.yml + .github/workflows/termux-release-checkpoint.yml + .github/workflows/termux-release-deploy.yml + .github/workflows/termux-release-promote.yml +) + +readonly -a TERMUX_RELEASE_BRANCH_SCRIPT_PATHS=( + scripts/termux-configure-git.sh + scripts/termux-create-checkpoint-pr.sh + scripts/termux-create-or-update-mirrored-release.sh + scripts/termux-download-release-artifact.sh + scripts/termux-find-release-pr.sh + scripts/termux-read-release-metadata.sh + scripts/termux-release-asset-state.sh + scripts/termux-release-paths.sh + scripts/termux-resolve-release-ref.sh + scripts/termux-validate-gh-env.sh +) + +readonly -a TERMUX_RELEASE_AUTOMATION_PATHS=( + "${TERMUX_RELEASE_WORKFLOW_PATHS[@]}" + "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +) + +readonly -a TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS=( + "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" + .github/termux-release.json +) + +termux_path_in_list() { + local candidate="$1" + shift + local listed_path + + for listed_path in "$@"; do + [[ "${candidate}" != "${listed_path}" ]] || return 0 + done + return 1 +} + +termux_is_release_automation_path() { + termux_path_in_list "$1" "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" +} + +termux_is_checkpoint_release_only_path() { + termux_path_in_list "$1" "${TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS[@]}" +} diff --git a/scripts/termux-resolve-release-ref.sh b/scripts/termux-resolve-release-ref.sh new file mode 100755 index 000000000000..194773d45bed --- /dev/null +++ b/scripts/termux-resolve-release-ref.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Resolve the release branch and SHA for deploy/promote workflows. + +set -euo pipefail + +: "${GITHUB_EVENT_NAME:?GITHUB_EVENT_NAME is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" + +input_release_branch="${INPUT_RELEASE_BRANCH:-${REQUESTED_RELEASE_BRANCH:-}}" +input_release_sha="${INPUT_RELEASE_SHA:-${REQUESTED_RELEASE_SHA:-}}" + +if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then + if [[ -z "${input_release_branch}" ]]; then + echo "release_branch is required for workflow_dispatch." >&2 + exit 1 + fi + + release_branch="${input_release_branch}" + if [[ -n "${input_release_sha}" ]]; then + git checkout --detach "${input_release_sha}" + release_sha="${input_release_sha}" + else + release_sha="$(git rev-parse HEAD)" + fi +else + : "${GITHUB_REF_NAME:?GITHUB_REF_NAME is required}" + : "${GITHUB_SHA:?GITHUB_SHA is required}" + release_branch="${GITHUB_REF_NAME}" + release_sha="${GITHUB_SHA}" +fi + +{ + echo "branch=${release_branch}" + echo "sha=${release_sha}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-validate-gh-env.sh b/scripts/termux-validate-gh-env.sh new file mode 100755 index 000000000000..02581fdebceb --- /dev/null +++ b/scripts/termux-validate-gh-env.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Lightweight post-toolbox check for jobs that rely on authenticated gh calls. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +command -v gh +gh auth status --hostname github.com + +printf 'GITHUB_REPOSITORY=%s\n' "${GITHUB_REPOSITORY}" +printf 'REPO=%s\n' "${REPO:-}" +printf 'GH_REPO_URL=%s\n' "${GH_REPO_URL:-}" +printf 'GH_WORKFLOW_URL=%s\n' "${GH_WORKFLOW_URL:-}" From 99614a67f6b9fe8eaf09ec78bdec7f46d0502ed1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 21:45:35 +0000 Subject: [PATCH 19/25] Prepare Termux rust-v0.134.0-alpha.2 --- .github/termux-release.json | 15 + .github/workflows/rust-release.yml | 1798 +++++++---------- .github/workflows/shell-tool-mcp.yml | 461 +++++ .../workflows/termux-release-checkpoint.yml | 103 + .github/workflows/termux-release-deploy.yml | 243 +++ .github/workflows/termux-release-promote.yml | 135 ++ codex-rs/Cargo.toml | 2 +- scripts/termux-configure-git.sh | 40 + scripts/termux-create-checkpoint-pr.sh | 275 +++ ...ermux-create-or-update-mirrored-release.sh | 111 + scripts/termux-download-release-artifact.sh | 78 + scripts/termux-find-release-pr.sh | 53 + scripts/termux-read-release-metadata.sh | 77 + scripts/termux-release-asset-state.sh | 37 + scripts/termux-release-paths.sh | 55 + scripts/termux-resolve-release-ref.sh | 36 + scripts/termux-validate-gh-env.sh | 16 + 17 files changed, 2460 insertions(+), 1075 deletions(-) create mode 100644 .github/termux-release.json create mode 100644 .github/workflows/shell-tool-mcp.yml create mode 100644 .github/workflows/termux-release-checkpoint.yml create mode 100644 .github/workflows/termux-release-deploy.yml create mode 100644 .github/workflows/termux-release-promote.yml create mode 100755 scripts/termux-configure-git.sh create mode 100755 scripts/termux-create-checkpoint-pr.sh create mode 100755 scripts/termux-create-or-update-mirrored-release.sh create mode 100755 scripts/termux-download-release-artifact.sh create mode 100755 scripts/termux-find-release-pr.sh create mode 100755 scripts/termux-read-release-metadata.sh create mode 100755 scripts/termux-release-asset-state.sh create mode 100755 scripts/termux-release-paths.sh create mode 100755 scripts/termux-resolve-release-ref.sh create mode 100755 scripts/termux-validate-gh-env.sh diff --git a/.github/termux-release.json b/.github/termux-release.json new file mode 100644 index 000000000000..459a467dca56 --- /dev/null +++ b/.github/termux-release.json @@ -0,0 +1,15 @@ +{ + "upstream_repo": "openai/codex", + "upstream_tag": "rust-v0.134.0-alpha.2", + "upstream_name": "0.134.0-alpha.2", + "upstream_html_url": "https://github.com/openai/codex/releases/tag/rust-v0.134.0-alpha.2", + "upstream_target": "main", + "upstream_release_id": "328157251", + "upstream_prerelease": true, + "release_train": "0.134.0", + "release_branch": "release/0.134.0", + "work_branch": "upstream/rust-v0.134.0", + "patch_branch": "wallentx/termux-target", + "patch_source_sha": "be3993303eb9b678fc81cb0a5d0de3bd2360f66a", + "termux_tag": "rust-v0.134.0-alpha.2-termux" +} diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index c55337ecfe6e..56d0977e6e67 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -4,46 +4,49 @@ # git tag -a rust-v0.1.0 -m "Release 0.1.0" # git push origin rust-v0.1.0 # ``` -# -# To use external macOS signing, manually dispatch `release_mode=build_unsigned`, -# sign the unsigned macOS artifacts in a secure enclave, upload the signed handoff -# archive as a GitHub Release asset, then manually dispatch -# `release_mode=promote_signed` with `unsigned_run_id` and `signed_macos_asset`. -# The signed handoff archive should contain target or artifact directories such -# as `aarch64-apple-darwin/` with signed binaries. name: rust-release on: - push: - tags: - - "rust-v*.*.*" + pull_request: + branches: + - "release/**" workflow_dispatch: inputs: - release_mode: - description: "build_unsigned creates unsigned macOS handoff artifacts; promote_signed finishes a release from signed macOS handoff artifacts." + source_ref: + description: "Branch, tag, or commit SHA to build (code only; keeps .github from dispatched ref)" required: false + type: string + default: "" + build_target: + description: "Target to build" + required: true type: choice - default: build_unsigned + default: all options: - - build_unsigned - - promote_signed - sign_macos: - description: "Deprecated compatibility input; use release_mode instead." - required: false - type: boolean - default: false - unsigned_run_id: - description: "For promote_signed: workflow run id from the build_unsigned run." - required: false - type: string - signed_macos_asset: - description: "For promote_signed: exact GitHub Release asset name containing signed macOS handoff artifacts." - required: false - type: string - signed_macos_sha256: - description: "For promote_signed: optional SHA-256 of signed_macos_asset." - required: false - type: string + - all + - aarch64-apple-darwin + - x86_64-apple-darwin + - x86_64-unknown-linux-musl + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-musl + - aarch64-unknown-linux-gnu + - aarch64-linux-android + - x86_64-pc-windows-msvc + - aarch64-pc-windows-msvc + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read concurrency: group: ${{ github.workflow }} @@ -51,72 +54,26 @@ concurrency: jobs: tag-check: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - uses: dtolnay/rust-toolchain@1.92 - name: Validate tag matches Cargo.toml version shell: bash - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - REQUESTED_SIGN_MACOS: ${{ inputs.sign_macos }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} run: | set -euo pipefail echo "::group::Tag validation" - case "${RELEASE_MODE}" in - signed) - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - echo "❌ Manual rust-release runs must use release_mode=build_unsigned or release_mode=promote_signed" - exit 1 - fi - ;; - build_unsigned) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=build_unsigned is only valid for manual runs" - exit 1 - fi - ;; - promote_signed) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=promote_signed is only valid for manual runs" - exit 1 - fi - if [[ ! "${UNSIGNED_RUN_ID}" =~ ^[0-9]+$ ]]; then - echo "❌ release_mode=promote_signed requires unsigned_run_id to be a workflow run id" - exit 1 - fi - if [[ -z "${SIGNED_MACOS_ASSET}" ]]; then - echo "❌ release_mode=promote_signed requires signed_macos_asset" - exit 1 - fi - if [[ "${SIGNED_MACOS_ASSET}" == */* || "${SIGNED_MACOS_ASSET}" == *"*"* || "${SIGNED_MACOS_ASSET}" == *"?"* || "${SIGNED_MACOS_ASSET}" == *"["* ]]; then - echo "❌ signed_macos_asset must be an exact release asset name, not a path or glob" - exit 1 - fi - if [[ "${UNSIGNED_RUN_ID}" == "${GITHUB_RUN_ID}" ]]; then - echo "❌ unsigned_run_id must refer to the earlier build_unsigned run, not this run" - exit 1 - fi - ;; - *) - echo "❌ Unknown release_mode '${RELEASE_MODE}'" - exit 1 - ;; - esac - - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${REQUESTED_SIGN_MACOS}" == "true" ]]; then - echo "::warning title=Deprecated sign_macos input ignored::Use release_mode=build_unsigned or release_mode=promote_signed instead." - fi - # 1. Must be a tag and match the regex [[ "${GITHUB_REF_TYPE}" == "tag" ]] \ || { echo "❌ Not a tag push"; exit 1; } - [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \ + [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?(-termux)?$ ]] \ || { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; } # 2. Extract versions @@ -131,115 +88,198 @@ jobs: echo "✅ Tag and Cargo.toml agree (${tag_ver})" echo "::endgroup::" - build: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} + select-build-matrix: + if: always() && (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') needs: tag-check - name: Build - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: ${{ matrix.runs_on || matrix.runner }} - # Release builds can take a long time, so leave some headroom to avoid - # having to restart the full workflow due to a timeout. - timeout-minutes: 90 - permissions: - contents: read - id-token: write + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.select.outputs.matrix }} + build_version: ${{ steps.select.outputs.build_version }} + steps: + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - id: select + shell: bash + env: + REPO_OWNER: ${{ github.repository_owner }} + EVENT_NAME: ${{ github.event_name }} + BUILD_TARGET: ${{ inputs.build_target }} + run: | + set -euo pipefail + + matrix='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04","target":"aarch64-linux-android"}, + {"runner":"windows-latest","target":"x86_64-pc-windows-msvc"}, + {"runner":"windows-11-arm","target":"aarch64-pc-windows-msvc"} + ]' + + if [[ "${REPO_OWNER}" != "openai" ]]; then + matrix="$(jq -c '[.[] | select(.runner | startswith("macos") | not)]' <<< "${matrix}")" + fi + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + matrix="$(jq -c '[.[] | select(.target == "aarch64-linux-android")]' <<< "${matrix}")" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" && -n "${BUILD_TARGET}" && "${BUILD_TARGET}" != "all" ]]; then + matrix="$(jq -c --arg build_target "${BUILD_TARGET}" '[.[] | select(.target == $build_target)]' <<< "${matrix}")" + fi + + if [[ "$(jq 'length' <<< "${matrix}")" -eq 0 ]]; then + echo "No build targets selected after applying owner/dispatch filters." >&2 + exit 1 + fi + + echo "matrix={\"include\":${matrix}}" >> "$GITHUB_OUTPUT" + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + metadata=".github/termux-release.json" + if [[ ! -f "${metadata}" ]]; then + echo "${metadata} is required for release train PR builds" >&2 + exit 1 + fi + upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" + if [[ -z "${upstream_tag}" || "${upstream_tag}" != rust-v* ]]; then + echo "Unable to determine upstream rust tag from ${metadata}" >&2 + exit 1 + fi + build_version="${upstream_tag#rust-v}" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then + latest_release_tag="$( + GH_TOKEN="${{ github.token }}" gh api repos/openai/codex/releases/latest --jq '.tag_name' + )" + + if [[ -z "${latest_release_tag}" || "${latest_release_tag}" != rust-v* ]]; then + echo "Unable to determine latest stable release tag from openai/codex" >&2 + exit 1 + fi + + latest_release_version="${latest_release_tag#rust-v}" + build_version="${latest_release_version}-dev" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + fi + + build: + if: >- + always() && + (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') && + needs.select-build-matrix.result == 'success' + needs: + - tag-check + - select-build-matrix + name: Build - ${{ matrix.runner }} - ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + permissions: write-all + timeout-minutes: 120 defaults: run: working-directory: codex-rs env: - # 2026-03-04: temporarily change releases to use thin LTO because - # Ubuntu ARM is timing out at 60 minutes. - CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' }} + CODEX_BWRAP_ENABLE_FFI: ${{ contains(matrix.target, 'unknown-linux') && '1' || '0' }} + CODEX_BUILD_VERSION: ${{ needs.select-build-matrix.outputs.build_version }} + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" strategy: fail-fast: false - matrix: - include: - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - # Release artifacts intentionally ship MUSL-linked Linux binaries. - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - bundle: primary - artifact_name: x86_64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - bundle: app-server - artifact_name: x86_64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - bundle: primary - artifact_name: aarch64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - bundle: app-server - artifact_name: aarch64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" + matrix: ${{ fromJson(needs.select-build-matrix.outputs.matrix) }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + with: + verbose: true + - name: Checkout selected source ref + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + uses: actions/checkout@v6 with: - persist-credentials: false - - name: Print runner specs (Linux) - if: ${{ runner.os == 'Linux' }} + ref: ${{ inputs.source_ref }} + path: __source__ + fetch-depth: 1 + - name: Overlay selected source ref (excluding .github) + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + shell: bash + working-directory: ${{ github.workspace }} + run: | + set -euo pipefail + echo "Using source ref: ${{ inputs.source_ref }}" + + # Keep workflow/actions from the dispatch ref while replacing all other files. + find . -mindepth 1 -maxdepth 1 \ + ! -name .git \ + ! -name .github \ + ! -name __source__ \ + -exec rm -rf -- {} + + tar --exclude='.github' -C __source__ -cf - . | tar -xf - + rm -rf __source__ + + - name: Apply selected build version + if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} shell: bash + working-directory: ${{ github.workspace }} run: | set -euo pipefail - cpu_model="$(lscpu | awk -F: '/Model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}')" - total_ram="$(awk '/MemTotal/ {printf "%.1f GiB\n", $2 / 1024 / 1024}' /proc/meminfo)" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(uname -a)" - echo "CPU model: ${cpu_model}" - echo "Logical CPUs: $(nproc)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . - - name: Print runner specs (macOS) - if: ${{ runner.os == 'macOS' }} + if [[ -z "${CODEX_BUILD_VERSION}" ]]; then + echo "CODEX_BUILD_VERSION is empty for workflow_dispatch" >&2 + exit 1 + fi + echo "Using build version: ${CODEX_BUILD_VERSION}" + BUILD_VERSION="${CODEX_BUILD_VERSION}" perl -0777 -i -pe \ + 's/(\[workspace\.package\][^\[]*?version = ")([^"]+)(")/${1}$ENV{BUILD_VERSION}$3/s' \ + codex-rs/Cargo.toml + + grep -n "^\[workspace.package\]\|^version = " codex-rs/Cargo.toml | head -n 4 + + - name: Select Android NDK + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail - total_ram="$(sysctl -n hw.memsize | awk '{printf "%.1f GiB\n", $1 / 1024 / 1024 / 1024}')" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(sw_vers -productName) $(sw_vers -productVersion)" - echo "Hardware model: $(sysctl -n hw.model)" - echo "CPU architecture: $(uname -m)" - echo "Logical CPUs: $(sysctl -n hw.logicalcpu)" - echo "Physical CPUs: $(sysctl -n hw.physicalcpu)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . + + fallback_version="29.0.13113456" + ndk_root="${ANDROID_HOME}/ndk" + selected_ndk="" + + if [[ -d "${ndk_root}" ]]; then + while IFS= read -r candidate; do + toolchain="${candidate}/toolchains/llvm/prebuilt/linux-x86_64" + if [[ -x "${toolchain}/bin/aarch64-linux-android24-clang" ]]; then + selected_ndk="${candidate}" + break + fi + done < <(find "${ndk_root}" -mindepth 1 -maxdepth 1 -type d | sort -Vr) + fi + + if [[ -z "${selected_ndk}" ]]; then + echo "No usable preinstalled Android NDK found; installing ${fallback_version}" + yes | sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses || true + sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" "ndk;${fallback_version}" + selected_ndk="${ndk_root}/${fallback_version}" + fi + + if [[ ! -x "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" ]]; then + echo "Selected Android NDK does not contain a usable aarch64 API 24 clang: ${selected_ndk}" >&2 + exit 1 + fi + + echo "Selected Android NDK: ${selected_ndk}" + "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" --version + echo "ANDROID_NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" + echo "NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" - name: Install Linux bwrap build dependencies - if: ${{ runner.os == 'Linux' }} + if: ${{ runner.os == 'Linux' && matrix.target != 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail @@ -254,9 +294,173 @@ jobs: sudo apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 fi - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@1.93 with: targets: ${{ matrix.target }} + - name: Set up sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + uses: mozilla-actions/sccache-action@main + - name: Enable sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + shell: bash + run: | + set -euo pipefail + echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_VERSION=android-release-${{ matrix.target }}" >> "$GITHUB_ENV" + echo "SCCACHE_CACHE_SIZE=5G" >> "$GITHUB_ENV" + - name: Ensure Rust target is installed + shell: bash + run: | + set -euo pipefail + rustup target add "${{ matrix.target }}" + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android build environment + shell: bash + run: | + set -euo pipefail + ndk="${ANDROID_NDK_HOME}" + target="${{ matrix.target }}" + + # Set up the Android toolchain + toolchain="${ndk}/toolchains/llvm/prebuilt/linux-x86_64" + + # Use API level 24 to follow termux-build convention + api_level="24" + + # Configure Cargo to use the NDK linker + cargo_target_var="CARGO_TARGET_${target^^}_LINKER" + cargo_target_var="${cargo_target_var//-/_}" + echo "${cargo_target_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + # Set CC and AR for the target + target_cc_var="CC_${target}" + target_cc_var="${target_cc_var//-/_}" + echo "${target_cc_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + target_cxx_var="CXX_${target}" + target_cxx_var="${target_cxx_var//-/_}" + echo "${target_cxx_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang++" >> "$GITHUB_ENV" + + target_ar_var="AR_${target}" + target_ar_var="${target_ar_var//-/_}" + echo "${target_ar_var}=${toolchain}/bin/llvm-ar" >> "$GITHUB_ENV" + + # Add toolchain to PATH + echo "${toolchain}/bin" >> "$GITHUB_PATH" + + tls_align_source="${RUNNER_TEMP}/codex-android-tls-align.S" + tls_align_object="${RUNNER_TEMP}/codex-android-tls-align.o" + cat > "${tls_align_source}" <<'EOF' + .section .tdata.codex_android_tls_align,"awT",%progbits + .p2align 6 + .globl __codex_android_tls_align + .hidden __codex_android_tls_align + .type __codex_android_tls_align, %object + __codex_android_tls_align: + .byte 0 + .size __codex_android_tls_align, 1 + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${tls_align_source}" -o "${tls_align_object}" + + v8_compat_source="${RUNNER_TEMP}/codex-android-v8-compat.c" + v8_compat_object="${RUNNER_TEMP}/codex-android-v8-compat.o" + cat > "${v8_compat_source}" <<'EOF' + #include + #include + + extern int posix_memalign(void **memptr, size_t alignment, size_t size); + extern double strtod(const char *nptr, char **endptr); + extern float strtof(const char *nptr, char **endptr); + + void *aligned_alloc(size_t alignment, size_t size) { + void *ptr = 0; + if (posix_memalign(&ptr, alignment, size) != 0) { + return 0; + } + return ptr; + } + + double strtod_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtod(nptr, endptr); + } + + float strtof_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtof(nptr, endptr); + } + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${v8_compat_source}" -o "${v8_compat_object}" + + builtins_archive="$( + find "${toolchain}/lib/clang" \ + -path "*/lib/linux/libclang_rt.builtins-aarch64-android.a" \ + -print -quit + )" + if [[ -z "${builtins_archive}" ]]; then + echo "Could not find Android compiler-rt builtins archive under ${toolchain}/lib/clang" >&2 + exit 1 + fi + builtins_dir="$(dirname "${builtins_archive}")" + + # Provide compatibility symbols and C++ ABI needed by the Android rusty_v8 archive. + rustflags_var="CARGO_TARGET_${target^^}_RUSTFLAGS" + rustflags_var="${rustflags_var//-/_}" + echo "${rustflags_var}=-C link-arg=${tls_align_object} -C link-arg=${v8_compat_object} -C link-arg=-Wl,-u,__codex_android_tls_align -C link-arg=-L${builtins_dir} -C link-arg=-lclang_rt.builtins-aarch64-android -C link-arg=-lc++abi -C link-arg=-Wl,-z,max-page-size=65536" >> "$GITHUB_ENV" + + + # Configure for vendored OpenSSL build + echo "CARGO_BUILD_TARGET_APPLIES_TO_HOST=false" >> "$GITHUB_ENV" + echo "CARGO_BUILD_TARGET=${{ matrix.target }}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android rusty_v8 artifact overrides + env: + GH_TOKEN: ${{ github.token }} + TARGET: ${{ matrix.target }} + shell: bash + run: | + set -euo pipefail + binding_dir="${RUNNER_TEMP}/rusty_v8" + archive="${binding_dir}/librusty_v8_release_${TARGET}.a.gz" + binding_path="${binding_dir}/src_binding_release_${TARGET}.rs" + checksums_path="${binding_dir}/rusty_v8_release_${TARGET}.sha256" + artifact_repository="wallentx/codex-termux" + release_tag="rusty-v8-v147.4.0" + mkdir -p "${binding_dir}" + echo "Downloading Android rusty_v8 artifacts from ${artifact_repository}@${release_tag}" + gh release download "${release_tag}" \ + --repo "${artifact_repository}" \ + --dir "${binding_dir}" \ + --pattern "librusty_v8_release_${TARGET}.a.gz" \ + --pattern "src_binding_release_${TARGET}.rs" \ + --pattern "rusty_v8_release_${TARGET}.sha256" + (cd "${binding_dir}" && sha256sum -c "$(basename "${checksums_path}")") + echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV" + echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Install termux-elf-cleaner + shell: bash + run: | + set -euo pipefail + version="v3.0.1" + src_dir="${RUNNER_TEMP}/termux-elf-cleaner-src" + build_dir="${RUNNER_TEMP}/termux-elf-cleaner-build" + bin_dir="${RUNNER_TEMP}/termux-elf-cleaner-bin" + + rm -rf "${src_dir}" "${build_dir}" "${bin_dir}" + mkdir -p "${src_dir}" "${build_dir}" "${bin_dir}" + + curl -fsSL "https://github.com/termux/termux-elf-cleaner/archive/refs/tags/${version}.tar.gz" \ + | tar -xzf - --strip-components=1 -C "${src_dir}" + + cmake -S "${src_dir}" -B "${build_dir}" -DCMAKE_BUILD_TYPE=Release + cmake --build "${build_dir}" --parallel + + install "${build_dir}/termux-elf-cleaner" "${bin_dir}/termux-elf-cleaner" + echo "${bin_dir}" >> "$GITHUB_PATH" - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Use hermetic Cargo home (musl) @@ -269,12 +473,25 @@ jobs: echo "${cargo_home}/bin" >> "$GITHUB_PATH" : > "${cargo_home}/config.toml" + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ${{ github.workspace }}/.cargo-home/bin/ + ${{ github.workspace }}/.cargo-home/registry/index/ + ${{ github.workspace }}/.cargo-home/registry/cache/ + ${{ github.workspace }}/.cargo-home/git/db/ + ${{ github.workspace }}/codex-rs/target/ + key: cargo-${{ matrix.runner }}-${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }} + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install Zig - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + uses: mlugg/setup-zig@v2 with: version: 0.14.0 - use-cache: false - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install musl build tools @@ -310,12 +527,6 @@ jobs: shell: bash run: | set -euo pipefail - # Avoid problematic aws-lc jitter entropy code path on musl builders. - echo "AWS_LC_SYS_NO_JITTER_ENTROPY=1" >> "$GITHUB_ENV" - target_no_jitter="AWS_LC_SYS_NO_JITTER_ENTROPY_${{ matrix.target }}" - target_no_jitter="${target_no_jitter//-/_}" - echo "${target_no_jitter}=1" >> "$GITHUB_ENV" - # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. echo "RUSTFLAGS=" >> "$GITHUB_ENV" echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" @@ -340,94 +551,70 @@ jobs: echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" - - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }} - name: Configure musl rusty_v8 artifact overrides and verify checksums - uses: ./.github/actions/setup-rusty-v8-musl - with: - target: ${{ matrix.target }} - - - if: ${{ contains(matrix.target, 'linux') && matrix.bundle == 'primary' }} - name: Build bwrap and export digest + - name: Cargo build shell: bash run: | set -euo pipefail - target="${{ matrix.target }}" - cargo build --target "$target" --release --timings --bin bwrap - bwrap_path="target/${target}/release/bwrap" - if [[ ! -f "$bwrap_path" ]]; then - echo "bwrap binary ${bwrap_path} not found" - exit 1 + if [[ "${{ matrix.target }}" == 'aarch64-linux-android' ]]; then + export CARGO_BUILD_JOBS=4 + export CARGO_PROFILE_RELEASE_LTO=thin + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 + cargo build --target ${{ matrix.target }} --release --bin codex + exit 0 fi - digest="$(sha256sum "$bwrap_path" | awk '{print $1}')" - echo "CODEX_BWRAP_SHA256=${digest}" >> "$GITHUB_ENV" - echo "Built bwrap ${bwrap_path} with sha256:${digest}" - - - name: Cargo build + if [[ "${{ contains(matrix.target, 'windows') }}" == 'true' ]]; then + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy --bin codex-windows-sandbox-setup --bin codex-command-runner + else + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy + fi + - name: sccache stats (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash - run: | - build_args=() - for binary in ${{ matrix.binaries }}; do - build_args+=(--bin "$binary") - done - echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}" - cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}" + run: ${SCCACHE_PATH:-sccache} --show-stats || true - - name: Upload Cargo timings - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }} - path: codex-rs/target/**/cargo-timings/cargo-timing.html - if-no-files-found: warn - - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Stage unsigned macOS artifacts + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Normalize Android ELF for Termux shell: bash run: | set -euo pipefail - - target="${{ matrix.target }}" - release_dir="target/${target}/release" - dest="unsigned-dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - unsigned_name="${binary}-${target}-unsigned" - unsigned_path="${dest}/${unsigned_name}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 + for binary in codex codex-responses-api-proxy; do + binary_path="target/${{ matrix.target }}/release/${binary}" + if [[ -f "${binary_path}" ]]; then + termux-elf-cleaner --api-level 24 "${binary_path}" + chmod +x "${binary_path}" + # if [[ "${binary}" == "codex" ]]; then + # # Patch PT_TLS (0x7) to PT_NULL (0x0) at offset 400 to bypass Bionic alignment checks. + # printf '\x00\x00\x00\x00' | dd of="${binary_path}" bs=1 seek=400 count=4 conv=notrunc + # fi fi - - cp "${binary_path}" "${unsigned_path}" - tar -C "$dest" -czf "${unsigned_path}.tar.gz" "${unsigned_name}" - zstd -T0 -19 --rm "${unsigned_path}" done - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Upload unsigned macOS artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }}-unsigned - path: codex-rs/unsigned-dist/${{ matrix.target }}/* - if-no-files-found: error - - - if: ${{ contains(matrix.target, 'linux') }} + - if: ${{ contains(matrix.target, 'linux') && !contains(matrix.target, 'android') && github.repository_owner == 'openai' }} name: Cosign Linux artifacts uses: ./.github/actions/linux-code-sign with: target: ${{ matrix.target }} artifacts-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release - binaries: ${{ matrix.binaries }} - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS == 'true' }} + - if: ${{ contains(matrix.target, 'windows') && github.repository_owner == 'openai' }} + name: Sign Windows binaries with Azure Trusted Signing + uses: ./.github/actions/windows-code-sign + with: + target: ${{ matrix.target }} + client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID }} + endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} + account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} + + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (binaries) uses: ./.github/actions/macos-code-sign with: target: ${{ matrix.target }} - binaries: ${{ matrix.binaries }} sign-binaries: "true" sign-dmg: "false" apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }} @@ -436,7 +623,7 @@ jobs: apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: Build macOS dmg shell: bash run: | @@ -451,17 +638,23 @@ jobs: # The previous "MacOS code signing (binaries)" step signs + notarizes the # built artifacts in `${release_dir}`. This step packages *those same* # signed binaries into a dmg. + codex_binary_path="${release_dir}/codex" + proxy_binary_path="${release_dir}/codex-responses-api-proxy" + rm -rf "$dmg_root" mkdir -p "$dmg_root" - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 - fi - ditto "${binary_path}" "${dmg_root}/${binary}" - done + if [[ ! -f "$codex_binary_path" ]]; then + echo "Binary $codex_binary_path not found" + exit 1 + fi + if [[ ! -f "$proxy_binary_path" ]]; then + echo "Binary $proxy_binary_path not found" + exit 1 + fi + + ditto "$codex_binary_path" "${dmg_root}/codex" + ditto "$proxy_binary_path" "${dmg_root}/codex-responses-api-proxy" rm -f "$dmg_path" hdiutil create \ @@ -476,7 +669,7 @@ jobs: exit 1 fi - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (dmg) uses: ./.github/actions/macos-code-sign with: @@ -490,121 +683,65 @@ jobs: apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - name: Stage artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | dest="dist/${{ matrix.target }}" mkdir -p "$dest" - for binary in ${{ matrix.binaries }}; do - cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" - if [[ "${{ matrix.target }}" == *linux* ]]; then - cp "target/${{ matrix.target }}/release/${binary}.sigstore" \ - "$dest/${binary}-${{ matrix.target }}.sigstore" + if [[ "${{ matrix.runner }}" == windows* ]]; then + cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe" + else + # For Android, we want the binary to be named just 'codex' in the archive. + if [[ "${{ matrix.target }}" == "aarch64-linux-android" ]]; then + cp target/${{ matrix.target }}/release/codex "$dest/codex" + else + cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" + fi + if [[ -f target/${{ matrix.target }}/release/codex-responses-api-proxy ]]; then + cp target/${{ matrix.target }}/release/codex-responses-api-proxy "$dest/codex-responses-api-proxy-${{ matrix.target }}" fi - done - - if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.bundle }}" == "primary" ]]; then - bundle_root="${RUNNER_TEMP}/codex-${{ matrix.target }}-bundle" - rm -rf "$bundle_root" - mkdir -p "$bundle_root/codex-resources" - cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex" - cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap" - chmod 0755 "$bundle_root/codex" "$bundle_root/codex-resources/bwrap" - tar -C "$bundle_root" -cf - codex codex-resources/bwrap | - zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst" fi - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" + if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.target }}" != *android* ]]; then + cp target/${{ matrix.target }}/release/codex.sigstore "$dest/codex-${{ matrix.target }}.sigstore" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.sigstore "$dest/codex-responses-api-proxy-${{ matrix.target }}.sigstore" fi - - name: Build Codex package archive - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "target/${TARGET}/release" \ - --archive-dir "dist/${TARGET}" - - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - aarch64-unknown-linux-musl) - platform_tag="manylinux_2_17_aarch64" - ;; - x86_64-unknown-linux-musl) - platform_tag="manylinux_2_17_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - # Do not install into the runner's system Python; macOS runners mark - # the Homebrew Python as externally managed under PEP 668. - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - stage_runtime_args=( - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" - stage-runtime - "$stage_dir" - "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/codex" - --codex-version "${GITHUB_REF_NAME}" - --platform-tag "$platform_tag" - ) - if [[ "${{ matrix.target }}" == *linux* ]]; then - # Keep bwrap in the runtime wheel so Linux sandbox fallback behavior - # matches the standalone release bundle on hosts without system bwrap. - stage_runtime_args+=( - --resource-binary - "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/bwrap" - ) + if [[ "${{ matrix.target }}" == *apple-darwin ]]; then + cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" fi - python3 "${stage_runtime_args[@]}" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error + - if: ${{ matrix.runner == 'windows-11-arm' }} + name: Install zstd + shell: powershell + run: choco install -y zstandard - name: Compress artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | # Path that contains the uncompressed binaries for the current # ${{ matrix.target }} dest="dist/${{ matrix.target }}" + repo_root=$PWD + + # We want to ship the raw Windows executables in the GitHub Release + # in addition to the compressed archives. Keep the originals for + # Windows targets; remove them elsewhere to limit the number of + # artifacts that end up in the GitHub Release. + keep_originals=false + if [[ "${{ matrix.runner }}" == windows* ]]; then + keep_originals=true + fi # For compatibility with environments that lack the `zstd` tool we - # additionally create a `.tar.gz` alongside every binary we publish. - # The end result is: + # additionally create a `.tar.gz` for all platforms and `.zip` for + # Windows alongside every single binary that we publish. The end result is: # codex-.zst (existing) # codex-.tar.gz (new) + # codex-.zip (only for Windows) # 1. Produce a .tar.gz for every file in the directory *before* we # run `zstd --rm`, because that flag deletes the original files. @@ -612,7 +749,7 @@ jobs: base="$(basename "$f")" # Skip files that are already archives (shouldn't happen, but be # safe). - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then + if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then continue fi @@ -622,344 +759,239 @@ jobs: fi # Create per-binary tar.gz - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - - # Also create .zst and remove the uncompressed binaries to keep - # non-Windows artifact directories small. - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - with: - name: ${{ matrix.artifact_name }} - # Upload the per-binary .zst files, .tar.gz equivalents, and any - # prebuilt archives staged above. - path: | - codex-rs/dist/${{ matrix.target }}/* - - stage-signed-macos: - if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode == 'promote_signed' }} - needs: tag-check - name: Stage signed macOS handoff - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: macos-15-xlarge - timeout-minutes: 30 - permissions: - contents: read - defaults: - run: - working-directory: codex-rs + # For Android, we want the archive to have the target suffix even though the binary is just 'codex'. + archive_name="${base}" + if [[ "${{ matrix.target }}" == "aarch64-linux-android" && "${base}" == "codex" ]]; then + archive_name="codex-${{ matrix.target }}" + fi + tar -C "$dest" -czf "$dest/${archive_name}.tar.gz" "$base" + + # Create zip archive for Windows binaries + # Must run from inside the dest dir so 7z won't + # embed the directory path inside the zip. + if [[ "${{ matrix.runner }}" == windows* ]]; then + if [[ "$base" == "codex-${{ matrix.target }}.exe" ]]; then + # Bundle the sandbox helper binaries into the main codex zip so + # WinGet installs include the required helpers next to codex.exe. + # Fall back to the single-binary zip if the helpers are missing + # to avoid breaking releases. + bundle_dir="$(mktemp -d)" + runner_src="$dest/codex-command-runner-${{ matrix.target }}.exe" + setup_src="$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + if [[ -f "$runner_src" && -f "$setup_src" ]]; then + cp "$dest/$base" "$bundle_dir/$base" + cp "$runner_src" "$bundle_dir/codex-command-runner.exe" + cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" + # Use an absolute path so bundle zips land in the real dist + # dir even when 7z runs from a temp directory. + (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) + else + echo "warning: missing sandbox binaries; falling back to single-binary zip" + echo "warning: expected $runner_src and $setup_src" + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + rm -rf "$bundle_dir" + else + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + fi - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" + # Also create .zst (existing behaviour) *and* remove the original + # uncompressed binary to keep the directory small. + zstd_args=(-T0 -19) + if [[ "${keep_originals}" == false ]]; then + zstd_args+=(--rm) + fi - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + if [[ "${archive_name}" != "${base}" ]]; then + zstd "${zstd_args[@]}" "$dest/$base" -o "$dest/${archive_name}.zst" + else + zstd "${zstd_args[@]}" "$dest/$base" + fi + done - - name: Download signed macOS handoff + - name: Add Termux release metadata + if: ${{ (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && matrix.target == 'aarch64-linux-android' }} shell: bash - env: - GH_TOKEN: ${{ github.token }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - SIGNED_MACOS_SHA256: ${{ inputs.signed_macos_sha256 }} run: | set -euo pipefail - - download_dir="${RUNNER_TEMP}/signed-macos-download" - handoff_dir="${RUNNER_TEMP}/signed-macos-handoff" - rm -rf "$download_dir" "$handoff_dir" - mkdir -p "$download_dir" "$handoff_dir" - - gh release download "$GITHUB_REF_NAME" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern "$SIGNED_MACOS_ASSET" \ - --dir "$download_dir" - - asset_count="$(find "$download_dir" -maxdepth 1 -type f | wc -l | tr -d '[:space:]')" - if [[ "$asset_count" != "1" ]]; then - echo "Expected exactly one signed macOS handoff asset named ${SIGNED_MACOS_ASSET}; found ${asset_count}" - find "$download_dir" -maxdepth 1 -type f -print - exit 1 - fi - - asset_path="$(find "$download_dir" -maxdepth 1 -type f -print -quit)" - if [[ -n "${SIGNED_MACOS_SHA256}" ]]; then - expected_sha="$(printf '%s' "$SIGNED_MACOS_SHA256" | tr '[:upper:]' '[:lower:]')" - actual_sha="$(shasum -a 256 "$asset_path" | awk '{print $1}')" - if [[ "$actual_sha" != "$expected_sha" ]]; then - echo "signed_macos_sha256 mismatch for ${SIGNED_MACOS_ASSET}" - echo "expected: ${expected_sha}" - echo "actual: ${actual_sha}" - exit 1 - fi + if [[ ! -f "${GITHUB_WORKSPACE}/.github/termux-release.json" ]]; then + echo "No Termux release metadata found; skipping metadata attachment." + exit 0 fi + dest="dist/${{ matrix.target }}" + cp "${GITHUB_WORKSPACE}/.github/termux-release.json" "${dest}/termux-release.json" + ( + cd "${dest}" + sha256sum ./* > SHA256SUMS + ) - asset_name="$(basename "$asset_path")" - case "$asset_name" in - *.tar.zst) - zstd -dc "$asset_path" | tar -C "$handoff_dir" -xf - - ;; - *.tar.gz|*.tgz) - tar -C "$handoff_dir" -xzf "$asset_path" - ;; - *.zip) - ditto -x -k "$asset_path" "$handoff_dir" - ;; - *) - echo "Unsupported signed macOS handoff archive format: ${asset_name}" - exit 1 - ;; - esac - - echo "SIGNED_MACOS_HANDOFF_DIR=$handoff_dir" >> "$GITHUB_ENV" + - id: upload-artifact + uses: actions/upload-artifact@v6 + with: + name: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + # Upload the per-binary .zst files as well as the new .tar.gz + # equivalents we generated in the previous step. + path: | + codex-rs/dist/${{ matrix.target }}/* - - name: Stage signed macOS artifacts + - name: Publish Termux artifact notification + if: ${{ github.event_name == 'pull_request' && startsWith(github.base_ref, 'release/') && matrix.target == 'aarch64-linux-android' }} shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_ID: ${{ steps.upload-artifact.outputs.artifact-id }} + ARTIFACT_NAME: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + PR_NUMBER: ${{ github.event.pull_request.number }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | set -euo pipefail - - target="${{ matrix.target }}" - artifact_name="${{ matrix.artifact_name }}" - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${target}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - fi - if [[ ! -d "$source_dir" ]]; then - echo "Signed macOS handoff is missing ${artifact_name}/" - echo "Expected either:" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${target}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - find "$SIGNED_MACOS_HANDOFF_DIR" -maxdepth 3 -type f -print + if [[ -z "${ARTIFACT_ID}" ]]; then + echo "upload-artifact did not return an artifact id" >&2 exit 1 fi - dest="dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - source_path="${source_dir}/${binary}" - if [[ ! -f "$source_path" ]]; then - source_path="${source_dir}/${binary}-${target}" - fi - if [[ ! -f "$source_path" ]]; then - echo "Signed macOS handoff is missing ${binary} for ${artifact_name}" - exit 1 - fi - - release_path="${dest}/${binary}-${target}" - ditto "$source_path" "$release_path" - chmod 0755 "$release_path" - codesign --verify --strict --verbose=2 "$release_path" - done - - # DMG staging is disabled for signed promotion because we no longer - # distribute DMGs from this release path. Keep the branch here so the - # handoff can opt back in by flipping matrix.build_dmg if needed. - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - dmg_name="codex-${target}.dmg" - dmg_source="${source_dir}/${dmg_name}" - if [[ ! -f "$dmg_source" ]]; then - echo "Signed macOS handoff is missing ${dmg_name} for ${artifact_name}" - exit 1 - fi - - codesign --verify --strict --verbose=2 "$dmg_source" - xcrun stapler validate "$dmg_source" - cp "$dmg_source" "$dest/$dmg_name" + marker="" + artifact_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts/${ARTIFACT_ID}" + run_url="${GH_WORKFLOW_URL:-https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + body_path="${RUNNER_TEMP}/termux-artifact-comment.md" + { + echo "${marker}" + echo "Termux Android artifact ready for testing:" + echo + echo "- Artifact: [${ARTIFACT_NAME}](${artifact_url})" + echo "- Workflow run: [${GITHUB_RUN_ID}](${run_url})" + echo + echo "You must be signed in to GitHub with repository access to download Actions artifacts." + } > "${body_path}" + + existing_comment_id="$( + gh pr view "${PR_NUMBER}" \ + --repo "${GITHUB_REPOSITORY}" \ + --json comments \ + --jq ".comments | map(select(.author.is_bot == true and (.body | contains(\"${marker}\")))) | .[-1].id // \"\"" + )" + if [[ -n "${existing_comment_id}" ]]; then + gh api graphql \ + -f query=' + mutation($id: ID!, $body: String!) { + updateIssueComment(input: {id: $id, body: $body}) { + issueComment { + id + } + } + } + ' \ + -f id="${existing_comment_id}" \ + -f body="$(cat "${body_path}")" \ + >/dev/null + else + gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body-file "${body_path}" fi - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - python3 \ - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \ - stage-runtime \ - "$stage_dir" \ - "${GITHUB_WORKSPACE}/codex-rs/dist/${{ matrix.target }}/codex-${{ matrix.target }}" \ - --codex-version "${GITHUB_REF_NAME}" \ - --platform-tag "$platform_tag" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - - name: Build Codex package archive - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "dist/${TARGET}" \ - --archive-dir "dist/${TARGET}" \ - --target-suffixed-entrypoint - - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error - - - name: Compress artifacts - shell: bash - run: | - set -euo pipefail - - dest="dist/${{ matrix.target }}" - for f in "$dest"/*; do - base="$(basename "$f")" - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then - continue + gh label create binary-ready \ + --repo "${GITHUB_REPOSITORY}" \ + --color 0e8a16 \ + --description "Android binary is ready for testing" \ + --force + gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --add-label binary-ready + + if [[ -n "${SLACK_WEBHOOK_URL:-}" ]]; then + slack_payload_path="${RUNNER_TEMP}/termux-artifact-slack.json" + pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}" + jq -n \ + --arg artifact_name "${ARTIFACT_NAME}" \ + --arg artifact_url "${artifact_url}" \ + --arg pr_number "${PR_NUMBER}" \ + --arg pr_url "${pr_url}" \ + --arg run_id "${GITHUB_RUN_ID}" \ + --arg run_url "${run_url}" \ + '{ + text: ("Termux Android artifact ready for testing: " + $artifact_url), + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: ("*Termux Android artifact ready for testing*\n" + $artifact_url) + } + }, + { + type: "section", + fields: [ + { + type: "mrkdwn", + text: ("*Artifact:*\n<" + $artifact_url + "|" + $artifact_name + ">") + }, + { + type: "mrkdwn", + text: ("*Pull request:*\n<" + $pr_url + "|#" + $pr_number + ">") + }, + { + type: "mrkdwn", + text: ("*Workflow run:*\n<" + $run_url + "|" + $run_id + ">") + } + ] + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: "You must be signed in to GitHub with repository access to download Actions artifacts." + } + ] + } + ] + }' > "${slack_payload_path}" + + if ! curl -fsS \ + -X POST \ + -H "Content-type: application/json" \ + --data @"${slack_payload_path}" \ + "${SLACK_WEBHOOK_URL}"; then + echo "::warning::Failed to send Slack artifact notification" fi + fi - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }} - path: | - codex-rs/dist/${{ matrix.target }}/* - - build-windows: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - needs: tag-check - uses: ./.github/workflows/rust-release-windows.yml - with: - release-lto: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }} - secrets: inherit - - argument-comment-lint-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: argument-comment-lint release assets + shell-tool-mcp: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'openai' + name: shell-tool-mcp needs: tag-check - uses: ./.github/workflows/rust-release-argument-comment-lint.yml + permissions: + contents: read + id-token: write + uses: ./.github/workflows/shell-tool-mcp.yml with: + release-tag: ${{ github.ref_name }} publish: true - - zsh-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: zsh release assets - needs: tag-check - uses: ./.github/workflows/rust-release-zsh.yml + secrets: inherit release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') needs: - - tag-check - build - - stage-signed-macos - - build-windows - - argument-comment-lint-release-assets - - zsh-release-assets - if: >- - ${{ - always() && - needs.tag-check.result == 'success' && - ( - ( - github.event_name == 'workflow_dispatch' && - inputs.release_mode == 'promote_signed' && - needs.stage-signed-macos.result == 'success' && - needs.build.result == 'skipped' && - needs.build-windows.result == 'skipped' && - needs.argument-comment-lint-release-assets.result == 'skipped' && - needs.zsh-release-assets.result == 'skipped' - ) || - ( - (github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed') && - needs.build.result == 'success' && - needs.stage-signed-macos.result == 'skipped' && - needs.build-windows.result == 'success' && - needs.argument-comment-lint-release-assets.result == 'success' && - needs.zsh-release-assets.result == 'success' - ) - ) - }} + - shell-tool-mcp name: release runs-on: ubuntu-latest permissions: contents: write actions: read - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode == 'promote_signed' }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} outputs: version: ${{ steps.release_name.outputs.name }} tag: ${{ github.ref_name }} - sign_macos: ${{ steps.release_mode.outputs.sign_macos }} should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }} npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }} - should_publish_python_runtime: ${{ steps.python_runtime_publish_settings.outputs.should_publish }} steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + uses: actions/checkout@v6 - - name: Define release mode - id: release_mode - run: | - echo "release_mode=${RELEASE_MODE}" >> "$GITHUB_OUTPUT" - echo "sign_macos=${SIGN_MACOS}" >> "$GITHUB_OUTPUT" + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main - name: Generate release notes from tag commit message id: release_notes @@ -981,138 +1013,18 @@ jobs: echo "path=${notes_path}" >> "${GITHUB_OUTPUT}" - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@v7 with: path: dist - - name: Validate unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - run_summary="$(gh run view "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --json conclusion,event,headBranch,headSha,status,workflowName,url \ - --jq '[.workflowName, .event, .headBranch, .headSha, .status, .conclusion, .url] | @tsv')" - IFS=$'\t' read -r workflow_name event head_branch head_sha status conclusion run_url <<< "$run_summary" - expected_head_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")" - - if [[ "$workflow_name" != "$GITHUB_WORKFLOW" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is for workflow '${workflow_name}', expected '${GITHUB_WORKFLOW}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$event" != "workflow_dispatch" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} was triggered by '${event}', expected 'workflow_dispatch'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_branch" != "$GITHUB_REF_NAME" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used ref '${head_branch}', expected '${GITHUB_REF_NAME}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_sha" != "$expected_head_sha" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used head SHA '${head_sha}', expected '${expected_head_sha}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is ${status}/${conclusion}, expected completed/success" - echo "Run URL: ${run_url}" - exit 1 - fi - - - name: Download artifacts from unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - gh run download "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --dir dist - - - name: Remove unsigned macOS staging artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - run: | - set -euo pipefail - find dist -mindepth 1 -maxdepth 1 -type d \ - -name '*-apple-darwin*-unsigned' \ - -exec rm -rf {} + - - - name: Re-upload promoted Linux x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-unknown-linux-musl - path: dist/x86_64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Linux arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-unknown-linux-musl - path: dist/aarch64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Windows x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-pc-windows-msvc - path: dist/x86_64-pc-windows-msvc/* - if-no-files-found: error - - - name: Re-upload promoted Windows arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-pc-windows-msvc - path: dist/aarch64-pc-windows-msvc/* - if-no-files-found: error - - name: List run: ls -R dist/ - - name: Prune artifacts excluded from unsigned macOS release - if: ${{ env.SIGN_MACOS == 'false' }} - run: | - find dist -mindepth 1 -maxdepth 1 -type d \ - ! -name '*-apple-darwin*-unsigned' \ - ! -name 'aarch64-unknown-linux-musl' \ - ! -name 'aarch64-unknown-linux-musl-app-server' \ - ! -name 'x86_64-unknown-linux-musl' \ - ! -name 'x86_64-unknown-linux-musl-app-server' \ - ! -name 'aarch64-pc-windows-msvc' \ - ! -name 'x86_64-pc-windows-msvc' \ - -exec rm -rf {} + - - if ! find dist -type f -name '*-apple-darwin*-unsigned*' | grep -q .; then - echo "No unsigned macOS artifacts found in downloaded workflow artifacts." - exit 1 - fi - + # This is a temporary fix: we should modify shell-tool-mcp.yml so these + # files do not end up in dist/ in the first place. - name: Delete entries from dist/ that should not go in the release run: | - rm -rf dist/windows-binaries* - # cargo-timing.html appears under multiple target-specific directories. - # If included in files: dist/**, release upload races on duplicate - # asset names and can fail with 404s. - find dist -type f -name 'cargo-timing.html' -delete - # Keep package-builder sidecar archives as workflow artifacts only - # until distribution channels are ready to consume them. - find dist -type f \ - \( -name 'codex-package-*' -o -name 'codex-app-server-package-*' \) \ - -delete - find dist -type d -empty -delete + rm -rf dist/shell-tool-mcp* ls -R dist/ @@ -1136,12 +1048,6 @@ jobs: set -euo pipefail version="${VERSION}" - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - echo "npm_tag=" >> "$GITHUB_OUTPUT" - exit 0 - fi - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "should_publish=true" >> "$GITHUB_OUTPUT" echo "npm_tag=" >> "$GITHUB_OUTPUT" @@ -1153,132 +1059,55 @@ jobs: echo "npm_tag=" >> "$GITHUB_OUTPUT" fi - - name: Determine Python runtime publish settings - id: python_runtime_publish_settings - env: - VERSION: ${{ steps.release_name.outputs.name }} - run: | - set -euo pipefail - version="${VERSION}" - - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - else - echo "should_publish=false" >> "$GITHUB_OUTPUT" - fi - - name: Setup pnpm - if: ${{ env.SIGN_MACOS == 'true' }} - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + uses: pnpm/action-setup@v4 with: run_install: false - name: Setup Node.js for npm packaging - if: ${{ env.SIGN_MACOS == 'true' }} - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: node-version: 22 - name: Install dependencies - if: ${{ env.SIGN_MACOS == 'true' }} run: pnpm install --frozen-lockfile # stage_npm_packages.py requires DotSlash when staging releases. - - uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2 + - uses: facebook/install-dotslash@v2 - name: Stage npm packages - if: ${{ env.SIGN_MACOS == 'true' }} + if: github.repository_owner == 'openai' env: GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ steps.release_name.outputs.name }} run: | - workflow_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" ./scripts/stage_npm_packages.py \ - --release-version "$RELEASE_VERSION" \ - --workflow-url "$workflow_url" \ + --release-version "${{ steps.release_name.outputs.name }}" \ --package codex \ --package codex-responses-api-proxy \ --package codex-sdk - - name: Stage installer scripts - if: ${{ env.SIGN_MACOS == 'true' }} - run: | - cp scripts/install/install.sh dist/install.sh - cp scripts/install/install.ps1 dist/install.ps1 - - name: Create GitHub Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@v2 with: name: ${{ steps.release_name.outputs.name }} tag_name: ${{ github.ref_name }} body_path: ${{ steps.release_notes.outputs.path }} files: dist/** - overwrite_files: true - make_latest: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} # Mark as prerelease only when the version has a suffix after x.y.z # (e.g. -alpha, -beta). Otherwise publish a normal release. prerelease: ${{ contains(steps.release_name.outputs.name, '-') }} - - name: Clean up signed promotion handoff assets - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" --jq '.id')" - gh api --paginate "repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets" \ - --jq '.[] | [.id, .name] | @tsv' | - while IFS=$'\t' read -r asset_id asset_name; do - if [[ -z "$asset_id" || -z "$asset_name" ]]; then - continue - fi - - delete_asset=false - if [[ "$asset_name" == *unsigned* || "$asset_name" == "$SIGNED_MACOS_ASSET" ]]; then - delete_asset=true - fi - - if [[ "$delete_asset" == "true" ]]; then - echo "Deleting release asset ${asset_name}" - gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" - fi - done - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 + - if: github.repository_owner == 'openai' + uses: facebook/dotslash-publish-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag: ${{ github.ref_name }} config: .github/dotslash-config.json - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-zsh-config.json - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-argument-comment-lint-config.json - - name: Trigger developers.openai.com deploy # Only trigger the deploy if the release is not a pre-release. # The deploy is used to update the developers.openai.com website with the new config schema json file. - if: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} + if: ${{ !contains(steps.release_name.outputs.name, '-') && github.repository_owner == 'openai' }} continue-on-error: true env: DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }} @@ -1293,15 +1122,7 @@ jobs: # npm docs: https://docs.npmjs.com/trusted-publishers publish-npm: # Publish to npm for stable releases and alpha pre-releases with numeric suffixes. - # promote_signed intentionally skips build jobs that are ancestors of release; - # include the !cancelled() status function so Actions does not apply its implicit - # success() check to the whole dependency chain before evaluating release outputs. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_npm == 'true' - }} + if: ${{ needs.release.outputs.should_publish_npm == 'true' && github.repository_owner == 'openai' }} name: publish-npm needs: release runs-on: ubuntu-latest @@ -1311,37 +1132,36 @@ jobs: steps: - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: - # Node 24 bundles npm >= 11.5.1, which trusted publishing requires. - node-version: 24 + node-version: 22 registry-url: "https://registry.npmjs.org" scope: "@openai" + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + - name: Download npm tarballs from release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} run: | set -euo pipefail - version="$RELEASE_VERSION" - tag="$RELEASE_TAG" + version="${{ needs.release.outputs.version }}" + tag="${{ needs.release.outputs.tag }}" mkdir -p dist/npm - patterns=( - "codex-npm-${version}.tgz" - "codex-npm-linux-*-${version}.tgz" - "codex-npm-darwin-*-${version}.tgz" - "codex-npm-win32-*-${version}.tgz" - "codex-responses-api-proxy-npm-${version}.tgz" - "codex-sdk-npm-${version}.tgz" - ) - for pattern in "${patterns[@]}"; do - gh release download "$tag" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "$pattern" \ - --dir dist/npm - done + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-responses-api-proxy-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-sdk-npm-${version}.tgz" \ + --dir dist/npm # No NODE_AUTH_TOKEN needed because we use OIDC. - name: Publish to npm @@ -1350,193 +1170,23 @@ jobs: NPM_TAG: ${{ needs.release.outputs.npm_tag }} run: | set -euo pipefail - prefix="" + tag_args=() if [[ -n "${NPM_TAG}" ]]; then - prefix="${NPM_TAG}-" + tag_args+=(--tag "${NPM_TAG}") fi - root_tarball="dist/npm/codex-npm-${VERSION}.tgz" - sdk_tarball="dist/npm/codex-sdk-npm-${VERSION}.tgz" - # Keep this list in sync with CODEX_PLATFORM_PACKAGES in - # codex-cli/scripts/build_npm_package.py. The root wrapper advances - # @openai/codex@latest as soon as it publishes, so every platform - # package it aliases must already exist in the registry first. - platform_tarballs=( - "dist/npm/codex-npm-linux-x64-${VERSION}.tgz" - "dist/npm/codex-npm-linux-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-x64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-x64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-arm64-${VERSION}.tgz" - ) - - for required_tarball in "${platform_tarballs[@]}" "${root_tarball}"; do - if [[ ! -f "${required_tarball}" ]]; then - echo "Missing npm tarball: ${required_tarball}" - exit 1 - fi - done - - shopt -s nullglob - other_tarballs=() - for tarball in dist/npm/*-"${VERSION}".tgz; do - if [[ "${tarball}" == "${root_tarball}" || "${tarball}" == "${sdk_tarball}" ]]; then - continue - fi - - is_platform_tarball=false - for platform_tarball in "${platform_tarballs[@]}"; do - if [[ "${tarball}" == "${platform_tarball}" ]]; then - is_platform_tarball=true - break - fi - done - if [[ "${is_platform_tarball}" == true ]]; then - continue - fi - - other_tarballs+=("${tarball}") - done - - # Publish the platform packages before the root CLI wrapper. The root - # wrapper advances @openai/codex@latest, so it should only publish - # after the optional dependency versions it references exist. tarballs=( - "${platform_tarballs[@]}" - "${other_tarballs[@]}" - "${root_tarball}" + "codex-npm-${VERSION}.tgz" + "codex-responses-api-proxy-npm-${VERSION}.tgz" + "codex-sdk-npm-${VERSION}.tgz" ) - if [[ -f "${sdk_tarball}" ]]; then - tarballs+=("${sdk_tarball}") - fi for tarball in "${tarballs[@]}"; do - filename="$(basename "${tarball}")" - tag="" - - case "${filename}" in - codex-npm-linux-*-"${VERSION}".tgz|codex-npm-darwin-*-"${VERSION}".tgz|codex-npm-win32-*-"${VERSION}".tgz) - platform="${filename#codex-npm-}" - platform="${platform%-${VERSION}.tgz}" - tag="${prefix}${platform}" - ;; - codex-npm-"${VERSION}".tgz|codex-responses-api-proxy-npm-"${VERSION}".tgz|codex-sdk-npm-"${VERSION}".tgz) - tag="${NPM_TAG}" - ;; - *) - echo "Unexpected npm tarball: ${filename}" - exit 1 - ;; - esac - - publish_cmd=(npm publish "${GITHUB_WORKSPACE}/${tarball}") - if [[ -n "${tag}" ]]; then - publish_cmd+=(--tag "${tag}") - fi - - echo "+ ${publish_cmd[*]}" - set +e - publish_output="$("${publish_cmd[@]}" 2>&1)" - publish_status=$? - set -e - - echo "${publish_output}" - if [[ ${publish_status} -eq 0 ]]; then - continue - fi - - if grep -qiE "previously published|cannot publish over|version already exists" <<< "${publish_output}"; then - echo "Skipping already-published package version for ${filename}" - continue - fi - - exit "${publish_status}" + npm publish "${GITHUB_WORKSPACE}/dist/npm/${tarball}" "${tag_args[@]}" done - # Publish the platform-specific Python runtime wheels using PyPI trusted publishing. - # PyPI project configuration must trust this workflow and job. Keep this - # non-blocking while the Python runtime publishing path is new; failures still - # need release follow-up, but should not invalidate the Rust release itself. - publish-python-runtime: - # Publish to PyPI for stable releases and alpha pre-releases with numeric suffixes. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_python_runtime == 'true' - }} - name: publish-python-runtime - needs: release - runs-on: ubuntu-latest - continue-on-error: true - environment: pypi - permissions: - id-token: write # Required for PyPI trusted publishing. - contents: read - - steps: - - name: Download Python runtime wheels from release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} - run: | - set -euo pipefail - python_version="$RELEASE_VERSION" - python_version="${python_version/-alpha./a}" - python_version="${python_version/-beta./b}" - python_version="${python_version/-rc./rc}" - - mkdir -p dist/python-runtime - gh release download "$RELEASE_TAG" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "openai_codex_cli_bin-${python_version}-*.whl" \ - --dir dist/python-runtime - ls -lh dist/python-runtime - - - name: Publish Python runtime wheels to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - with: - packages-dir: dist/python-runtime - skip-existing: true - - winget: - name: winget - needs: release - # Only publish stable/mainline releases to WinGet; pre-releases include a - # '-' in the semver string (e.g., 1.2.3-alpha.1). - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' && - !contains(needs.release.outputs.version, '-') - }} - # This job only invokes a GitHub Action to open/update the winget-pkgs PR; - # it does not execute Windows-only tooling, so Linux is sufficient. - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Publish to WinGet - uses: vedantmgoyal9/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5 - with: - identifier: OpenAI.Codex - version: ${{ needs.release.outputs.version }} - release-tag: ${{ needs.release.outputs.tag }} - fork-user: openai-oss-forks - installers-regex: '^codex-(?:x86_64|aarch64)-pc-windows-msvc\.exe\.zip$' - token: ${{ secrets.WINGET_PUBLISH_PAT }} - update-branch: name: Update latest-alpha-cli branch - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' - }} permissions: contents: write needs: release diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml new file mode 100644 index 000000000000..66a76aa4d938 --- /dev/null +++ b/.github/workflows/shell-tool-mcp.yml @@ -0,0 +1,461 @@ +name: shell-tool-mcp + +on: + workflow_call: + inputs: + release-version: + description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v. + required: false + type: string + release-tag: + description: Tag name to use when downloading release artifacts (defaults to rust-v). + required: false + type: string + publish: + description: Whether to publish to npm when the version is releasable. + required: false + default: true + type: boolean + +env: + NODE_VERSION: 22 + +jobs: + metadata: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.compute.outputs.version }} + release_tag: ${{ steps.compute.outputs.release_tag }} + should_publish: ${{ steps.compute.outputs.should_publish }} + npm_tag: ${{ steps.compute.outputs.npm_tag }} + steps: + - name: Compute version and tags + id: compute + run: | + set -euo pipefail + + version="${{ inputs.release-version }}" + release_tag="${{ inputs.release-tag }}" + + if [[ -z "$version" ]]; then + if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then + version="${release_tag#rust-v}" + elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then + version="${GITHUB_REF_NAME#rust-v}" + release_tag="${GITHUB_REF_NAME}" + else + echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag." + exit 1 + fi + fi + + if [[ -z "$release_tag" ]]; then + release_tag="rust-v${version}" + fi + + npm_tag="" + should_publish="false" + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + should_publish="true" + elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then + should_publish="true" + npm_tag="alpha" + fi + + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" + echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT" + echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT" + + matrix-setup: + runs-on: ubuntu-latest + outputs: + rust-binaries: ${{ steps.compute.outputs.rust-binaries }} + bash-linux: ${{ steps.compute.outputs.bash-linux }} + bash-darwin: ${{ steps.compute.outputs.bash-darwin }} + steps: + - name: Compute matrices + id: compute + shell: bash + run: | + set -euo pipefail + is_openai="${{ github.repository_owner == 'openai' }}" + + # rust-binaries: always include x86_64-musl; conditionally include paid/fork runners + if [[ "$is_openai" == "true" ]]; then + rust='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","install_musl":true} + ]' + else + rust='[{"runner":"macos-latest","target":"aarch64-apple-darwin"}]' + fi + rust=$(echo "$rust" | jq -c '. + [{"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","install_musl":true}]') + echo "rust-binaries={\"include\":$rust}" >> "$GITHUB_OUTPUT" + + # bash-linux: always include x86_64 variants; add arm64 on openai + bash_linux='[ + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-24.04","image":"ubuntu:24.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-22.04","image":"ubuntu:22.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-12","image":"debian:12"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-11","image":"debian:11"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]' + if [[ "$is_openai" == "true" ]]; then + bash_linux=$(echo "$bash_linux" | jq -c '. + [ + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-24.04","image":"arm64v8/ubuntu:24.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-22.04","image":"arm64v8/ubuntu:22.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-20.04","image":"arm64v8/ubuntu:20.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-12","image":"arm64v8/debian:12"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-11","image":"arm64v8/debian:11"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]') + fi + echo "bash-linux={\"include\":$(echo "$bash_linux" | jq -c)}" >> "$GITHUB_OUTPUT" + + # bash-darwin: always include macos-14; add macos-15-xlarge on openai + bash_darwin='[{"runner":"macos-14","target":"aarch64-apple-darwin","variant":"macos-14"}]' + if [[ "$is_openai" == "true" ]]; then + bash_darwin=$(echo "$bash_darwin" | jq -c '. + [ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin","variant":"macos-15"} + ]') + fi + echo "bash-darwin={\"include\":$(echo "$bash_darwin" | jq -c)}" >> "$GITHUB_OUTPUT" + + rust-binaries: + name: Build Rust - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + defaults: + run: + working-directory: codex-rs + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.rust-binaries) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install UBSan runtime (musl) + if: ${{ matrix.install_musl }} + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -y + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 + fi + + - uses: dtolnay/rust-toolchain@1.93 + with: + targets: ${{ matrix.target }} + + - if: ${{ matrix.install_musl }} + name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.14.0 + + - if: ${{ matrix.install_musl }} + name: Install musl build dependencies + env: + TARGET: ${{ matrix.target }} + run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh" + + - if: ${{ matrix.install_musl }} + name: Configure rustc UBSan wrapper (musl host) + shell: bash + run: | + set -euo pipefail + ubsan="" + if command -v ldconfig >/dev/null 2>&1; then + ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')" + fi + wrapper_root="${RUNNER_TEMP:-/tmp}" + wrapper="${wrapper_root}/rustc-ubsan-wrapper" + cat > "${wrapper}" <> "$GITHUB_ENV" + echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV" + + - if: ${{ matrix.install_musl }} + name: Clear sanitizer flags (musl) + shell: bash + run: | + set -euo pipefail + # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. + echo "RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV" + # Override any runner-level Cargo config rustflags as well. + echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + + sanitize_flags() { + local input="$1" + input="${input//-fsanitize=undefined/}" + input="${input//-fno-sanitize-recover=undefined/}" + input="${input//-fno-sanitize-trap=undefined/}" + echo "$input" + } + + cflags="$(sanitize_flags "${CFLAGS-}")" + cxxflags="$(sanitize_flags "${CXXFLAGS-}")" + echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" + echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + + - name: Build exec server binaries + run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper + + - name: Stage exec server binaries + run: | + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}" + mkdir -p "$dest" + cp "target/${{ matrix.target }}/release/codex-exec-mcp-server" "$dest/" + cp "target/${{ matrix.target }}/release/codex-execve-wrapper" "$dest/" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-rust-${{ matrix.target }} + path: artifacts/** + if-no-files-found: error + + bash-linux: + name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + container: + image: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-linux) }} + steps: + - name: Install build prerequisites + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext + elif command -v dnf >/dev/null 2>&1; then + dnf install -y git gcc gcc-c++ make bison autoconf gettext + elif command -v yum >/dev/null 2>&1; then + yum install -y git gcc gcc-c++ make bison autoconf gettext + else + echo "Unsupported package manager in container" + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + bash-darwin: + name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-darwin) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + package: + name: Package npm module + needs: + - metadata + - rust-binaries + - bash-linux + - bash-darwin + runs-on: ubuntu-latest + env: + PACKAGE_VERSION: ${{ needs.metadata.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install JavaScript dependencies + run: pnpm install --frozen-lockfile + + - name: Build (shell-tool-mcp) + run: pnpm --filter @openai/codex-shell-tool-mcp run build + + - name: Download build artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts + + - name: Assemble staging directory + id: staging + shell: bash + run: | + set -euo pipefail + staging="${STAGING_DIR}" + mkdir -p "$staging" "$staging/vendor" + cp shell-tool-mcp/README.md "$staging/" + cp shell-tool-mcp/package.json "$staging/" + cp -R shell-tool-mcp/bin "$staging/" + + found_vendor="false" + shopt -s nullglob + for vendor_dir in artifacts/*/vendor; do + rsync -av "$vendor_dir/" "$staging/vendor/" + found_vendor="true" + done + if [[ "$found_vendor" == "false" ]]; then + echo "No vendor payloads were downloaded." + exit 1 + fi + + node - <<'NODE' + import fs from "node:fs"; + import path from "node:path"; + + const stagingDir = process.env.STAGING_DIR; + const version = process.env.PACKAGE_VERSION; + const pkgPath = path.join(stagingDir, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + pkg.version = version; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + NODE + + echo "dir=$staging" >> "$GITHUB_OUTPUT" + env: + STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp + + - name: Ensure binaries are executable + run: | + set -euo pipefail + staging="${{ steps.staging.outputs.dir }}" + chmod +x \ + "$staging"/vendor/*/codex-exec-mcp-server \ + "$staging"/vendor/*/codex-execve-wrapper \ + "$staging"/vendor/*/bash/*/bash + + - name: Create npm tarball + shell: bash + run: | + set -euo pipefail + mkdir -p dist/npm + staging="${{ steps.staging.outputs.dir }}" + pack_info=$(cd "$staging" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm") + filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);') + mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz" + + - uses: actions/upload-artifact@v6 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz + if-no-files-found: error + + publish: + name: Publish npm package + needs: + - metadata + - package + if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }} + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: https://registry.npmjs.org + scope: "@openai" + + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + + - name: Download npm tarball + uses: actions/download-artifact@v7 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm + + - name: Publish to npm + if: github.repository == 'openai/codex' + env: + NPM_TAG: ${{ needs.metadata.outputs.npm_tag }} + VERSION: ${{ needs.metadata.outputs.version }} + shell: bash + run: | + set -euo pipefail + tag_args=() + if [[ -n "${NPM_TAG}" ]]; then + tag_args+=(--tag "${NPM_TAG}") + fi + npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}" diff --git a/.github/workflows/termux-release-checkpoint.yml b/.github/workflows/termux-release-checkpoint.yml new file mode 100644 index 000000000000..0347e6f1b3d6 --- /dev/null +++ b/.github/workflows/termux-release-checkpoint.yml @@ -0,0 +1,103 @@ +name: termux-release-checkpoint + +on: + workflow_dispatch: + inputs: + source_branch: + description: "Release branch to checkpoint from, for example release/0.123.0" + required: false + type: string + default: "" + source_sha: + description: "Specific source commit SHA to checkpoint; defaults to the source branch tip" + required: false + type: string + default: "" + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + type: string + default: "wallentx/termux-target" + reviewer: + description: "GitHub username to request as reviewer" + required: false + type: string + default: "wallentx" + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-checkpoint-${{ inputs.source_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + checkpoint: + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + env: + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + REQUESTED_SOURCE_BRANCH: ${{ inputs.source_branch }} + REQUESTED_SOURCE_SHA: ${{ inputs.source_sha }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ inputs.source_branch || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + source_branch="${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${source_branch}" + + - name: Create checkpoint PR + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" diff --git a/.github/workflows/termux-release-deploy.yml b/.github/workflows/termux-release-deploy.yml new file mode 100644 index 000000000000..2071611dacc4 --- /dev/null +++ b/.github/workflows/termux-release-deploy.yml @@ -0,0 +1,243 @@ +name: termux-release-deploy + +on: + push: + branches: + - "release/**" + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to deploy, for example release/0.124.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to deploy. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged release PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged release PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + default: "wallentx/termux-target" + type: string + reviewer: + description: "GitHub username to request as reviewer on the checkpoint PR" + required: false + default: "wallentx" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-deploy-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + deploy: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' || !startsWith(github.event.head_commit.message, 'Seed Termux release automation') }} + permissions: + actions: read + contents: write + deployments: write + issues: write + pull-requests: write + env: + REQUESTED_RELEASE_BRANCH: ${{ inputs.release_branch }} + REQUESTED_RELEASE_SHA: ${{ inputs.release_sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + release_branch="${REQUESTED_RELEASE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${release_branch}" + + - name: Resolve release ref + id: release-ref + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: deploy + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Create deployment + if: steps.metadata.outputs.deploy == 'true' + id: deployment + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + deployment_id="$( + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments" \ + -f ref="${RELEASE_SHA}" \ + -f environment="termux-release" \ + -F auto_merge=false \ + -F required_contexts[] \ + -f description="Termux release deployment for ${TERMUX_TAG}" \ + --jq '.id' + )" + echo "id=${deployment_id}" >> "$GITHUB_OUTPUT" + + - name: Mark deployment in progress + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="in_progress" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Promoting Termux release artifact and preparing checkpoint PR" \ + >/dev/null + + - name: Locate merged pull request + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" + + - name: Ensure checkpoint PR + if: steps.metadata.outputs.deploy == 'true' + id: checkpoint + env: + SOURCE_BRANCH: ${{ steps.release-ref.outputs.branch }} + SOURCE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" + + - name: Mark deployment success + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="success" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment completed for ${TERMUX_TAG}" \ + >/dev/null + + - name: Mark deployment failure + if: failure() && steps.deployment.outputs.id != '' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="failure" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment failed" \ + >/dev/null diff --git a/.github/workflows/termux-release-promote.yml b/.github/workflows/termux-release-promote.yml new file mode 100644 index 000000000000..22c277c21a70 --- /dev/null +++ b/.github/workflows/termux-release-promote.yml @@ -0,0 +1,135 @@ +name: termux-release-promote + +on: + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to promote, for example release/0.122.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to promote. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-promote-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + promote: + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: write + pull-requests: read + env: + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Resolve release ref + id: release-ref + env: + INPUT_RELEASE_BRANCH: ${{ inputs.release_branch }} + INPUT_RELEASE_SHA: ${{ inputs.release_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: promote + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Locate merged pull request + if: steps.metadata.outputs.promote == 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.promote == 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.promote == 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 739f710f6111..bb02db818059 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -118,7 +118,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.133.0" +version = "0.134.0-alpha.2" # Track the edition for all workspace crates in one place. Individual # crates can still override this value, but keeping it here means new # crates created with `cargo new -w ...` automatically inherit the 2024 diff --git a/scripts/termux-configure-git.sh b/scripts/termux-configure-git.sh new file mode 100755 index 000000000000..fae3abcbab48 --- /dev/null +++ b/scripts/termux-configure-git.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Configure the explicit bot identity used by release automation and fetch any +# refs requested by the calling workflow. + +set -euo pipefail + +git config user.name "github-actions[bot]" +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + +while (($#)); do + case "$1" in + --origin) + shift + origin_refs=() + while (($#)) && [[ "$1" != --* ]]; do + origin_refs+=("$1") + shift + done + if ((${#origin_refs[@]})); then + git fetch --prune origin "${origin_refs[@]}" + fi + ;; + --upstream-tag) + if (($# < 3)); then + echo "--upstream-tag requires ." >&2 + exit 1 + fi + upstream_repo="$2" + upstream_tag="$3" + git remote add upstream "https://github.com/${upstream_repo}.git" 2>/dev/null || true + git fetch --prune --no-tags upstream "+refs/tags/${upstream_tag}:refs/tags/${upstream_tag}" + shift 3 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done diff --git a/scripts/termux-create-checkpoint-pr.sh b/scripts/termux-create-checkpoint-pr.sh new file mode 100755 index 000000000000..f315a5b91822 --- /dev/null +++ b/scripts/termux-create-checkpoint-pr.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/termux-release-paths.sh +source "${script_dir}/termux-release-paths.sh" + +source_branch="${SOURCE_BRANCH:-${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}}" +source_sha="${SOURCE_SHA:-${REQUESTED_SOURCE_SHA:-}}" +if [[ -z "${source_sha}" ]]; then + if [[ "${GITHUB_EVENT_NAME:-}" == "push" && "${source_branch}" == "${GITHUB_REF_NAME:-}" ]]; then + source_sha="${GITHUB_SHA}" + else + source_sha="$(git rev-parse "origin/${source_branch}")" + fi +fi + +if [[ -z "${DESTINATION_BRANCH:-}" ]]; then + echo "DESTINATION_BRANCH is required." >&2 + exit 1 +fi + +release_only_checkpoint_paths() { + printf '%s\n' "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +} + +resolve_source_version_conflicts() { + local path="$1" + local resolved_path + resolved_path="$(mktemp)" + + if ! awk ' + function normalize_versions(text) { + gsub(/version = "[^"]+"/, "version = \"\"", text) + return text + } + + BEGIN { + in_block = 0 + side = "" + ours = "" + theirs = "" + blocks = 0 + } + + /^<<<<<<< / { + if (in_block) { + exit 1 + } + in_block = 1 + side = "ours" + ours = "" + theirs = "" + blocks++ + next + } + + /^=======$/ && in_block { + side = "theirs" + next + } + + /^>>>>>>> / && in_block { + if (normalize_versions(ours) != normalize_versions(theirs)) { + exit 1 + } + printf "%s", theirs + in_block = 0 + side = "" + next + } + + { + if (!in_block) { + print + } else if (side == "ours") { + ours = ours $0 ORS + } else if (side == "theirs") { + theirs = theirs $0 ORS + } else { + exit 1 + } + } + + END { + if (in_block || blocks == 0) { + exit 1 + } + } + ' "${path}" > "${resolved_path}"; then + rm -f "${resolved_path}" + return 1 + fi + + cp "${resolved_path}" "${path}" + rm -f "${resolved_path}" +} + +short_sha="${source_sha:0:12}" +source_slug="${source_branch//\//_}" +dest_slug="${DESTINATION_BRANCH//\//_}" +checkpoint_branch="checkpoint/${dest_slug}_from_${source_slug}_${short_sha}" +pr_title="checkpoint: into ${DESTINATION_BRANCH} from ${source_branch} @ ${short_sha}" +merge_conflicted=false +conflict_summary="" + +existing_pr="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --head "${checkpoint_branch}" \ + --state all \ + --json number,state,mergedAt,url \ + --jq '[.[] | select(.state == "OPEN" or .mergedAt != null)] | .[0] // empty' +)" +if [[ -n "${existing_pr}" ]]; then + existing_url="$(jq -r '.url' <<< "${existing_pr}")" + existing_state="$(jq -r '.state' <<< "${existing_pr}")" + echo "Checkpoint PR already exists for ${checkpoint_branch}: ${existing_url} (${existing_state})." + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${existing_url}" >> "${GITHUB_OUTPUT}" + fi + exit 0 +fi + +git checkout -B "${checkpoint_branch}" "origin/${DESTINATION_BRANCH}" + +if ! git merge --no-ff --no-edit "${source_sha}"; then + mapfile -t conflicted_paths < <(git diff --name-only --diff-filter=U) + for conflicted_path in "${conflicted_paths[@]}"; do + if termux_is_checkpoint_release_only_path "${conflicted_path}"; then + echo "Auto-resolving release-only checkpoint conflict in ${conflicted_path} by keeping ${DESTINATION_BRANCH}." + if git cat-file -e "HEAD:${conflicted_path}" 2>/dev/null; then + git checkout --ours -- "${conflicted_path}" + git add "${conflicted_path}" + else + git rm -f --ignore-unmatch "${conflicted_path}" + fi + fi + done + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if ((${#remaining_conflicts[@]})); then + cargo_version_conflicts=true + for remaining_conflict in "${remaining_conflicts[@]}"; do + case "${remaining_conflict}" in + codex-rs/Cargo.toml|codex-rs/Cargo.lock) + ;; + *) + cargo_version_conflicts=false + ;; + esac + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + for remaining_conflict in "${remaining_conflicts[@]}"; do + if ! resolve_source_version_conflicts "${remaining_conflict}"; then + cargo_version_conflicts=false + break + fi + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + echo "Auto-resolving recurring Cargo version checkpoint conflicts by keeping ${source_branch} versions." + git add -- "${remaining_conflicts[@]}" + fi + fi + fi + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if [[ "${#remaining_conflicts[@]}" -eq 0 ]]; then + git commit --no-edit + else + merge_conflicted=true + conflict_summary="$( + printf '%s\n' "${remaining_conflicts[@]}" | awk '{ print "- `" $0 "`" }' + )" + echo "Automatic checkpoint merge failed; creating a manual-resolution PR instead." >&2 + if git rev-parse -q --verify MERGE_HEAD >/dev/null; then + git merge --abort + fi + git checkout -B "${checkpoint_branch}" "${source_sha}" + fi +fi + +if git cat-file -e "origin/${DESTINATION_BRANCH}:.github" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- .github + mapfile -t added_github_paths < <( + git diff --name-only --diff-filter=A "origin/${DESTINATION_BRANCH}" -- .github + ) + if ((${#added_github_paths[@]})); then + git rm -f --ignore-unmatch -- "${added_github_paths[@]}" + fi +else + git rm -r --ignore-unmatch .github +fi + +while IFS= read -r release_only_path; do + if git cat-file -e "origin/${DESTINATION_BRANCH}:${release_only_path}" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- "${release_only_path}" + else + git rm -f --ignore-unmatch -- "${release_only_path}" + fi +done < <(release_only_checkpoint_paths) + +if ! git diff --quiet || ! git diff --cached --quiet; then + if [[ -e .github || -L .github ]] || git ls-files --error-unmatch -- .github >/dev/null 2>&1; then + git add -A .github + fi + while IFS= read -r release_only_path; do + if [[ -e "${release_only_path}" || -L "${release_only_path}" ]] || git ls-files --error-unmatch -- "${release_only_path}" >/dev/null 2>&1; then + git add -A -- "${release_only_path}" + fi + done < <(release_only_checkpoint_paths) + if [[ "${merge_conflicted}" == "true" ]]; then + git commit -m "checkpoint: prepare ${source_branch} for ${DESTINATION_BRANCH}" + else + git commit --amend --no-edit + fi +fi + +if git diff --quiet "origin/${DESTINATION_BRANCH}" HEAD; then + echo "Checkpoint merge produced no destination changes after release-only files were restored." + exit 0 +fi + +git push --force-with-lease origin "${checkpoint_branch}" + +remaining="$( + git log --first-parent --pretty=format:%H "${source_sha}..origin/${source_branch}" | wc -w +)" + +body_path="${RUNNER_TEMP}/termux-checkpoint-pr.md" +{ + echo "## Termux release checkpoint" + echo + echo "- Source branch: \`${source_branch}\`" + echo "- Source hash: \`${source_sha}\`" + echo "- Destination branch: \`${DESTINATION_BRANCH}\`" + echo "- Remaining first-parent commits on source: ${remaining}" + echo + echo "This PR carries release-train conflict fixes and follow-up changes back into the reusable Termux patch branch." + if [[ "${merge_conflicted}" == "true" ]]; then + echo + echo "## Merge conflicts" + echo + echo "GitHub Actions could not create the checkpoint merge commit automatically, so this PR was created from the source branch state for manual conflict resolution." + echo + echo "Conflicted paths from the failed merge attempt:" + if [[ -n "${conflict_summary}" ]]; then + printf '%s\n' "${conflict_summary}" + else + echo "- Conflict details unavailable" + fi + fi + echo + echo "Release-only workflow files and metadata under \`.github\` were restored to the destination branch versions before opening this PR." +} > "${body_path}" + +pr_url="$( + gh pr create \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${DESTINATION_BRANCH}" \ + --head "${checkpoint_branch}" \ + --title "${pr_title}" \ + --body-file "${body_path}" +)" +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-reviewer "${REVIEWER}" || true +gh label create checkpoint --repo "${GITHUB_REPOSITORY}" --color c5def5 --description "Checkpoint merge" --force +gh label create termux-release --repo "${GITHUB_REPOSITORY}" --color 0e8a16 --description "Termux release automation" --force +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-label "checkpoint" --add-label "termux-release" + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${pr_url}" >> "${GITHUB_OUTPUT}" +fi diff --git a/scripts/termux-create-or-update-mirrored-release.sh b/scripts/termux-create-or-update-mirrored-release.sh new file mode 100755 index 000000000000..5787e894582b --- /dev/null +++ b/scripts/termux-create-or-update-mirrored-release.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +# Create a mirrored Termux release or repair an existing release that is missing +# the Android tarball. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" +: "${RUNNER_TEMP:?RUNNER_TEMP is required}" +: "${UPSTREAM_TAG:?UPSTREAM_TAG is required}" +: "${UPSTREAM_REPO:?UPSTREAM_REPO is required}" +: "${UPSTREAM_NAME?UPSTREAM_NAME is required}" +: "${TERMUX_TAG:?TERMUX_TAG is required}" +: "${UPSTREAM_PRERELEASE:?UPSTREAM_PRERELEASE is required}" +: "${UPSTREAM_HTML_URL?UPSTREAM_HTML_URL is required}" +: "${RELEASE_TRAIN?RELEASE_TRAIN is required}" +: "${RELEASE_EXISTS:?RELEASE_EXISTS is required}" +: "${PR_NUMBER:?PR_NUMBER is required}" +: "${HEAD_SHA:?HEAD_SHA is required}" +: "${RELEASE_SHA:?RELEASE_SHA is required}" + +promoted_dir="${PROMOTED_DIR:-promoted}" +asset_path="${promoted_dir}/codex-aarch64-linux-android.tar.gz" + +body_path="${RUNNER_TEMP}/release-body.md" +upstream_body_path="${RUNNER_TEMP}/upstream-release-body.md" +upstream_body_without_changelog_path="${RUNNER_TEMP}/upstream-release-body-without-changelog.md" +if gh release view "${UPSTREAM_TAG}" \ + --repo "${UPSTREAM_REPO}" \ + --json body \ + --jq '.body // ""' > "${upstream_body_path}"; then + awk ' + function heading_level(line, text) { + if (match(line, /^(#{1,6})[[:space:]]+(.+)$/, parts)) { + text = tolower(parts[2]) + sub(/[[:space:]]+#+[[:space:]]*$/, "", text) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", text) + if (text == "changelog" || text == "change log") { + return length(parts[1]) + } + } + return 0 + } + + { + if (!skip) { + level = heading_level($0) + if (level > 0) { + skip = 1 + skip_level = level + next + } + print + next + } + + if (match($0, /^(#{1,6})[[:space:]]+/, parts) && length(parts[1]) <= skip_level) { + skip = 0 + print + } + } + ' "${upstream_body_path}" > "${upstream_body_without_changelog_path}" +else + echo "::warning title=Upstream release notes unavailable::Could not read ${UPSTREAM_REPO} release ${UPSTREAM_TAG}." + : > "${upstream_body_without_changelog_path}" +fi + +{ + echo "Termux Android build for ${UPSTREAM_TAG}." + echo + echo "- Upstream release: ${UPSTREAM_HTML_URL}" + echo "- Release train: \`${RELEASE_TRAIN}\`" + echo "- Promoted PR: #${PR_NUMBER}" + echo "- Promoted PR head SHA: \`${HEAD_SHA}\`" +} > "${body_path}" +if [[ -s "${upstream_body_without_changelog_path}" ]]; then + { + echo + echo "## Upstream release notes" + echo + cat "${upstream_body_without_changelog_path}" + } >> "${body_path}" +fi + +release_title="${UPSTREAM_NAME}" +if [[ -z "${release_title}" || "${release_title}" == "null" ]]; then + release_title="${TERMUX_TAG}" +fi + +if [[ "${RELEASE_EXISTS}" == "true" ]]; then + gh release upload \ + "${TERMUX_TAG}" \ + "${asset_path}#codex-termux" \ + --repo "${GITHUB_REPOSITORY}" \ + --clobber + exit 0 +fi + +release_args=( + gh release create "${TERMUX_TAG}" + --repo "${GITHUB_REPOSITORY}" + --target "${RELEASE_SHA}" + --title "${release_title}" + --notes-file "${body_path}" +) +if [[ "${UPSTREAM_PRERELEASE}" == "true" ]]; then + release_args+=(--prerelease) +fi +release_args+=("${asset_path}#codex-termux") +"${release_args[@]}" diff --git a/scripts/termux-download-release-artifact.sh b/scripts/termux-download-release-artifact.sh new file mode 100755 index 000000000000..2acdba9025a0 --- /dev/null +++ b/scripts/termux-download-release-artifact.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${HEAD_SHA:-}" ]]; then + echo "HEAD_SHA is required." >&2 + exit 1 +fi + +if [[ -z "${PR_ARTIFACT_NAME:-}" ]]; then + echo "PR_ARTIFACT_NAME is required." >&2 + exit 1 +fi + +promoted_dir="${PROMOTED_DIR:-promoted}" + +find_run_id() { + local event="$1" + gh run list \ + --repo "${GITHUB_REPOSITORY}" \ + --workflow rust-release.yml \ + --event "${event}" \ + --status success \ + --commit "${HEAD_SHA}" \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId // empty' +} + +find_artifact_run_id() { + local artifact_name="$1" + + gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=${artifact_name}" \ + | jq -rs \ + --arg artifact_name "${artifact_name}" \ + --arg head_sha "${HEAD_SHA}" \ + ' + [ + .[].artifacts[] + | select(.name == $artifact_name) + | select(.expired == false) + | select(.workflow_run.head_sha == $head_sha) + ] + | sort_by(.created_at) + | reverse + | .[0].workflow_run.id // empty + ' +} + +artifact_name="${PR_ARTIFACT_NAME}" +run_id="$(find_artifact_run_id "${artifact_name}")" +mkdir -p "${promoted_dir}" +if [[ -n "${run_id}" ]] && gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}"; then + : +else + artifact_name="aarch64-linux-android" + run_id="$(find_run_id workflow_dispatch)" + if [[ -z "${run_id}" ]]; then + echo "No successful rust-release run found for ${HEAD_SHA}" >&2 + exit 1 + fi + gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}" +fi + +ls -la "${promoted_dir}" +if [[ -f "${promoted_dir}/SHA256SUMS" ]]; then + (cd "${promoted_dir}" && sha256sum -c SHA256SUMS) +fi +if [[ ! -f "${promoted_dir}/codex-aarch64-linux-android.tar.gz" ]]; then + echo "Expected ${promoted_dir}/codex-aarch64-linux-android.tar.gz in the downloaded artifact." >&2 + exit 1 +fi diff --git a/scripts/termux-find-release-pr.sh b/scripts/termux-find-release-pr.sh new file mode 100755 index 000000000000..3e35fa42eabe --- /dev/null +++ b/scripts/termux-find-release-pr.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -n "${INPUT_PR_NUMBER:-}" || -n "${INPUT_PR_HEAD_SHA:-}" ]]; then + if [[ -z "${INPUT_PR_NUMBER:-}" || -z "${INPUT_PR_HEAD_SHA:-}" ]]; then + echo "workflow_dispatch inputs pr_number and pr_head_sha must be provided together." >&2 + exit 1 + fi + + pr_number="${INPUT_PR_NUMBER}" + head_sha="${INPUT_PR_HEAD_SHA}" + head_ref="" +else + if [[ -z "${RELEASE_BRANCH:-}" || -z "${RELEASE_SHA:-}" ]]; then + echo "RELEASE_BRANCH and RELEASE_SHA are required." >&2 + exit 1 + fi + + pr_json="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${RELEASE_BRANCH}" \ + --state merged \ + --limit 100 \ + --json number,headRefOid,headRefName,mergeCommit \ + --jq "[.[] | select(.mergeCommit.oid == \"${RELEASE_SHA}\")] | sort_by(.number) | reverse | .[0] // empty" + )" + if [[ -z "${pr_json}" ]]; then + echo "Unable to find merged PR for ${RELEASE_SHA} into ${RELEASE_BRANCH}" >&2 + exit 1 + fi + + pr_number="$(jq -r '.number' <<< "${pr_json}")" + head_sha="$(jq -r '.headRefOid // .head.sha' <<< "${pr_json}")" + head_ref="$(jq -r '.headRefName // .head.ref' <<< "${pr_json}")" +fi + +artifact_name="termux-android-pr-${pr_number}-${head_sha}" +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "number=${pr_number}" + echo "head_sha=${head_sha}" + echo "head_ref=${head_ref}" + echo "artifact_name=${artifact_name}" + } >> "${GITHUB_OUTPUT}" +else + printf 'number=%s\nhead_sha=%s\nhead_ref=%s\nartifact_name=%s\n' \ + "${pr_number}" \ + "${head_sha}" \ + "${head_ref}" \ + "${artifact_name}" +fi diff --git a/scripts/termux-read-release-metadata.sh b/scripts/termux-read-release-metadata.sh new file mode 100755 index 000000000000..519a0635a368 --- /dev/null +++ b/scripts/termux-read-release-metadata.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# Parse .github/termux-release.json and emit workflow outputs for deploy or +# promote jobs. TERMUX_RELEASE_ACTION must be "deploy" or "promote". + +set -euo pipefail + +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +metadata=".github/termux-release.json" +action="${TERMUX_RELEASE_ACTION:-deploy}" +case "${action}" in + deploy) + missing_context="deployment" + ;; + promote) + missing_context="promotion" + ;; + *) + echo "TERMUX_RELEASE_ACTION must be deploy or promote." >&2 + exit 1 + ;; +esac + +if [[ ! -f "${metadata}" ]]; then + echo "No ${metadata}; this push is not a Termux release ${missing_context}." + echo "${action}=false" >> "${GITHUB_OUTPUT}" + exit 0 +fi + +upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" +upstream_name="$(jq -r '.upstream_name // .upstream_tag // empty' "${metadata}")" +termux_tag="$(jq -r '.termux_tag // empty' "${metadata}")" +upstream_version="${upstream_tag#rust-v}" +upstream_version="${upstream_version%-termux}" +upstream_prerelease=false +if [[ "${upstream_version}" == *-* ]]; then + upstream_prerelease=true +fi +upstream_html_url="$(jq -r '.upstream_html_url // ""' "${metadata}")" +upstream_repo="$(jq -r '.upstream_repo // "openai/codex"' "${metadata}")" +release_train="$(jq -r '.release_train // ""' "${metadata}")" +if [[ -z "${upstream_tag}" || -z "${termux_tag}" ]]; then + echo "Missing upstream_tag or termux_tag in ${metadata}" >&2 + exit 1 +fi + +release_state="$(TERMUX_TAG="${termux_tag}" "${script_dir}/termux-release-asset-state.sh")" +release_exists="$(awk -F= '$1 == "release_exists" { print $2 }' <<< "${release_state}")" +asset_exists="$(awk -F= '$1 == "asset_exists" { print $2 }' <<< "${release_state}")" + +if [[ "${action}" == "promote" ]]; then + if [[ "${asset_exists}" == "true" ]]; then + echo "${termux_tag} already exists with codex-aarch64-linux-android.tar.gz; skipping promotion." + echo "promote=false" >> "${GITHUB_OUTPUT}" + exit 0 + fi + if [[ "${release_exists}" == "true" ]]; then + echo "${termux_tag} exists but is missing codex-aarch64-linux-android.tar.gz; repairing promotion." + fi +fi + +{ + echo "${action}=true" + echo "upstream_tag=${upstream_tag}" + echo "upstream_name=${upstream_name}" + echo "termux_tag=${termux_tag}" + echo "upstream_prerelease=${upstream_prerelease}" + echo "upstream_html_url=${upstream_html_url}" + echo "upstream_repo=${upstream_repo}" + echo "release_train=${release_train}" + echo "release_exists=${release_exists}" + echo "asset_exists=${asset_exists}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-release-asset-state.sh b/scripts/termux-release-asset-state.sh new file mode 100755 index 000000000000..8694480b6a36 --- /dev/null +++ b/scripts/termux-release-asset-state.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +termux_tag="${1:-${TERMUX_TAG:-}}" +release_repo="${RELEASE_REPO:-${GITHUB_REPOSITORY:-}}" +asset_name="${ASSET_NAME:-codex-aarch64-linux-android.tar.gz}" + +if [[ -z "${termux_tag}" ]]; then + echo "TERMUX_TAG or tag argument is required." >&2 + exit 1 +fi + +if [[ -z "${release_repo}" ]]; then + echo "GITHUB_REPOSITORY or RELEASE_REPO is required." >&2 + exit 1 +fi + +release_exists=false +asset_exists=false + +if gh release view "${termux_tag}" --repo "${release_repo}" >/dev/null 2>&1; then + release_exists=true + release_asset_exists="$( + gh release view "${termux_tag}" \ + --repo "${release_repo}" \ + --json assets \ + | jq -r --arg asset_name "${asset_name}" \ + '.assets | map(.name) | any(. == $asset_name)' + )" + if [[ "${release_asset_exists}" == "true" ]]; then + asset_exists=true + fi +fi + +printf 'release_exists=%s\n' "${release_exists}" +printf 'asset_exists=%s\n' "${asset_exists}" diff --git a/scripts/termux-release-paths.sh b/scripts/termux-release-paths.sh new file mode 100755 index 000000000000..6990f323ec44 --- /dev/null +++ b/scripts/termux-release-paths.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# Sourceable path lists for files owned by the Termux release automation. + +set -euo pipefail + +readonly -a TERMUX_RELEASE_WORKFLOW_PATHS=( + .github/workflows/rust-release.yml + .github/workflows/shell-tool-mcp.yml + .github/workflows/termux-release-checkpoint.yml + .github/workflows/termux-release-deploy.yml + .github/workflows/termux-release-promote.yml +) + +readonly -a TERMUX_RELEASE_BRANCH_SCRIPT_PATHS=( + scripts/termux-configure-git.sh + scripts/termux-create-checkpoint-pr.sh + scripts/termux-create-or-update-mirrored-release.sh + scripts/termux-download-release-artifact.sh + scripts/termux-find-release-pr.sh + scripts/termux-read-release-metadata.sh + scripts/termux-release-asset-state.sh + scripts/termux-release-paths.sh + scripts/termux-resolve-release-ref.sh + scripts/termux-validate-gh-env.sh +) + +readonly -a TERMUX_RELEASE_AUTOMATION_PATHS=( + "${TERMUX_RELEASE_WORKFLOW_PATHS[@]}" + "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +) + +readonly -a TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS=( + "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" + .github/termux-release.json +) + +termux_path_in_list() { + local candidate="$1" + shift + local listed_path + + for listed_path in "$@"; do + [[ "${candidate}" != "${listed_path}" ]] || return 0 + done + return 1 +} + +termux_is_release_automation_path() { + termux_path_in_list "$1" "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" +} + +termux_is_checkpoint_release_only_path() { + termux_path_in_list "$1" "${TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS[@]}" +} diff --git a/scripts/termux-resolve-release-ref.sh b/scripts/termux-resolve-release-ref.sh new file mode 100755 index 000000000000..194773d45bed --- /dev/null +++ b/scripts/termux-resolve-release-ref.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Resolve the release branch and SHA for deploy/promote workflows. + +set -euo pipefail + +: "${GITHUB_EVENT_NAME:?GITHUB_EVENT_NAME is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" + +input_release_branch="${INPUT_RELEASE_BRANCH:-${REQUESTED_RELEASE_BRANCH:-}}" +input_release_sha="${INPUT_RELEASE_SHA:-${REQUESTED_RELEASE_SHA:-}}" + +if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then + if [[ -z "${input_release_branch}" ]]; then + echo "release_branch is required for workflow_dispatch." >&2 + exit 1 + fi + + release_branch="${input_release_branch}" + if [[ -n "${input_release_sha}" ]]; then + git checkout --detach "${input_release_sha}" + release_sha="${input_release_sha}" + else + release_sha="$(git rev-parse HEAD)" + fi +else + : "${GITHUB_REF_NAME:?GITHUB_REF_NAME is required}" + : "${GITHUB_SHA:?GITHUB_SHA is required}" + release_branch="${GITHUB_REF_NAME}" + release_sha="${GITHUB_SHA}" +fi + +{ + echo "branch=${release_branch}" + echo "sha=${release_sha}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-validate-gh-env.sh b/scripts/termux-validate-gh-env.sh new file mode 100755 index 000000000000..02581fdebceb --- /dev/null +++ b/scripts/termux-validate-gh-env.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Lightweight post-toolbox check for jobs that rely on authenticated gh calls. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +command -v gh +gh auth status --hostname github.com + +printf 'GITHUB_REPOSITORY=%s\n' "${GITHUB_REPOSITORY}" +printf 'REPO=%s\n' "${REPO:-}" +printf 'GH_REPO_URL=%s\n' "${GH_REPO_URL:-}" +printf 'GH_WORKFLOW_URL=%s\n' "${GH_WORKFLOW_URL:-}" From b11a7c17278e819917152997b0c038e3bfc44545 Mon Sep 17 00:00:00 2001 From: Shijie Rao Date: Fri, 22 May 2026 17:13:54 -0700 Subject: [PATCH 20/25] Release 0.134.0-alpha.3 --- codex-rs/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 95c065c74bdf..b75cd1ba5265 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -117,7 +117,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.0.0" +version = "0.134.0-alpha.3" # Track the edition for all workspace crates in one place. Individual # crates can still override this value, but keeping it here means new # crates created with `cargo new -w ...` automatically inherit the 2024 From 8a8bb2155303c2c212344906795b7c034f875fe1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 01:17:37 +0000 Subject: [PATCH 21/25] Seed Termux release automation --- .github/workflows/rust-release.yml | 1807 +++++++---------- .github/workflows/shell-tool-mcp.yml | 461 +++++ .../workflows/termux-release-checkpoint.yml | 103 + .github/workflows/termux-release-deploy.yml | 243 +++ .github/workflows/termux-release-promote.yml | 135 ++ scripts/termux-configure-git.sh | 40 + scripts/termux-create-checkpoint-pr.sh | 275 +++ ...ermux-create-or-update-mirrored-release.sh | 111 + scripts/termux-download-release-artifact.sh | 78 + scripts/termux-find-release-pr.sh | 53 + scripts/termux-read-release-metadata.sh | 77 + scripts/termux-release-asset-state.sh | 37 + scripts/termux-release-paths.sh | 55 + scripts/termux-resolve-release-ref.sh | 36 + scripts/termux-validate-gh-env.sh | 16 + 15 files changed, 2445 insertions(+), 1082 deletions(-) create mode 100644 .github/workflows/shell-tool-mcp.yml create mode 100644 .github/workflows/termux-release-checkpoint.yml create mode 100644 .github/workflows/termux-release-deploy.yml create mode 100644 .github/workflows/termux-release-promote.yml create mode 100755 scripts/termux-configure-git.sh create mode 100755 scripts/termux-create-checkpoint-pr.sh create mode 100755 scripts/termux-create-or-update-mirrored-release.sh create mode 100755 scripts/termux-download-release-artifact.sh create mode 100755 scripts/termux-find-release-pr.sh create mode 100755 scripts/termux-read-release-metadata.sh create mode 100755 scripts/termux-release-asset-state.sh create mode 100755 scripts/termux-release-paths.sh create mode 100755 scripts/termux-resolve-release-ref.sh create mode 100755 scripts/termux-validate-gh-env.sh diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 2cd11e66fe0f..56d0977e6e67 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -4,46 +4,49 @@ # git tag -a rust-v0.1.0 -m "Release 0.1.0" # git push origin rust-v0.1.0 # ``` -# -# To use external macOS signing, manually dispatch `release_mode=build_unsigned`, -# sign the unsigned macOS artifacts in a secure enclave, upload the signed handoff -# archive as a GitHub Release asset, then manually dispatch -# `release_mode=promote_signed` with `unsigned_run_id` and `signed_macos_asset`. -# The signed handoff archive should contain target or artifact directories such -# as `aarch64-apple-darwin/` with signed binaries. name: rust-release on: - push: - tags: - - "rust-v*.*.*" + pull_request: + branches: + - "release/**" workflow_dispatch: inputs: - release_mode: - description: "build_unsigned creates unsigned macOS handoff artifacts; promote_signed finishes a release from signed macOS handoff artifacts." + source_ref: + description: "Branch, tag, or commit SHA to build (code only; keeps .github from dispatched ref)" required: false + type: string + default: "" + build_target: + description: "Target to build" + required: true type: choice - default: build_unsigned + default: all options: - - build_unsigned - - promote_signed - sign_macos: - description: "Deprecated compatibility input; use release_mode instead." - required: false - type: boolean - default: false - unsigned_run_id: - description: "For promote_signed: workflow run id from the build_unsigned run." - required: false - type: string - signed_macos_asset: - description: "For promote_signed: exact GitHub Release asset name containing signed macOS handoff artifacts." - required: false - type: string - signed_macos_sha256: - description: "For promote_signed: optional SHA-256 of signed_macos_asset." - required: false - type: string + - all + - aarch64-apple-darwin + - x86_64-apple-darwin + - x86_64-unknown-linux-musl + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-musl + - aarch64-unknown-linux-gnu + - aarch64-linux-android + - x86_64-pc-windows-msvc + - aarch64-pc-windows-msvc + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read concurrency: group: ${{ github.workflow }} @@ -51,72 +54,26 @@ concurrency: jobs: tag-check: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - uses: dtolnay/rust-toolchain@1.92 - name: Validate tag matches Cargo.toml version shell: bash - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - REQUESTED_SIGN_MACOS: ${{ inputs.sign_macos }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} run: | set -euo pipefail echo "::group::Tag validation" - case "${RELEASE_MODE}" in - signed) - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - echo "❌ Manual rust-release runs must use release_mode=build_unsigned or release_mode=promote_signed" - exit 1 - fi - ;; - build_unsigned) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=build_unsigned is only valid for manual runs" - exit 1 - fi - ;; - promote_signed) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=promote_signed is only valid for manual runs" - exit 1 - fi - if [[ ! "${UNSIGNED_RUN_ID}" =~ ^[0-9]+$ ]]; then - echo "❌ release_mode=promote_signed requires unsigned_run_id to be a workflow run id" - exit 1 - fi - if [[ -z "${SIGNED_MACOS_ASSET}" ]]; then - echo "❌ release_mode=promote_signed requires signed_macos_asset" - exit 1 - fi - if [[ "${SIGNED_MACOS_ASSET}" == */* || "${SIGNED_MACOS_ASSET}" == *"*"* || "${SIGNED_MACOS_ASSET}" == *"?"* || "${SIGNED_MACOS_ASSET}" == *"["* ]]; then - echo "❌ signed_macos_asset must be an exact release asset name, not a path or glob" - exit 1 - fi - if [[ "${UNSIGNED_RUN_ID}" == "${GITHUB_RUN_ID}" ]]; then - echo "❌ unsigned_run_id must refer to the earlier build_unsigned run, not this run" - exit 1 - fi - ;; - *) - echo "❌ Unknown release_mode '${RELEASE_MODE}'" - exit 1 - ;; - esac - - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${REQUESTED_SIGN_MACOS}" == "true" ]]; then - echo "::warning title=Deprecated sign_macos input ignored::Use release_mode=build_unsigned or release_mode=promote_signed instead." - fi - # 1. Must be a tag and match the regex [[ "${GITHUB_REF_TYPE}" == "tag" ]] \ || { echo "❌ Not a tag push"; exit 1; } - [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \ + [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?(-termux)?$ ]] \ || { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; } # 2. Extract versions @@ -131,115 +88,198 @@ jobs: echo "✅ Tag and Cargo.toml agree (${tag_ver})" echo "::endgroup::" - build: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} + select-build-matrix: + if: always() && (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') needs: tag-check - name: Build - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: ${{ matrix.runs_on || matrix.runner }} - # Release builds can take a long time, so leave some headroom to avoid - # having to restart the full workflow due to a timeout. - timeout-minutes: 90 - permissions: - contents: read - id-token: write + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.select.outputs.matrix }} + build_version: ${{ steps.select.outputs.build_version }} + steps: + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - id: select + shell: bash + env: + REPO_OWNER: ${{ github.repository_owner }} + EVENT_NAME: ${{ github.event_name }} + BUILD_TARGET: ${{ inputs.build_target }} + run: | + set -euo pipefail + + matrix='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04","target":"aarch64-linux-android"}, + {"runner":"windows-latest","target":"x86_64-pc-windows-msvc"}, + {"runner":"windows-11-arm","target":"aarch64-pc-windows-msvc"} + ]' + + if [[ "${REPO_OWNER}" != "openai" ]]; then + matrix="$(jq -c '[.[] | select(.runner | startswith("macos") | not)]' <<< "${matrix}")" + fi + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + matrix="$(jq -c '[.[] | select(.target == "aarch64-linux-android")]' <<< "${matrix}")" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" && -n "${BUILD_TARGET}" && "${BUILD_TARGET}" != "all" ]]; then + matrix="$(jq -c --arg build_target "${BUILD_TARGET}" '[.[] | select(.target == $build_target)]' <<< "${matrix}")" + fi + + if [[ "$(jq 'length' <<< "${matrix}")" -eq 0 ]]; then + echo "No build targets selected after applying owner/dispatch filters." >&2 + exit 1 + fi + + echo "matrix={\"include\":${matrix}}" >> "$GITHUB_OUTPUT" + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + metadata=".github/termux-release.json" + if [[ ! -f "${metadata}" ]]; then + echo "${metadata} is required for release train PR builds" >&2 + exit 1 + fi + upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" + if [[ -z "${upstream_tag}" || "${upstream_tag}" != rust-v* ]]; then + echo "Unable to determine upstream rust tag from ${metadata}" >&2 + exit 1 + fi + build_version="${upstream_tag#rust-v}" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then + latest_release_tag="$( + GH_TOKEN="${{ github.token }}" gh api repos/openai/codex/releases/latest --jq '.tag_name' + )" + + if [[ -z "${latest_release_tag}" || "${latest_release_tag}" != rust-v* ]]; then + echo "Unable to determine latest stable release tag from openai/codex" >&2 + exit 1 + fi + + latest_release_version="${latest_release_tag#rust-v}" + build_version="${latest_release_version}-dev" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + fi + + build: + if: >- + always() && + (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') && + needs.select-build-matrix.result == 'success' + needs: + - tag-check + - select-build-matrix + name: Build - ${{ matrix.runner }} - ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + permissions: write-all + timeout-minutes: 120 defaults: run: working-directory: codex-rs env: - # 2026-03-04: temporarily change releases to use thin LTO because - # Ubuntu ARM is timing out at 60 minutes. - CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' }} + CODEX_BWRAP_ENABLE_FFI: ${{ contains(matrix.target, 'unknown-linux') && '1' || '0' }} + CODEX_BUILD_VERSION: ${{ needs.select-build-matrix.outputs.build_version }} + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" strategy: fail-fast: false - matrix: - include: - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - # Release artifacts intentionally ship MUSL-linked Linux binaries. - - runner: codex-linux-x64-xl - target: x86_64-unknown-linux-musl - bundle: primary - artifact_name: x86_64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: codex-linux-x64-xl - target: x86_64-unknown-linux-musl - bundle: app-server - artifact_name: x86_64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: codex-linux-arm64 - target: aarch64-unknown-linux-musl - bundle: primary - artifact_name: aarch64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: codex-linux-arm64 - target: aarch64-unknown-linux-musl - bundle: app-server - artifact_name: aarch64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" + matrix: ${{ fromJson(needs.select-build-matrix.outputs.matrix) }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + with: + verbose: true + - name: Checkout selected source ref + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + uses: actions/checkout@v6 with: - persist-credentials: false - - name: Print runner specs (Linux) - if: ${{ runner.os == 'Linux' }} + ref: ${{ inputs.source_ref }} + path: __source__ + fetch-depth: 1 + - name: Overlay selected source ref (excluding .github) + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + shell: bash + working-directory: ${{ github.workspace }} + run: | + set -euo pipefail + echo "Using source ref: ${{ inputs.source_ref }}" + + # Keep workflow/actions from the dispatch ref while replacing all other files. + find . -mindepth 1 -maxdepth 1 \ + ! -name .git \ + ! -name .github \ + ! -name __source__ \ + -exec rm -rf -- {} + + tar --exclude='.github' -C __source__ -cf - . | tar -xf - + rm -rf __source__ + + - name: Apply selected build version + if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} shell: bash + working-directory: ${{ github.workspace }} run: | set -euo pipefail - cpu_model="$(lscpu | awk -F: '/Model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}')" - total_ram="$(awk '/MemTotal/ {printf "%.1f GiB\n", $2 / 1024 / 1024}' /proc/meminfo)" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(uname -a)" - echo "CPU model: ${cpu_model}" - echo "Logical CPUs: $(nproc)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . - - name: Print runner specs (macOS) - if: ${{ runner.os == 'macOS' }} + if [[ -z "${CODEX_BUILD_VERSION}" ]]; then + echo "CODEX_BUILD_VERSION is empty for workflow_dispatch" >&2 + exit 1 + fi + echo "Using build version: ${CODEX_BUILD_VERSION}" + BUILD_VERSION="${CODEX_BUILD_VERSION}" perl -0777 -i -pe \ + 's/(\[workspace\.package\][^\[]*?version = ")([^"]+)(")/${1}$ENV{BUILD_VERSION}$3/s' \ + codex-rs/Cargo.toml + + grep -n "^\[workspace.package\]\|^version = " codex-rs/Cargo.toml | head -n 4 + + - name: Select Android NDK + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail - total_ram="$(sysctl -n hw.memsize | awk '{printf "%.1f GiB\n", $1 / 1024 / 1024 / 1024}')" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(sw_vers -productName) $(sw_vers -productVersion)" - echo "Hardware model: $(sysctl -n hw.model)" - echo "CPU architecture: $(uname -m)" - echo "Logical CPUs: $(sysctl -n hw.logicalcpu)" - echo "Physical CPUs: $(sysctl -n hw.physicalcpu)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . + + fallback_version="29.0.13113456" + ndk_root="${ANDROID_HOME}/ndk" + selected_ndk="" + + if [[ -d "${ndk_root}" ]]; then + while IFS= read -r candidate; do + toolchain="${candidate}/toolchains/llvm/prebuilt/linux-x86_64" + if [[ -x "${toolchain}/bin/aarch64-linux-android24-clang" ]]; then + selected_ndk="${candidate}" + break + fi + done < <(find "${ndk_root}" -mindepth 1 -maxdepth 1 -type d | sort -Vr) + fi + + if [[ -z "${selected_ndk}" ]]; then + echo "No usable preinstalled Android NDK found; installing ${fallback_version}" + yes | sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses || true + sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" "ndk;${fallback_version}" + selected_ndk="${ndk_root}/${fallback_version}" + fi + + if [[ ! -x "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" ]]; then + echo "Selected Android NDK does not contain a usable aarch64 API 24 clang: ${selected_ndk}" >&2 + exit 1 + fi + + echo "Selected Android NDK: ${selected_ndk}" + "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" --version + echo "ANDROID_NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" + echo "NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" - name: Install Linux bwrap build dependencies - if: ${{ runner.os == 'Linux' }} + if: ${{ runner.os == 'Linux' && matrix.target != 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail @@ -254,9 +294,173 @@ jobs: sudo apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 fi - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@1.93 with: targets: ${{ matrix.target }} + - name: Set up sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + uses: mozilla-actions/sccache-action@main + - name: Enable sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + shell: bash + run: | + set -euo pipefail + echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_VERSION=android-release-${{ matrix.target }}" >> "$GITHUB_ENV" + echo "SCCACHE_CACHE_SIZE=5G" >> "$GITHUB_ENV" + - name: Ensure Rust target is installed + shell: bash + run: | + set -euo pipefail + rustup target add "${{ matrix.target }}" + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android build environment + shell: bash + run: | + set -euo pipefail + ndk="${ANDROID_NDK_HOME}" + target="${{ matrix.target }}" + + # Set up the Android toolchain + toolchain="${ndk}/toolchains/llvm/prebuilt/linux-x86_64" + + # Use API level 24 to follow termux-build convention + api_level="24" + + # Configure Cargo to use the NDK linker + cargo_target_var="CARGO_TARGET_${target^^}_LINKER" + cargo_target_var="${cargo_target_var//-/_}" + echo "${cargo_target_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + # Set CC and AR for the target + target_cc_var="CC_${target}" + target_cc_var="${target_cc_var//-/_}" + echo "${target_cc_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + target_cxx_var="CXX_${target}" + target_cxx_var="${target_cxx_var//-/_}" + echo "${target_cxx_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang++" >> "$GITHUB_ENV" + + target_ar_var="AR_${target}" + target_ar_var="${target_ar_var//-/_}" + echo "${target_ar_var}=${toolchain}/bin/llvm-ar" >> "$GITHUB_ENV" + + # Add toolchain to PATH + echo "${toolchain}/bin" >> "$GITHUB_PATH" + + tls_align_source="${RUNNER_TEMP}/codex-android-tls-align.S" + tls_align_object="${RUNNER_TEMP}/codex-android-tls-align.o" + cat > "${tls_align_source}" <<'EOF' + .section .tdata.codex_android_tls_align,"awT",%progbits + .p2align 6 + .globl __codex_android_tls_align + .hidden __codex_android_tls_align + .type __codex_android_tls_align, %object + __codex_android_tls_align: + .byte 0 + .size __codex_android_tls_align, 1 + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${tls_align_source}" -o "${tls_align_object}" + + v8_compat_source="${RUNNER_TEMP}/codex-android-v8-compat.c" + v8_compat_object="${RUNNER_TEMP}/codex-android-v8-compat.o" + cat > "${v8_compat_source}" <<'EOF' + #include + #include + + extern int posix_memalign(void **memptr, size_t alignment, size_t size); + extern double strtod(const char *nptr, char **endptr); + extern float strtof(const char *nptr, char **endptr); + + void *aligned_alloc(size_t alignment, size_t size) { + void *ptr = 0; + if (posix_memalign(&ptr, alignment, size) != 0) { + return 0; + } + return ptr; + } + + double strtod_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtod(nptr, endptr); + } + + float strtof_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtof(nptr, endptr); + } + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${v8_compat_source}" -o "${v8_compat_object}" + + builtins_archive="$( + find "${toolchain}/lib/clang" \ + -path "*/lib/linux/libclang_rt.builtins-aarch64-android.a" \ + -print -quit + )" + if [[ -z "${builtins_archive}" ]]; then + echo "Could not find Android compiler-rt builtins archive under ${toolchain}/lib/clang" >&2 + exit 1 + fi + builtins_dir="$(dirname "${builtins_archive}")" + + # Provide compatibility symbols and C++ ABI needed by the Android rusty_v8 archive. + rustflags_var="CARGO_TARGET_${target^^}_RUSTFLAGS" + rustflags_var="${rustflags_var//-/_}" + echo "${rustflags_var}=-C link-arg=${tls_align_object} -C link-arg=${v8_compat_object} -C link-arg=-Wl,-u,__codex_android_tls_align -C link-arg=-L${builtins_dir} -C link-arg=-lclang_rt.builtins-aarch64-android -C link-arg=-lc++abi -C link-arg=-Wl,-z,max-page-size=65536" >> "$GITHUB_ENV" + + + # Configure for vendored OpenSSL build + echo "CARGO_BUILD_TARGET_APPLIES_TO_HOST=false" >> "$GITHUB_ENV" + echo "CARGO_BUILD_TARGET=${{ matrix.target }}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android rusty_v8 artifact overrides + env: + GH_TOKEN: ${{ github.token }} + TARGET: ${{ matrix.target }} + shell: bash + run: | + set -euo pipefail + binding_dir="${RUNNER_TEMP}/rusty_v8" + archive="${binding_dir}/librusty_v8_release_${TARGET}.a.gz" + binding_path="${binding_dir}/src_binding_release_${TARGET}.rs" + checksums_path="${binding_dir}/rusty_v8_release_${TARGET}.sha256" + artifact_repository="wallentx/codex-termux" + release_tag="rusty-v8-v147.4.0" + mkdir -p "${binding_dir}" + echo "Downloading Android rusty_v8 artifacts from ${artifact_repository}@${release_tag}" + gh release download "${release_tag}" \ + --repo "${artifact_repository}" \ + --dir "${binding_dir}" \ + --pattern "librusty_v8_release_${TARGET}.a.gz" \ + --pattern "src_binding_release_${TARGET}.rs" \ + --pattern "rusty_v8_release_${TARGET}.sha256" + (cd "${binding_dir}" && sha256sum -c "$(basename "${checksums_path}")") + echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV" + echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Install termux-elf-cleaner + shell: bash + run: | + set -euo pipefail + version="v3.0.1" + src_dir="${RUNNER_TEMP}/termux-elf-cleaner-src" + build_dir="${RUNNER_TEMP}/termux-elf-cleaner-build" + bin_dir="${RUNNER_TEMP}/termux-elf-cleaner-bin" + + rm -rf "${src_dir}" "${build_dir}" "${bin_dir}" + mkdir -p "${src_dir}" "${build_dir}" "${bin_dir}" + + curl -fsSL "https://github.com/termux/termux-elf-cleaner/archive/refs/tags/${version}.tar.gz" \ + | tar -xzf - --strip-components=1 -C "${src_dir}" + + cmake -S "${src_dir}" -B "${build_dir}" -DCMAKE_BUILD_TYPE=Release + cmake --build "${build_dir}" --parallel + + install "${build_dir}/termux-elf-cleaner" "${bin_dir}/termux-elf-cleaner" + echo "${bin_dir}" >> "$GITHUB_PATH" - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Use hermetic Cargo home (musl) @@ -269,12 +473,25 @@ jobs: echo "${cargo_home}/bin" >> "$GITHUB_PATH" : > "${cargo_home}/config.toml" + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ${{ github.workspace }}/.cargo-home/bin/ + ${{ github.workspace }}/.cargo-home/registry/index/ + ${{ github.workspace }}/.cargo-home/registry/cache/ + ${{ github.workspace }}/.cargo-home/git/db/ + ${{ github.workspace }}/codex-rs/target/ + key: cargo-${{ matrix.runner }}-${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }} + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install Zig - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + uses: mlugg/setup-zig@v2 with: version: 0.14.0 - use-cache: false - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install musl build tools @@ -310,12 +527,6 @@ jobs: shell: bash run: | set -euo pipefail - # Avoid problematic aws-lc jitter entropy code path on musl builders. - echo "AWS_LC_SYS_NO_JITTER_ENTROPY=1" >> "$GITHUB_ENV" - target_no_jitter="AWS_LC_SYS_NO_JITTER_ENTROPY_${{ matrix.target }}" - target_no_jitter="${target_no_jitter//-/_}" - echo "${target_no_jitter}=1" >> "$GITHUB_ENV" - # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. echo "RUSTFLAGS=" >> "$GITHUB_ENV" echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" @@ -340,93 +551,70 @@ jobs: echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" - - name: Configure rusty_v8 artifact overrides and verify checksums - uses: ./.github/actions/setup-rusty-v8 - with: - target: ${{ matrix.target }} - - - if: ${{ contains(matrix.target, 'linux') }} - name: Build bwrap and export digest + - name: Cargo build shell: bash run: | set -euo pipefail - target="${{ matrix.target }}" - cargo build --target "$target" --release --timings --bin bwrap - bwrap_path="target/${target}/release/bwrap" - if [[ ! -f "$bwrap_path" ]]; then - echo "bwrap binary ${bwrap_path} not found" - exit 1 + if [[ "${{ matrix.target }}" == 'aarch64-linux-android' ]]; then + export CARGO_BUILD_JOBS=4 + export CARGO_PROFILE_RELEASE_LTO=thin + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 + cargo build --target ${{ matrix.target }} --release --bin codex + exit 0 fi - digest="$(sha256sum "$bwrap_path" | awk '{print $1}')" - echo "CODEX_BWRAP_SHA256=${digest}" >> "$GITHUB_ENV" - echo "Built bwrap ${bwrap_path} with sha256:${digest}" - - - name: Cargo build + if [[ "${{ contains(matrix.target, 'windows') }}" == 'true' ]]; then + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy --bin codex-windows-sandbox-setup --bin codex-command-runner + else + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy + fi + - name: sccache stats (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash - run: | - build_args=() - for binary in ${{ matrix.binaries }}; do - build_args+=(--bin "$binary") - done - echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}" - cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}" - - - name: Upload Cargo timings - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }} - path: codex-rs/target/**/cargo-timings/cargo-timing.html - if-no-files-found: warn + run: ${SCCACHE_PATH:-sccache} --show-stats || true - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Stage unsigned macOS artifacts + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Normalize Android ELF for Termux shell: bash run: | set -euo pipefail - - target="${{ matrix.target }}" - release_dir="target/${target}/release" - dest="unsigned-dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - unsigned_name="${binary}-${target}-unsigned" - unsigned_path="${dest}/${unsigned_name}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 + for binary in codex codex-responses-api-proxy; do + binary_path="target/${{ matrix.target }}/release/${binary}" + if [[ -f "${binary_path}" ]]; then + termux-elf-cleaner --api-level 24 "${binary_path}" + chmod +x "${binary_path}" + # if [[ "${binary}" == "codex" ]]; then + # # Patch PT_TLS (0x7) to PT_NULL (0x0) at offset 400 to bypass Bionic alignment checks. + # printf '\x00\x00\x00\x00' | dd of="${binary_path}" bs=1 seek=400 count=4 conv=notrunc + # fi fi - - cp "${binary_path}" "${unsigned_path}" - tar -C "$dest" -czf "${unsigned_path}.tar.gz" "${unsigned_name}" - zstd -T0 -19 --rm "${unsigned_path}" done - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Upload unsigned macOS artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }}-unsigned - path: codex-rs/unsigned-dist/${{ matrix.target }}/* - if-no-files-found: error - - - if: ${{ contains(matrix.target, 'linux') }} + - if: ${{ contains(matrix.target, 'linux') && !contains(matrix.target, 'android') && github.repository_owner == 'openai' }} name: Cosign Linux artifacts uses: ./.github/actions/linux-code-sign with: target: ${{ matrix.target }} artifacts-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release - binaries: ${{ matrix.binaries }} - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS == 'true' }} + - if: ${{ contains(matrix.target, 'windows') && github.repository_owner == 'openai' }} + name: Sign Windows binaries with Azure Trusted Signing + uses: ./.github/actions/windows-code-sign + with: + target: ${{ matrix.target }} + client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID }} + endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} + account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} + + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (binaries) uses: ./.github/actions/macos-code-sign with: target: ${{ matrix.target }} - binaries: ${{ matrix.binaries }} sign-binaries: "true" sign-dmg: "false" apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }} @@ -435,7 +623,7 @@ jobs: apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: Build macOS dmg shell: bash run: | @@ -450,17 +638,23 @@ jobs: # The previous "MacOS code signing (binaries)" step signs + notarizes the # built artifacts in `${release_dir}`. This step packages *those same* # signed binaries into a dmg. + codex_binary_path="${release_dir}/codex" + proxy_binary_path="${release_dir}/codex-responses-api-proxy" + rm -rf "$dmg_root" mkdir -p "$dmg_root" - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 - fi - ditto "${binary_path}" "${dmg_root}/${binary}" - done + if [[ ! -f "$codex_binary_path" ]]; then + echo "Binary $codex_binary_path not found" + exit 1 + fi + if [[ ! -f "$proxy_binary_path" ]]; then + echo "Binary $proxy_binary_path not found" + exit 1 + fi + + ditto "$codex_binary_path" "${dmg_root}/codex" + ditto "$proxy_binary_path" "${dmg_root}/codex-responses-api-proxy" rm -f "$dmg_path" hdiutil create \ @@ -475,7 +669,7 @@ jobs: exit 1 fi - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (dmg) uses: ./.github/actions/macos-code-sign with: @@ -489,113 +683,65 @@ jobs: apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - name: Stage artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | dest="dist/${{ matrix.target }}" mkdir -p "$dest" - for binary in ${{ matrix.binaries }}; do - cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" - if [[ "${{ matrix.target }}" == *linux* ]]; then - cp "target/${{ matrix.target }}/release/${binary}.sigstore" \ - "$dest/${binary}-${{ matrix.target }}.sigstore" + if [[ "${{ matrix.runner }}" == windows* ]]; then + cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe" + else + # For Android, we want the binary to be named just 'codex' in the archive. + if [[ "${{ matrix.target }}" == "aarch64-linux-android" ]]; then + cp target/${{ matrix.target }}/release/codex "$dest/codex" + else + cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" fi - done + if [[ -f target/${{ matrix.target }}/release/codex-responses-api-proxy ]]; then + cp target/${{ matrix.target }}/release/codex-responses-api-proxy "$dest/codex-responses-api-proxy-${{ matrix.target }}" + fi + fi - if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.bundle }}" == "primary" ]]; then - bundle_root="${RUNNER_TEMP}/codex-${{ matrix.target }}-bundle" - rm -rf "$bundle_root" - mkdir -p "$bundle_root/codex-resources" - cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex" - cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap" - chmod 0755 "$bundle_root/codex" "$bundle_root/codex-resources/bwrap" - tar -C "$bundle_root" -cf - codex codex-resources/bwrap | - zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst" + if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.target }}" != *android* ]]; then + cp target/${{ matrix.target }}/release/codex.sigstore "$dest/codex-${{ matrix.target }}.sigstore" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.sigstore "$dest/codex-responses-api-proxy-${{ matrix.target }}.sigstore" fi - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then + if [[ "${{ matrix.target }}" == *apple-darwin ]]; then cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" fi - - name: Build Codex package archive - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "target/${TARGET}/release" \ - --archive-dir "dist/${TARGET}" - - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - aarch64-unknown-linux-musl) - platform_tag="manylinux_2_17_aarch64" - ;; - x86_64-unknown-linux-musl) - platform_tag="manylinux_2_17_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - # Do not install into the runner's system Python; macOS runners mark - # the Homebrew Python as externally managed under PEP 668. - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - stage_runtime_args=( - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" - stage-runtime - "$stage_dir" - "dist/${{ matrix.target }}/codex-package-${{ matrix.target }}.tar.gz" - --codex-version "${GITHUB_REF_NAME}" - --platform-tag "$platform_tag" - ) - python3 "${stage_runtime_args[@]}" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error + - if: ${{ matrix.runner == 'windows-11-arm' }} + name: Install zstd + shell: powershell + run: choco install -y zstandard - name: Compress artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | # Path that contains the uncompressed binaries for the current # ${{ matrix.target }} dest="dist/${{ matrix.target }}" + repo_root=$PWD + + # We want to ship the raw Windows executables in the GitHub Release + # in addition to the compressed archives. Keep the originals for + # Windows targets; remove them elsewhere to limit the number of + # artifacts that end up in the GitHub Release. + keep_originals=false + if [[ "${{ matrix.runner }}" == windows* ]]; then + keep_originals=true + fi # For compatibility with environments that lack the `zstd` tool we - # additionally create a `.tar.gz` alongside every binary we publish. - # The end result is: + # additionally create a `.tar.gz` for all platforms and `.zip` for + # Windows alongside every single binary that we publish. The end result is: # codex-.zst (existing) # codex-.tar.gz (new) + # codex-.zip (only for Windows) # 1. Produce a .tar.gz for every file in the directory *before* we # run `zstd --rm`, because that flag deletes the original files. @@ -603,7 +749,7 @@ jobs: base="$(basename "$f")" # Skip files that are already archives (shouldn't happen, but be # safe). - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then + if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then continue fi @@ -613,344 +759,239 @@ jobs: fi # Create per-binary tar.gz - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - - # Also create .zst and remove the uncompressed binaries to keep - # non-Windows artifact directories small. - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - with: - name: ${{ matrix.artifact_name }} - # Upload the per-binary .zst files, .tar.gz equivalents, and any - # prebuilt archives staged above. - path: | - codex-rs/dist/${{ matrix.target }}/* - - stage-signed-macos: - if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode == 'promote_signed' }} - needs: tag-check - name: Stage signed macOS handoff - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: macos-15-xlarge - timeout-minutes: 30 - permissions: - contents: read - defaults: - run: - working-directory: codex-rs + # For Android, we want the archive to have the target suffix even though the binary is just 'codex'. + archive_name="${base}" + if [[ "${{ matrix.target }}" == "aarch64-linux-android" && "${base}" == "codex" ]]; then + archive_name="codex-${{ matrix.target }}" + fi + tar -C "$dest" -czf "$dest/${archive_name}.tar.gz" "$base" + + # Create zip archive for Windows binaries + # Must run from inside the dest dir so 7z won't + # embed the directory path inside the zip. + if [[ "${{ matrix.runner }}" == windows* ]]; then + if [[ "$base" == "codex-${{ matrix.target }}.exe" ]]; then + # Bundle the sandbox helper binaries into the main codex zip so + # WinGet installs include the required helpers next to codex.exe. + # Fall back to the single-binary zip if the helpers are missing + # to avoid breaking releases. + bundle_dir="$(mktemp -d)" + runner_src="$dest/codex-command-runner-${{ matrix.target }}.exe" + setup_src="$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + if [[ -f "$runner_src" && -f "$setup_src" ]]; then + cp "$dest/$base" "$bundle_dir/$base" + cp "$runner_src" "$bundle_dir/codex-command-runner.exe" + cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" + # Use an absolute path so bundle zips land in the real dist + # dir even when 7z runs from a temp directory. + (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) + else + echo "warning: missing sandbox binaries; falling back to single-binary zip" + echo "warning: expected $runner_src and $setup_src" + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + rm -rf "$bundle_dir" + else + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + fi - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" + # Also create .zst (existing behaviour) *and* remove the original + # uncompressed binary to keep the directory small. + zstd_args=(-T0 -19) + if [[ "${keep_originals}" == false ]]; then + zstd_args+=(--rm) + fi - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + if [[ "${archive_name}" != "${base}" ]]; then + zstd "${zstd_args[@]}" "$dest/$base" -o "$dest/${archive_name}.zst" + else + zstd "${zstd_args[@]}" "$dest/$base" + fi + done - - name: Download signed macOS handoff + - name: Add Termux release metadata + if: ${{ (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && matrix.target == 'aarch64-linux-android' }} shell: bash - env: - GH_TOKEN: ${{ github.token }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - SIGNED_MACOS_SHA256: ${{ inputs.signed_macos_sha256 }} run: | set -euo pipefail - - download_dir="${RUNNER_TEMP}/signed-macos-download" - handoff_dir="${RUNNER_TEMP}/signed-macos-handoff" - rm -rf "$download_dir" "$handoff_dir" - mkdir -p "$download_dir" "$handoff_dir" - - gh release download "$GITHUB_REF_NAME" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern "$SIGNED_MACOS_ASSET" \ - --dir "$download_dir" - - asset_count="$(find "$download_dir" -maxdepth 1 -type f | wc -l | tr -d '[:space:]')" - if [[ "$asset_count" != "1" ]]; then - echo "Expected exactly one signed macOS handoff asset named ${SIGNED_MACOS_ASSET}; found ${asset_count}" - find "$download_dir" -maxdepth 1 -type f -print - exit 1 - fi - - asset_path="$(find "$download_dir" -maxdepth 1 -type f -print -quit)" - if [[ -n "${SIGNED_MACOS_SHA256}" ]]; then - expected_sha="$(printf '%s' "$SIGNED_MACOS_SHA256" | tr '[:upper:]' '[:lower:]')" - actual_sha="$(shasum -a 256 "$asset_path" | awk '{print $1}')" - if [[ "$actual_sha" != "$expected_sha" ]]; then - echo "signed_macos_sha256 mismatch for ${SIGNED_MACOS_ASSET}" - echo "expected: ${expected_sha}" - echo "actual: ${actual_sha}" - exit 1 - fi + if [[ ! -f "${GITHUB_WORKSPACE}/.github/termux-release.json" ]]; then + echo "No Termux release metadata found; skipping metadata attachment." + exit 0 fi + dest="dist/${{ matrix.target }}" + cp "${GITHUB_WORKSPACE}/.github/termux-release.json" "${dest}/termux-release.json" + ( + cd "${dest}" + sha256sum ./* > SHA256SUMS + ) - asset_name="$(basename "$asset_path")" - case "$asset_name" in - *.tar.zst) - zstd -dc "$asset_path" | tar -C "$handoff_dir" -xf - - ;; - *.tar.gz|*.tgz) - tar -C "$handoff_dir" -xzf "$asset_path" - ;; - *.zip) - ditto -x -k "$asset_path" "$handoff_dir" - ;; - *) - echo "Unsupported signed macOS handoff archive format: ${asset_name}" - exit 1 - ;; - esac - - echo "SIGNED_MACOS_HANDOFF_DIR=$handoff_dir" >> "$GITHUB_ENV" + - id: upload-artifact + uses: actions/upload-artifact@v6 + with: + name: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + # Upload the per-binary .zst files as well as the new .tar.gz + # equivalents we generated in the previous step. + path: | + codex-rs/dist/${{ matrix.target }}/* - - name: Stage signed macOS artifacts + - name: Publish Termux artifact notification + if: ${{ github.event_name == 'pull_request' && startsWith(github.base_ref, 'release/') && matrix.target == 'aarch64-linux-android' }} shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_ID: ${{ steps.upload-artifact.outputs.artifact-id }} + ARTIFACT_NAME: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + PR_NUMBER: ${{ github.event.pull_request.number }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | set -euo pipefail - - target="${{ matrix.target }}" - artifact_name="${{ matrix.artifact_name }}" - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${target}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - fi - if [[ ! -d "$source_dir" ]]; then - echo "Signed macOS handoff is missing ${artifact_name}/" - echo "Expected either:" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${target}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - find "$SIGNED_MACOS_HANDOFF_DIR" -maxdepth 3 -type f -print + if [[ -z "${ARTIFACT_ID}" ]]; then + echo "upload-artifact did not return an artifact id" >&2 exit 1 fi - dest="dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - source_path="${source_dir}/${binary}" - if [[ ! -f "$source_path" ]]; then - source_path="${source_dir}/${binary}-${target}" - fi - if [[ ! -f "$source_path" ]]; then - echo "Signed macOS handoff is missing ${binary} for ${artifact_name}" - exit 1 - fi - - release_path="${dest}/${binary}-${target}" - ditto "$source_path" "$release_path" - chmod 0755 "$release_path" - codesign --verify --strict --verbose=2 "$release_path" - done - - # DMG staging is disabled for signed promotion because we no longer - # distribute DMGs from this release path. Keep the branch here so the - # handoff can opt back in by flipping matrix.build_dmg if needed. - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - dmg_name="codex-${target}.dmg" - dmg_source="${source_dir}/${dmg_name}" - if [[ ! -f "$dmg_source" ]]; then - echo "Signed macOS handoff is missing ${dmg_name} for ${artifact_name}" - exit 1 - fi - - codesign --verify --strict --verbose=2 "$dmg_source" - xcrun stapler validate "$dmg_source" - cp "$dmg_source" "$dest/$dmg_name" + marker="" + artifact_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts/${ARTIFACT_ID}" + run_url="${GH_WORKFLOW_URL:-https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + body_path="${RUNNER_TEMP}/termux-artifact-comment.md" + { + echo "${marker}" + echo "Termux Android artifact ready for testing:" + echo + echo "- Artifact: [${ARTIFACT_NAME}](${artifact_url})" + echo "- Workflow run: [${GITHUB_RUN_ID}](${run_url})" + echo + echo "You must be signed in to GitHub with repository access to download Actions artifacts." + } > "${body_path}" + + existing_comment_id="$( + gh pr view "${PR_NUMBER}" \ + --repo "${GITHUB_REPOSITORY}" \ + --json comments \ + --jq ".comments | map(select(.author.is_bot == true and (.body | contains(\"${marker}\")))) | .[-1].id // \"\"" + )" + if [[ -n "${existing_comment_id}" ]]; then + gh api graphql \ + -f query=' + mutation($id: ID!, $body: String!) { + updateIssueComment(input: {id: $id, body: $body}) { + issueComment { + id + } + } + } + ' \ + -f id="${existing_comment_id}" \ + -f body="$(cat "${body_path}")" \ + >/dev/null + else + gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body-file "${body_path}" fi - - name: Build Codex package archive - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "dist/${TARGET}" \ - --archive-dir "dist/${TARGET}" \ - --target-suffixed-entrypoint - - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - python3 \ - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \ - stage-runtime \ - "$stage_dir" \ - "dist/${{ matrix.target }}/codex-package-${{ matrix.target }}.tar.gz" \ - --codex-version "${GITHUB_REF_NAME}" \ - --platform-tag "$platform_tag" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error - - - name: Compress artifacts - shell: bash - run: | - set -euo pipefail - - dest="dist/${{ matrix.target }}" - for f in "$dest"/*; do - base="$(basename "$f")" - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then - continue + gh label create binary-ready \ + --repo "${GITHUB_REPOSITORY}" \ + --color 0e8a16 \ + --description "Android binary is ready for testing" \ + --force + gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --add-label binary-ready + + if [[ -n "${SLACK_WEBHOOK_URL:-}" ]]; then + slack_payload_path="${RUNNER_TEMP}/termux-artifact-slack.json" + pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}" + jq -n \ + --arg artifact_name "${ARTIFACT_NAME}" \ + --arg artifact_url "${artifact_url}" \ + --arg pr_number "${PR_NUMBER}" \ + --arg pr_url "${pr_url}" \ + --arg run_id "${GITHUB_RUN_ID}" \ + --arg run_url "${run_url}" \ + '{ + text: ("Termux Android artifact ready for testing: " + $artifact_url), + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: ("*Termux Android artifact ready for testing*\n" + $artifact_url) + } + }, + { + type: "section", + fields: [ + { + type: "mrkdwn", + text: ("*Artifact:*\n<" + $artifact_url + "|" + $artifact_name + ">") + }, + { + type: "mrkdwn", + text: ("*Pull request:*\n<" + $pr_url + "|#" + $pr_number + ">") + }, + { + type: "mrkdwn", + text: ("*Workflow run:*\n<" + $run_url + "|" + $run_id + ">") + } + ] + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: "You must be signed in to GitHub with repository access to download Actions artifacts." + } + ] + } + ] + }' > "${slack_payload_path}" + + if ! curl -fsS \ + -X POST \ + -H "Content-type: application/json" \ + --data @"${slack_payload_path}" \ + "${SLACK_WEBHOOK_URL}"; then + echo "::warning::Failed to send Slack artifact notification" fi + fi - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }} - path: | - codex-rs/dist/${{ matrix.target }}/* - - build-windows: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - needs: tag-check - uses: ./.github/workflows/rust-release-windows.yml - with: - release-lto: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }} - secrets: inherit - - argument-comment-lint-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: argument-comment-lint release assets + shell-tool-mcp: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'openai' + name: shell-tool-mcp needs: tag-check - uses: ./.github/workflows/rust-release-argument-comment-lint.yml + permissions: + contents: read + id-token: write + uses: ./.github/workflows/shell-tool-mcp.yml with: + release-tag: ${{ github.ref_name }} publish: true - - zsh-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: zsh release assets - needs: tag-check - uses: ./.github/workflows/rust-release-zsh.yml + secrets: inherit release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') needs: - - tag-check - build - - stage-signed-macos - - build-windows - - argument-comment-lint-release-assets - - zsh-release-assets - if: >- - ${{ - always() && - needs.tag-check.result == 'success' && - ( - ( - github.event_name == 'workflow_dispatch' && - inputs.release_mode == 'promote_signed' && - needs.stage-signed-macos.result == 'success' && - needs.build.result == 'skipped' && - needs.build-windows.result == 'skipped' && - needs.argument-comment-lint-release-assets.result == 'skipped' && - needs.zsh-release-assets.result == 'skipped' - ) || - ( - (github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed') && - needs.build.result == 'success' && - needs.stage-signed-macos.result == 'skipped' && - needs.build-windows.result == 'success' && - needs.argument-comment-lint-release-assets.result == 'success' && - needs.zsh-release-assets.result == 'success' - ) - ) - }} + - shell-tool-mcp name: release runs-on: ubuntu-latest permissions: contents: write actions: read - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode == 'promote_signed' }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} outputs: version: ${{ steps.release_name.outputs.name }} tag: ${{ github.ref_name }} - sign_macos: ${{ steps.release_mode.outputs.sign_macos }} should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }} npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }} - should_publish_python_runtime: ${{ steps.python_runtime_publish_settings.outputs.should_publish }} steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + uses: actions/checkout@v6 - - name: Define release mode - id: release_mode - run: | - echo "release_mode=${RELEASE_MODE}" >> "$GITHUB_OUTPUT" - echo "sign_macos=${SIGN_MACOS}" >> "$GITHUB_OUTPUT" + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main - name: Generate release notes from tag commit message id: release_notes @@ -972,159 +1013,21 @@ jobs: echo "path=${notes_path}" >> "${GITHUB_OUTPUT}" - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@v7 with: path: dist - - name: Validate unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - run_summary="$(gh run view "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --json conclusion,event,headBranch,headSha,status,workflowName,url \ - --jq '[.workflowName, .event, .headBranch, .headSha, .status, .conclusion, .url] | @tsv')" - IFS=$'\t' read -r workflow_name event head_branch head_sha status conclusion run_url <<< "$run_summary" - expected_head_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")" - - if [[ "$workflow_name" != "$GITHUB_WORKFLOW" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is for workflow '${workflow_name}', expected '${GITHUB_WORKFLOW}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$event" != "workflow_dispatch" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} was triggered by '${event}', expected 'workflow_dispatch'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_branch" != "$GITHUB_REF_NAME" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used ref '${head_branch}', expected '${GITHUB_REF_NAME}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_sha" != "$expected_head_sha" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used head SHA '${head_sha}', expected '${expected_head_sha}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is ${status}/${conclusion}, expected completed/success" - echo "Run URL: ${run_url}" - exit 1 - fi - - - name: Download artifacts from unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - gh run download "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --dir dist - - - name: Remove unsigned macOS staging artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - run: | - set -euo pipefail - find dist -mindepth 1 -maxdepth 1 -type d \ - -name '*-apple-darwin*-unsigned' \ - -exec rm -rf {} + - - - name: Re-upload promoted Linux x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-unknown-linux-musl - path: dist/x86_64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Linux arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-unknown-linux-musl - path: dist/aarch64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Windows x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-pc-windows-msvc - path: dist/x86_64-pc-windows-msvc/* - if-no-files-found: error - - - name: Re-upload promoted Windows arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-pc-windows-msvc - path: dist/aarch64-pc-windows-msvc/* - if-no-files-found: error - - name: List run: ls -R dist/ - - name: Prune artifacts excluded from unsigned macOS release - if: ${{ env.SIGN_MACOS == 'false' }} - run: | - find dist -mindepth 1 -maxdepth 1 -type d \ - ! -name '*-apple-darwin*-unsigned' \ - ! -name 'aarch64-unknown-linux-musl' \ - ! -name 'aarch64-unknown-linux-musl-app-server' \ - ! -name 'x86_64-unknown-linux-musl' \ - ! -name 'x86_64-unknown-linux-musl-app-server' \ - ! -name 'aarch64-pc-windows-msvc' \ - ! -name 'x86_64-pc-windows-msvc' \ - -exec rm -rf {} + - - if ! find dist -type f -name '*-apple-darwin*-unsigned*' | grep -q .; then - echo "No unsigned macOS artifacts found in downloaded workflow artifacts." - exit 1 - fi - + # This is a temporary fix: we should modify shell-tool-mcp.yml so these + # files do not end up in dist/ in the first place. - name: Delete entries from dist/ that should not go in the release run: | - rm -rf dist/windows-binaries* - # cargo-timing.html appears under multiple target-specific directories. - # If included in files: dist/**, release upload races on duplicate - # asset names and can fail with 404s. - find dist -type f -name 'cargo-timing.html' -delete - find dist -type d -empty -delete + rm -rf dist/shell-tool-mcp* ls -R dist/ - - name: Add Codex package checksum manifest - run: | - set -euo pipefail - - manifest="dist/codex-package_SHA256SUMS" - tmp_manifest="$(mktemp)" - find dist -type f \ - \( -name 'codex-package-*.tar.gz' -o -name 'codex-app-server-package-*.tar.gz' \) \ - -print | - sort | - while IFS= read -r archive; do - sha256sum "$archive" | - awk -v name="$(basename "$archive")" '{ print $1 " " name }' - done > "$tmp_manifest" - - if [[ ! -s "$tmp_manifest" ]]; then - echo "No Codex package archives found for checksum manifest" - exit 1 - fi - - mv "$tmp_manifest" "$manifest" - cat "$manifest" - - name: Add config schema release asset run: | cp codex-rs/core/config.schema.json dist/config-schema.json @@ -1145,12 +1048,6 @@ jobs: set -euo pipefail version="${VERSION}" - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - echo "npm_tag=" >> "$GITHUB_OUTPUT" - exit 0 - fi - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "should_publish=true" >> "$GITHUB_OUTPUT" echo "npm_tag=" >> "$GITHUB_OUTPUT" @@ -1162,130 +1059,55 @@ jobs: echo "npm_tag=" >> "$GITHUB_OUTPUT" fi - - name: Determine Python runtime publish settings - id: python_runtime_publish_settings - env: - VERSION: ${{ steps.release_name.outputs.name }} - run: | - set -euo pipefail - version="${VERSION}" - - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - else - echo "should_publish=false" >> "$GITHUB_OUTPUT" - fi - - name: Setup pnpm - if: ${{ env.SIGN_MACOS == 'true' }} - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + uses: pnpm/action-setup@v4 with: run_install: false - name: Setup Node.js for npm packaging - if: ${{ env.SIGN_MACOS == 'true' }} - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: node-version: 22 - name: Install dependencies - if: ${{ env.SIGN_MACOS == 'true' }} run: pnpm install --frozen-lockfile + # stage_npm_packages.py requires DotSlash when staging releases. + - uses: facebook/install-dotslash@v2 - name: Stage npm packages - if: ${{ env.SIGN_MACOS == 'true' }} + if: github.repository_owner == 'openai' env: GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ steps.release_name.outputs.name }} run: | - workflow_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" ./scripts/stage_npm_packages.py \ - --release-version "$RELEASE_VERSION" \ - --workflow-url "$workflow_url" \ + --release-version "${{ steps.release_name.outputs.name }}" \ --package codex \ --package codex-responses-api-proxy \ --package codex-sdk - - name: Stage installer scripts - if: ${{ env.SIGN_MACOS == 'true' }} - run: | - cp scripts/install/install.sh dist/install.sh - cp scripts/install/install.ps1 dist/install.ps1 - - name: Create GitHub Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@v2 with: name: ${{ steps.release_name.outputs.name }} tag_name: ${{ github.ref_name }} body_path: ${{ steps.release_notes.outputs.path }} files: dist/** - overwrite_files: true - make_latest: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} # Mark as prerelease only when the version has a suffix after x.y.z # (e.g. -alpha, -beta). Otherwise publish a normal release. prerelease: ${{ contains(steps.release_name.outputs.name, '-') }} - - name: Clean up signed promotion handoff assets - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" --jq '.id')" - gh api --paginate "repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets" \ - --jq '.[] | [.id, .name] | @tsv' | - while IFS=$'\t' read -r asset_id asset_name; do - if [[ -z "$asset_id" || -z "$asset_name" ]]; then - continue - fi - - delete_asset=false - if [[ "$asset_name" == *unsigned* || "$asset_name" == "$SIGNED_MACOS_ASSET" ]]; then - delete_asset=true - fi - - if [[ "$delete_asset" == "true" ]]; then - echo "Deleting release asset ${asset_name}" - gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" - fi - done - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 + - if: github.repository_owner == 'openai' + uses: facebook/dotslash-publish-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag: ${{ github.ref_name }} config: .github/dotslash-config.json - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-zsh-config.json - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-argument-comment-lint-config.json - - name: Trigger developers.openai.com deploy # Only trigger the deploy if the release is not a pre-release. # The deploy is used to update the developers.openai.com website with the new config schema json file. - if: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} + if: ${{ !contains(steps.release_name.outputs.name, '-') && github.repository_owner == 'openai' }} continue-on-error: true env: DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }} @@ -1300,15 +1122,7 @@ jobs: # npm docs: https://docs.npmjs.com/trusted-publishers publish-npm: # Publish to npm for stable releases and alpha pre-releases with numeric suffixes. - # promote_signed intentionally skips build jobs that are ancestors of release; - # include the !cancelled() status function so Actions does not apply its implicit - # success() check to the whole dependency chain before evaluating release outputs. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_npm == 'true' - }} + if: ${{ needs.release.outputs.should_publish_npm == 'true' && github.repository_owner == 'openai' }} name: publish-npm needs: release runs-on: ubuntu-latest @@ -1318,37 +1132,36 @@ jobs: steps: - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: - # Node 24 bundles npm >= 11.5.1, which trusted publishing requires. - node-version: 24 + node-version: 22 registry-url: "https://registry.npmjs.org" scope: "@openai" + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + - name: Download npm tarballs from release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} run: | set -euo pipefail - version="$RELEASE_VERSION" - tag="$RELEASE_TAG" + version="${{ needs.release.outputs.version }}" + tag="${{ needs.release.outputs.tag }}" mkdir -p dist/npm - patterns=( - "codex-npm-${version}.tgz" - "codex-npm-linux-*-${version}.tgz" - "codex-npm-darwin-*-${version}.tgz" - "codex-npm-win32-*-${version}.tgz" - "codex-responses-api-proxy-npm-${version}.tgz" - "codex-sdk-npm-${version}.tgz" - ) - for pattern in "${patterns[@]}"; do - gh release download "$tag" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "$pattern" \ - --dir dist/npm - done + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-responses-api-proxy-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-sdk-npm-${version}.tgz" \ + --dir dist/npm # No NODE_AUTH_TOKEN needed because we use OIDC. - name: Publish to npm @@ -1357,193 +1170,23 @@ jobs: NPM_TAG: ${{ needs.release.outputs.npm_tag }} run: | set -euo pipefail - prefix="" + tag_args=() if [[ -n "${NPM_TAG}" ]]; then - prefix="${NPM_TAG}-" + tag_args+=(--tag "${NPM_TAG}") fi - root_tarball="dist/npm/codex-npm-${VERSION}.tgz" - sdk_tarball="dist/npm/codex-sdk-npm-${VERSION}.tgz" - # Keep this list in sync with CODEX_PLATFORM_PACKAGES in - # codex-cli/scripts/build_npm_package.py. The root wrapper advances - # @openai/codex@latest as soon as it publishes, so every platform - # package it aliases must already exist in the registry first. - platform_tarballs=( - "dist/npm/codex-npm-linux-x64-${VERSION}.tgz" - "dist/npm/codex-npm-linux-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-x64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-x64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-arm64-${VERSION}.tgz" - ) - - for required_tarball in "${platform_tarballs[@]}" "${root_tarball}"; do - if [[ ! -f "${required_tarball}" ]]; then - echo "Missing npm tarball: ${required_tarball}" - exit 1 - fi - done - - shopt -s nullglob - other_tarballs=() - for tarball in dist/npm/*-"${VERSION}".tgz; do - if [[ "${tarball}" == "${root_tarball}" || "${tarball}" == "${sdk_tarball}" ]]; then - continue - fi - - is_platform_tarball=false - for platform_tarball in "${platform_tarballs[@]}"; do - if [[ "${tarball}" == "${platform_tarball}" ]]; then - is_platform_tarball=true - break - fi - done - if [[ "${is_platform_tarball}" == true ]]; then - continue - fi - - other_tarballs+=("${tarball}") - done - - # Publish the platform packages before the root CLI wrapper. The root - # wrapper advances @openai/codex@latest, so it should only publish - # after the optional dependency versions it references exist. tarballs=( - "${platform_tarballs[@]}" - "${other_tarballs[@]}" - "${root_tarball}" + "codex-npm-${VERSION}.tgz" + "codex-responses-api-proxy-npm-${VERSION}.tgz" + "codex-sdk-npm-${VERSION}.tgz" ) - if [[ -f "${sdk_tarball}" ]]; then - tarballs+=("${sdk_tarball}") - fi for tarball in "${tarballs[@]}"; do - filename="$(basename "${tarball}")" - tag="" - - case "${filename}" in - codex-npm-linux-*-"${VERSION}".tgz|codex-npm-darwin-*-"${VERSION}".tgz|codex-npm-win32-*-"${VERSION}".tgz) - platform="${filename#codex-npm-}" - platform="${platform%-${VERSION}.tgz}" - tag="${prefix}${platform}" - ;; - codex-npm-"${VERSION}".tgz|codex-responses-api-proxy-npm-"${VERSION}".tgz|codex-sdk-npm-"${VERSION}".tgz) - tag="${NPM_TAG}" - ;; - *) - echo "Unexpected npm tarball: ${filename}" - exit 1 - ;; - esac - - publish_cmd=(npm publish "${GITHUB_WORKSPACE}/${tarball}") - if [[ -n "${tag}" ]]; then - publish_cmd+=(--tag "${tag}") - fi - - echo "+ ${publish_cmd[*]}" - set +e - publish_output="$("${publish_cmd[@]}" 2>&1)" - publish_status=$? - set -e - - echo "${publish_output}" - if [[ ${publish_status} -eq 0 ]]; then - continue - fi - - if grep -qiE "previously published|cannot publish over|version already exists" <<< "${publish_output}"; then - echo "Skipping already-published package version for ${filename}" - continue - fi - - exit "${publish_status}" + npm publish "${GITHUB_WORKSPACE}/dist/npm/${tarball}" "${tag_args[@]}" done - # Publish the platform-specific Python runtime wheels using PyPI trusted publishing. - # PyPI project configuration must trust this workflow and job. Keep this - # non-blocking while the Python runtime publishing path is new; failures still - # need release follow-up, but should not invalidate the Rust release itself. - publish-python-runtime: - # Publish to PyPI for stable releases and alpha pre-releases with numeric suffixes. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_python_runtime == 'true' - }} - name: publish-python-runtime - needs: release - runs-on: ubuntu-latest - continue-on-error: true - environment: pypi - permissions: - id-token: write # Required for PyPI trusted publishing. - contents: read - - steps: - - name: Download Python runtime wheels from release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} - run: | - set -euo pipefail - python_version="$RELEASE_VERSION" - python_version="${python_version/-alpha./a}" - python_version="${python_version/-beta./b}" - python_version="${python_version/-rc./rc}" - - mkdir -p dist/python-runtime - gh release download "$RELEASE_TAG" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "openai_codex_cli_bin-${python_version}-*.whl" \ - --dir dist/python-runtime - ls -lh dist/python-runtime - - - name: Publish Python runtime wheels to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - with: - packages-dir: dist/python-runtime - skip-existing: true - - winget: - name: winget - needs: release - # Only publish stable/mainline releases to WinGet; pre-releases include a - # '-' in the semver string (e.g., 1.2.3-alpha.1). - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' && - !contains(needs.release.outputs.version, '-') - }} - # This job only invokes a GitHub Action to open/update the winget-pkgs PR; - # it does not execute Windows-only tooling, so Linux is sufficient. - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Publish to WinGet - uses: vedantmgoyal9/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5 - with: - identifier: OpenAI.Codex - version: ${{ needs.release.outputs.version }} - release-tag: ${{ needs.release.outputs.tag }} - fork-user: openai-oss-forks - installers-regex: '^codex-(?:x86_64|aarch64)-pc-windows-msvc\.exe\.zip$' - token: ${{ secrets.WINGET_PUBLISH_PAT }} - update-branch: name: Update latest-alpha-cli branch - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' - }} permissions: contents: write needs: release diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml new file mode 100644 index 000000000000..66a76aa4d938 --- /dev/null +++ b/.github/workflows/shell-tool-mcp.yml @@ -0,0 +1,461 @@ +name: shell-tool-mcp + +on: + workflow_call: + inputs: + release-version: + description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v. + required: false + type: string + release-tag: + description: Tag name to use when downloading release artifacts (defaults to rust-v). + required: false + type: string + publish: + description: Whether to publish to npm when the version is releasable. + required: false + default: true + type: boolean + +env: + NODE_VERSION: 22 + +jobs: + metadata: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.compute.outputs.version }} + release_tag: ${{ steps.compute.outputs.release_tag }} + should_publish: ${{ steps.compute.outputs.should_publish }} + npm_tag: ${{ steps.compute.outputs.npm_tag }} + steps: + - name: Compute version and tags + id: compute + run: | + set -euo pipefail + + version="${{ inputs.release-version }}" + release_tag="${{ inputs.release-tag }}" + + if [[ -z "$version" ]]; then + if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then + version="${release_tag#rust-v}" + elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then + version="${GITHUB_REF_NAME#rust-v}" + release_tag="${GITHUB_REF_NAME}" + else + echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag." + exit 1 + fi + fi + + if [[ -z "$release_tag" ]]; then + release_tag="rust-v${version}" + fi + + npm_tag="" + should_publish="false" + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + should_publish="true" + elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then + should_publish="true" + npm_tag="alpha" + fi + + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" + echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT" + echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT" + + matrix-setup: + runs-on: ubuntu-latest + outputs: + rust-binaries: ${{ steps.compute.outputs.rust-binaries }} + bash-linux: ${{ steps.compute.outputs.bash-linux }} + bash-darwin: ${{ steps.compute.outputs.bash-darwin }} + steps: + - name: Compute matrices + id: compute + shell: bash + run: | + set -euo pipefail + is_openai="${{ github.repository_owner == 'openai' }}" + + # rust-binaries: always include x86_64-musl; conditionally include paid/fork runners + if [[ "$is_openai" == "true" ]]; then + rust='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","install_musl":true} + ]' + else + rust='[{"runner":"macos-latest","target":"aarch64-apple-darwin"}]' + fi + rust=$(echo "$rust" | jq -c '. + [{"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","install_musl":true}]') + echo "rust-binaries={\"include\":$rust}" >> "$GITHUB_OUTPUT" + + # bash-linux: always include x86_64 variants; add arm64 on openai + bash_linux='[ + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-24.04","image":"ubuntu:24.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-22.04","image":"ubuntu:22.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-12","image":"debian:12"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-11","image":"debian:11"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]' + if [[ "$is_openai" == "true" ]]; then + bash_linux=$(echo "$bash_linux" | jq -c '. + [ + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-24.04","image":"arm64v8/ubuntu:24.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-22.04","image":"arm64v8/ubuntu:22.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-20.04","image":"arm64v8/ubuntu:20.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-12","image":"arm64v8/debian:12"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-11","image":"arm64v8/debian:11"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]') + fi + echo "bash-linux={\"include\":$(echo "$bash_linux" | jq -c)}" >> "$GITHUB_OUTPUT" + + # bash-darwin: always include macos-14; add macos-15-xlarge on openai + bash_darwin='[{"runner":"macos-14","target":"aarch64-apple-darwin","variant":"macos-14"}]' + if [[ "$is_openai" == "true" ]]; then + bash_darwin=$(echo "$bash_darwin" | jq -c '. + [ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin","variant":"macos-15"} + ]') + fi + echo "bash-darwin={\"include\":$(echo "$bash_darwin" | jq -c)}" >> "$GITHUB_OUTPUT" + + rust-binaries: + name: Build Rust - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + defaults: + run: + working-directory: codex-rs + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.rust-binaries) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install UBSan runtime (musl) + if: ${{ matrix.install_musl }} + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -y + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 + fi + + - uses: dtolnay/rust-toolchain@1.93 + with: + targets: ${{ matrix.target }} + + - if: ${{ matrix.install_musl }} + name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.14.0 + + - if: ${{ matrix.install_musl }} + name: Install musl build dependencies + env: + TARGET: ${{ matrix.target }} + run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh" + + - if: ${{ matrix.install_musl }} + name: Configure rustc UBSan wrapper (musl host) + shell: bash + run: | + set -euo pipefail + ubsan="" + if command -v ldconfig >/dev/null 2>&1; then + ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')" + fi + wrapper_root="${RUNNER_TEMP:-/tmp}" + wrapper="${wrapper_root}/rustc-ubsan-wrapper" + cat > "${wrapper}" <> "$GITHUB_ENV" + echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV" + + - if: ${{ matrix.install_musl }} + name: Clear sanitizer flags (musl) + shell: bash + run: | + set -euo pipefail + # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. + echo "RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV" + # Override any runner-level Cargo config rustflags as well. + echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + + sanitize_flags() { + local input="$1" + input="${input//-fsanitize=undefined/}" + input="${input//-fno-sanitize-recover=undefined/}" + input="${input//-fno-sanitize-trap=undefined/}" + echo "$input" + } + + cflags="$(sanitize_flags "${CFLAGS-}")" + cxxflags="$(sanitize_flags "${CXXFLAGS-}")" + echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" + echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + + - name: Build exec server binaries + run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper + + - name: Stage exec server binaries + run: | + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}" + mkdir -p "$dest" + cp "target/${{ matrix.target }}/release/codex-exec-mcp-server" "$dest/" + cp "target/${{ matrix.target }}/release/codex-execve-wrapper" "$dest/" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-rust-${{ matrix.target }} + path: artifacts/** + if-no-files-found: error + + bash-linux: + name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + container: + image: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-linux) }} + steps: + - name: Install build prerequisites + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext + elif command -v dnf >/dev/null 2>&1; then + dnf install -y git gcc gcc-c++ make bison autoconf gettext + elif command -v yum >/dev/null 2>&1; then + yum install -y git gcc gcc-c++ make bison autoconf gettext + else + echo "Unsupported package manager in container" + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + bash-darwin: + name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-darwin) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + package: + name: Package npm module + needs: + - metadata + - rust-binaries + - bash-linux + - bash-darwin + runs-on: ubuntu-latest + env: + PACKAGE_VERSION: ${{ needs.metadata.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install JavaScript dependencies + run: pnpm install --frozen-lockfile + + - name: Build (shell-tool-mcp) + run: pnpm --filter @openai/codex-shell-tool-mcp run build + + - name: Download build artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts + + - name: Assemble staging directory + id: staging + shell: bash + run: | + set -euo pipefail + staging="${STAGING_DIR}" + mkdir -p "$staging" "$staging/vendor" + cp shell-tool-mcp/README.md "$staging/" + cp shell-tool-mcp/package.json "$staging/" + cp -R shell-tool-mcp/bin "$staging/" + + found_vendor="false" + shopt -s nullglob + for vendor_dir in artifacts/*/vendor; do + rsync -av "$vendor_dir/" "$staging/vendor/" + found_vendor="true" + done + if [[ "$found_vendor" == "false" ]]; then + echo "No vendor payloads were downloaded." + exit 1 + fi + + node - <<'NODE' + import fs from "node:fs"; + import path from "node:path"; + + const stagingDir = process.env.STAGING_DIR; + const version = process.env.PACKAGE_VERSION; + const pkgPath = path.join(stagingDir, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + pkg.version = version; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + NODE + + echo "dir=$staging" >> "$GITHUB_OUTPUT" + env: + STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp + + - name: Ensure binaries are executable + run: | + set -euo pipefail + staging="${{ steps.staging.outputs.dir }}" + chmod +x \ + "$staging"/vendor/*/codex-exec-mcp-server \ + "$staging"/vendor/*/codex-execve-wrapper \ + "$staging"/vendor/*/bash/*/bash + + - name: Create npm tarball + shell: bash + run: | + set -euo pipefail + mkdir -p dist/npm + staging="${{ steps.staging.outputs.dir }}" + pack_info=$(cd "$staging" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm") + filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);') + mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz" + + - uses: actions/upload-artifact@v6 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz + if-no-files-found: error + + publish: + name: Publish npm package + needs: + - metadata + - package + if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }} + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: https://registry.npmjs.org + scope: "@openai" + + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + + - name: Download npm tarball + uses: actions/download-artifact@v7 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm + + - name: Publish to npm + if: github.repository == 'openai/codex' + env: + NPM_TAG: ${{ needs.metadata.outputs.npm_tag }} + VERSION: ${{ needs.metadata.outputs.version }} + shell: bash + run: | + set -euo pipefail + tag_args=() + if [[ -n "${NPM_TAG}" ]]; then + tag_args+=(--tag "${NPM_TAG}") + fi + npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}" diff --git a/.github/workflows/termux-release-checkpoint.yml b/.github/workflows/termux-release-checkpoint.yml new file mode 100644 index 000000000000..0347e6f1b3d6 --- /dev/null +++ b/.github/workflows/termux-release-checkpoint.yml @@ -0,0 +1,103 @@ +name: termux-release-checkpoint + +on: + workflow_dispatch: + inputs: + source_branch: + description: "Release branch to checkpoint from, for example release/0.123.0" + required: false + type: string + default: "" + source_sha: + description: "Specific source commit SHA to checkpoint; defaults to the source branch tip" + required: false + type: string + default: "" + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + type: string + default: "wallentx/termux-target" + reviewer: + description: "GitHub username to request as reviewer" + required: false + type: string + default: "wallentx" + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-checkpoint-${{ inputs.source_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + checkpoint: + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + env: + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + REQUESTED_SOURCE_BRANCH: ${{ inputs.source_branch }} + REQUESTED_SOURCE_SHA: ${{ inputs.source_sha }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ inputs.source_branch || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + source_branch="${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${source_branch}" + + - name: Create checkpoint PR + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" diff --git a/.github/workflows/termux-release-deploy.yml b/.github/workflows/termux-release-deploy.yml new file mode 100644 index 000000000000..2071611dacc4 --- /dev/null +++ b/.github/workflows/termux-release-deploy.yml @@ -0,0 +1,243 @@ +name: termux-release-deploy + +on: + push: + branches: + - "release/**" + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to deploy, for example release/0.124.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to deploy. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged release PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged release PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + default: "wallentx/termux-target" + type: string + reviewer: + description: "GitHub username to request as reviewer on the checkpoint PR" + required: false + default: "wallentx" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-deploy-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + deploy: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' || !startsWith(github.event.head_commit.message, 'Seed Termux release automation') }} + permissions: + actions: read + contents: write + deployments: write + issues: write + pull-requests: write + env: + REQUESTED_RELEASE_BRANCH: ${{ inputs.release_branch }} + REQUESTED_RELEASE_SHA: ${{ inputs.release_sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + release_branch="${REQUESTED_RELEASE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${release_branch}" + + - name: Resolve release ref + id: release-ref + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: deploy + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Create deployment + if: steps.metadata.outputs.deploy == 'true' + id: deployment + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + deployment_id="$( + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments" \ + -f ref="${RELEASE_SHA}" \ + -f environment="termux-release" \ + -F auto_merge=false \ + -F required_contexts[] \ + -f description="Termux release deployment for ${TERMUX_TAG}" \ + --jq '.id' + )" + echo "id=${deployment_id}" >> "$GITHUB_OUTPUT" + + - name: Mark deployment in progress + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="in_progress" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Promoting Termux release artifact and preparing checkpoint PR" \ + >/dev/null + + - name: Locate merged pull request + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" + + - name: Ensure checkpoint PR + if: steps.metadata.outputs.deploy == 'true' + id: checkpoint + env: + SOURCE_BRANCH: ${{ steps.release-ref.outputs.branch }} + SOURCE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" + + - name: Mark deployment success + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="success" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment completed for ${TERMUX_TAG}" \ + >/dev/null + + - name: Mark deployment failure + if: failure() && steps.deployment.outputs.id != '' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="failure" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment failed" \ + >/dev/null diff --git a/.github/workflows/termux-release-promote.yml b/.github/workflows/termux-release-promote.yml new file mode 100644 index 000000000000..22c277c21a70 --- /dev/null +++ b/.github/workflows/termux-release-promote.yml @@ -0,0 +1,135 @@ +name: termux-release-promote + +on: + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to promote, for example release/0.122.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to promote. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-promote-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + promote: + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: write + pull-requests: read + env: + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Resolve release ref + id: release-ref + env: + INPUT_RELEASE_BRANCH: ${{ inputs.release_branch }} + INPUT_RELEASE_SHA: ${{ inputs.release_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: promote + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Locate merged pull request + if: steps.metadata.outputs.promote == 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.promote == 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.promote == 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" diff --git a/scripts/termux-configure-git.sh b/scripts/termux-configure-git.sh new file mode 100755 index 000000000000..fae3abcbab48 --- /dev/null +++ b/scripts/termux-configure-git.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Configure the explicit bot identity used by release automation and fetch any +# refs requested by the calling workflow. + +set -euo pipefail + +git config user.name "github-actions[bot]" +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + +while (($#)); do + case "$1" in + --origin) + shift + origin_refs=() + while (($#)) && [[ "$1" != --* ]]; do + origin_refs+=("$1") + shift + done + if ((${#origin_refs[@]})); then + git fetch --prune origin "${origin_refs[@]}" + fi + ;; + --upstream-tag) + if (($# < 3)); then + echo "--upstream-tag requires ." >&2 + exit 1 + fi + upstream_repo="$2" + upstream_tag="$3" + git remote add upstream "https://github.com/${upstream_repo}.git" 2>/dev/null || true + git fetch --prune --no-tags upstream "+refs/tags/${upstream_tag}:refs/tags/${upstream_tag}" + shift 3 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done diff --git a/scripts/termux-create-checkpoint-pr.sh b/scripts/termux-create-checkpoint-pr.sh new file mode 100755 index 000000000000..f315a5b91822 --- /dev/null +++ b/scripts/termux-create-checkpoint-pr.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/termux-release-paths.sh +source "${script_dir}/termux-release-paths.sh" + +source_branch="${SOURCE_BRANCH:-${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}}" +source_sha="${SOURCE_SHA:-${REQUESTED_SOURCE_SHA:-}}" +if [[ -z "${source_sha}" ]]; then + if [[ "${GITHUB_EVENT_NAME:-}" == "push" && "${source_branch}" == "${GITHUB_REF_NAME:-}" ]]; then + source_sha="${GITHUB_SHA}" + else + source_sha="$(git rev-parse "origin/${source_branch}")" + fi +fi + +if [[ -z "${DESTINATION_BRANCH:-}" ]]; then + echo "DESTINATION_BRANCH is required." >&2 + exit 1 +fi + +release_only_checkpoint_paths() { + printf '%s\n' "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +} + +resolve_source_version_conflicts() { + local path="$1" + local resolved_path + resolved_path="$(mktemp)" + + if ! awk ' + function normalize_versions(text) { + gsub(/version = "[^"]+"/, "version = \"\"", text) + return text + } + + BEGIN { + in_block = 0 + side = "" + ours = "" + theirs = "" + blocks = 0 + } + + /^<<<<<<< / { + if (in_block) { + exit 1 + } + in_block = 1 + side = "ours" + ours = "" + theirs = "" + blocks++ + next + } + + /^=======$/ && in_block { + side = "theirs" + next + } + + /^>>>>>>> / && in_block { + if (normalize_versions(ours) != normalize_versions(theirs)) { + exit 1 + } + printf "%s", theirs + in_block = 0 + side = "" + next + } + + { + if (!in_block) { + print + } else if (side == "ours") { + ours = ours $0 ORS + } else if (side == "theirs") { + theirs = theirs $0 ORS + } else { + exit 1 + } + } + + END { + if (in_block || blocks == 0) { + exit 1 + } + } + ' "${path}" > "${resolved_path}"; then + rm -f "${resolved_path}" + return 1 + fi + + cp "${resolved_path}" "${path}" + rm -f "${resolved_path}" +} + +short_sha="${source_sha:0:12}" +source_slug="${source_branch//\//_}" +dest_slug="${DESTINATION_BRANCH//\//_}" +checkpoint_branch="checkpoint/${dest_slug}_from_${source_slug}_${short_sha}" +pr_title="checkpoint: into ${DESTINATION_BRANCH} from ${source_branch} @ ${short_sha}" +merge_conflicted=false +conflict_summary="" + +existing_pr="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --head "${checkpoint_branch}" \ + --state all \ + --json number,state,mergedAt,url \ + --jq '[.[] | select(.state == "OPEN" or .mergedAt != null)] | .[0] // empty' +)" +if [[ -n "${existing_pr}" ]]; then + existing_url="$(jq -r '.url' <<< "${existing_pr}")" + existing_state="$(jq -r '.state' <<< "${existing_pr}")" + echo "Checkpoint PR already exists for ${checkpoint_branch}: ${existing_url} (${existing_state})." + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${existing_url}" >> "${GITHUB_OUTPUT}" + fi + exit 0 +fi + +git checkout -B "${checkpoint_branch}" "origin/${DESTINATION_BRANCH}" + +if ! git merge --no-ff --no-edit "${source_sha}"; then + mapfile -t conflicted_paths < <(git diff --name-only --diff-filter=U) + for conflicted_path in "${conflicted_paths[@]}"; do + if termux_is_checkpoint_release_only_path "${conflicted_path}"; then + echo "Auto-resolving release-only checkpoint conflict in ${conflicted_path} by keeping ${DESTINATION_BRANCH}." + if git cat-file -e "HEAD:${conflicted_path}" 2>/dev/null; then + git checkout --ours -- "${conflicted_path}" + git add "${conflicted_path}" + else + git rm -f --ignore-unmatch "${conflicted_path}" + fi + fi + done + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if ((${#remaining_conflicts[@]})); then + cargo_version_conflicts=true + for remaining_conflict in "${remaining_conflicts[@]}"; do + case "${remaining_conflict}" in + codex-rs/Cargo.toml|codex-rs/Cargo.lock) + ;; + *) + cargo_version_conflicts=false + ;; + esac + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + for remaining_conflict in "${remaining_conflicts[@]}"; do + if ! resolve_source_version_conflicts "${remaining_conflict}"; then + cargo_version_conflicts=false + break + fi + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + echo "Auto-resolving recurring Cargo version checkpoint conflicts by keeping ${source_branch} versions." + git add -- "${remaining_conflicts[@]}" + fi + fi + fi + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if [[ "${#remaining_conflicts[@]}" -eq 0 ]]; then + git commit --no-edit + else + merge_conflicted=true + conflict_summary="$( + printf '%s\n' "${remaining_conflicts[@]}" | awk '{ print "- `" $0 "`" }' + )" + echo "Automatic checkpoint merge failed; creating a manual-resolution PR instead." >&2 + if git rev-parse -q --verify MERGE_HEAD >/dev/null; then + git merge --abort + fi + git checkout -B "${checkpoint_branch}" "${source_sha}" + fi +fi + +if git cat-file -e "origin/${DESTINATION_BRANCH}:.github" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- .github + mapfile -t added_github_paths < <( + git diff --name-only --diff-filter=A "origin/${DESTINATION_BRANCH}" -- .github + ) + if ((${#added_github_paths[@]})); then + git rm -f --ignore-unmatch -- "${added_github_paths[@]}" + fi +else + git rm -r --ignore-unmatch .github +fi + +while IFS= read -r release_only_path; do + if git cat-file -e "origin/${DESTINATION_BRANCH}:${release_only_path}" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- "${release_only_path}" + else + git rm -f --ignore-unmatch -- "${release_only_path}" + fi +done < <(release_only_checkpoint_paths) + +if ! git diff --quiet || ! git diff --cached --quiet; then + if [[ -e .github || -L .github ]] || git ls-files --error-unmatch -- .github >/dev/null 2>&1; then + git add -A .github + fi + while IFS= read -r release_only_path; do + if [[ -e "${release_only_path}" || -L "${release_only_path}" ]] || git ls-files --error-unmatch -- "${release_only_path}" >/dev/null 2>&1; then + git add -A -- "${release_only_path}" + fi + done < <(release_only_checkpoint_paths) + if [[ "${merge_conflicted}" == "true" ]]; then + git commit -m "checkpoint: prepare ${source_branch} for ${DESTINATION_BRANCH}" + else + git commit --amend --no-edit + fi +fi + +if git diff --quiet "origin/${DESTINATION_BRANCH}" HEAD; then + echo "Checkpoint merge produced no destination changes after release-only files were restored." + exit 0 +fi + +git push --force-with-lease origin "${checkpoint_branch}" + +remaining="$( + git log --first-parent --pretty=format:%H "${source_sha}..origin/${source_branch}" | wc -w +)" + +body_path="${RUNNER_TEMP}/termux-checkpoint-pr.md" +{ + echo "## Termux release checkpoint" + echo + echo "- Source branch: \`${source_branch}\`" + echo "- Source hash: \`${source_sha}\`" + echo "- Destination branch: \`${DESTINATION_BRANCH}\`" + echo "- Remaining first-parent commits on source: ${remaining}" + echo + echo "This PR carries release-train conflict fixes and follow-up changes back into the reusable Termux patch branch." + if [[ "${merge_conflicted}" == "true" ]]; then + echo + echo "## Merge conflicts" + echo + echo "GitHub Actions could not create the checkpoint merge commit automatically, so this PR was created from the source branch state for manual conflict resolution." + echo + echo "Conflicted paths from the failed merge attempt:" + if [[ -n "${conflict_summary}" ]]; then + printf '%s\n' "${conflict_summary}" + else + echo "- Conflict details unavailable" + fi + fi + echo + echo "Release-only workflow files and metadata under \`.github\` were restored to the destination branch versions before opening this PR." +} > "${body_path}" + +pr_url="$( + gh pr create \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${DESTINATION_BRANCH}" \ + --head "${checkpoint_branch}" \ + --title "${pr_title}" \ + --body-file "${body_path}" +)" +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-reviewer "${REVIEWER}" || true +gh label create checkpoint --repo "${GITHUB_REPOSITORY}" --color c5def5 --description "Checkpoint merge" --force +gh label create termux-release --repo "${GITHUB_REPOSITORY}" --color 0e8a16 --description "Termux release automation" --force +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-label "checkpoint" --add-label "termux-release" + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${pr_url}" >> "${GITHUB_OUTPUT}" +fi diff --git a/scripts/termux-create-or-update-mirrored-release.sh b/scripts/termux-create-or-update-mirrored-release.sh new file mode 100755 index 000000000000..5787e894582b --- /dev/null +++ b/scripts/termux-create-or-update-mirrored-release.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +# Create a mirrored Termux release or repair an existing release that is missing +# the Android tarball. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" +: "${RUNNER_TEMP:?RUNNER_TEMP is required}" +: "${UPSTREAM_TAG:?UPSTREAM_TAG is required}" +: "${UPSTREAM_REPO:?UPSTREAM_REPO is required}" +: "${UPSTREAM_NAME?UPSTREAM_NAME is required}" +: "${TERMUX_TAG:?TERMUX_TAG is required}" +: "${UPSTREAM_PRERELEASE:?UPSTREAM_PRERELEASE is required}" +: "${UPSTREAM_HTML_URL?UPSTREAM_HTML_URL is required}" +: "${RELEASE_TRAIN?RELEASE_TRAIN is required}" +: "${RELEASE_EXISTS:?RELEASE_EXISTS is required}" +: "${PR_NUMBER:?PR_NUMBER is required}" +: "${HEAD_SHA:?HEAD_SHA is required}" +: "${RELEASE_SHA:?RELEASE_SHA is required}" + +promoted_dir="${PROMOTED_DIR:-promoted}" +asset_path="${promoted_dir}/codex-aarch64-linux-android.tar.gz" + +body_path="${RUNNER_TEMP}/release-body.md" +upstream_body_path="${RUNNER_TEMP}/upstream-release-body.md" +upstream_body_without_changelog_path="${RUNNER_TEMP}/upstream-release-body-without-changelog.md" +if gh release view "${UPSTREAM_TAG}" \ + --repo "${UPSTREAM_REPO}" \ + --json body \ + --jq '.body // ""' > "${upstream_body_path}"; then + awk ' + function heading_level(line, text) { + if (match(line, /^(#{1,6})[[:space:]]+(.+)$/, parts)) { + text = tolower(parts[2]) + sub(/[[:space:]]+#+[[:space:]]*$/, "", text) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", text) + if (text == "changelog" || text == "change log") { + return length(parts[1]) + } + } + return 0 + } + + { + if (!skip) { + level = heading_level($0) + if (level > 0) { + skip = 1 + skip_level = level + next + } + print + next + } + + if (match($0, /^(#{1,6})[[:space:]]+/, parts) && length(parts[1]) <= skip_level) { + skip = 0 + print + } + } + ' "${upstream_body_path}" > "${upstream_body_without_changelog_path}" +else + echo "::warning title=Upstream release notes unavailable::Could not read ${UPSTREAM_REPO} release ${UPSTREAM_TAG}." + : > "${upstream_body_without_changelog_path}" +fi + +{ + echo "Termux Android build for ${UPSTREAM_TAG}." + echo + echo "- Upstream release: ${UPSTREAM_HTML_URL}" + echo "- Release train: \`${RELEASE_TRAIN}\`" + echo "- Promoted PR: #${PR_NUMBER}" + echo "- Promoted PR head SHA: \`${HEAD_SHA}\`" +} > "${body_path}" +if [[ -s "${upstream_body_without_changelog_path}" ]]; then + { + echo + echo "## Upstream release notes" + echo + cat "${upstream_body_without_changelog_path}" + } >> "${body_path}" +fi + +release_title="${UPSTREAM_NAME}" +if [[ -z "${release_title}" || "${release_title}" == "null" ]]; then + release_title="${TERMUX_TAG}" +fi + +if [[ "${RELEASE_EXISTS}" == "true" ]]; then + gh release upload \ + "${TERMUX_TAG}" \ + "${asset_path}#codex-termux" \ + --repo "${GITHUB_REPOSITORY}" \ + --clobber + exit 0 +fi + +release_args=( + gh release create "${TERMUX_TAG}" + --repo "${GITHUB_REPOSITORY}" + --target "${RELEASE_SHA}" + --title "${release_title}" + --notes-file "${body_path}" +) +if [[ "${UPSTREAM_PRERELEASE}" == "true" ]]; then + release_args+=(--prerelease) +fi +release_args+=("${asset_path}#codex-termux") +"${release_args[@]}" diff --git a/scripts/termux-download-release-artifact.sh b/scripts/termux-download-release-artifact.sh new file mode 100755 index 000000000000..2acdba9025a0 --- /dev/null +++ b/scripts/termux-download-release-artifact.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${HEAD_SHA:-}" ]]; then + echo "HEAD_SHA is required." >&2 + exit 1 +fi + +if [[ -z "${PR_ARTIFACT_NAME:-}" ]]; then + echo "PR_ARTIFACT_NAME is required." >&2 + exit 1 +fi + +promoted_dir="${PROMOTED_DIR:-promoted}" + +find_run_id() { + local event="$1" + gh run list \ + --repo "${GITHUB_REPOSITORY}" \ + --workflow rust-release.yml \ + --event "${event}" \ + --status success \ + --commit "${HEAD_SHA}" \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId // empty' +} + +find_artifact_run_id() { + local artifact_name="$1" + + gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=${artifact_name}" \ + | jq -rs \ + --arg artifact_name "${artifact_name}" \ + --arg head_sha "${HEAD_SHA}" \ + ' + [ + .[].artifacts[] + | select(.name == $artifact_name) + | select(.expired == false) + | select(.workflow_run.head_sha == $head_sha) + ] + | sort_by(.created_at) + | reverse + | .[0].workflow_run.id // empty + ' +} + +artifact_name="${PR_ARTIFACT_NAME}" +run_id="$(find_artifact_run_id "${artifact_name}")" +mkdir -p "${promoted_dir}" +if [[ -n "${run_id}" ]] && gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}"; then + : +else + artifact_name="aarch64-linux-android" + run_id="$(find_run_id workflow_dispatch)" + if [[ -z "${run_id}" ]]; then + echo "No successful rust-release run found for ${HEAD_SHA}" >&2 + exit 1 + fi + gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}" +fi + +ls -la "${promoted_dir}" +if [[ -f "${promoted_dir}/SHA256SUMS" ]]; then + (cd "${promoted_dir}" && sha256sum -c SHA256SUMS) +fi +if [[ ! -f "${promoted_dir}/codex-aarch64-linux-android.tar.gz" ]]; then + echo "Expected ${promoted_dir}/codex-aarch64-linux-android.tar.gz in the downloaded artifact." >&2 + exit 1 +fi diff --git a/scripts/termux-find-release-pr.sh b/scripts/termux-find-release-pr.sh new file mode 100755 index 000000000000..3e35fa42eabe --- /dev/null +++ b/scripts/termux-find-release-pr.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -n "${INPUT_PR_NUMBER:-}" || -n "${INPUT_PR_HEAD_SHA:-}" ]]; then + if [[ -z "${INPUT_PR_NUMBER:-}" || -z "${INPUT_PR_HEAD_SHA:-}" ]]; then + echo "workflow_dispatch inputs pr_number and pr_head_sha must be provided together." >&2 + exit 1 + fi + + pr_number="${INPUT_PR_NUMBER}" + head_sha="${INPUT_PR_HEAD_SHA}" + head_ref="" +else + if [[ -z "${RELEASE_BRANCH:-}" || -z "${RELEASE_SHA:-}" ]]; then + echo "RELEASE_BRANCH and RELEASE_SHA are required." >&2 + exit 1 + fi + + pr_json="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${RELEASE_BRANCH}" \ + --state merged \ + --limit 100 \ + --json number,headRefOid,headRefName,mergeCommit \ + --jq "[.[] | select(.mergeCommit.oid == \"${RELEASE_SHA}\")] | sort_by(.number) | reverse | .[0] // empty" + )" + if [[ -z "${pr_json}" ]]; then + echo "Unable to find merged PR for ${RELEASE_SHA} into ${RELEASE_BRANCH}" >&2 + exit 1 + fi + + pr_number="$(jq -r '.number' <<< "${pr_json}")" + head_sha="$(jq -r '.headRefOid // .head.sha' <<< "${pr_json}")" + head_ref="$(jq -r '.headRefName // .head.ref' <<< "${pr_json}")" +fi + +artifact_name="termux-android-pr-${pr_number}-${head_sha}" +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "number=${pr_number}" + echo "head_sha=${head_sha}" + echo "head_ref=${head_ref}" + echo "artifact_name=${artifact_name}" + } >> "${GITHUB_OUTPUT}" +else + printf 'number=%s\nhead_sha=%s\nhead_ref=%s\nartifact_name=%s\n' \ + "${pr_number}" \ + "${head_sha}" \ + "${head_ref}" \ + "${artifact_name}" +fi diff --git a/scripts/termux-read-release-metadata.sh b/scripts/termux-read-release-metadata.sh new file mode 100755 index 000000000000..519a0635a368 --- /dev/null +++ b/scripts/termux-read-release-metadata.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# Parse .github/termux-release.json and emit workflow outputs for deploy or +# promote jobs. TERMUX_RELEASE_ACTION must be "deploy" or "promote". + +set -euo pipefail + +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +metadata=".github/termux-release.json" +action="${TERMUX_RELEASE_ACTION:-deploy}" +case "${action}" in + deploy) + missing_context="deployment" + ;; + promote) + missing_context="promotion" + ;; + *) + echo "TERMUX_RELEASE_ACTION must be deploy or promote." >&2 + exit 1 + ;; +esac + +if [[ ! -f "${metadata}" ]]; then + echo "No ${metadata}; this push is not a Termux release ${missing_context}." + echo "${action}=false" >> "${GITHUB_OUTPUT}" + exit 0 +fi + +upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" +upstream_name="$(jq -r '.upstream_name // .upstream_tag // empty' "${metadata}")" +termux_tag="$(jq -r '.termux_tag // empty' "${metadata}")" +upstream_version="${upstream_tag#rust-v}" +upstream_version="${upstream_version%-termux}" +upstream_prerelease=false +if [[ "${upstream_version}" == *-* ]]; then + upstream_prerelease=true +fi +upstream_html_url="$(jq -r '.upstream_html_url // ""' "${metadata}")" +upstream_repo="$(jq -r '.upstream_repo // "openai/codex"' "${metadata}")" +release_train="$(jq -r '.release_train // ""' "${metadata}")" +if [[ -z "${upstream_tag}" || -z "${termux_tag}" ]]; then + echo "Missing upstream_tag or termux_tag in ${metadata}" >&2 + exit 1 +fi + +release_state="$(TERMUX_TAG="${termux_tag}" "${script_dir}/termux-release-asset-state.sh")" +release_exists="$(awk -F= '$1 == "release_exists" { print $2 }' <<< "${release_state}")" +asset_exists="$(awk -F= '$1 == "asset_exists" { print $2 }' <<< "${release_state}")" + +if [[ "${action}" == "promote" ]]; then + if [[ "${asset_exists}" == "true" ]]; then + echo "${termux_tag} already exists with codex-aarch64-linux-android.tar.gz; skipping promotion." + echo "promote=false" >> "${GITHUB_OUTPUT}" + exit 0 + fi + if [[ "${release_exists}" == "true" ]]; then + echo "${termux_tag} exists but is missing codex-aarch64-linux-android.tar.gz; repairing promotion." + fi +fi + +{ + echo "${action}=true" + echo "upstream_tag=${upstream_tag}" + echo "upstream_name=${upstream_name}" + echo "termux_tag=${termux_tag}" + echo "upstream_prerelease=${upstream_prerelease}" + echo "upstream_html_url=${upstream_html_url}" + echo "upstream_repo=${upstream_repo}" + echo "release_train=${release_train}" + echo "release_exists=${release_exists}" + echo "asset_exists=${asset_exists}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-release-asset-state.sh b/scripts/termux-release-asset-state.sh new file mode 100755 index 000000000000..8694480b6a36 --- /dev/null +++ b/scripts/termux-release-asset-state.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +termux_tag="${1:-${TERMUX_TAG:-}}" +release_repo="${RELEASE_REPO:-${GITHUB_REPOSITORY:-}}" +asset_name="${ASSET_NAME:-codex-aarch64-linux-android.tar.gz}" + +if [[ -z "${termux_tag}" ]]; then + echo "TERMUX_TAG or tag argument is required." >&2 + exit 1 +fi + +if [[ -z "${release_repo}" ]]; then + echo "GITHUB_REPOSITORY or RELEASE_REPO is required." >&2 + exit 1 +fi + +release_exists=false +asset_exists=false + +if gh release view "${termux_tag}" --repo "${release_repo}" >/dev/null 2>&1; then + release_exists=true + release_asset_exists="$( + gh release view "${termux_tag}" \ + --repo "${release_repo}" \ + --json assets \ + | jq -r --arg asset_name "${asset_name}" \ + '.assets | map(.name) | any(. == $asset_name)' + )" + if [[ "${release_asset_exists}" == "true" ]]; then + asset_exists=true + fi +fi + +printf 'release_exists=%s\n' "${release_exists}" +printf 'asset_exists=%s\n' "${asset_exists}" diff --git a/scripts/termux-release-paths.sh b/scripts/termux-release-paths.sh new file mode 100755 index 000000000000..6990f323ec44 --- /dev/null +++ b/scripts/termux-release-paths.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# Sourceable path lists for files owned by the Termux release automation. + +set -euo pipefail + +readonly -a TERMUX_RELEASE_WORKFLOW_PATHS=( + .github/workflows/rust-release.yml + .github/workflows/shell-tool-mcp.yml + .github/workflows/termux-release-checkpoint.yml + .github/workflows/termux-release-deploy.yml + .github/workflows/termux-release-promote.yml +) + +readonly -a TERMUX_RELEASE_BRANCH_SCRIPT_PATHS=( + scripts/termux-configure-git.sh + scripts/termux-create-checkpoint-pr.sh + scripts/termux-create-or-update-mirrored-release.sh + scripts/termux-download-release-artifact.sh + scripts/termux-find-release-pr.sh + scripts/termux-read-release-metadata.sh + scripts/termux-release-asset-state.sh + scripts/termux-release-paths.sh + scripts/termux-resolve-release-ref.sh + scripts/termux-validate-gh-env.sh +) + +readonly -a TERMUX_RELEASE_AUTOMATION_PATHS=( + "${TERMUX_RELEASE_WORKFLOW_PATHS[@]}" + "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +) + +readonly -a TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS=( + "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" + .github/termux-release.json +) + +termux_path_in_list() { + local candidate="$1" + shift + local listed_path + + for listed_path in "$@"; do + [[ "${candidate}" != "${listed_path}" ]] || return 0 + done + return 1 +} + +termux_is_release_automation_path() { + termux_path_in_list "$1" "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" +} + +termux_is_checkpoint_release_only_path() { + termux_path_in_list "$1" "${TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS[@]}" +} diff --git a/scripts/termux-resolve-release-ref.sh b/scripts/termux-resolve-release-ref.sh new file mode 100755 index 000000000000..194773d45bed --- /dev/null +++ b/scripts/termux-resolve-release-ref.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Resolve the release branch and SHA for deploy/promote workflows. + +set -euo pipefail + +: "${GITHUB_EVENT_NAME:?GITHUB_EVENT_NAME is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" + +input_release_branch="${INPUT_RELEASE_BRANCH:-${REQUESTED_RELEASE_BRANCH:-}}" +input_release_sha="${INPUT_RELEASE_SHA:-${REQUESTED_RELEASE_SHA:-}}" + +if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then + if [[ -z "${input_release_branch}" ]]; then + echo "release_branch is required for workflow_dispatch." >&2 + exit 1 + fi + + release_branch="${input_release_branch}" + if [[ -n "${input_release_sha}" ]]; then + git checkout --detach "${input_release_sha}" + release_sha="${input_release_sha}" + else + release_sha="$(git rev-parse HEAD)" + fi +else + : "${GITHUB_REF_NAME:?GITHUB_REF_NAME is required}" + : "${GITHUB_SHA:?GITHUB_SHA is required}" + release_branch="${GITHUB_REF_NAME}" + release_sha="${GITHUB_SHA}" +fi + +{ + echo "branch=${release_branch}" + echo "sha=${release_sha}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-validate-gh-env.sh b/scripts/termux-validate-gh-env.sh new file mode 100755 index 000000000000..02581fdebceb --- /dev/null +++ b/scripts/termux-validate-gh-env.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Lightweight post-toolbox check for jobs that rely on authenticated gh calls. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +command -v gh +gh auth status --hostname github.com + +printf 'GITHUB_REPOSITORY=%s\n' "${GITHUB_REPOSITORY}" +printf 'REPO=%s\n' "${REPO:-}" +printf 'GH_REPO_URL=%s\n' "${GH_REPO_URL:-}" +printf 'GH_WORKFLOW_URL=%s\n' "${GH_WORKFLOW_URL:-}" From 00ce9485a70e7783d5162f6ccc4d5ff4c91b2d92 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 01:17:39 +0000 Subject: [PATCH 22/25] Prepare Termux rust-v0.134.0-alpha.3 --- .github/termux-release.json | 15 + .github/workflows/rust-release.yml | 1798 +++++++---------- .github/workflows/shell-tool-mcp.yml | 461 +++++ .../workflows/termux-release-checkpoint.yml | 103 + .github/workflows/termux-release-deploy.yml | 243 +++ .github/workflows/termux-release-promote.yml | 135 ++ codex-rs/Cargo.toml | 2 +- scripts/termux-configure-git.sh | 40 + scripts/termux-create-checkpoint-pr.sh | 275 +++ ...ermux-create-or-update-mirrored-release.sh | 111 + scripts/termux-download-release-artifact.sh | 78 + scripts/termux-find-release-pr.sh | 53 + scripts/termux-read-release-metadata.sh | 77 + scripts/termux-release-asset-state.sh | 37 + scripts/termux-release-paths.sh | 55 + scripts/termux-resolve-release-ref.sh | 36 + scripts/termux-validate-gh-env.sh | 16 + 17 files changed, 2460 insertions(+), 1075 deletions(-) create mode 100644 .github/termux-release.json create mode 100644 .github/workflows/shell-tool-mcp.yml create mode 100644 .github/workflows/termux-release-checkpoint.yml create mode 100644 .github/workflows/termux-release-deploy.yml create mode 100644 .github/workflows/termux-release-promote.yml create mode 100755 scripts/termux-configure-git.sh create mode 100755 scripts/termux-create-checkpoint-pr.sh create mode 100755 scripts/termux-create-or-update-mirrored-release.sh create mode 100755 scripts/termux-download-release-artifact.sh create mode 100755 scripts/termux-find-release-pr.sh create mode 100755 scripts/termux-read-release-metadata.sh create mode 100755 scripts/termux-release-asset-state.sh create mode 100755 scripts/termux-release-paths.sh create mode 100755 scripts/termux-resolve-release-ref.sh create mode 100755 scripts/termux-validate-gh-env.sh diff --git a/.github/termux-release.json b/.github/termux-release.json new file mode 100644 index 000000000000..557ac08657ea --- /dev/null +++ b/.github/termux-release.json @@ -0,0 +1,15 @@ +{ + "upstream_repo": "openai/codex", + "upstream_tag": "rust-v0.134.0-alpha.3", + "upstream_name": "0.134.0-alpha.3", + "upstream_html_url": "https://github.com/openai/codex/releases/tag/rust-v0.134.0-alpha.3", + "upstream_target": "main", + "upstream_release_id": "328195858", + "upstream_prerelease": true, + "release_train": "0.134.0", + "release_branch": "release/0.134.0", + "work_branch": "upstream/rust-v0.134.0", + "patch_branch": "wallentx/termux-target", + "patch_source_sha": "f9ffdd551125e12530a5abdee255565bae052c6a", + "termux_tag": "rust-v0.134.0-alpha.3-termux" +} diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index c55337ecfe6e..56d0977e6e67 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -4,46 +4,49 @@ # git tag -a rust-v0.1.0 -m "Release 0.1.0" # git push origin rust-v0.1.0 # ``` -# -# To use external macOS signing, manually dispatch `release_mode=build_unsigned`, -# sign the unsigned macOS artifacts in a secure enclave, upload the signed handoff -# archive as a GitHub Release asset, then manually dispatch -# `release_mode=promote_signed` with `unsigned_run_id` and `signed_macos_asset`. -# The signed handoff archive should contain target or artifact directories such -# as `aarch64-apple-darwin/` with signed binaries. name: rust-release on: - push: - tags: - - "rust-v*.*.*" + pull_request: + branches: + - "release/**" workflow_dispatch: inputs: - release_mode: - description: "build_unsigned creates unsigned macOS handoff artifacts; promote_signed finishes a release from signed macOS handoff artifacts." + source_ref: + description: "Branch, tag, or commit SHA to build (code only; keeps .github from dispatched ref)" required: false + type: string + default: "" + build_target: + description: "Target to build" + required: true type: choice - default: build_unsigned + default: all options: - - build_unsigned - - promote_signed - sign_macos: - description: "Deprecated compatibility input; use release_mode instead." - required: false - type: boolean - default: false - unsigned_run_id: - description: "For promote_signed: workflow run id from the build_unsigned run." - required: false - type: string - signed_macos_asset: - description: "For promote_signed: exact GitHub Release asset name containing signed macOS handoff artifacts." - required: false - type: string - signed_macos_sha256: - description: "For promote_signed: optional SHA-256 of signed_macos_asset." - required: false - type: string + - all + - aarch64-apple-darwin + - x86_64-apple-darwin + - x86_64-unknown-linux-musl + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-musl + - aarch64-unknown-linux-gnu + - aarch64-linux-android + - x86_64-pc-windows-msvc + - aarch64-pc-windows-msvc + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read concurrency: group: ${{ github.workflow }} @@ -51,72 +54,26 @@ concurrency: jobs: tag-check: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - uses: dtolnay/rust-toolchain@1.92 - name: Validate tag matches Cargo.toml version shell: bash - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - REQUESTED_SIGN_MACOS: ${{ inputs.sign_macos }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} run: | set -euo pipefail echo "::group::Tag validation" - case "${RELEASE_MODE}" in - signed) - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - echo "❌ Manual rust-release runs must use release_mode=build_unsigned or release_mode=promote_signed" - exit 1 - fi - ;; - build_unsigned) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=build_unsigned is only valid for manual runs" - exit 1 - fi - ;; - promote_signed) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=promote_signed is only valid for manual runs" - exit 1 - fi - if [[ ! "${UNSIGNED_RUN_ID}" =~ ^[0-9]+$ ]]; then - echo "❌ release_mode=promote_signed requires unsigned_run_id to be a workflow run id" - exit 1 - fi - if [[ -z "${SIGNED_MACOS_ASSET}" ]]; then - echo "❌ release_mode=promote_signed requires signed_macos_asset" - exit 1 - fi - if [[ "${SIGNED_MACOS_ASSET}" == */* || "${SIGNED_MACOS_ASSET}" == *"*"* || "${SIGNED_MACOS_ASSET}" == *"?"* || "${SIGNED_MACOS_ASSET}" == *"["* ]]; then - echo "❌ signed_macos_asset must be an exact release asset name, not a path or glob" - exit 1 - fi - if [[ "${UNSIGNED_RUN_ID}" == "${GITHUB_RUN_ID}" ]]; then - echo "❌ unsigned_run_id must refer to the earlier build_unsigned run, not this run" - exit 1 - fi - ;; - *) - echo "❌ Unknown release_mode '${RELEASE_MODE}'" - exit 1 - ;; - esac - - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${REQUESTED_SIGN_MACOS}" == "true" ]]; then - echo "::warning title=Deprecated sign_macos input ignored::Use release_mode=build_unsigned or release_mode=promote_signed instead." - fi - # 1. Must be a tag and match the regex [[ "${GITHUB_REF_TYPE}" == "tag" ]] \ || { echo "❌ Not a tag push"; exit 1; } - [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \ + [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?(-termux)?$ ]] \ || { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; } # 2. Extract versions @@ -131,115 +88,198 @@ jobs: echo "✅ Tag and Cargo.toml agree (${tag_ver})" echo "::endgroup::" - build: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} + select-build-matrix: + if: always() && (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') needs: tag-check - name: Build - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: ${{ matrix.runs_on || matrix.runner }} - # Release builds can take a long time, so leave some headroom to avoid - # having to restart the full workflow due to a timeout. - timeout-minutes: 90 - permissions: - contents: read - id-token: write + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.select.outputs.matrix }} + build_version: ${{ steps.select.outputs.build_version }} + steps: + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - id: select + shell: bash + env: + REPO_OWNER: ${{ github.repository_owner }} + EVENT_NAME: ${{ github.event_name }} + BUILD_TARGET: ${{ inputs.build_target }} + run: | + set -euo pipefail + + matrix='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04","target":"aarch64-linux-android"}, + {"runner":"windows-latest","target":"x86_64-pc-windows-msvc"}, + {"runner":"windows-11-arm","target":"aarch64-pc-windows-msvc"} + ]' + + if [[ "${REPO_OWNER}" != "openai" ]]; then + matrix="$(jq -c '[.[] | select(.runner | startswith("macos") | not)]' <<< "${matrix}")" + fi + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + matrix="$(jq -c '[.[] | select(.target == "aarch64-linux-android")]' <<< "${matrix}")" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" && -n "${BUILD_TARGET}" && "${BUILD_TARGET}" != "all" ]]; then + matrix="$(jq -c --arg build_target "${BUILD_TARGET}" '[.[] | select(.target == $build_target)]' <<< "${matrix}")" + fi + + if [[ "$(jq 'length' <<< "${matrix}")" -eq 0 ]]; then + echo "No build targets selected after applying owner/dispatch filters." >&2 + exit 1 + fi + + echo "matrix={\"include\":${matrix}}" >> "$GITHUB_OUTPUT" + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + metadata=".github/termux-release.json" + if [[ ! -f "${metadata}" ]]; then + echo "${metadata} is required for release train PR builds" >&2 + exit 1 + fi + upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" + if [[ -z "${upstream_tag}" || "${upstream_tag}" != rust-v* ]]; then + echo "Unable to determine upstream rust tag from ${metadata}" >&2 + exit 1 + fi + build_version="${upstream_tag#rust-v}" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then + latest_release_tag="$( + GH_TOKEN="${{ github.token }}" gh api repos/openai/codex/releases/latest --jq '.tag_name' + )" + + if [[ -z "${latest_release_tag}" || "${latest_release_tag}" != rust-v* ]]; then + echo "Unable to determine latest stable release tag from openai/codex" >&2 + exit 1 + fi + + latest_release_version="${latest_release_tag#rust-v}" + build_version="${latest_release_version}-dev" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + fi + + build: + if: >- + always() && + (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') && + needs.select-build-matrix.result == 'success' + needs: + - tag-check + - select-build-matrix + name: Build - ${{ matrix.runner }} - ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + permissions: write-all + timeout-minutes: 120 defaults: run: working-directory: codex-rs env: - # 2026-03-04: temporarily change releases to use thin LTO because - # Ubuntu ARM is timing out at 60 minutes. - CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' }} + CODEX_BWRAP_ENABLE_FFI: ${{ contains(matrix.target, 'unknown-linux') && '1' || '0' }} + CODEX_BUILD_VERSION: ${{ needs.select-build-matrix.outputs.build_version }} + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" strategy: fail-fast: false - matrix: - include: - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - # Release artifacts intentionally ship MUSL-linked Linux binaries. - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - bundle: primary - artifact_name: x86_64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - bundle: app-server - artifact_name: x86_64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - bundle: primary - artifact_name: aarch64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - bundle: app-server - artifact_name: aarch64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" + matrix: ${{ fromJson(needs.select-build-matrix.outputs.matrix) }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + with: + verbose: true + - name: Checkout selected source ref + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + uses: actions/checkout@v6 with: - persist-credentials: false - - name: Print runner specs (Linux) - if: ${{ runner.os == 'Linux' }} + ref: ${{ inputs.source_ref }} + path: __source__ + fetch-depth: 1 + - name: Overlay selected source ref (excluding .github) + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + shell: bash + working-directory: ${{ github.workspace }} + run: | + set -euo pipefail + echo "Using source ref: ${{ inputs.source_ref }}" + + # Keep workflow/actions from the dispatch ref while replacing all other files. + find . -mindepth 1 -maxdepth 1 \ + ! -name .git \ + ! -name .github \ + ! -name __source__ \ + -exec rm -rf -- {} + + tar --exclude='.github' -C __source__ -cf - . | tar -xf - + rm -rf __source__ + + - name: Apply selected build version + if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} shell: bash + working-directory: ${{ github.workspace }} run: | set -euo pipefail - cpu_model="$(lscpu | awk -F: '/Model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}')" - total_ram="$(awk '/MemTotal/ {printf "%.1f GiB\n", $2 / 1024 / 1024}' /proc/meminfo)" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(uname -a)" - echo "CPU model: ${cpu_model}" - echo "Logical CPUs: $(nproc)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . - - name: Print runner specs (macOS) - if: ${{ runner.os == 'macOS' }} + if [[ -z "${CODEX_BUILD_VERSION}" ]]; then + echo "CODEX_BUILD_VERSION is empty for workflow_dispatch" >&2 + exit 1 + fi + echo "Using build version: ${CODEX_BUILD_VERSION}" + BUILD_VERSION="${CODEX_BUILD_VERSION}" perl -0777 -i -pe \ + 's/(\[workspace\.package\][^\[]*?version = ")([^"]+)(")/${1}$ENV{BUILD_VERSION}$3/s' \ + codex-rs/Cargo.toml + + grep -n "^\[workspace.package\]\|^version = " codex-rs/Cargo.toml | head -n 4 + + - name: Select Android NDK + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail - total_ram="$(sysctl -n hw.memsize | awk '{printf "%.1f GiB\n", $1 / 1024 / 1024 / 1024}')" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(sw_vers -productName) $(sw_vers -productVersion)" - echo "Hardware model: $(sysctl -n hw.model)" - echo "CPU architecture: $(uname -m)" - echo "Logical CPUs: $(sysctl -n hw.logicalcpu)" - echo "Physical CPUs: $(sysctl -n hw.physicalcpu)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . + + fallback_version="29.0.13113456" + ndk_root="${ANDROID_HOME}/ndk" + selected_ndk="" + + if [[ -d "${ndk_root}" ]]; then + while IFS= read -r candidate; do + toolchain="${candidate}/toolchains/llvm/prebuilt/linux-x86_64" + if [[ -x "${toolchain}/bin/aarch64-linux-android24-clang" ]]; then + selected_ndk="${candidate}" + break + fi + done < <(find "${ndk_root}" -mindepth 1 -maxdepth 1 -type d | sort -Vr) + fi + + if [[ -z "${selected_ndk}" ]]; then + echo "No usable preinstalled Android NDK found; installing ${fallback_version}" + yes | sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses || true + sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" "ndk;${fallback_version}" + selected_ndk="${ndk_root}/${fallback_version}" + fi + + if [[ ! -x "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" ]]; then + echo "Selected Android NDK does not contain a usable aarch64 API 24 clang: ${selected_ndk}" >&2 + exit 1 + fi + + echo "Selected Android NDK: ${selected_ndk}" + "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" --version + echo "ANDROID_NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" + echo "NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" - name: Install Linux bwrap build dependencies - if: ${{ runner.os == 'Linux' }} + if: ${{ runner.os == 'Linux' && matrix.target != 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail @@ -254,9 +294,173 @@ jobs: sudo apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 fi - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@1.93 with: targets: ${{ matrix.target }} + - name: Set up sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + uses: mozilla-actions/sccache-action@main + - name: Enable sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + shell: bash + run: | + set -euo pipefail + echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_VERSION=android-release-${{ matrix.target }}" >> "$GITHUB_ENV" + echo "SCCACHE_CACHE_SIZE=5G" >> "$GITHUB_ENV" + - name: Ensure Rust target is installed + shell: bash + run: | + set -euo pipefail + rustup target add "${{ matrix.target }}" + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android build environment + shell: bash + run: | + set -euo pipefail + ndk="${ANDROID_NDK_HOME}" + target="${{ matrix.target }}" + + # Set up the Android toolchain + toolchain="${ndk}/toolchains/llvm/prebuilt/linux-x86_64" + + # Use API level 24 to follow termux-build convention + api_level="24" + + # Configure Cargo to use the NDK linker + cargo_target_var="CARGO_TARGET_${target^^}_LINKER" + cargo_target_var="${cargo_target_var//-/_}" + echo "${cargo_target_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + # Set CC and AR for the target + target_cc_var="CC_${target}" + target_cc_var="${target_cc_var//-/_}" + echo "${target_cc_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + target_cxx_var="CXX_${target}" + target_cxx_var="${target_cxx_var//-/_}" + echo "${target_cxx_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang++" >> "$GITHUB_ENV" + + target_ar_var="AR_${target}" + target_ar_var="${target_ar_var//-/_}" + echo "${target_ar_var}=${toolchain}/bin/llvm-ar" >> "$GITHUB_ENV" + + # Add toolchain to PATH + echo "${toolchain}/bin" >> "$GITHUB_PATH" + + tls_align_source="${RUNNER_TEMP}/codex-android-tls-align.S" + tls_align_object="${RUNNER_TEMP}/codex-android-tls-align.o" + cat > "${tls_align_source}" <<'EOF' + .section .tdata.codex_android_tls_align,"awT",%progbits + .p2align 6 + .globl __codex_android_tls_align + .hidden __codex_android_tls_align + .type __codex_android_tls_align, %object + __codex_android_tls_align: + .byte 0 + .size __codex_android_tls_align, 1 + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${tls_align_source}" -o "${tls_align_object}" + + v8_compat_source="${RUNNER_TEMP}/codex-android-v8-compat.c" + v8_compat_object="${RUNNER_TEMP}/codex-android-v8-compat.o" + cat > "${v8_compat_source}" <<'EOF' + #include + #include + + extern int posix_memalign(void **memptr, size_t alignment, size_t size); + extern double strtod(const char *nptr, char **endptr); + extern float strtof(const char *nptr, char **endptr); + + void *aligned_alloc(size_t alignment, size_t size) { + void *ptr = 0; + if (posix_memalign(&ptr, alignment, size) != 0) { + return 0; + } + return ptr; + } + + double strtod_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtod(nptr, endptr); + } + + float strtof_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtof(nptr, endptr); + } + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${v8_compat_source}" -o "${v8_compat_object}" + + builtins_archive="$( + find "${toolchain}/lib/clang" \ + -path "*/lib/linux/libclang_rt.builtins-aarch64-android.a" \ + -print -quit + )" + if [[ -z "${builtins_archive}" ]]; then + echo "Could not find Android compiler-rt builtins archive under ${toolchain}/lib/clang" >&2 + exit 1 + fi + builtins_dir="$(dirname "${builtins_archive}")" + + # Provide compatibility symbols and C++ ABI needed by the Android rusty_v8 archive. + rustflags_var="CARGO_TARGET_${target^^}_RUSTFLAGS" + rustflags_var="${rustflags_var//-/_}" + echo "${rustflags_var}=-C link-arg=${tls_align_object} -C link-arg=${v8_compat_object} -C link-arg=-Wl,-u,__codex_android_tls_align -C link-arg=-L${builtins_dir} -C link-arg=-lclang_rt.builtins-aarch64-android -C link-arg=-lc++abi -C link-arg=-Wl,-z,max-page-size=65536" >> "$GITHUB_ENV" + + + # Configure for vendored OpenSSL build + echo "CARGO_BUILD_TARGET_APPLIES_TO_HOST=false" >> "$GITHUB_ENV" + echo "CARGO_BUILD_TARGET=${{ matrix.target }}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android rusty_v8 artifact overrides + env: + GH_TOKEN: ${{ github.token }} + TARGET: ${{ matrix.target }} + shell: bash + run: | + set -euo pipefail + binding_dir="${RUNNER_TEMP}/rusty_v8" + archive="${binding_dir}/librusty_v8_release_${TARGET}.a.gz" + binding_path="${binding_dir}/src_binding_release_${TARGET}.rs" + checksums_path="${binding_dir}/rusty_v8_release_${TARGET}.sha256" + artifact_repository="wallentx/codex-termux" + release_tag="rusty-v8-v147.4.0" + mkdir -p "${binding_dir}" + echo "Downloading Android rusty_v8 artifacts from ${artifact_repository}@${release_tag}" + gh release download "${release_tag}" \ + --repo "${artifact_repository}" \ + --dir "${binding_dir}" \ + --pattern "librusty_v8_release_${TARGET}.a.gz" \ + --pattern "src_binding_release_${TARGET}.rs" \ + --pattern "rusty_v8_release_${TARGET}.sha256" + (cd "${binding_dir}" && sha256sum -c "$(basename "${checksums_path}")") + echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV" + echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Install termux-elf-cleaner + shell: bash + run: | + set -euo pipefail + version="v3.0.1" + src_dir="${RUNNER_TEMP}/termux-elf-cleaner-src" + build_dir="${RUNNER_TEMP}/termux-elf-cleaner-build" + bin_dir="${RUNNER_TEMP}/termux-elf-cleaner-bin" + + rm -rf "${src_dir}" "${build_dir}" "${bin_dir}" + mkdir -p "${src_dir}" "${build_dir}" "${bin_dir}" + + curl -fsSL "https://github.com/termux/termux-elf-cleaner/archive/refs/tags/${version}.tar.gz" \ + | tar -xzf - --strip-components=1 -C "${src_dir}" + + cmake -S "${src_dir}" -B "${build_dir}" -DCMAKE_BUILD_TYPE=Release + cmake --build "${build_dir}" --parallel + + install "${build_dir}/termux-elf-cleaner" "${bin_dir}/termux-elf-cleaner" + echo "${bin_dir}" >> "$GITHUB_PATH" - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Use hermetic Cargo home (musl) @@ -269,12 +473,25 @@ jobs: echo "${cargo_home}/bin" >> "$GITHUB_PATH" : > "${cargo_home}/config.toml" + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ${{ github.workspace }}/.cargo-home/bin/ + ${{ github.workspace }}/.cargo-home/registry/index/ + ${{ github.workspace }}/.cargo-home/registry/cache/ + ${{ github.workspace }}/.cargo-home/git/db/ + ${{ github.workspace }}/codex-rs/target/ + key: cargo-${{ matrix.runner }}-${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }} + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install Zig - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + uses: mlugg/setup-zig@v2 with: version: 0.14.0 - use-cache: false - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install musl build tools @@ -310,12 +527,6 @@ jobs: shell: bash run: | set -euo pipefail - # Avoid problematic aws-lc jitter entropy code path on musl builders. - echo "AWS_LC_SYS_NO_JITTER_ENTROPY=1" >> "$GITHUB_ENV" - target_no_jitter="AWS_LC_SYS_NO_JITTER_ENTROPY_${{ matrix.target }}" - target_no_jitter="${target_no_jitter//-/_}" - echo "${target_no_jitter}=1" >> "$GITHUB_ENV" - # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. echo "RUSTFLAGS=" >> "$GITHUB_ENV" echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" @@ -340,94 +551,70 @@ jobs: echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" - - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }} - name: Configure musl rusty_v8 artifact overrides and verify checksums - uses: ./.github/actions/setup-rusty-v8-musl - with: - target: ${{ matrix.target }} - - - if: ${{ contains(matrix.target, 'linux') && matrix.bundle == 'primary' }} - name: Build bwrap and export digest + - name: Cargo build shell: bash run: | set -euo pipefail - target="${{ matrix.target }}" - cargo build --target "$target" --release --timings --bin bwrap - bwrap_path="target/${target}/release/bwrap" - if [[ ! -f "$bwrap_path" ]]; then - echo "bwrap binary ${bwrap_path} not found" - exit 1 + if [[ "${{ matrix.target }}" == 'aarch64-linux-android' ]]; then + export CARGO_BUILD_JOBS=4 + export CARGO_PROFILE_RELEASE_LTO=thin + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 + cargo build --target ${{ matrix.target }} --release --bin codex + exit 0 fi - digest="$(sha256sum "$bwrap_path" | awk '{print $1}')" - echo "CODEX_BWRAP_SHA256=${digest}" >> "$GITHUB_ENV" - echo "Built bwrap ${bwrap_path} with sha256:${digest}" - - - name: Cargo build + if [[ "${{ contains(matrix.target, 'windows') }}" == 'true' ]]; then + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy --bin codex-windows-sandbox-setup --bin codex-command-runner + else + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy + fi + - name: sccache stats (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash - run: | - build_args=() - for binary in ${{ matrix.binaries }}; do - build_args+=(--bin "$binary") - done - echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}" - cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}" + run: ${SCCACHE_PATH:-sccache} --show-stats || true - - name: Upload Cargo timings - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }} - path: codex-rs/target/**/cargo-timings/cargo-timing.html - if-no-files-found: warn - - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Stage unsigned macOS artifacts + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Normalize Android ELF for Termux shell: bash run: | set -euo pipefail - - target="${{ matrix.target }}" - release_dir="target/${target}/release" - dest="unsigned-dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - unsigned_name="${binary}-${target}-unsigned" - unsigned_path="${dest}/${unsigned_name}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 + for binary in codex codex-responses-api-proxy; do + binary_path="target/${{ matrix.target }}/release/${binary}" + if [[ -f "${binary_path}" ]]; then + termux-elf-cleaner --api-level 24 "${binary_path}" + chmod +x "${binary_path}" + # if [[ "${binary}" == "codex" ]]; then + # # Patch PT_TLS (0x7) to PT_NULL (0x0) at offset 400 to bypass Bionic alignment checks. + # printf '\x00\x00\x00\x00' | dd of="${binary_path}" bs=1 seek=400 count=4 conv=notrunc + # fi fi - - cp "${binary_path}" "${unsigned_path}" - tar -C "$dest" -czf "${unsigned_path}.tar.gz" "${unsigned_name}" - zstd -T0 -19 --rm "${unsigned_path}" done - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Upload unsigned macOS artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }}-unsigned - path: codex-rs/unsigned-dist/${{ matrix.target }}/* - if-no-files-found: error - - - if: ${{ contains(matrix.target, 'linux') }} + - if: ${{ contains(matrix.target, 'linux') && !contains(matrix.target, 'android') && github.repository_owner == 'openai' }} name: Cosign Linux artifacts uses: ./.github/actions/linux-code-sign with: target: ${{ matrix.target }} artifacts-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release - binaries: ${{ matrix.binaries }} - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS == 'true' }} + - if: ${{ contains(matrix.target, 'windows') && github.repository_owner == 'openai' }} + name: Sign Windows binaries with Azure Trusted Signing + uses: ./.github/actions/windows-code-sign + with: + target: ${{ matrix.target }} + client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID }} + endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} + account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} + + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (binaries) uses: ./.github/actions/macos-code-sign with: target: ${{ matrix.target }} - binaries: ${{ matrix.binaries }} sign-binaries: "true" sign-dmg: "false" apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }} @@ -436,7 +623,7 @@ jobs: apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: Build macOS dmg shell: bash run: | @@ -451,17 +638,23 @@ jobs: # The previous "MacOS code signing (binaries)" step signs + notarizes the # built artifacts in `${release_dir}`. This step packages *those same* # signed binaries into a dmg. + codex_binary_path="${release_dir}/codex" + proxy_binary_path="${release_dir}/codex-responses-api-proxy" + rm -rf "$dmg_root" mkdir -p "$dmg_root" - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 - fi - ditto "${binary_path}" "${dmg_root}/${binary}" - done + if [[ ! -f "$codex_binary_path" ]]; then + echo "Binary $codex_binary_path not found" + exit 1 + fi + if [[ ! -f "$proxy_binary_path" ]]; then + echo "Binary $proxy_binary_path not found" + exit 1 + fi + + ditto "$codex_binary_path" "${dmg_root}/codex" + ditto "$proxy_binary_path" "${dmg_root}/codex-responses-api-proxy" rm -f "$dmg_path" hdiutil create \ @@ -476,7 +669,7 @@ jobs: exit 1 fi - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (dmg) uses: ./.github/actions/macos-code-sign with: @@ -490,121 +683,65 @@ jobs: apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - name: Stage artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | dest="dist/${{ matrix.target }}" mkdir -p "$dest" - for binary in ${{ matrix.binaries }}; do - cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" - if [[ "${{ matrix.target }}" == *linux* ]]; then - cp "target/${{ matrix.target }}/release/${binary}.sigstore" \ - "$dest/${binary}-${{ matrix.target }}.sigstore" + if [[ "${{ matrix.runner }}" == windows* ]]; then + cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe" + else + # For Android, we want the binary to be named just 'codex' in the archive. + if [[ "${{ matrix.target }}" == "aarch64-linux-android" ]]; then + cp target/${{ matrix.target }}/release/codex "$dest/codex" + else + cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" + fi + if [[ -f target/${{ matrix.target }}/release/codex-responses-api-proxy ]]; then + cp target/${{ matrix.target }}/release/codex-responses-api-proxy "$dest/codex-responses-api-proxy-${{ matrix.target }}" fi - done - - if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.bundle }}" == "primary" ]]; then - bundle_root="${RUNNER_TEMP}/codex-${{ matrix.target }}-bundle" - rm -rf "$bundle_root" - mkdir -p "$bundle_root/codex-resources" - cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex" - cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap" - chmod 0755 "$bundle_root/codex" "$bundle_root/codex-resources/bwrap" - tar -C "$bundle_root" -cf - codex codex-resources/bwrap | - zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst" fi - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" + if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.target }}" != *android* ]]; then + cp target/${{ matrix.target }}/release/codex.sigstore "$dest/codex-${{ matrix.target }}.sigstore" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.sigstore "$dest/codex-responses-api-proxy-${{ matrix.target }}.sigstore" fi - - name: Build Codex package archive - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "target/${TARGET}/release" \ - --archive-dir "dist/${TARGET}" - - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - aarch64-unknown-linux-musl) - platform_tag="manylinux_2_17_aarch64" - ;; - x86_64-unknown-linux-musl) - platform_tag="manylinux_2_17_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - # Do not install into the runner's system Python; macOS runners mark - # the Homebrew Python as externally managed under PEP 668. - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - stage_runtime_args=( - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" - stage-runtime - "$stage_dir" - "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/codex" - --codex-version "${GITHUB_REF_NAME}" - --platform-tag "$platform_tag" - ) - if [[ "${{ matrix.target }}" == *linux* ]]; then - # Keep bwrap in the runtime wheel so Linux sandbox fallback behavior - # matches the standalone release bundle on hosts without system bwrap. - stage_runtime_args+=( - --resource-binary - "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/bwrap" - ) + if [[ "${{ matrix.target }}" == *apple-darwin ]]; then + cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" fi - python3 "${stage_runtime_args[@]}" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error + - if: ${{ matrix.runner == 'windows-11-arm' }} + name: Install zstd + shell: powershell + run: choco install -y zstandard - name: Compress artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | # Path that contains the uncompressed binaries for the current # ${{ matrix.target }} dest="dist/${{ matrix.target }}" + repo_root=$PWD + + # We want to ship the raw Windows executables in the GitHub Release + # in addition to the compressed archives. Keep the originals for + # Windows targets; remove them elsewhere to limit the number of + # artifacts that end up in the GitHub Release. + keep_originals=false + if [[ "${{ matrix.runner }}" == windows* ]]; then + keep_originals=true + fi # For compatibility with environments that lack the `zstd` tool we - # additionally create a `.tar.gz` alongside every binary we publish. - # The end result is: + # additionally create a `.tar.gz` for all platforms and `.zip` for + # Windows alongside every single binary that we publish. The end result is: # codex-.zst (existing) # codex-.tar.gz (new) + # codex-.zip (only for Windows) # 1. Produce a .tar.gz for every file in the directory *before* we # run `zstd --rm`, because that flag deletes the original files. @@ -612,7 +749,7 @@ jobs: base="$(basename "$f")" # Skip files that are already archives (shouldn't happen, but be # safe). - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then + if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then continue fi @@ -622,344 +759,239 @@ jobs: fi # Create per-binary tar.gz - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - - # Also create .zst and remove the uncompressed binaries to keep - # non-Windows artifact directories small. - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - with: - name: ${{ matrix.artifact_name }} - # Upload the per-binary .zst files, .tar.gz equivalents, and any - # prebuilt archives staged above. - path: | - codex-rs/dist/${{ matrix.target }}/* - - stage-signed-macos: - if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode == 'promote_signed' }} - needs: tag-check - name: Stage signed macOS handoff - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: macos-15-xlarge - timeout-minutes: 30 - permissions: - contents: read - defaults: - run: - working-directory: codex-rs + # For Android, we want the archive to have the target suffix even though the binary is just 'codex'. + archive_name="${base}" + if [[ "${{ matrix.target }}" == "aarch64-linux-android" && "${base}" == "codex" ]]; then + archive_name="codex-${{ matrix.target }}" + fi + tar -C "$dest" -czf "$dest/${archive_name}.tar.gz" "$base" + + # Create zip archive for Windows binaries + # Must run from inside the dest dir so 7z won't + # embed the directory path inside the zip. + if [[ "${{ matrix.runner }}" == windows* ]]; then + if [[ "$base" == "codex-${{ matrix.target }}.exe" ]]; then + # Bundle the sandbox helper binaries into the main codex zip so + # WinGet installs include the required helpers next to codex.exe. + # Fall back to the single-binary zip if the helpers are missing + # to avoid breaking releases. + bundle_dir="$(mktemp -d)" + runner_src="$dest/codex-command-runner-${{ matrix.target }}.exe" + setup_src="$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + if [[ -f "$runner_src" && -f "$setup_src" ]]; then + cp "$dest/$base" "$bundle_dir/$base" + cp "$runner_src" "$bundle_dir/codex-command-runner.exe" + cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" + # Use an absolute path so bundle zips land in the real dist + # dir even when 7z runs from a temp directory. + (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) + else + echo "warning: missing sandbox binaries; falling back to single-binary zip" + echo "warning: expected $runner_src and $setup_src" + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + rm -rf "$bundle_dir" + else + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + fi - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" + # Also create .zst (existing behaviour) *and* remove the original + # uncompressed binary to keep the directory small. + zstd_args=(-T0 -19) + if [[ "${keep_originals}" == false ]]; then + zstd_args+=(--rm) + fi - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + if [[ "${archive_name}" != "${base}" ]]; then + zstd "${zstd_args[@]}" "$dest/$base" -o "$dest/${archive_name}.zst" + else + zstd "${zstd_args[@]}" "$dest/$base" + fi + done - - name: Download signed macOS handoff + - name: Add Termux release metadata + if: ${{ (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && matrix.target == 'aarch64-linux-android' }} shell: bash - env: - GH_TOKEN: ${{ github.token }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - SIGNED_MACOS_SHA256: ${{ inputs.signed_macos_sha256 }} run: | set -euo pipefail - - download_dir="${RUNNER_TEMP}/signed-macos-download" - handoff_dir="${RUNNER_TEMP}/signed-macos-handoff" - rm -rf "$download_dir" "$handoff_dir" - mkdir -p "$download_dir" "$handoff_dir" - - gh release download "$GITHUB_REF_NAME" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern "$SIGNED_MACOS_ASSET" \ - --dir "$download_dir" - - asset_count="$(find "$download_dir" -maxdepth 1 -type f | wc -l | tr -d '[:space:]')" - if [[ "$asset_count" != "1" ]]; then - echo "Expected exactly one signed macOS handoff asset named ${SIGNED_MACOS_ASSET}; found ${asset_count}" - find "$download_dir" -maxdepth 1 -type f -print - exit 1 - fi - - asset_path="$(find "$download_dir" -maxdepth 1 -type f -print -quit)" - if [[ -n "${SIGNED_MACOS_SHA256}" ]]; then - expected_sha="$(printf '%s' "$SIGNED_MACOS_SHA256" | tr '[:upper:]' '[:lower:]')" - actual_sha="$(shasum -a 256 "$asset_path" | awk '{print $1}')" - if [[ "$actual_sha" != "$expected_sha" ]]; then - echo "signed_macos_sha256 mismatch for ${SIGNED_MACOS_ASSET}" - echo "expected: ${expected_sha}" - echo "actual: ${actual_sha}" - exit 1 - fi + if [[ ! -f "${GITHUB_WORKSPACE}/.github/termux-release.json" ]]; then + echo "No Termux release metadata found; skipping metadata attachment." + exit 0 fi + dest="dist/${{ matrix.target }}" + cp "${GITHUB_WORKSPACE}/.github/termux-release.json" "${dest}/termux-release.json" + ( + cd "${dest}" + sha256sum ./* > SHA256SUMS + ) - asset_name="$(basename "$asset_path")" - case "$asset_name" in - *.tar.zst) - zstd -dc "$asset_path" | tar -C "$handoff_dir" -xf - - ;; - *.tar.gz|*.tgz) - tar -C "$handoff_dir" -xzf "$asset_path" - ;; - *.zip) - ditto -x -k "$asset_path" "$handoff_dir" - ;; - *) - echo "Unsupported signed macOS handoff archive format: ${asset_name}" - exit 1 - ;; - esac - - echo "SIGNED_MACOS_HANDOFF_DIR=$handoff_dir" >> "$GITHUB_ENV" + - id: upload-artifact + uses: actions/upload-artifact@v6 + with: + name: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + # Upload the per-binary .zst files as well as the new .tar.gz + # equivalents we generated in the previous step. + path: | + codex-rs/dist/${{ matrix.target }}/* - - name: Stage signed macOS artifacts + - name: Publish Termux artifact notification + if: ${{ github.event_name == 'pull_request' && startsWith(github.base_ref, 'release/') && matrix.target == 'aarch64-linux-android' }} shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_ID: ${{ steps.upload-artifact.outputs.artifact-id }} + ARTIFACT_NAME: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + PR_NUMBER: ${{ github.event.pull_request.number }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | set -euo pipefail - - target="${{ matrix.target }}" - artifact_name="${{ matrix.artifact_name }}" - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${target}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - fi - if [[ ! -d "$source_dir" ]]; then - echo "Signed macOS handoff is missing ${artifact_name}/" - echo "Expected either:" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${target}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - find "$SIGNED_MACOS_HANDOFF_DIR" -maxdepth 3 -type f -print + if [[ -z "${ARTIFACT_ID}" ]]; then + echo "upload-artifact did not return an artifact id" >&2 exit 1 fi - dest="dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - source_path="${source_dir}/${binary}" - if [[ ! -f "$source_path" ]]; then - source_path="${source_dir}/${binary}-${target}" - fi - if [[ ! -f "$source_path" ]]; then - echo "Signed macOS handoff is missing ${binary} for ${artifact_name}" - exit 1 - fi - - release_path="${dest}/${binary}-${target}" - ditto "$source_path" "$release_path" - chmod 0755 "$release_path" - codesign --verify --strict --verbose=2 "$release_path" - done - - # DMG staging is disabled for signed promotion because we no longer - # distribute DMGs from this release path. Keep the branch here so the - # handoff can opt back in by flipping matrix.build_dmg if needed. - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - dmg_name="codex-${target}.dmg" - dmg_source="${source_dir}/${dmg_name}" - if [[ ! -f "$dmg_source" ]]; then - echo "Signed macOS handoff is missing ${dmg_name} for ${artifact_name}" - exit 1 - fi - - codesign --verify --strict --verbose=2 "$dmg_source" - xcrun stapler validate "$dmg_source" - cp "$dmg_source" "$dest/$dmg_name" + marker="" + artifact_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts/${ARTIFACT_ID}" + run_url="${GH_WORKFLOW_URL:-https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + body_path="${RUNNER_TEMP}/termux-artifact-comment.md" + { + echo "${marker}" + echo "Termux Android artifact ready for testing:" + echo + echo "- Artifact: [${ARTIFACT_NAME}](${artifact_url})" + echo "- Workflow run: [${GITHUB_RUN_ID}](${run_url})" + echo + echo "You must be signed in to GitHub with repository access to download Actions artifacts." + } > "${body_path}" + + existing_comment_id="$( + gh pr view "${PR_NUMBER}" \ + --repo "${GITHUB_REPOSITORY}" \ + --json comments \ + --jq ".comments | map(select(.author.is_bot == true and (.body | contains(\"${marker}\")))) | .[-1].id // \"\"" + )" + if [[ -n "${existing_comment_id}" ]]; then + gh api graphql \ + -f query=' + mutation($id: ID!, $body: String!) { + updateIssueComment(input: {id: $id, body: $body}) { + issueComment { + id + } + } + } + ' \ + -f id="${existing_comment_id}" \ + -f body="$(cat "${body_path}")" \ + >/dev/null + else + gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body-file "${body_path}" fi - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - python3 \ - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \ - stage-runtime \ - "$stage_dir" \ - "${GITHUB_WORKSPACE}/codex-rs/dist/${{ matrix.target }}/codex-${{ matrix.target }}" \ - --codex-version "${GITHUB_REF_NAME}" \ - --platform-tag "$platform_tag" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - - name: Build Codex package archive - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "dist/${TARGET}" \ - --archive-dir "dist/${TARGET}" \ - --target-suffixed-entrypoint - - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error - - - name: Compress artifacts - shell: bash - run: | - set -euo pipefail - - dest="dist/${{ matrix.target }}" - for f in "$dest"/*; do - base="$(basename "$f")" - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then - continue + gh label create binary-ready \ + --repo "${GITHUB_REPOSITORY}" \ + --color 0e8a16 \ + --description "Android binary is ready for testing" \ + --force + gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --add-label binary-ready + + if [[ -n "${SLACK_WEBHOOK_URL:-}" ]]; then + slack_payload_path="${RUNNER_TEMP}/termux-artifact-slack.json" + pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}" + jq -n \ + --arg artifact_name "${ARTIFACT_NAME}" \ + --arg artifact_url "${artifact_url}" \ + --arg pr_number "${PR_NUMBER}" \ + --arg pr_url "${pr_url}" \ + --arg run_id "${GITHUB_RUN_ID}" \ + --arg run_url "${run_url}" \ + '{ + text: ("Termux Android artifact ready for testing: " + $artifact_url), + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: ("*Termux Android artifact ready for testing*\n" + $artifact_url) + } + }, + { + type: "section", + fields: [ + { + type: "mrkdwn", + text: ("*Artifact:*\n<" + $artifact_url + "|" + $artifact_name + ">") + }, + { + type: "mrkdwn", + text: ("*Pull request:*\n<" + $pr_url + "|#" + $pr_number + ">") + }, + { + type: "mrkdwn", + text: ("*Workflow run:*\n<" + $run_url + "|" + $run_id + ">") + } + ] + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: "You must be signed in to GitHub with repository access to download Actions artifacts." + } + ] + } + ] + }' > "${slack_payload_path}" + + if ! curl -fsS \ + -X POST \ + -H "Content-type: application/json" \ + --data @"${slack_payload_path}" \ + "${SLACK_WEBHOOK_URL}"; then + echo "::warning::Failed to send Slack artifact notification" fi + fi - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }} - path: | - codex-rs/dist/${{ matrix.target }}/* - - build-windows: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - needs: tag-check - uses: ./.github/workflows/rust-release-windows.yml - with: - release-lto: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }} - secrets: inherit - - argument-comment-lint-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: argument-comment-lint release assets + shell-tool-mcp: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'openai' + name: shell-tool-mcp needs: tag-check - uses: ./.github/workflows/rust-release-argument-comment-lint.yml + permissions: + contents: read + id-token: write + uses: ./.github/workflows/shell-tool-mcp.yml with: + release-tag: ${{ github.ref_name }} publish: true - - zsh-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: zsh release assets - needs: tag-check - uses: ./.github/workflows/rust-release-zsh.yml + secrets: inherit release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') needs: - - tag-check - build - - stage-signed-macos - - build-windows - - argument-comment-lint-release-assets - - zsh-release-assets - if: >- - ${{ - always() && - needs.tag-check.result == 'success' && - ( - ( - github.event_name == 'workflow_dispatch' && - inputs.release_mode == 'promote_signed' && - needs.stage-signed-macos.result == 'success' && - needs.build.result == 'skipped' && - needs.build-windows.result == 'skipped' && - needs.argument-comment-lint-release-assets.result == 'skipped' && - needs.zsh-release-assets.result == 'skipped' - ) || - ( - (github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed') && - needs.build.result == 'success' && - needs.stage-signed-macos.result == 'skipped' && - needs.build-windows.result == 'success' && - needs.argument-comment-lint-release-assets.result == 'success' && - needs.zsh-release-assets.result == 'success' - ) - ) - }} + - shell-tool-mcp name: release runs-on: ubuntu-latest permissions: contents: write actions: read - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode == 'promote_signed' }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} outputs: version: ${{ steps.release_name.outputs.name }} tag: ${{ github.ref_name }} - sign_macos: ${{ steps.release_mode.outputs.sign_macos }} should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }} npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }} - should_publish_python_runtime: ${{ steps.python_runtime_publish_settings.outputs.should_publish }} steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + uses: actions/checkout@v6 - - name: Define release mode - id: release_mode - run: | - echo "release_mode=${RELEASE_MODE}" >> "$GITHUB_OUTPUT" - echo "sign_macos=${SIGN_MACOS}" >> "$GITHUB_OUTPUT" + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main - name: Generate release notes from tag commit message id: release_notes @@ -981,138 +1013,18 @@ jobs: echo "path=${notes_path}" >> "${GITHUB_OUTPUT}" - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@v7 with: path: dist - - name: Validate unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - run_summary="$(gh run view "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --json conclusion,event,headBranch,headSha,status,workflowName,url \ - --jq '[.workflowName, .event, .headBranch, .headSha, .status, .conclusion, .url] | @tsv')" - IFS=$'\t' read -r workflow_name event head_branch head_sha status conclusion run_url <<< "$run_summary" - expected_head_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")" - - if [[ "$workflow_name" != "$GITHUB_WORKFLOW" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is for workflow '${workflow_name}', expected '${GITHUB_WORKFLOW}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$event" != "workflow_dispatch" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} was triggered by '${event}', expected 'workflow_dispatch'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_branch" != "$GITHUB_REF_NAME" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used ref '${head_branch}', expected '${GITHUB_REF_NAME}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_sha" != "$expected_head_sha" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used head SHA '${head_sha}', expected '${expected_head_sha}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is ${status}/${conclusion}, expected completed/success" - echo "Run URL: ${run_url}" - exit 1 - fi - - - name: Download artifacts from unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - gh run download "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --dir dist - - - name: Remove unsigned macOS staging artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - run: | - set -euo pipefail - find dist -mindepth 1 -maxdepth 1 -type d \ - -name '*-apple-darwin*-unsigned' \ - -exec rm -rf {} + - - - name: Re-upload promoted Linux x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-unknown-linux-musl - path: dist/x86_64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Linux arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-unknown-linux-musl - path: dist/aarch64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Windows x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-pc-windows-msvc - path: dist/x86_64-pc-windows-msvc/* - if-no-files-found: error - - - name: Re-upload promoted Windows arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-pc-windows-msvc - path: dist/aarch64-pc-windows-msvc/* - if-no-files-found: error - - name: List run: ls -R dist/ - - name: Prune artifacts excluded from unsigned macOS release - if: ${{ env.SIGN_MACOS == 'false' }} - run: | - find dist -mindepth 1 -maxdepth 1 -type d \ - ! -name '*-apple-darwin*-unsigned' \ - ! -name 'aarch64-unknown-linux-musl' \ - ! -name 'aarch64-unknown-linux-musl-app-server' \ - ! -name 'x86_64-unknown-linux-musl' \ - ! -name 'x86_64-unknown-linux-musl-app-server' \ - ! -name 'aarch64-pc-windows-msvc' \ - ! -name 'x86_64-pc-windows-msvc' \ - -exec rm -rf {} + - - if ! find dist -type f -name '*-apple-darwin*-unsigned*' | grep -q .; then - echo "No unsigned macOS artifacts found in downloaded workflow artifacts." - exit 1 - fi - + # This is a temporary fix: we should modify shell-tool-mcp.yml so these + # files do not end up in dist/ in the first place. - name: Delete entries from dist/ that should not go in the release run: | - rm -rf dist/windows-binaries* - # cargo-timing.html appears under multiple target-specific directories. - # If included in files: dist/**, release upload races on duplicate - # asset names and can fail with 404s. - find dist -type f -name 'cargo-timing.html' -delete - # Keep package-builder sidecar archives as workflow artifacts only - # until distribution channels are ready to consume them. - find dist -type f \ - \( -name 'codex-package-*' -o -name 'codex-app-server-package-*' \) \ - -delete - find dist -type d -empty -delete + rm -rf dist/shell-tool-mcp* ls -R dist/ @@ -1136,12 +1048,6 @@ jobs: set -euo pipefail version="${VERSION}" - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - echo "npm_tag=" >> "$GITHUB_OUTPUT" - exit 0 - fi - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "should_publish=true" >> "$GITHUB_OUTPUT" echo "npm_tag=" >> "$GITHUB_OUTPUT" @@ -1153,132 +1059,55 @@ jobs: echo "npm_tag=" >> "$GITHUB_OUTPUT" fi - - name: Determine Python runtime publish settings - id: python_runtime_publish_settings - env: - VERSION: ${{ steps.release_name.outputs.name }} - run: | - set -euo pipefail - version="${VERSION}" - - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - else - echo "should_publish=false" >> "$GITHUB_OUTPUT" - fi - - name: Setup pnpm - if: ${{ env.SIGN_MACOS == 'true' }} - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + uses: pnpm/action-setup@v4 with: run_install: false - name: Setup Node.js for npm packaging - if: ${{ env.SIGN_MACOS == 'true' }} - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: node-version: 22 - name: Install dependencies - if: ${{ env.SIGN_MACOS == 'true' }} run: pnpm install --frozen-lockfile # stage_npm_packages.py requires DotSlash when staging releases. - - uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2 + - uses: facebook/install-dotslash@v2 - name: Stage npm packages - if: ${{ env.SIGN_MACOS == 'true' }} + if: github.repository_owner == 'openai' env: GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ steps.release_name.outputs.name }} run: | - workflow_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" ./scripts/stage_npm_packages.py \ - --release-version "$RELEASE_VERSION" \ - --workflow-url "$workflow_url" \ + --release-version "${{ steps.release_name.outputs.name }}" \ --package codex \ --package codex-responses-api-proxy \ --package codex-sdk - - name: Stage installer scripts - if: ${{ env.SIGN_MACOS == 'true' }} - run: | - cp scripts/install/install.sh dist/install.sh - cp scripts/install/install.ps1 dist/install.ps1 - - name: Create GitHub Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@v2 with: name: ${{ steps.release_name.outputs.name }} tag_name: ${{ github.ref_name }} body_path: ${{ steps.release_notes.outputs.path }} files: dist/** - overwrite_files: true - make_latest: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} # Mark as prerelease only when the version has a suffix after x.y.z # (e.g. -alpha, -beta). Otherwise publish a normal release. prerelease: ${{ contains(steps.release_name.outputs.name, '-') }} - - name: Clean up signed promotion handoff assets - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" --jq '.id')" - gh api --paginate "repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets" \ - --jq '.[] | [.id, .name] | @tsv' | - while IFS=$'\t' read -r asset_id asset_name; do - if [[ -z "$asset_id" || -z "$asset_name" ]]; then - continue - fi - - delete_asset=false - if [[ "$asset_name" == *unsigned* || "$asset_name" == "$SIGNED_MACOS_ASSET" ]]; then - delete_asset=true - fi - - if [[ "$delete_asset" == "true" ]]; then - echo "Deleting release asset ${asset_name}" - gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" - fi - done - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 + - if: github.repository_owner == 'openai' + uses: facebook/dotslash-publish-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag: ${{ github.ref_name }} config: .github/dotslash-config.json - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-zsh-config.json - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-argument-comment-lint-config.json - - name: Trigger developers.openai.com deploy # Only trigger the deploy if the release is not a pre-release. # The deploy is used to update the developers.openai.com website with the new config schema json file. - if: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} + if: ${{ !contains(steps.release_name.outputs.name, '-') && github.repository_owner == 'openai' }} continue-on-error: true env: DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }} @@ -1293,15 +1122,7 @@ jobs: # npm docs: https://docs.npmjs.com/trusted-publishers publish-npm: # Publish to npm for stable releases and alpha pre-releases with numeric suffixes. - # promote_signed intentionally skips build jobs that are ancestors of release; - # include the !cancelled() status function so Actions does not apply its implicit - # success() check to the whole dependency chain before evaluating release outputs. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_npm == 'true' - }} + if: ${{ needs.release.outputs.should_publish_npm == 'true' && github.repository_owner == 'openai' }} name: publish-npm needs: release runs-on: ubuntu-latest @@ -1311,37 +1132,36 @@ jobs: steps: - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: - # Node 24 bundles npm >= 11.5.1, which trusted publishing requires. - node-version: 24 + node-version: 22 registry-url: "https://registry.npmjs.org" scope: "@openai" + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + - name: Download npm tarballs from release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} run: | set -euo pipefail - version="$RELEASE_VERSION" - tag="$RELEASE_TAG" + version="${{ needs.release.outputs.version }}" + tag="${{ needs.release.outputs.tag }}" mkdir -p dist/npm - patterns=( - "codex-npm-${version}.tgz" - "codex-npm-linux-*-${version}.tgz" - "codex-npm-darwin-*-${version}.tgz" - "codex-npm-win32-*-${version}.tgz" - "codex-responses-api-proxy-npm-${version}.tgz" - "codex-sdk-npm-${version}.tgz" - ) - for pattern in "${patterns[@]}"; do - gh release download "$tag" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "$pattern" \ - --dir dist/npm - done + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-responses-api-proxy-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-sdk-npm-${version}.tgz" \ + --dir dist/npm # No NODE_AUTH_TOKEN needed because we use OIDC. - name: Publish to npm @@ -1350,193 +1170,23 @@ jobs: NPM_TAG: ${{ needs.release.outputs.npm_tag }} run: | set -euo pipefail - prefix="" + tag_args=() if [[ -n "${NPM_TAG}" ]]; then - prefix="${NPM_TAG}-" + tag_args+=(--tag "${NPM_TAG}") fi - root_tarball="dist/npm/codex-npm-${VERSION}.tgz" - sdk_tarball="dist/npm/codex-sdk-npm-${VERSION}.tgz" - # Keep this list in sync with CODEX_PLATFORM_PACKAGES in - # codex-cli/scripts/build_npm_package.py. The root wrapper advances - # @openai/codex@latest as soon as it publishes, so every platform - # package it aliases must already exist in the registry first. - platform_tarballs=( - "dist/npm/codex-npm-linux-x64-${VERSION}.tgz" - "dist/npm/codex-npm-linux-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-x64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-x64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-arm64-${VERSION}.tgz" - ) - - for required_tarball in "${platform_tarballs[@]}" "${root_tarball}"; do - if [[ ! -f "${required_tarball}" ]]; then - echo "Missing npm tarball: ${required_tarball}" - exit 1 - fi - done - - shopt -s nullglob - other_tarballs=() - for tarball in dist/npm/*-"${VERSION}".tgz; do - if [[ "${tarball}" == "${root_tarball}" || "${tarball}" == "${sdk_tarball}" ]]; then - continue - fi - - is_platform_tarball=false - for platform_tarball in "${platform_tarballs[@]}"; do - if [[ "${tarball}" == "${platform_tarball}" ]]; then - is_platform_tarball=true - break - fi - done - if [[ "${is_platform_tarball}" == true ]]; then - continue - fi - - other_tarballs+=("${tarball}") - done - - # Publish the platform packages before the root CLI wrapper. The root - # wrapper advances @openai/codex@latest, so it should only publish - # after the optional dependency versions it references exist. tarballs=( - "${platform_tarballs[@]}" - "${other_tarballs[@]}" - "${root_tarball}" + "codex-npm-${VERSION}.tgz" + "codex-responses-api-proxy-npm-${VERSION}.tgz" + "codex-sdk-npm-${VERSION}.tgz" ) - if [[ -f "${sdk_tarball}" ]]; then - tarballs+=("${sdk_tarball}") - fi for tarball in "${tarballs[@]}"; do - filename="$(basename "${tarball}")" - tag="" - - case "${filename}" in - codex-npm-linux-*-"${VERSION}".tgz|codex-npm-darwin-*-"${VERSION}".tgz|codex-npm-win32-*-"${VERSION}".tgz) - platform="${filename#codex-npm-}" - platform="${platform%-${VERSION}.tgz}" - tag="${prefix}${platform}" - ;; - codex-npm-"${VERSION}".tgz|codex-responses-api-proxy-npm-"${VERSION}".tgz|codex-sdk-npm-"${VERSION}".tgz) - tag="${NPM_TAG}" - ;; - *) - echo "Unexpected npm tarball: ${filename}" - exit 1 - ;; - esac - - publish_cmd=(npm publish "${GITHUB_WORKSPACE}/${tarball}") - if [[ -n "${tag}" ]]; then - publish_cmd+=(--tag "${tag}") - fi - - echo "+ ${publish_cmd[*]}" - set +e - publish_output="$("${publish_cmd[@]}" 2>&1)" - publish_status=$? - set -e - - echo "${publish_output}" - if [[ ${publish_status} -eq 0 ]]; then - continue - fi - - if grep -qiE "previously published|cannot publish over|version already exists" <<< "${publish_output}"; then - echo "Skipping already-published package version for ${filename}" - continue - fi - - exit "${publish_status}" + npm publish "${GITHUB_WORKSPACE}/dist/npm/${tarball}" "${tag_args[@]}" done - # Publish the platform-specific Python runtime wheels using PyPI trusted publishing. - # PyPI project configuration must trust this workflow and job. Keep this - # non-blocking while the Python runtime publishing path is new; failures still - # need release follow-up, but should not invalidate the Rust release itself. - publish-python-runtime: - # Publish to PyPI for stable releases and alpha pre-releases with numeric suffixes. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_python_runtime == 'true' - }} - name: publish-python-runtime - needs: release - runs-on: ubuntu-latest - continue-on-error: true - environment: pypi - permissions: - id-token: write # Required for PyPI trusted publishing. - contents: read - - steps: - - name: Download Python runtime wheels from release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} - run: | - set -euo pipefail - python_version="$RELEASE_VERSION" - python_version="${python_version/-alpha./a}" - python_version="${python_version/-beta./b}" - python_version="${python_version/-rc./rc}" - - mkdir -p dist/python-runtime - gh release download "$RELEASE_TAG" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "openai_codex_cli_bin-${python_version}-*.whl" \ - --dir dist/python-runtime - ls -lh dist/python-runtime - - - name: Publish Python runtime wheels to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - with: - packages-dir: dist/python-runtime - skip-existing: true - - winget: - name: winget - needs: release - # Only publish stable/mainline releases to WinGet; pre-releases include a - # '-' in the semver string (e.g., 1.2.3-alpha.1). - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' && - !contains(needs.release.outputs.version, '-') - }} - # This job only invokes a GitHub Action to open/update the winget-pkgs PR; - # it does not execute Windows-only tooling, so Linux is sufficient. - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Publish to WinGet - uses: vedantmgoyal9/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5 - with: - identifier: OpenAI.Codex - version: ${{ needs.release.outputs.version }} - release-tag: ${{ needs.release.outputs.tag }} - fork-user: openai-oss-forks - installers-regex: '^codex-(?:x86_64|aarch64)-pc-windows-msvc\.exe\.zip$' - token: ${{ secrets.WINGET_PUBLISH_PAT }} - update-branch: name: Update latest-alpha-cli branch - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' - }} permissions: contents: write needs: release diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml new file mode 100644 index 000000000000..66a76aa4d938 --- /dev/null +++ b/.github/workflows/shell-tool-mcp.yml @@ -0,0 +1,461 @@ +name: shell-tool-mcp + +on: + workflow_call: + inputs: + release-version: + description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v. + required: false + type: string + release-tag: + description: Tag name to use when downloading release artifacts (defaults to rust-v). + required: false + type: string + publish: + description: Whether to publish to npm when the version is releasable. + required: false + default: true + type: boolean + +env: + NODE_VERSION: 22 + +jobs: + metadata: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.compute.outputs.version }} + release_tag: ${{ steps.compute.outputs.release_tag }} + should_publish: ${{ steps.compute.outputs.should_publish }} + npm_tag: ${{ steps.compute.outputs.npm_tag }} + steps: + - name: Compute version and tags + id: compute + run: | + set -euo pipefail + + version="${{ inputs.release-version }}" + release_tag="${{ inputs.release-tag }}" + + if [[ -z "$version" ]]; then + if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then + version="${release_tag#rust-v}" + elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then + version="${GITHUB_REF_NAME#rust-v}" + release_tag="${GITHUB_REF_NAME}" + else + echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag." + exit 1 + fi + fi + + if [[ -z "$release_tag" ]]; then + release_tag="rust-v${version}" + fi + + npm_tag="" + should_publish="false" + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + should_publish="true" + elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then + should_publish="true" + npm_tag="alpha" + fi + + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" + echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT" + echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT" + + matrix-setup: + runs-on: ubuntu-latest + outputs: + rust-binaries: ${{ steps.compute.outputs.rust-binaries }} + bash-linux: ${{ steps.compute.outputs.bash-linux }} + bash-darwin: ${{ steps.compute.outputs.bash-darwin }} + steps: + - name: Compute matrices + id: compute + shell: bash + run: | + set -euo pipefail + is_openai="${{ github.repository_owner == 'openai' }}" + + # rust-binaries: always include x86_64-musl; conditionally include paid/fork runners + if [[ "$is_openai" == "true" ]]; then + rust='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","install_musl":true} + ]' + else + rust='[{"runner":"macos-latest","target":"aarch64-apple-darwin"}]' + fi + rust=$(echo "$rust" | jq -c '. + [{"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","install_musl":true}]') + echo "rust-binaries={\"include\":$rust}" >> "$GITHUB_OUTPUT" + + # bash-linux: always include x86_64 variants; add arm64 on openai + bash_linux='[ + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-24.04","image":"ubuntu:24.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-22.04","image":"ubuntu:22.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-12","image":"debian:12"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-11","image":"debian:11"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]' + if [[ "$is_openai" == "true" ]]; then + bash_linux=$(echo "$bash_linux" | jq -c '. + [ + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-24.04","image":"arm64v8/ubuntu:24.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-22.04","image":"arm64v8/ubuntu:22.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-20.04","image":"arm64v8/ubuntu:20.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-12","image":"arm64v8/debian:12"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-11","image":"arm64v8/debian:11"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]') + fi + echo "bash-linux={\"include\":$(echo "$bash_linux" | jq -c)}" >> "$GITHUB_OUTPUT" + + # bash-darwin: always include macos-14; add macos-15-xlarge on openai + bash_darwin='[{"runner":"macos-14","target":"aarch64-apple-darwin","variant":"macos-14"}]' + if [[ "$is_openai" == "true" ]]; then + bash_darwin=$(echo "$bash_darwin" | jq -c '. + [ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin","variant":"macos-15"} + ]') + fi + echo "bash-darwin={\"include\":$(echo "$bash_darwin" | jq -c)}" >> "$GITHUB_OUTPUT" + + rust-binaries: + name: Build Rust - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + defaults: + run: + working-directory: codex-rs + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.rust-binaries) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install UBSan runtime (musl) + if: ${{ matrix.install_musl }} + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -y + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 + fi + + - uses: dtolnay/rust-toolchain@1.93 + with: + targets: ${{ matrix.target }} + + - if: ${{ matrix.install_musl }} + name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.14.0 + + - if: ${{ matrix.install_musl }} + name: Install musl build dependencies + env: + TARGET: ${{ matrix.target }} + run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh" + + - if: ${{ matrix.install_musl }} + name: Configure rustc UBSan wrapper (musl host) + shell: bash + run: | + set -euo pipefail + ubsan="" + if command -v ldconfig >/dev/null 2>&1; then + ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')" + fi + wrapper_root="${RUNNER_TEMP:-/tmp}" + wrapper="${wrapper_root}/rustc-ubsan-wrapper" + cat > "${wrapper}" <> "$GITHUB_ENV" + echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV" + + - if: ${{ matrix.install_musl }} + name: Clear sanitizer flags (musl) + shell: bash + run: | + set -euo pipefail + # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. + echo "RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV" + # Override any runner-level Cargo config rustflags as well. + echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + + sanitize_flags() { + local input="$1" + input="${input//-fsanitize=undefined/}" + input="${input//-fno-sanitize-recover=undefined/}" + input="${input//-fno-sanitize-trap=undefined/}" + echo "$input" + } + + cflags="$(sanitize_flags "${CFLAGS-}")" + cxxflags="$(sanitize_flags "${CXXFLAGS-}")" + echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" + echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + + - name: Build exec server binaries + run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper + + - name: Stage exec server binaries + run: | + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}" + mkdir -p "$dest" + cp "target/${{ matrix.target }}/release/codex-exec-mcp-server" "$dest/" + cp "target/${{ matrix.target }}/release/codex-execve-wrapper" "$dest/" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-rust-${{ matrix.target }} + path: artifacts/** + if-no-files-found: error + + bash-linux: + name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + container: + image: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-linux) }} + steps: + - name: Install build prerequisites + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext + elif command -v dnf >/dev/null 2>&1; then + dnf install -y git gcc gcc-c++ make bison autoconf gettext + elif command -v yum >/dev/null 2>&1; then + yum install -y git gcc gcc-c++ make bison autoconf gettext + else + echo "Unsupported package manager in container" + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + bash-darwin: + name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-darwin) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + package: + name: Package npm module + needs: + - metadata + - rust-binaries + - bash-linux + - bash-darwin + runs-on: ubuntu-latest + env: + PACKAGE_VERSION: ${{ needs.metadata.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install JavaScript dependencies + run: pnpm install --frozen-lockfile + + - name: Build (shell-tool-mcp) + run: pnpm --filter @openai/codex-shell-tool-mcp run build + + - name: Download build artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts + + - name: Assemble staging directory + id: staging + shell: bash + run: | + set -euo pipefail + staging="${STAGING_DIR}" + mkdir -p "$staging" "$staging/vendor" + cp shell-tool-mcp/README.md "$staging/" + cp shell-tool-mcp/package.json "$staging/" + cp -R shell-tool-mcp/bin "$staging/" + + found_vendor="false" + shopt -s nullglob + for vendor_dir in artifacts/*/vendor; do + rsync -av "$vendor_dir/" "$staging/vendor/" + found_vendor="true" + done + if [[ "$found_vendor" == "false" ]]; then + echo "No vendor payloads were downloaded." + exit 1 + fi + + node - <<'NODE' + import fs from "node:fs"; + import path from "node:path"; + + const stagingDir = process.env.STAGING_DIR; + const version = process.env.PACKAGE_VERSION; + const pkgPath = path.join(stagingDir, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + pkg.version = version; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + NODE + + echo "dir=$staging" >> "$GITHUB_OUTPUT" + env: + STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp + + - name: Ensure binaries are executable + run: | + set -euo pipefail + staging="${{ steps.staging.outputs.dir }}" + chmod +x \ + "$staging"/vendor/*/codex-exec-mcp-server \ + "$staging"/vendor/*/codex-execve-wrapper \ + "$staging"/vendor/*/bash/*/bash + + - name: Create npm tarball + shell: bash + run: | + set -euo pipefail + mkdir -p dist/npm + staging="${{ steps.staging.outputs.dir }}" + pack_info=$(cd "$staging" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm") + filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);') + mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz" + + - uses: actions/upload-artifact@v6 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz + if-no-files-found: error + + publish: + name: Publish npm package + needs: + - metadata + - package + if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }} + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: https://registry.npmjs.org + scope: "@openai" + + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + + - name: Download npm tarball + uses: actions/download-artifact@v7 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm + + - name: Publish to npm + if: github.repository == 'openai/codex' + env: + NPM_TAG: ${{ needs.metadata.outputs.npm_tag }} + VERSION: ${{ needs.metadata.outputs.version }} + shell: bash + run: | + set -euo pipefail + tag_args=() + if [[ -n "${NPM_TAG}" ]]; then + tag_args+=(--tag "${NPM_TAG}") + fi + npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}" diff --git a/.github/workflows/termux-release-checkpoint.yml b/.github/workflows/termux-release-checkpoint.yml new file mode 100644 index 000000000000..0347e6f1b3d6 --- /dev/null +++ b/.github/workflows/termux-release-checkpoint.yml @@ -0,0 +1,103 @@ +name: termux-release-checkpoint + +on: + workflow_dispatch: + inputs: + source_branch: + description: "Release branch to checkpoint from, for example release/0.123.0" + required: false + type: string + default: "" + source_sha: + description: "Specific source commit SHA to checkpoint; defaults to the source branch tip" + required: false + type: string + default: "" + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + type: string + default: "wallentx/termux-target" + reviewer: + description: "GitHub username to request as reviewer" + required: false + type: string + default: "wallentx" + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-checkpoint-${{ inputs.source_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + checkpoint: + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + env: + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + REQUESTED_SOURCE_BRANCH: ${{ inputs.source_branch }} + REQUESTED_SOURCE_SHA: ${{ inputs.source_sha }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ inputs.source_branch || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + source_branch="${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${source_branch}" + + - name: Create checkpoint PR + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" diff --git a/.github/workflows/termux-release-deploy.yml b/.github/workflows/termux-release-deploy.yml new file mode 100644 index 000000000000..2071611dacc4 --- /dev/null +++ b/.github/workflows/termux-release-deploy.yml @@ -0,0 +1,243 @@ +name: termux-release-deploy + +on: + push: + branches: + - "release/**" + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to deploy, for example release/0.124.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to deploy. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged release PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged release PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + default: "wallentx/termux-target" + type: string + reviewer: + description: "GitHub username to request as reviewer on the checkpoint PR" + required: false + default: "wallentx" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-deploy-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + deploy: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' || !startsWith(github.event.head_commit.message, 'Seed Termux release automation') }} + permissions: + actions: read + contents: write + deployments: write + issues: write + pull-requests: write + env: + REQUESTED_RELEASE_BRANCH: ${{ inputs.release_branch }} + REQUESTED_RELEASE_SHA: ${{ inputs.release_sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + release_branch="${REQUESTED_RELEASE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${release_branch}" + + - name: Resolve release ref + id: release-ref + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: deploy + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Create deployment + if: steps.metadata.outputs.deploy == 'true' + id: deployment + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + deployment_id="$( + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments" \ + -f ref="${RELEASE_SHA}" \ + -f environment="termux-release" \ + -F auto_merge=false \ + -F required_contexts[] \ + -f description="Termux release deployment for ${TERMUX_TAG}" \ + --jq '.id' + )" + echo "id=${deployment_id}" >> "$GITHUB_OUTPUT" + + - name: Mark deployment in progress + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="in_progress" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Promoting Termux release artifact and preparing checkpoint PR" \ + >/dev/null + + - name: Locate merged pull request + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" + + - name: Ensure checkpoint PR + if: steps.metadata.outputs.deploy == 'true' + id: checkpoint + env: + SOURCE_BRANCH: ${{ steps.release-ref.outputs.branch }} + SOURCE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" + + - name: Mark deployment success + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="success" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment completed for ${TERMUX_TAG}" \ + >/dev/null + + - name: Mark deployment failure + if: failure() && steps.deployment.outputs.id != '' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="failure" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment failed" \ + >/dev/null diff --git a/.github/workflows/termux-release-promote.yml b/.github/workflows/termux-release-promote.yml new file mode 100644 index 000000000000..22c277c21a70 --- /dev/null +++ b/.github/workflows/termux-release-promote.yml @@ -0,0 +1,135 @@ +name: termux-release-promote + +on: + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to promote, for example release/0.122.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to promote. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-promote-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + promote: + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: write + pull-requests: read + env: + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Resolve release ref + id: release-ref + env: + INPUT_RELEASE_BRANCH: ${{ inputs.release_branch }} + INPUT_RELEASE_SHA: ${{ inputs.release_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: promote + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Locate merged pull request + if: steps.metadata.outputs.promote == 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.promote == 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.promote == 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index c9aab8a5c793..f73d14ef3e27 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -118,7 +118,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.134.0-alpha.2" +version = "0.134.0-alpha.3" # Track the edition for all workspace crates in one place. Individual # crates can still override this value, but keeping it here means new # crates created with `cargo new -w ...` automatically inherit the 2024 diff --git a/scripts/termux-configure-git.sh b/scripts/termux-configure-git.sh new file mode 100755 index 000000000000..fae3abcbab48 --- /dev/null +++ b/scripts/termux-configure-git.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Configure the explicit bot identity used by release automation and fetch any +# refs requested by the calling workflow. + +set -euo pipefail + +git config user.name "github-actions[bot]" +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + +while (($#)); do + case "$1" in + --origin) + shift + origin_refs=() + while (($#)) && [[ "$1" != --* ]]; do + origin_refs+=("$1") + shift + done + if ((${#origin_refs[@]})); then + git fetch --prune origin "${origin_refs[@]}" + fi + ;; + --upstream-tag) + if (($# < 3)); then + echo "--upstream-tag requires ." >&2 + exit 1 + fi + upstream_repo="$2" + upstream_tag="$3" + git remote add upstream "https://github.com/${upstream_repo}.git" 2>/dev/null || true + git fetch --prune --no-tags upstream "+refs/tags/${upstream_tag}:refs/tags/${upstream_tag}" + shift 3 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done diff --git a/scripts/termux-create-checkpoint-pr.sh b/scripts/termux-create-checkpoint-pr.sh new file mode 100755 index 000000000000..f315a5b91822 --- /dev/null +++ b/scripts/termux-create-checkpoint-pr.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/termux-release-paths.sh +source "${script_dir}/termux-release-paths.sh" + +source_branch="${SOURCE_BRANCH:-${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}}" +source_sha="${SOURCE_SHA:-${REQUESTED_SOURCE_SHA:-}}" +if [[ -z "${source_sha}" ]]; then + if [[ "${GITHUB_EVENT_NAME:-}" == "push" && "${source_branch}" == "${GITHUB_REF_NAME:-}" ]]; then + source_sha="${GITHUB_SHA}" + else + source_sha="$(git rev-parse "origin/${source_branch}")" + fi +fi + +if [[ -z "${DESTINATION_BRANCH:-}" ]]; then + echo "DESTINATION_BRANCH is required." >&2 + exit 1 +fi + +release_only_checkpoint_paths() { + printf '%s\n' "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +} + +resolve_source_version_conflicts() { + local path="$1" + local resolved_path + resolved_path="$(mktemp)" + + if ! awk ' + function normalize_versions(text) { + gsub(/version = "[^"]+"/, "version = \"\"", text) + return text + } + + BEGIN { + in_block = 0 + side = "" + ours = "" + theirs = "" + blocks = 0 + } + + /^<<<<<<< / { + if (in_block) { + exit 1 + } + in_block = 1 + side = "ours" + ours = "" + theirs = "" + blocks++ + next + } + + /^=======$/ && in_block { + side = "theirs" + next + } + + /^>>>>>>> / && in_block { + if (normalize_versions(ours) != normalize_versions(theirs)) { + exit 1 + } + printf "%s", theirs + in_block = 0 + side = "" + next + } + + { + if (!in_block) { + print + } else if (side == "ours") { + ours = ours $0 ORS + } else if (side == "theirs") { + theirs = theirs $0 ORS + } else { + exit 1 + } + } + + END { + if (in_block || blocks == 0) { + exit 1 + } + } + ' "${path}" > "${resolved_path}"; then + rm -f "${resolved_path}" + return 1 + fi + + cp "${resolved_path}" "${path}" + rm -f "${resolved_path}" +} + +short_sha="${source_sha:0:12}" +source_slug="${source_branch//\//_}" +dest_slug="${DESTINATION_BRANCH//\//_}" +checkpoint_branch="checkpoint/${dest_slug}_from_${source_slug}_${short_sha}" +pr_title="checkpoint: into ${DESTINATION_BRANCH} from ${source_branch} @ ${short_sha}" +merge_conflicted=false +conflict_summary="" + +existing_pr="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --head "${checkpoint_branch}" \ + --state all \ + --json number,state,mergedAt,url \ + --jq '[.[] | select(.state == "OPEN" or .mergedAt != null)] | .[0] // empty' +)" +if [[ -n "${existing_pr}" ]]; then + existing_url="$(jq -r '.url' <<< "${existing_pr}")" + existing_state="$(jq -r '.state' <<< "${existing_pr}")" + echo "Checkpoint PR already exists for ${checkpoint_branch}: ${existing_url} (${existing_state})." + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${existing_url}" >> "${GITHUB_OUTPUT}" + fi + exit 0 +fi + +git checkout -B "${checkpoint_branch}" "origin/${DESTINATION_BRANCH}" + +if ! git merge --no-ff --no-edit "${source_sha}"; then + mapfile -t conflicted_paths < <(git diff --name-only --diff-filter=U) + for conflicted_path in "${conflicted_paths[@]}"; do + if termux_is_checkpoint_release_only_path "${conflicted_path}"; then + echo "Auto-resolving release-only checkpoint conflict in ${conflicted_path} by keeping ${DESTINATION_BRANCH}." + if git cat-file -e "HEAD:${conflicted_path}" 2>/dev/null; then + git checkout --ours -- "${conflicted_path}" + git add "${conflicted_path}" + else + git rm -f --ignore-unmatch "${conflicted_path}" + fi + fi + done + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if ((${#remaining_conflicts[@]})); then + cargo_version_conflicts=true + for remaining_conflict in "${remaining_conflicts[@]}"; do + case "${remaining_conflict}" in + codex-rs/Cargo.toml|codex-rs/Cargo.lock) + ;; + *) + cargo_version_conflicts=false + ;; + esac + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + for remaining_conflict in "${remaining_conflicts[@]}"; do + if ! resolve_source_version_conflicts "${remaining_conflict}"; then + cargo_version_conflicts=false + break + fi + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + echo "Auto-resolving recurring Cargo version checkpoint conflicts by keeping ${source_branch} versions." + git add -- "${remaining_conflicts[@]}" + fi + fi + fi + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if [[ "${#remaining_conflicts[@]}" -eq 0 ]]; then + git commit --no-edit + else + merge_conflicted=true + conflict_summary="$( + printf '%s\n' "${remaining_conflicts[@]}" | awk '{ print "- `" $0 "`" }' + )" + echo "Automatic checkpoint merge failed; creating a manual-resolution PR instead." >&2 + if git rev-parse -q --verify MERGE_HEAD >/dev/null; then + git merge --abort + fi + git checkout -B "${checkpoint_branch}" "${source_sha}" + fi +fi + +if git cat-file -e "origin/${DESTINATION_BRANCH}:.github" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- .github + mapfile -t added_github_paths < <( + git diff --name-only --diff-filter=A "origin/${DESTINATION_BRANCH}" -- .github + ) + if ((${#added_github_paths[@]})); then + git rm -f --ignore-unmatch -- "${added_github_paths[@]}" + fi +else + git rm -r --ignore-unmatch .github +fi + +while IFS= read -r release_only_path; do + if git cat-file -e "origin/${DESTINATION_BRANCH}:${release_only_path}" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- "${release_only_path}" + else + git rm -f --ignore-unmatch -- "${release_only_path}" + fi +done < <(release_only_checkpoint_paths) + +if ! git diff --quiet || ! git diff --cached --quiet; then + if [[ -e .github || -L .github ]] || git ls-files --error-unmatch -- .github >/dev/null 2>&1; then + git add -A .github + fi + while IFS= read -r release_only_path; do + if [[ -e "${release_only_path}" || -L "${release_only_path}" ]] || git ls-files --error-unmatch -- "${release_only_path}" >/dev/null 2>&1; then + git add -A -- "${release_only_path}" + fi + done < <(release_only_checkpoint_paths) + if [[ "${merge_conflicted}" == "true" ]]; then + git commit -m "checkpoint: prepare ${source_branch} for ${DESTINATION_BRANCH}" + else + git commit --amend --no-edit + fi +fi + +if git diff --quiet "origin/${DESTINATION_BRANCH}" HEAD; then + echo "Checkpoint merge produced no destination changes after release-only files were restored." + exit 0 +fi + +git push --force-with-lease origin "${checkpoint_branch}" + +remaining="$( + git log --first-parent --pretty=format:%H "${source_sha}..origin/${source_branch}" | wc -w +)" + +body_path="${RUNNER_TEMP}/termux-checkpoint-pr.md" +{ + echo "## Termux release checkpoint" + echo + echo "- Source branch: \`${source_branch}\`" + echo "- Source hash: \`${source_sha}\`" + echo "- Destination branch: \`${DESTINATION_BRANCH}\`" + echo "- Remaining first-parent commits on source: ${remaining}" + echo + echo "This PR carries release-train conflict fixes and follow-up changes back into the reusable Termux patch branch." + if [[ "${merge_conflicted}" == "true" ]]; then + echo + echo "## Merge conflicts" + echo + echo "GitHub Actions could not create the checkpoint merge commit automatically, so this PR was created from the source branch state for manual conflict resolution." + echo + echo "Conflicted paths from the failed merge attempt:" + if [[ -n "${conflict_summary}" ]]; then + printf '%s\n' "${conflict_summary}" + else + echo "- Conflict details unavailable" + fi + fi + echo + echo "Release-only workflow files and metadata under \`.github\` were restored to the destination branch versions before opening this PR." +} > "${body_path}" + +pr_url="$( + gh pr create \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${DESTINATION_BRANCH}" \ + --head "${checkpoint_branch}" \ + --title "${pr_title}" \ + --body-file "${body_path}" +)" +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-reviewer "${REVIEWER}" || true +gh label create checkpoint --repo "${GITHUB_REPOSITORY}" --color c5def5 --description "Checkpoint merge" --force +gh label create termux-release --repo "${GITHUB_REPOSITORY}" --color 0e8a16 --description "Termux release automation" --force +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-label "checkpoint" --add-label "termux-release" + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${pr_url}" >> "${GITHUB_OUTPUT}" +fi diff --git a/scripts/termux-create-or-update-mirrored-release.sh b/scripts/termux-create-or-update-mirrored-release.sh new file mode 100755 index 000000000000..5787e894582b --- /dev/null +++ b/scripts/termux-create-or-update-mirrored-release.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +# Create a mirrored Termux release or repair an existing release that is missing +# the Android tarball. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" +: "${RUNNER_TEMP:?RUNNER_TEMP is required}" +: "${UPSTREAM_TAG:?UPSTREAM_TAG is required}" +: "${UPSTREAM_REPO:?UPSTREAM_REPO is required}" +: "${UPSTREAM_NAME?UPSTREAM_NAME is required}" +: "${TERMUX_TAG:?TERMUX_TAG is required}" +: "${UPSTREAM_PRERELEASE:?UPSTREAM_PRERELEASE is required}" +: "${UPSTREAM_HTML_URL?UPSTREAM_HTML_URL is required}" +: "${RELEASE_TRAIN?RELEASE_TRAIN is required}" +: "${RELEASE_EXISTS:?RELEASE_EXISTS is required}" +: "${PR_NUMBER:?PR_NUMBER is required}" +: "${HEAD_SHA:?HEAD_SHA is required}" +: "${RELEASE_SHA:?RELEASE_SHA is required}" + +promoted_dir="${PROMOTED_DIR:-promoted}" +asset_path="${promoted_dir}/codex-aarch64-linux-android.tar.gz" + +body_path="${RUNNER_TEMP}/release-body.md" +upstream_body_path="${RUNNER_TEMP}/upstream-release-body.md" +upstream_body_without_changelog_path="${RUNNER_TEMP}/upstream-release-body-without-changelog.md" +if gh release view "${UPSTREAM_TAG}" \ + --repo "${UPSTREAM_REPO}" \ + --json body \ + --jq '.body // ""' > "${upstream_body_path}"; then + awk ' + function heading_level(line, text) { + if (match(line, /^(#{1,6})[[:space:]]+(.+)$/, parts)) { + text = tolower(parts[2]) + sub(/[[:space:]]+#+[[:space:]]*$/, "", text) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", text) + if (text == "changelog" || text == "change log") { + return length(parts[1]) + } + } + return 0 + } + + { + if (!skip) { + level = heading_level($0) + if (level > 0) { + skip = 1 + skip_level = level + next + } + print + next + } + + if (match($0, /^(#{1,6})[[:space:]]+/, parts) && length(parts[1]) <= skip_level) { + skip = 0 + print + } + } + ' "${upstream_body_path}" > "${upstream_body_without_changelog_path}" +else + echo "::warning title=Upstream release notes unavailable::Could not read ${UPSTREAM_REPO} release ${UPSTREAM_TAG}." + : > "${upstream_body_without_changelog_path}" +fi + +{ + echo "Termux Android build for ${UPSTREAM_TAG}." + echo + echo "- Upstream release: ${UPSTREAM_HTML_URL}" + echo "- Release train: \`${RELEASE_TRAIN}\`" + echo "- Promoted PR: #${PR_NUMBER}" + echo "- Promoted PR head SHA: \`${HEAD_SHA}\`" +} > "${body_path}" +if [[ -s "${upstream_body_without_changelog_path}" ]]; then + { + echo + echo "## Upstream release notes" + echo + cat "${upstream_body_without_changelog_path}" + } >> "${body_path}" +fi + +release_title="${UPSTREAM_NAME}" +if [[ -z "${release_title}" || "${release_title}" == "null" ]]; then + release_title="${TERMUX_TAG}" +fi + +if [[ "${RELEASE_EXISTS}" == "true" ]]; then + gh release upload \ + "${TERMUX_TAG}" \ + "${asset_path}#codex-termux" \ + --repo "${GITHUB_REPOSITORY}" \ + --clobber + exit 0 +fi + +release_args=( + gh release create "${TERMUX_TAG}" + --repo "${GITHUB_REPOSITORY}" + --target "${RELEASE_SHA}" + --title "${release_title}" + --notes-file "${body_path}" +) +if [[ "${UPSTREAM_PRERELEASE}" == "true" ]]; then + release_args+=(--prerelease) +fi +release_args+=("${asset_path}#codex-termux") +"${release_args[@]}" diff --git a/scripts/termux-download-release-artifact.sh b/scripts/termux-download-release-artifact.sh new file mode 100755 index 000000000000..2acdba9025a0 --- /dev/null +++ b/scripts/termux-download-release-artifact.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${HEAD_SHA:-}" ]]; then + echo "HEAD_SHA is required." >&2 + exit 1 +fi + +if [[ -z "${PR_ARTIFACT_NAME:-}" ]]; then + echo "PR_ARTIFACT_NAME is required." >&2 + exit 1 +fi + +promoted_dir="${PROMOTED_DIR:-promoted}" + +find_run_id() { + local event="$1" + gh run list \ + --repo "${GITHUB_REPOSITORY}" \ + --workflow rust-release.yml \ + --event "${event}" \ + --status success \ + --commit "${HEAD_SHA}" \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId // empty' +} + +find_artifact_run_id() { + local artifact_name="$1" + + gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=${artifact_name}" \ + | jq -rs \ + --arg artifact_name "${artifact_name}" \ + --arg head_sha "${HEAD_SHA}" \ + ' + [ + .[].artifacts[] + | select(.name == $artifact_name) + | select(.expired == false) + | select(.workflow_run.head_sha == $head_sha) + ] + | sort_by(.created_at) + | reverse + | .[0].workflow_run.id // empty + ' +} + +artifact_name="${PR_ARTIFACT_NAME}" +run_id="$(find_artifact_run_id "${artifact_name}")" +mkdir -p "${promoted_dir}" +if [[ -n "${run_id}" ]] && gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}"; then + : +else + artifact_name="aarch64-linux-android" + run_id="$(find_run_id workflow_dispatch)" + if [[ -z "${run_id}" ]]; then + echo "No successful rust-release run found for ${HEAD_SHA}" >&2 + exit 1 + fi + gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}" +fi + +ls -la "${promoted_dir}" +if [[ -f "${promoted_dir}/SHA256SUMS" ]]; then + (cd "${promoted_dir}" && sha256sum -c SHA256SUMS) +fi +if [[ ! -f "${promoted_dir}/codex-aarch64-linux-android.tar.gz" ]]; then + echo "Expected ${promoted_dir}/codex-aarch64-linux-android.tar.gz in the downloaded artifact." >&2 + exit 1 +fi diff --git a/scripts/termux-find-release-pr.sh b/scripts/termux-find-release-pr.sh new file mode 100755 index 000000000000..3e35fa42eabe --- /dev/null +++ b/scripts/termux-find-release-pr.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -n "${INPUT_PR_NUMBER:-}" || -n "${INPUT_PR_HEAD_SHA:-}" ]]; then + if [[ -z "${INPUT_PR_NUMBER:-}" || -z "${INPUT_PR_HEAD_SHA:-}" ]]; then + echo "workflow_dispatch inputs pr_number and pr_head_sha must be provided together." >&2 + exit 1 + fi + + pr_number="${INPUT_PR_NUMBER}" + head_sha="${INPUT_PR_HEAD_SHA}" + head_ref="" +else + if [[ -z "${RELEASE_BRANCH:-}" || -z "${RELEASE_SHA:-}" ]]; then + echo "RELEASE_BRANCH and RELEASE_SHA are required." >&2 + exit 1 + fi + + pr_json="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${RELEASE_BRANCH}" \ + --state merged \ + --limit 100 \ + --json number,headRefOid,headRefName,mergeCommit \ + --jq "[.[] | select(.mergeCommit.oid == \"${RELEASE_SHA}\")] | sort_by(.number) | reverse | .[0] // empty" + )" + if [[ -z "${pr_json}" ]]; then + echo "Unable to find merged PR for ${RELEASE_SHA} into ${RELEASE_BRANCH}" >&2 + exit 1 + fi + + pr_number="$(jq -r '.number' <<< "${pr_json}")" + head_sha="$(jq -r '.headRefOid // .head.sha' <<< "${pr_json}")" + head_ref="$(jq -r '.headRefName // .head.ref' <<< "${pr_json}")" +fi + +artifact_name="termux-android-pr-${pr_number}-${head_sha}" +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "number=${pr_number}" + echo "head_sha=${head_sha}" + echo "head_ref=${head_ref}" + echo "artifact_name=${artifact_name}" + } >> "${GITHUB_OUTPUT}" +else + printf 'number=%s\nhead_sha=%s\nhead_ref=%s\nartifact_name=%s\n' \ + "${pr_number}" \ + "${head_sha}" \ + "${head_ref}" \ + "${artifact_name}" +fi diff --git a/scripts/termux-read-release-metadata.sh b/scripts/termux-read-release-metadata.sh new file mode 100755 index 000000000000..519a0635a368 --- /dev/null +++ b/scripts/termux-read-release-metadata.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# Parse .github/termux-release.json and emit workflow outputs for deploy or +# promote jobs. TERMUX_RELEASE_ACTION must be "deploy" or "promote". + +set -euo pipefail + +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +metadata=".github/termux-release.json" +action="${TERMUX_RELEASE_ACTION:-deploy}" +case "${action}" in + deploy) + missing_context="deployment" + ;; + promote) + missing_context="promotion" + ;; + *) + echo "TERMUX_RELEASE_ACTION must be deploy or promote." >&2 + exit 1 + ;; +esac + +if [[ ! -f "${metadata}" ]]; then + echo "No ${metadata}; this push is not a Termux release ${missing_context}." + echo "${action}=false" >> "${GITHUB_OUTPUT}" + exit 0 +fi + +upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" +upstream_name="$(jq -r '.upstream_name // .upstream_tag // empty' "${metadata}")" +termux_tag="$(jq -r '.termux_tag // empty' "${metadata}")" +upstream_version="${upstream_tag#rust-v}" +upstream_version="${upstream_version%-termux}" +upstream_prerelease=false +if [[ "${upstream_version}" == *-* ]]; then + upstream_prerelease=true +fi +upstream_html_url="$(jq -r '.upstream_html_url // ""' "${metadata}")" +upstream_repo="$(jq -r '.upstream_repo // "openai/codex"' "${metadata}")" +release_train="$(jq -r '.release_train // ""' "${metadata}")" +if [[ -z "${upstream_tag}" || -z "${termux_tag}" ]]; then + echo "Missing upstream_tag or termux_tag in ${metadata}" >&2 + exit 1 +fi + +release_state="$(TERMUX_TAG="${termux_tag}" "${script_dir}/termux-release-asset-state.sh")" +release_exists="$(awk -F= '$1 == "release_exists" { print $2 }' <<< "${release_state}")" +asset_exists="$(awk -F= '$1 == "asset_exists" { print $2 }' <<< "${release_state}")" + +if [[ "${action}" == "promote" ]]; then + if [[ "${asset_exists}" == "true" ]]; then + echo "${termux_tag} already exists with codex-aarch64-linux-android.tar.gz; skipping promotion." + echo "promote=false" >> "${GITHUB_OUTPUT}" + exit 0 + fi + if [[ "${release_exists}" == "true" ]]; then + echo "${termux_tag} exists but is missing codex-aarch64-linux-android.tar.gz; repairing promotion." + fi +fi + +{ + echo "${action}=true" + echo "upstream_tag=${upstream_tag}" + echo "upstream_name=${upstream_name}" + echo "termux_tag=${termux_tag}" + echo "upstream_prerelease=${upstream_prerelease}" + echo "upstream_html_url=${upstream_html_url}" + echo "upstream_repo=${upstream_repo}" + echo "release_train=${release_train}" + echo "release_exists=${release_exists}" + echo "asset_exists=${asset_exists}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-release-asset-state.sh b/scripts/termux-release-asset-state.sh new file mode 100755 index 000000000000..8694480b6a36 --- /dev/null +++ b/scripts/termux-release-asset-state.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +termux_tag="${1:-${TERMUX_TAG:-}}" +release_repo="${RELEASE_REPO:-${GITHUB_REPOSITORY:-}}" +asset_name="${ASSET_NAME:-codex-aarch64-linux-android.tar.gz}" + +if [[ -z "${termux_tag}" ]]; then + echo "TERMUX_TAG or tag argument is required." >&2 + exit 1 +fi + +if [[ -z "${release_repo}" ]]; then + echo "GITHUB_REPOSITORY or RELEASE_REPO is required." >&2 + exit 1 +fi + +release_exists=false +asset_exists=false + +if gh release view "${termux_tag}" --repo "${release_repo}" >/dev/null 2>&1; then + release_exists=true + release_asset_exists="$( + gh release view "${termux_tag}" \ + --repo "${release_repo}" \ + --json assets \ + | jq -r --arg asset_name "${asset_name}" \ + '.assets | map(.name) | any(. == $asset_name)' + )" + if [[ "${release_asset_exists}" == "true" ]]; then + asset_exists=true + fi +fi + +printf 'release_exists=%s\n' "${release_exists}" +printf 'asset_exists=%s\n' "${asset_exists}" diff --git a/scripts/termux-release-paths.sh b/scripts/termux-release-paths.sh new file mode 100755 index 000000000000..6990f323ec44 --- /dev/null +++ b/scripts/termux-release-paths.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# Sourceable path lists for files owned by the Termux release automation. + +set -euo pipefail + +readonly -a TERMUX_RELEASE_WORKFLOW_PATHS=( + .github/workflows/rust-release.yml + .github/workflows/shell-tool-mcp.yml + .github/workflows/termux-release-checkpoint.yml + .github/workflows/termux-release-deploy.yml + .github/workflows/termux-release-promote.yml +) + +readonly -a TERMUX_RELEASE_BRANCH_SCRIPT_PATHS=( + scripts/termux-configure-git.sh + scripts/termux-create-checkpoint-pr.sh + scripts/termux-create-or-update-mirrored-release.sh + scripts/termux-download-release-artifact.sh + scripts/termux-find-release-pr.sh + scripts/termux-read-release-metadata.sh + scripts/termux-release-asset-state.sh + scripts/termux-release-paths.sh + scripts/termux-resolve-release-ref.sh + scripts/termux-validate-gh-env.sh +) + +readonly -a TERMUX_RELEASE_AUTOMATION_PATHS=( + "${TERMUX_RELEASE_WORKFLOW_PATHS[@]}" + "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +) + +readonly -a TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS=( + "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" + .github/termux-release.json +) + +termux_path_in_list() { + local candidate="$1" + shift + local listed_path + + for listed_path in "$@"; do + [[ "${candidate}" != "${listed_path}" ]] || return 0 + done + return 1 +} + +termux_is_release_automation_path() { + termux_path_in_list "$1" "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" +} + +termux_is_checkpoint_release_only_path() { + termux_path_in_list "$1" "${TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS[@]}" +} diff --git a/scripts/termux-resolve-release-ref.sh b/scripts/termux-resolve-release-ref.sh new file mode 100755 index 000000000000..194773d45bed --- /dev/null +++ b/scripts/termux-resolve-release-ref.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Resolve the release branch and SHA for deploy/promote workflows. + +set -euo pipefail + +: "${GITHUB_EVENT_NAME:?GITHUB_EVENT_NAME is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" + +input_release_branch="${INPUT_RELEASE_BRANCH:-${REQUESTED_RELEASE_BRANCH:-}}" +input_release_sha="${INPUT_RELEASE_SHA:-${REQUESTED_RELEASE_SHA:-}}" + +if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then + if [[ -z "${input_release_branch}" ]]; then + echo "release_branch is required for workflow_dispatch." >&2 + exit 1 + fi + + release_branch="${input_release_branch}" + if [[ -n "${input_release_sha}" ]]; then + git checkout --detach "${input_release_sha}" + release_sha="${input_release_sha}" + else + release_sha="$(git rev-parse HEAD)" + fi +else + : "${GITHUB_REF_NAME:?GITHUB_REF_NAME is required}" + : "${GITHUB_SHA:?GITHUB_SHA is required}" + release_branch="${GITHUB_REF_NAME}" + release_sha="${GITHUB_SHA}" +fi + +{ + echo "branch=${release_branch}" + echo "sha=${release_sha}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-validate-gh-env.sh b/scripts/termux-validate-gh-env.sh new file mode 100755 index 000000000000..02581fdebceb --- /dev/null +++ b/scripts/termux-validate-gh-env.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Lightweight post-toolbox check for jobs that rely on authenticated gh calls. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +command -v gh +gh auth status --hostname github.com + +printf 'GITHUB_REPOSITORY=%s\n' "${GITHUB_REPOSITORY}" +printf 'REPO=%s\n' "${REPO:-}" +printf 'GH_REPO_URL=%s\n' "${GH_REPO_URL:-}" +printf 'GH_WORKFLOW_URL=%s\n' "${GH_WORKFLOW_URL:-}" From a75c443fdb64db48c3cf4bdb247c7ee52c0144c9 Mon Sep 17 00:00:00 2001 From: Shijie Rao Date: Tue, 26 May 2026 09:46:10 -0700 Subject: [PATCH 23/25] ## New Features - Added search across local conversation history, including case-insensitive content matches with result previews. (#23519, #23921) - Made `--profile` the primary profile selector across CLI, TUI permissions, and sandbox flows, with legacy profile configs rejected through migration guidance. (#23708, #23883, #23890, #24051, #24055, #24059, #24067, #24110) - Improved MCP setup with per-server environment targeting and OAuth options for streamable HTTP servers. (#23583, #24120) - Made connector tool schemas more reliable by preserving local `$ref`/`$defs` structures and compacting oversized schemas before exposure. (#23357, #23904) - Let read-only MCP tools run concurrently when they advertise `readOnlyHint`. (#23750) - Added richer extension and hook context, including conversation history for extension tools and subagent identity in hook inputs. (#22882, #23963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Bug Fixes - Improved remote reliability by reconnecting stale exec-server websocket clients, retrying remote control immediately after auth recovery, and retrying remote compaction v2 streams. (#23867, #23775, #23951) - Fixed Windows TUI rendering corruption by restoring virtual terminal mode before drawing. (#24082) - Displayed workspace-specific usage-limit messages for credit and spend-cap failures. (#24114) - Allowed plugin skills to reuse shared plugin-level icon assets. (#23776) - Preserved active permission profile metadata when syncing auto-review runtime settings. (#23956) - Ensured Node-based tools honor Codex’s managed network proxy environment. (#23905) ## Documentation - Documented the curl and PowerShell installer paths in the README. (#24106) - Updated developer docs to prefer `just test` over direct `cargo test` for repo-local test runs. (#23910) - Added profile migration documentation links to relevant config errors. (#23879) ## Chores - Simplified release packaging around canonical native artifacts, reusable DotSlash fetching, and a new macOS x64 zsh artifact. (#23833, #23836, #24129, #24165) - Added release-build support for Codex-produced V8 artifacts. (#23934) - Added image re-encoding benchmarks and connector-style JSON schema policy fixtures. (#23935, #24152) - Improved tracing and analytics for websocket requests, turn starts, and remote compaction v2. (#23581, #23980, #24146) ## Changelog Full Changelog: https://github.com/openai/codex/compare/rust-v0.133.0...rust-v0.134.0 - #23581 Trace logical websocket request after untraced warmup @jif-oai - #23718 [codex] Steer budget-limited goal extension turns @jif-oai - #23861 fix: cargo lock @jif-oai - #23728 feat: retain remote compaction truncation parity in v2 @jif-oai - #23870 Make tool executor specs mandatory @jif-oai - #23882 [codex] Stabilize subagent start hook test @jif-oai - #23876 refactor: centralize tool exposure planning @jif-oai - #23879 chore: link doc in profile error messages @jif-oai - #23883 cli: rename profile v2 flag to --profile @jif-oai - #23835 docs: add description to codex-cli/package.json @bolinfest - #23583 Route MCP servers through explicit environments @starr-openai - #23886 cli: remove legacy profile v1 plumbing @jif-oai - #23708 tui: plumb permission profile selection @viyatb-oai - #23833 packaging: move rg manifest out of npm bin @bolinfest - #23796 Improve `/goal` error messages for ephemeral sessions @etraut-openai - #23867 Reconnect disconnected exec-server websocket clients with fresh sessions @starr-openai - #23792 TUI: skip goal replace prompt for completed goals @etraut-openai - #23519 [codex] Add rollout-backed thread content search @fc-oai - #22552 Remove plugin hooks feature flag @abhinav-oai - #23836 npm: remove legacy package artifact synthesis @bolinfest - #23921 [codex] Make thread search case-insensitive @fc-oai - #23775 fix(remote-control): retry after auth recovery @apanasenko-oai - #22882 Add subagent identity to hook inputs @abhinav-oai - #22915 [3 of 4] tui: route feature and memory toggles through app server @etraut-openai - #23776 fix: Allow plugin skills to share plugin-level icon assets @xl-openai - #23860 Add Bedrock Mantle GovCloud region @CHARLESPALEN-OAI - #23956 Fix auto-review permission profile override @etraut-openai - #23357 feat: support local refs and defs in tool input schemas @celia-oai - #23963 Expose conversation history to extension tools @sayan-oai - #23904 feat: best-effort compact large tool schemas @celia-oai - #23750 Allow parallel MCP tool calls when annotated readOnly @anp-oai - #23905 [codex] Enable Node env proxy for managed network proxy @rreichel3-oai - #23890 mcp: surface profile migration guidance under --profile @jif-oai - #24051 config: remove legacy profile v1 resolution @jif-oai - #24055 config: remove legacy profile write paths @jif-oai - #24057 Avoid config snapshots in live agent subtree traversal @jif-oai - #24061 otel: drop legacy profile usage telemetry @jif-oai - #24059 fix: reject legacy profile selectors @jif-oai - #23934 ci: Use codex produced v8 artifacts for release builds @cconger - #24099 fix(app-server): fix optional bool annotations @owenlin0 - #23910 Prefer `just test` over `cargo test` in docs @anp-oai - #23951 retry remote compaction v2 requests @rhan-oai - #24081 tui: make `codex-tui.log` opt-in @jif-oai - #24102 cli: infer host sandbox backend @bolinfest - #24067 app-server: drop legacy profile config surface @jif-oai - #23736 Add new enterprise requirement gate @adams-oai - #24117 [codex] Use rolling files for Windows sandbox logs @iceweasel-oai - #24106 docs: update README.md to mention curl-based installer @bolinfest - #24082 fix(tui): restore Windows VT before TUI renders @fcoury-oai - #24110 cli: support --profile for codex sandbox @bolinfest - #23980 Add trace_id to TurnStartedEvent @mchen-oai - #24120 Support OAuth options in codex mcp add @mzeng-openai - #23989 Add typed Images client to codex-api @won-openai - #24146 [codex-analytics] split compaction v2 analytics implementation @rhan-oai - #24129 package: factor DotSlash executable fetching @bolinfest - #24151 [codex] Use TurnInput for session task input @pakrym-oai - #23935 [codex] Add image re-encoding benchmarks @anp-oai - #24152 chore: add JSON schema policy fixture coverage @celia-oai - #24157 [codex] Remove external client session reset plumbing @pakrym-oai - #24114 Display workspace usage limit error copy from response header @dhruvgupta-oai - #24165 release: build macOS x64 zsh artifact @bolinfest --- codex-rs/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 95c065c74bdf..0e5730356380 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -117,7 +117,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.0.0" +version = "0.134.0" # Track the edition for all workspace crates in one place. Individual # crates can still override this value, but keeping it here means new # crates created with `cargo new -w ...` automatically inherit the 2024 From 9e518fe53c45f5756613072b773123ff512167f4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 21:58:24 +0000 Subject: [PATCH 24/25] Seed Termux release automation --- .github/workflows/rust-release.yml | 1807 +++++++---------- .github/workflows/shell-tool-mcp.yml | 461 +++++ .../workflows/termux-release-checkpoint.yml | 103 + .github/workflows/termux-release-deploy.yml | 243 +++ .github/workflows/termux-release-promote.yml | 135 ++ scripts/termux-configure-git.sh | 40 + scripts/termux-create-checkpoint-pr.sh | 275 +++ ...ermux-create-or-update-mirrored-release.sh | 111 + scripts/termux-download-release-artifact.sh | 78 + scripts/termux-find-release-pr.sh | 53 + scripts/termux-read-release-metadata.sh | 77 + scripts/termux-release-asset-state.sh | 37 + scripts/termux-release-paths.sh | 55 + scripts/termux-resolve-release-ref.sh | 36 + scripts/termux-validate-gh-env.sh | 16 + 15 files changed, 2445 insertions(+), 1082 deletions(-) create mode 100644 .github/workflows/shell-tool-mcp.yml create mode 100644 .github/workflows/termux-release-checkpoint.yml create mode 100644 .github/workflows/termux-release-deploy.yml create mode 100644 .github/workflows/termux-release-promote.yml create mode 100755 scripts/termux-configure-git.sh create mode 100755 scripts/termux-create-checkpoint-pr.sh create mode 100755 scripts/termux-create-or-update-mirrored-release.sh create mode 100755 scripts/termux-download-release-artifact.sh create mode 100755 scripts/termux-find-release-pr.sh create mode 100755 scripts/termux-read-release-metadata.sh create mode 100755 scripts/termux-release-asset-state.sh create mode 100755 scripts/termux-release-paths.sh create mode 100755 scripts/termux-resolve-release-ref.sh create mode 100755 scripts/termux-validate-gh-env.sh diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 2cd11e66fe0f..56d0977e6e67 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -4,46 +4,49 @@ # git tag -a rust-v0.1.0 -m "Release 0.1.0" # git push origin rust-v0.1.0 # ``` -# -# To use external macOS signing, manually dispatch `release_mode=build_unsigned`, -# sign the unsigned macOS artifacts in a secure enclave, upload the signed handoff -# archive as a GitHub Release asset, then manually dispatch -# `release_mode=promote_signed` with `unsigned_run_id` and `signed_macos_asset`. -# The signed handoff archive should contain target or artifact directories such -# as `aarch64-apple-darwin/` with signed binaries. name: rust-release on: - push: - tags: - - "rust-v*.*.*" + pull_request: + branches: + - "release/**" workflow_dispatch: inputs: - release_mode: - description: "build_unsigned creates unsigned macOS handoff artifacts; promote_signed finishes a release from signed macOS handoff artifacts." + source_ref: + description: "Branch, tag, or commit SHA to build (code only; keeps .github from dispatched ref)" required: false + type: string + default: "" + build_target: + description: "Target to build" + required: true type: choice - default: build_unsigned + default: all options: - - build_unsigned - - promote_signed - sign_macos: - description: "Deprecated compatibility input; use release_mode instead." - required: false - type: boolean - default: false - unsigned_run_id: - description: "For promote_signed: workflow run id from the build_unsigned run." - required: false - type: string - signed_macos_asset: - description: "For promote_signed: exact GitHub Release asset name containing signed macOS handoff artifacts." - required: false - type: string - signed_macos_sha256: - description: "For promote_signed: optional SHA-256 of signed_macos_asset." - required: false - type: string + - all + - aarch64-apple-darwin + - x86_64-apple-darwin + - x86_64-unknown-linux-musl + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-musl + - aarch64-unknown-linux-gnu + - aarch64-linux-android + - x86_64-pc-windows-msvc + - aarch64-pc-windows-msvc + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read concurrency: group: ${{ github.workflow }} @@ -51,72 +54,26 @@ concurrency: jobs: tag-check: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - uses: dtolnay/rust-toolchain@1.92 - name: Validate tag matches Cargo.toml version shell: bash - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - REQUESTED_SIGN_MACOS: ${{ inputs.sign_macos }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} run: | set -euo pipefail echo "::group::Tag validation" - case "${RELEASE_MODE}" in - signed) - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - echo "❌ Manual rust-release runs must use release_mode=build_unsigned or release_mode=promote_signed" - exit 1 - fi - ;; - build_unsigned) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=build_unsigned is only valid for manual runs" - exit 1 - fi - ;; - promote_signed) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=promote_signed is only valid for manual runs" - exit 1 - fi - if [[ ! "${UNSIGNED_RUN_ID}" =~ ^[0-9]+$ ]]; then - echo "❌ release_mode=promote_signed requires unsigned_run_id to be a workflow run id" - exit 1 - fi - if [[ -z "${SIGNED_MACOS_ASSET}" ]]; then - echo "❌ release_mode=promote_signed requires signed_macos_asset" - exit 1 - fi - if [[ "${SIGNED_MACOS_ASSET}" == */* || "${SIGNED_MACOS_ASSET}" == *"*"* || "${SIGNED_MACOS_ASSET}" == *"?"* || "${SIGNED_MACOS_ASSET}" == *"["* ]]; then - echo "❌ signed_macos_asset must be an exact release asset name, not a path or glob" - exit 1 - fi - if [[ "${UNSIGNED_RUN_ID}" == "${GITHUB_RUN_ID}" ]]; then - echo "❌ unsigned_run_id must refer to the earlier build_unsigned run, not this run" - exit 1 - fi - ;; - *) - echo "❌ Unknown release_mode '${RELEASE_MODE}'" - exit 1 - ;; - esac - - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${REQUESTED_SIGN_MACOS}" == "true" ]]; then - echo "::warning title=Deprecated sign_macos input ignored::Use release_mode=build_unsigned or release_mode=promote_signed instead." - fi - # 1. Must be a tag and match the regex [[ "${GITHUB_REF_TYPE}" == "tag" ]] \ || { echo "❌ Not a tag push"; exit 1; } - [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \ + [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?(-termux)?$ ]] \ || { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; } # 2. Extract versions @@ -131,115 +88,198 @@ jobs: echo "✅ Tag and Cargo.toml agree (${tag_ver})" echo "::endgroup::" - build: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} + select-build-matrix: + if: always() && (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') needs: tag-check - name: Build - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: ${{ matrix.runs_on || matrix.runner }} - # Release builds can take a long time, so leave some headroom to avoid - # having to restart the full workflow due to a timeout. - timeout-minutes: 90 - permissions: - contents: read - id-token: write + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.select.outputs.matrix }} + build_version: ${{ steps.select.outputs.build_version }} + steps: + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - id: select + shell: bash + env: + REPO_OWNER: ${{ github.repository_owner }} + EVENT_NAME: ${{ github.event_name }} + BUILD_TARGET: ${{ inputs.build_target }} + run: | + set -euo pipefail + + matrix='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04","target":"aarch64-linux-android"}, + {"runner":"windows-latest","target":"x86_64-pc-windows-msvc"}, + {"runner":"windows-11-arm","target":"aarch64-pc-windows-msvc"} + ]' + + if [[ "${REPO_OWNER}" != "openai" ]]; then + matrix="$(jq -c '[.[] | select(.runner | startswith("macos") | not)]' <<< "${matrix}")" + fi + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + matrix="$(jq -c '[.[] | select(.target == "aarch64-linux-android")]' <<< "${matrix}")" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" && -n "${BUILD_TARGET}" && "${BUILD_TARGET}" != "all" ]]; then + matrix="$(jq -c --arg build_target "${BUILD_TARGET}" '[.[] | select(.target == $build_target)]' <<< "${matrix}")" + fi + + if [[ "$(jq 'length' <<< "${matrix}")" -eq 0 ]]; then + echo "No build targets selected after applying owner/dispatch filters." >&2 + exit 1 + fi + + echo "matrix={\"include\":${matrix}}" >> "$GITHUB_OUTPUT" + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + metadata=".github/termux-release.json" + if [[ ! -f "${metadata}" ]]; then + echo "${metadata} is required for release train PR builds" >&2 + exit 1 + fi + upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" + if [[ -z "${upstream_tag}" || "${upstream_tag}" != rust-v* ]]; then + echo "Unable to determine upstream rust tag from ${metadata}" >&2 + exit 1 + fi + build_version="${upstream_tag#rust-v}" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then + latest_release_tag="$( + GH_TOKEN="${{ github.token }}" gh api repos/openai/codex/releases/latest --jq '.tag_name' + )" + + if [[ -z "${latest_release_tag}" || "${latest_release_tag}" != rust-v* ]]; then + echo "Unable to determine latest stable release tag from openai/codex" >&2 + exit 1 + fi + + latest_release_version="${latest_release_tag#rust-v}" + build_version="${latest_release_version}-dev" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + fi + + build: + if: >- + always() && + (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') && + needs.select-build-matrix.result == 'success' + needs: + - tag-check + - select-build-matrix + name: Build - ${{ matrix.runner }} - ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + permissions: write-all + timeout-minutes: 120 defaults: run: working-directory: codex-rs env: - # 2026-03-04: temporarily change releases to use thin LTO because - # Ubuntu ARM is timing out at 60 minutes. - CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' }} + CODEX_BWRAP_ENABLE_FFI: ${{ contains(matrix.target, 'unknown-linux') && '1' || '0' }} + CODEX_BUILD_VERSION: ${{ needs.select-build-matrix.outputs.build_version }} + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" strategy: fail-fast: false - matrix: - include: - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - # Release artifacts intentionally ship MUSL-linked Linux binaries. - - runner: codex-linux-x64-xl - target: x86_64-unknown-linux-musl - bundle: primary - artifact_name: x86_64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: codex-linux-x64-xl - target: x86_64-unknown-linux-musl - bundle: app-server - artifact_name: x86_64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: codex-linux-arm64 - target: aarch64-unknown-linux-musl - bundle: primary - artifact_name: aarch64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: codex-linux-arm64 - target: aarch64-unknown-linux-musl - bundle: app-server - artifact_name: aarch64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" + matrix: ${{ fromJson(needs.select-build-matrix.outputs.matrix) }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + with: + verbose: true + - name: Checkout selected source ref + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + uses: actions/checkout@v6 with: - persist-credentials: false - - name: Print runner specs (Linux) - if: ${{ runner.os == 'Linux' }} + ref: ${{ inputs.source_ref }} + path: __source__ + fetch-depth: 1 + - name: Overlay selected source ref (excluding .github) + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + shell: bash + working-directory: ${{ github.workspace }} + run: | + set -euo pipefail + echo "Using source ref: ${{ inputs.source_ref }}" + + # Keep workflow/actions from the dispatch ref while replacing all other files. + find . -mindepth 1 -maxdepth 1 \ + ! -name .git \ + ! -name .github \ + ! -name __source__ \ + -exec rm -rf -- {} + + tar --exclude='.github' -C __source__ -cf - . | tar -xf - + rm -rf __source__ + + - name: Apply selected build version + if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} shell: bash + working-directory: ${{ github.workspace }} run: | set -euo pipefail - cpu_model="$(lscpu | awk -F: '/Model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}')" - total_ram="$(awk '/MemTotal/ {printf "%.1f GiB\n", $2 / 1024 / 1024}' /proc/meminfo)" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(uname -a)" - echo "CPU model: ${cpu_model}" - echo "Logical CPUs: $(nproc)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . - - name: Print runner specs (macOS) - if: ${{ runner.os == 'macOS' }} + if [[ -z "${CODEX_BUILD_VERSION}" ]]; then + echo "CODEX_BUILD_VERSION is empty for workflow_dispatch" >&2 + exit 1 + fi + echo "Using build version: ${CODEX_BUILD_VERSION}" + BUILD_VERSION="${CODEX_BUILD_VERSION}" perl -0777 -i -pe \ + 's/(\[workspace\.package\][^\[]*?version = ")([^"]+)(")/${1}$ENV{BUILD_VERSION}$3/s' \ + codex-rs/Cargo.toml + + grep -n "^\[workspace.package\]\|^version = " codex-rs/Cargo.toml | head -n 4 + + - name: Select Android NDK + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail - total_ram="$(sysctl -n hw.memsize | awk '{printf "%.1f GiB\n", $1 / 1024 / 1024 / 1024}')" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(sw_vers -productName) $(sw_vers -productVersion)" - echo "Hardware model: $(sysctl -n hw.model)" - echo "CPU architecture: $(uname -m)" - echo "Logical CPUs: $(sysctl -n hw.logicalcpu)" - echo "Physical CPUs: $(sysctl -n hw.physicalcpu)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . + + fallback_version="29.0.13113456" + ndk_root="${ANDROID_HOME}/ndk" + selected_ndk="" + + if [[ -d "${ndk_root}" ]]; then + while IFS= read -r candidate; do + toolchain="${candidate}/toolchains/llvm/prebuilt/linux-x86_64" + if [[ -x "${toolchain}/bin/aarch64-linux-android24-clang" ]]; then + selected_ndk="${candidate}" + break + fi + done < <(find "${ndk_root}" -mindepth 1 -maxdepth 1 -type d | sort -Vr) + fi + + if [[ -z "${selected_ndk}" ]]; then + echo "No usable preinstalled Android NDK found; installing ${fallback_version}" + yes | sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses || true + sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" "ndk;${fallback_version}" + selected_ndk="${ndk_root}/${fallback_version}" + fi + + if [[ ! -x "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" ]]; then + echo "Selected Android NDK does not contain a usable aarch64 API 24 clang: ${selected_ndk}" >&2 + exit 1 + fi + + echo "Selected Android NDK: ${selected_ndk}" + "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" --version + echo "ANDROID_NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" + echo "NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" - name: Install Linux bwrap build dependencies - if: ${{ runner.os == 'Linux' }} + if: ${{ runner.os == 'Linux' && matrix.target != 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail @@ -254,9 +294,173 @@ jobs: sudo apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 fi - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@1.93 with: targets: ${{ matrix.target }} + - name: Set up sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + uses: mozilla-actions/sccache-action@main + - name: Enable sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + shell: bash + run: | + set -euo pipefail + echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_VERSION=android-release-${{ matrix.target }}" >> "$GITHUB_ENV" + echo "SCCACHE_CACHE_SIZE=5G" >> "$GITHUB_ENV" + - name: Ensure Rust target is installed + shell: bash + run: | + set -euo pipefail + rustup target add "${{ matrix.target }}" + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android build environment + shell: bash + run: | + set -euo pipefail + ndk="${ANDROID_NDK_HOME}" + target="${{ matrix.target }}" + + # Set up the Android toolchain + toolchain="${ndk}/toolchains/llvm/prebuilt/linux-x86_64" + + # Use API level 24 to follow termux-build convention + api_level="24" + + # Configure Cargo to use the NDK linker + cargo_target_var="CARGO_TARGET_${target^^}_LINKER" + cargo_target_var="${cargo_target_var//-/_}" + echo "${cargo_target_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + # Set CC and AR for the target + target_cc_var="CC_${target}" + target_cc_var="${target_cc_var//-/_}" + echo "${target_cc_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + target_cxx_var="CXX_${target}" + target_cxx_var="${target_cxx_var//-/_}" + echo "${target_cxx_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang++" >> "$GITHUB_ENV" + + target_ar_var="AR_${target}" + target_ar_var="${target_ar_var//-/_}" + echo "${target_ar_var}=${toolchain}/bin/llvm-ar" >> "$GITHUB_ENV" + + # Add toolchain to PATH + echo "${toolchain}/bin" >> "$GITHUB_PATH" + + tls_align_source="${RUNNER_TEMP}/codex-android-tls-align.S" + tls_align_object="${RUNNER_TEMP}/codex-android-tls-align.o" + cat > "${tls_align_source}" <<'EOF' + .section .tdata.codex_android_tls_align,"awT",%progbits + .p2align 6 + .globl __codex_android_tls_align + .hidden __codex_android_tls_align + .type __codex_android_tls_align, %object + __codex_android_tls_align: + .byte 0 + .size __codex_android_tls_align, 1 + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${tls_align_source}" -o "${tls_align_object}" + + v8_compat_source="${RUNNER_TEMP}/codex-android-v8-compat.c" + v8_compat_object="${RUNNER_TEMP}/codex-android-v8-compat.o" + cat > "${v8_compat_source}" <<'EOF' + #include + #include + + extern int posix_memalign(void **memptr, size_t alignment, size_t size); + extern double strtod(const char *nptr, char **endptr); + extern float strtof(const char *nptr, char **endptr); + + void *aligned_alloc(size_t alignment, size_t size) { + void *ptr = 0; + if (posix_memalign(&ptr, alignment, size) != 0) { + return 0; + } + return ptr; + } + + double strtod_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtod(nptr, endptr); + } + + float strtof_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtof(nptr, endptr); + } + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${v8_compat_source}" -o "${v8_compat_object}" + + builtins_archive="$( + find "${toolchain}/lib/clang" \ + -path "*/lib/linux/libclang_rt.builtins-aarch64-android.a" \ + -print -quit + )" + if [[ -z "${builtins_archive}" ]]; then + echo "Could not find Android compiler-rt builtins archive under ${toolchain}/lib/clang" >&2 + exit 1 + fi + builtins_dir="$(dirname "${builtins_archive}")" + + # Provide compatibility symbols and C++ ABI needed by the Android rusty_v8 archive. + rustflags_var="CARGO_TARGET_${target^^}_RUSTFLAGS" + rustflags_var="${rustflags_var//-/_}" + echo "${rustflags_var}=-C link-arg=${tls_align_object} -C link-arg=${v8_compat_object} -C link-arg=-Wl,-u,__codex_android_tls_align -C link-arg=-L${builtins_dir} -C link-arg=-lclang_rt.builtins-aarch64-android -C link-arg=-lc++abi -C link-arg=-Wl,-z,max-page-size=65536" >> "$GITHUB_ENV" + + + # Configure for vendored OpenSSL build + echo "CARGO_BUILD_TARGET_APPLIES_TO_HOST=false" >> "$GITHUB_ENV" + echo "CARGO_BUILD_TARGET=${{ matrix.target }}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android rusty_v8 artifact overrides + env: + GH_TOKEN: ${{ github.token }} + TARGET: ${{ matrix.target }} + shell: bash + run: | + set -euo pipefail + binding_dir="${RUNNER_TEMP}/rusty_v8" + archive="${binding_dir}/librusty_v8_release_${TARGET}.a.gz" + binding_path="${binding_dir}/src_binding_release_${TARGET}.rs" + checksums_path="${binding_dir}/rusty_v8_release_${TARGET}.sha256" + artifact_repository="wallentx/codex-termux" + release_tag="rusty-v8-v147.4.0" + mkdir -p "${binding_dir}" + echo "Downloading Android rusty_v8 artifacts from ${artifact_repository}@${release_tag}" + gh release download "${release_tag}" \ + --repo "${artifact_repository}" \ + --dir "${binding_dir}" \ + --pattern "librusty_v8_release_${TARGET}.a.gz" \ + --pattern "src_binding_release_${TARGET}.rs" \ + --pattern "rusty_v8_release_${TARGET}.sha256" + (cd "${binding_dir}" && sha256sum -c "$(basename "${checksums_path}")") + echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV" + echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Install termux-elf-cleaner + shell: bash + run: | + set -euo pipefail + version="v3.0.1" + src_dir="${RUNNER_TEMP}/termux-elf-cleaner-src" + build_dir="${RUNNER_TEMP}/termux-elf-cleaner-build" + bin_dir="${RUNNER_TEMP}/termux-elf-cleaner-bin" + + rm -rf "${src_dir}" "${build_dir}" "${bin_dir}" + mkdir -p "${src_dir}" "${build_dir}" "${bin_dir}" + + curl -fsSL "https://github.com/termux/termux-elf-cleaner/archive/refs/tags/${version}.tar.gz" \ + | tar -xzf - --strip-components=1 -C "${src_dir}" + + cmake -S "${src_dir}" -B "${build_dir}" -DCMAKE_BUILD_TYPE=Release + cmake --build "${build_dir}" --parallel + + install "${build_dir}/termux-elf-cleaner" "${bin_dir}/termux-elf-cleaner" + echo "${bin_dir}" >> "$GITHUB_PATH" - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Use hermetic Cargo home (musl) @@ -269,12 +473,25 @@ jobs: echo "${cargo_home}/bin" >> "$GITHUB_PATH" : > "${cargo_home}/config.toml" + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ${{ github.workspace }}/.cargo-home/bin/ + ${{ github.workspace }}/.cargo-home/registry/index/ + ${{ github.workspace }}/.cargo-home/registry/cache/ + ${{ github.workspace }}/.cargo-home/git/db/ + ${{ github.workspace }}/codex-rs/target/ + key: cargo-${{ matrix.runner }}-${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }} + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install Zig - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + uses: mlugg/setup-zig@v2 with: version: 0.14.0 - use-cache: false - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install musl build tools @@ -310,12 +527,6 @@ jobs: shell: bash run: | set -euo pipefail - # Avoid problematic aws-lc jitter entropy code path on musl builders. - echo "AWS_LC_SYS_NO_JITTER_ENTROPY=1" >> "$GITHUB_ENV" - target_no_jitter="AWS_LC_SYS_NO_JITTER_ENTROPY_${{ matrix.target }}" - target_no_jitter="${target_no_jitter//-/_}" - echo "${target_no_jitter}=1" >> "$GITHUB_ENV" - # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. echo "RUSTFLAGS=" >> "$GITHUB_ENV" echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" @@ -340,93 +551,70 @@ jobs: echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" - - name: Configure rusty_v8 artifact overrides and verify checksums - uses: ./.github/actions/setup-rusty-v8 - with: - target: ${{ matrix.target }} - - - if: ${{ contains(matrix.target, 'linux') }} - name: Build bwrap and export digest + - name: Cargo build shell: bash run: | set -euo pipefail - target="${{ matrix.target }}" - cargo build --target "$target" --release --timings --bin bwrap - bwrap_path="target/${target}/release/bwrap" - if [[ ! -f "$bwrap_path" ]]; then - echo "bwrap binary ${bwrap_path} not found" - exit 1 + if [[ "${{ matrix.target }}" == 'aarch64-linux-android' ]]; then + export CARGO_BUILD_JOBS=4 + export CARGO_PROFILE_RELEASE_LTO=thin + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 + cargo build --target ${{ matrix.target }} --release --bin codex + exit 0 fi - digest="$(sha256sum "$bwrap_path" | awk '{print $1}')" - echo "CODEX_BWRAP_SHA256=${digest}" >> "$GITHUB_ENV" - echo "Built bwrap ${bwrap_path} with sha256:${digest}" - - - name: Cargo build + if [[ "${{ contains(matrix.target, 'windows') }}" == 'true' ]]; then + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy --bin codex-windows-sandbox-setup --bin codex-command-runner + else + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy + fi + - name: sccache stats (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash - run: | - build_args=() - for binary in ${{ matrix.binaries }}; do - build_args+=(--bin "$binary") - done - echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}" - cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}" - - - name: Upload Cargo timings - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }} - path: codex-rs/target/**/cargo-timings/cargo-timing.html - if-no-files-found: warn + run: ${SCCACHE_PATH:-sccache} --show-stats || true - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Stage unsigned macOS artifacts + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Normalize Android ELF for Termux shell: bash run: | set -euo pipefail - - target="${{ matrix.target }}" - release_dir="target/${target}/release" - dest="unsigned-dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - unsigned_name="${binary}-${target}-unsigned" - unsigned_path="${dest}/${unsigned_name}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 + for binary in codex codex-responses-api-proxy; do + binary_path="target/${{ matrix.target }}/release/${binary}" + if [[ -f "${binary_path}" ]]; then + termux-elf-cleaner --api-level 24 "${binary_path}" + chmod +x "${binary_path}" + # if [[ "${binary}" == "codex" ]]; then + # # Patch PT_TLS (0x7) to PT_NULL (0x0) at offset 400 to bypass Bionic alignment checks. + # printf '\x00\x00\x00\x00' | dd of="${binary_path}" bs=1 seek=400 count=4 conv=notrunc + # fi fi - - cp "${binary_path}" "${unsigned_path}" - tar -C "$dest" -czf "${unsigned_path}.tar.gz" "${unsigned_name}" - zstd -T0 -19 --rm "${unsigned_path}" done - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Upload unsigned macOS artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }}-unsigned - path: codex-rs/unsigned-dist/${{ matrix.target }}/* - if-no-files-found: error - - - if: ${{ contains(matrix.target, 'linux') }} + - if: ${{ contains(matrix.target, 'linux') && !contains(matrix.target, 'android') && github.repository_owner == 'openai' }} name: Cosign Linux artifacts uses: ./.github/actions/linux-code-sign with: target: ${{ matrix.target }} artifacts-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release - binaries: ${{ matrix.binaries }} - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS == 'true' }} + - if: ${{ contains(matrix.target, 'windows') && github.repository_owner == 'openai' }} + name: Sign Windows binaries with Azure Trusted Signing + uses: ./.github/actions/windows-code-sign + with: + target: ${{ matrix.target }} + client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID }} + endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} + account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} + + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (binaries) uses: ./.github/actions/macos-code-sign with: target: ${{ matrix.target }} - binaries: ${{ matrix.binaries }} sign-binaries: "true" sign-dmg: "false" apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }} @@ -435,7 +623,7 @@ jobs: apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: Build macOS dmg shell: bash run: | @@ -450,17 +638,23 @@ jobs: # The previous "MacOS code signing (binaries)" step signs + notarizes the # built artifacts in `${release_dir}`. This step packages *those same* # signed binaries into a dmg. + codex_binary_path="${release_dir}/codex" + proxy_binary_path="${release_dir}/codex-responses-api-proxy" + rm -rf "$dmg_root" mkdir -p "$dmg_root" - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 - fi - ditto "${binary_path}" "${dmg_root}/${binary}" - done + if [[ ! -f "$codex_binary_path" ]]; then + echo "Binary $codex_binary_path not found" + exit 1 + fi + if [[ ! -f "$proxy_binary_path" ]]; then + echo "Binary $proxy_binary_path not found" + exit 1 + fi + + ditto "$codex_binary_path" "${dmg_root}/codex" + ditto "$proxy_binary_path" "${dmg_root}/codex-responses-api-proxy" rm -f "$dmg_path" hdiutil create \ @@ -475,7 +669,7 @@ jobs: exit 1 fi - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (dmg) uses: ./.github/actions/macos-code-sign with: @@ -489,113 +683,65 @@ jobs: apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - name: Stage artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | dest="dist/${{ matrix.target }}" mkdir -p "$dest" - for binary in ${{ matrix.binaries }}; do - cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" - if [[ "${{ matrix.target }}" == *linux* ]]; then - cp "target/${{ matrix.target }}/release/${binary}.sigstore" \ - "$dest/${binary}-${{ matrix.target }}.sigstore" + if [[ "${{ matrix.runner }}" == windows* ]]; then + cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe" + else + # For Android, we want the binary to be named just 'codex' in the archive. + if [[ "${{ matrix.target }}" == "aarch64-linux-android" ]]; then + cp target/${{ matrix.target }}/release/codex "$dest/codex" + else + cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" fi - done + if [[ -f target/${{ matrix.target }}/release/codex-responses-api-proxy ]]; then + cp target/${{ matrix.target }}/release/codex-responses-api-proxy "$dest/codex-responses-api-proxy-${{ matrix.target }}" + fi + fi - if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.bundle }}" == "primary" ]]; then - bundle_root="${RUNNER_TEMP}/codex-${{ matrix.target }}-bundle" - rm -rf "$bundle_root" - mkdir -p "$bundle_root/codex-resources" - cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex" - cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap" - chmod 0755 "$bundle_root/codex" "$bundle_root/codex-resources/bwrap" - tar -C "$bundle_root" -cf - codex codex-resources/bwrap | - zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst" + if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.target }}" != *android* ]]; then + cp target/${{ matrix.target }}/release/codex.sigstore "$dest/codex-${{ matrix.target }}.sigstore" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.sigstore "$dest/codex-responses-api-proxy-${{ matrix.target }}.sigstore" fi - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then + if [[ "${{ matrix.target }}" == *apple-darwin ]]; then cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" fi - - name: Build Codex package archive - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "target/${TARGET}/release" \ - --archive-dir "dist/${TARGET}" - - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - aarch64-unknown-linux-musl) - platform_tag="manylinux_2_17_aarch64" - ;; - x86_64-unknown-linux-musl) - platform_tag="manylinux_2_17_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - # Do not install into the runner's system Python; macOS runners mark - # the Homebrew Python as externally managed under PEP 668. - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - stage_runtime_args=( - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" - stage-runtime - "$stage_dir" - "dist/${{ matrix.target }}/codex-package-${{ matrix.target }}.tar.gz" - --codex-version "${GITHUB_REF_NAME}" - --platform-tag "$platform_tag" - ) - python3 "${stage_runtime_args[@]}" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error + - if: ${{ matrix.runner == 'windows-11-arm' }} + name: Install zstd + shell: powershell + run: choco install -y zstandard - name: Compress artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | # Path that contains the uncompressed binaries for the current # ${{ matrix.target }} dest="dist/${{ matrix.target }}" + repo_root=$PWD + + # We want to ship the raw Windows executables in the GitHub Release + # in addition to the compressed archives. Keep the originals for + # Windows targets; remove them elsewhere to limit the number of + # artifacts that end up in the GitHub Release. + keep_originals=false + if [[ "${{ matrix.runner }}" == windows* ]]; then + keep_originals=true + fi # For compatibility with environments that lack the `zstd` tool we - # additionally create a `.tar.gz` alongside every binary we publish. - # The end result is: + # additionally create a `.tar.gz` for all platforms and `.zip` for + # Windows alongside every single binary that we publish. The end result is: # codex-.zst (existing) # codex-.tar.gz (new) + # codex-.zip (only for Windows) # 1. Produce a .tar.gz for every file in the directory *before* we # run `zstd --rm`, because that flag deletes the original files. @@ -603,7 +749,7 @@ jobs: base="$(basename "$f")" # Skip files that are already archives (shouldn't happen, but be # safe). - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then + if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then continue fi @@ -613,344 +759,239 @@ jobs: fi # Create per-binary tar.gz - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - - # Also create .zst and remove the uncompressed binaries to keep - # non-Windows artifact directories small. - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - with: - name: ${{ matrix.artifact_name }} - # Upload the per-binary .zst files, .tar.gz equivalents, and any - # prebuilt archives staged above. - path: | - codex-rs/dist/${{ matrix.target }}/* - - stage-signed-macos: - if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode == 'promote_signed' }} - needs: tag-check - name: Stage signed macOS handoff - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: macos-15-xlarge - timeout-minutes: 30 - permissions: - contents: read - defaults: - run: - working-directory: codex-rs + # For Android, we want the archive to have the target suffix even though the binary is just 'codex'. + archive_name="${base}" + if [[ "${{ matrix.target }}" == "aarch64-linux-android" && "${base}" == "codex" ]]; then + archive_name="codex-${{ matrix.target }}" + fi + tar -C "$dest" -czf "$dest/${archive_name}.tar.gz" "$base" + + # Create zip archive for Windows binaries + # Must run from inside the dest dir so 7z won't + # embed the directory path inside the zip. + if [[ "${{ matrix.runner }}" == windows* ]]; then + if [[ "$base" == "codex-${{ matrix.target }}.exe" ]]; then + # Bundle the sandbox helper binaries into the main codex zip so + # WinGet installs include the required helpers next to codex.exe. + # Fall back to the single-binary zip if the helpers are missing + # to avoid breaking releases. + bundle_dir="$(mktemp -d)" + runner_src="$dest/codex-command-runner-${{ matrix.target }}.exe" + setup_src="$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + if [[ -f "$runner_src" && -f "$setup_src" ]]; then + cp "$dest/$base" "$bundle_dir/$base" + cp "$runner_src" "$bundle_dir/codex-command-runner.exe" + cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" + # Use an absolute path so bundle zips land in the real dist + # dir even when 7z runs from a temp directory. + (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) + else + echo "warning: missing sandbox binaries; falling back to single-binary zip" + echo "warning: expected $runner_src and $setup_src" + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + rm -rf "$bundle_dir" + else + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + fi - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" + # Also create .zst (existing behaviour) *and* remove the original + # uncompressed binary to keep the directory small. + zstd_args=(-T0 -19) + if [[ "${keep_originals}" == false ]]; then + zstd_args+=(--rm) + fi - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + if [[ "${archive_name}" != "${base}" ]]; then + zstd "${zstd_args[@]}" "$dest/$base" -o "$dest/${archive_name}.zst" + else + zstd "${zstd_args[@]}" "$dest/$base" + fi + done - - name: Download signed macOS handoff + - name: Add Termux release metadata + if: ${{ (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && matrix.target == 'aarch64-linux-android' }} shell: bash - env: - GH_TOKEN: ${{ github.token }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - SIGNED_MACOS_SHA256: ${{ inputs.signed_macos_sha256 }} run: | set -euo pipefail - - download_dir="${RUNNER_TEMP}/signed-macos-download" - handoff_dir="${RUNNER_TEMP}/signed-macos-handoff" - rm -rf "$download_dir" "$handoff_dir" - mkdir -p "$download_dir" "$handoff_dir" - - gh release download "$GITHUB_REF_NAME" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern "$SIGNED_MACOS_ASSET" \ - --dir "$download_dir" - - asset_count="$(find "$download_dir" -maxdepth 1 -type f | wc -l | tr -d '[:space:]')" - if [[ "$asset_count" != "1" ]]; then - echo "Expected exactly one signed macOS handoff asset named ${SIGNED_MACOS_ASSET}; found ${asset_count}" - find "$download_dir" -maxdepth 1 -type f -print - exit 1 - fi - - asset_path="$(find "$download_dir" -maxdepth 1 -type f -print -quit)" - if [[ -n "${SIGNED_MACOS_SHA256}" ]]; then - expected_sha="$(printf '%s' "$SIGNED_MACOS_SHA256" | tr '[:upper:]' '[:lower:]')" - actual_sha="$(shasum -a 256 "$asset_path" | awk '{print $1}')" - if [[ "$actual_sha" != "$expected_sha" ]]; then - echo "signed_macos_sha256 mismatch for ${SIGNED_MACOS_ASSET}" - echo "expected: ${expected_sha}" - echo "actual: ${actual_sha}" - exit 1 - fi + if [[ ! -f "${GITHUB_WORKSPACE}/.github/termux-release.json" ]]; then + echo "No Termux release metadata found; skipping metadata attachment." + exit 0 fi + dest="dist/${{ matrix.target }}" + cp "${GITHUB_WORKSPACE}/.github/termux-release.json" "${dest}/termux-release.json" + ( + cd "${dest}" + sha256sum ./* > SHA256SUMS + ) - asset_name="$(basename "$asset_path")" - case "$asset_name" in - *.tar.zst) - zstd -dc "$asset_path" | tar -C "$handoff_dir" -xf - - ;; - *.tar.gz|*.tgz) - tar -C "$handoff_dir" -xzf "$asset_path" - ;; - *.zip) - ditto -x -k "$asset_path" "$handoff_dir" - ;; - *) - echo "Unsupported signed macOS handoff archive format: ${asset_name}" - exit 1 - ;; - esac - - echo "SIGNED_MACOS_HANDOFF_DIR=$handoff_dir" >> "$GITHUB_ENV" + - id: upload-artifact + uses: actions/upload-artifact@v6 + with: + name: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + # Upload the per-binary .zst files as well as the new .tar.gz + # equivalents we generated in the previous step. + path: | + codex-rs/dist/${{ matrix.target }}/* - - name: Stage signed macOS artifacts + - name: Publish Termux artifact notification + if: ${{ github.event_name == 'pull_request' && startsWith(github.base_ref, 'release/') && matrix.target == 'aarch64-linux-android' }} shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_ID: ${{ steps.upload-artifact.outputs.artifact-id }} + ARTIFACT_NAME: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + PR_NUMBER: ${{ github.event.pull_request.number }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | set -euo pipefail - - target="${{ matrix.target }}" - artifact_name="${{ matrix.artifact_name }}" - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${target}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - fi - if [[ ! -d "$source_dir" ]]; then - echo "Signed macOS handoff is missing ${artifact_name}/" - echo "Expected either:" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${target}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - find "$SIGNED_MACOS_HANDOFF_DIR" -maxdepth 3 -type f -print + if [[ -z "${ARTIFACT_ID}" ]]; then + echo "upload-artifact did not return an artifact id" >&2 exit 1 fi - dest="dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - source_path="${source_dir}/${binary}" - if [[ ! -f "$source_path" ]]; then - source_path="${source_dir}/${binary}-${target}" - fi - if [[ ! -f "$source_path" ]]; then - echo "Signed macOS handoff is missing ${binary} for ${artifact_name}" - exit 1 - fi - - release_path="${dest}/${binary}-${target}" - ditto "$source_path" "$release_path" - chmod 0755 "$release_path" - codesign --verify --strict --verbose=2 "$release_path" - done - - # DMG staging is disabled for signed promotion because we no longer - # distribute DMGs from this release path. Keep the branch here so the - # handoff can opt back in by flipping matrix.build_dmg if needed. - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - dmg_name="codex-${target}.dmg" - dmg_source="${source_dir}/${dmg_name}" - if [[ ! -f "$dmg_source" ]]; then - echo "Signed macOS handoff is missing ${dmg_name} for ${artifact_name}" - exit 1 - fi - - codesign --verify --strict --verbose=2 "$dmg_source" - xcrun stapler validate "$dmg_source" - cp "$dmg_source" "$dest/$dmg_name" + marker="" + artifact_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts/${ARTIFACT_ID}" + run_url="${GH_WORKFLOW_URL:-https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + body_path="${RUNNER_TEMP}/termux-artifact-comment.md" + { + echo "${marker}" + echo "Termux Android artifact ready for testing:" + echo + echo "- Artifact: [${ARTIFACT_NAME}](${artifact_url})" + echo "- Workflow run: [${GITHUB_RUN_ID}](${run_url})" + echo + echo "You must be signed in to GitHub with repository access to download Actions artifacts." + } > "${body_path}" + + existing_comment_id="$( + gh pr view "${PR_NUMBER}" \ + --repo "${GITHUB_REPOSITORY}" \ + --json comments \ + --jq ".comments | map(select(.author.is_bot == true and (.body | contains(\"${marker}\")))) | .[-1].id // \"\"" + )" + if [[ -n "${existing_comment_id}" ]]; then + gh api graphql \ + -f query=' + mutation($id: ID!, $body: String!) { + updateIssueComment(input: {id: $id, body: $body}) { + issueComment { + id + } + } + } + ' \ + -f id="${existing_comment_id}" \ + -f body="$(cat "${body_path}")" \ + >/dev/null + else + gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body-file "${body_path}" fi - - name: Build Codex package archive - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "dist/${TARGET}" \ - --archive-dir "dist/${TARGET}" \ - --target-suffixed-entrypoint - - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - python3 \ - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \ - stage-runtime \ - "$stage_dir" \ - "dist/${{ matrix.target }}/codex-package-${{ matrix.target }}.tar.gz" \ - --codex-version "${GITHUB_REF_NAME}" \ - --platform-tag "$platform_tag" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error - - - name: Compress artifacts - shell: bash - run: | - set -euo pipefail - - dest="dist/${{ matrix.target }}" - for f in "$dest"/*; do - base="$(basename "$f")" - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then - continue + gh label create binary-ready \ + --repo "${GITHUB_REPOSITORY}" \ + --color 0e8a16 \ + --description "Android binary is ready for testing" \ + --force + gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --add-label binary-ready + + if [[ -n "${SLACK_WEBHOOK_URL:-}" ]]; then + slack_payload_path="${RUNNER_TEMP}/termux-artifact-slack.json" + pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}" + jq -n \ + --arg artifact_name "${ARTIFACT_NAME}" \ + --arg artifact_url "${artifact_url}" \ + --arg pr_number "${PR_NUMBER}" \ + --arg pr_url "${pr_url}" \ + --arg run_id "${GITHUB_RUN_ID}" \ + --arg run_url "${run_url}" \ + '{ + text: ("Termux Android artifact ready for testing: " + $artifact_url), + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: ("*Termux Android artifact ready for testing*\n" + $artifact_url) + } + }, + { + type: "section", + fields: [ + { + type: "mrkdwn", + text: ("*Artifact:*\n<" + $artifact_url + "|" + $artifact_name + ">") + }, + { + type: "mrkdwn", + text: ("*Pull request:*\n<" + $pr_url + "|#" + $pr_number + ">") + }, + { + type: "mrkdwn", + text: ("*Workflow run:*\n<" + $run_url + "|" + $run_id + ">") + } + ] + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: "You must be signed in to GitHub with repository access to download Actions artifacts." + } + ] + } + ] + }' > "${slack_payload_path}" + + if ! curl -fsS \ + -X POST \ + -H "Content-type: application/json" \ + --data @"${slack_payload_path}" \ + "${SLACK_WEBHOOK_URL}"; then + echo "::warning::Failed to send Slack artifact notification" fi + fi - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }} - path: | - codex-rs/dist/${{ matrix.target }}/* - - build-windows: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - needs: tag-check - uses: ./.github/workflows/rust-release-windows.yml - with: - release-lto: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }} - secrets: inherit - - argument-comment-lint-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: argument-comment-lint release assets + shell-tool-mcp: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'openai' + name: shell-tool-mcp needs: tag-check - uses: ./.github/workflows/rust-release-argument-comment-lint.yml + permissions: + contents: read + id-token: write + uses: ./.github/workflows/shell-tool-mcp.yml with: + release-tag: ${{ github.ref_name }} publish: true - - zsh-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: zsh release assets - needs: tag-check - uses: ./.github/workflows/rust-release-zsh.yml + secrets: inherit release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') needs: - - tag-check - build - - stage-signed-macos - - build-windows - - argument-comment-lint-release-assets - - zsh-release-assets - if: >- - ${{ - always() && - needs.tag-check.result == 'success' && - ( - ( - github.event_name == 'workflow_dispatch' && - inputs.release_mode == 'promote_signed' && - needs.stage-signed-macos.result == 'success' && - needs.build.result == 'skipped' && - needs.build-windows.result == 'skipped' && - needs.argument-comment-lint-release-assets.result == 'skipped' && - needs.zsh-release-assets.result == 'skipped' - ) || - ( - (github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed') && - needs.build.result == 'success' && - needs.stage-signed-macos.result == 'skipped' && - needs.build-windows.result == 'success' && - needs.argument-comment-lint-release-assets.result == 'success' && - needs.zsh-release-assets.result == 'success' - ) - ) - }} + - shell-tool-mcp name: release runs-on: ubuntu-latest permissions: contents: write actions: read - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode == 'promote_signed' }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} outputs: version: ${{ steps.release_name.outputs.name }} tag: ${{ github.ref_name }} - sign_macos: ${{ steps.release_mode.outputs.sign_macos }} should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }} npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }} - should_publish_python_runtime: ${{ steps.python_runtime_publish_settings.outputs.should_publish }} steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + uses: actions/checkout@v6 - - name: Define release mode - id: release_mode - run: | - echo "release_mode=${RELEASE_MODE}" >> "$GITHUB_OUTPUT" - echo "sign_macos=${SIGN_MACOS}" >> "$GITHUB_OUTPUT" + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main - name: Generate release notes from tag commit message id: release_notes @@ -972,159 +1013,21 @@ jobs: echo "path=${notes_path}" >> "${GITHUB_OUTPUT}" - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@v7 with: path: dist - - name: Validate unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - run_summary="$(gh run view "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --json conclusion,event,headBranch,headSha,status,workflowName,url \ - --jq '[.workflowName, .event, .headBranch, .headSha, .status, .conclusion, .url] | @tsv')" - IFS=$'\t' read -r workflow_name event head_branch head_sha status conclusion run_url <<< "$run_summary" - expected_head_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")" - - if [[ "$workflow_name" != "$GITHUB_WORKFLOW" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is for workflow '${workflow_name}', expected '${GITHUB_WORKFLOW}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$event" != "workflow_dispatch" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} was triggered by '${event}', expected 'workflow_dispatch'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_branch" != "$GITHUB_REF_NAME" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used ref '${head_branch}', expected '${GITHUB_REF_NAME}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_sha" != "$expected_head_sha" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used head SHA '${head_sha}', expected '${expected_head_sha}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is ${status}/${conclusion}, expected completed/success" - echo "Run URL: ${run_url}" - exit 1 - fi - - - name: Download artifacts from unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - gh run download "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --dir dist - - - name: Remove unsigned macOS staging artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - run: | - set -euo pipefail - find dist -mindepth 1 -maxdepth 1 -type d \ - -name '*-apple-darwin*-unsigned' \ - -exec rm -rf {} + - - - name: Re-upload promoted Linux x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-unknown-linux-musl - path: dist/x86_64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Linux arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-unknown-linux-musl - path: dist/aarch64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Windows x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-pc-windows-msvc - path: dist/x86_64-pc-windows-msvc/* - if-no-files-found: error - - - name: Re-upload promoted Windows arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-pc-windows-msvc - path: dist/aarch64-pc-windows-msvc/* - if-no-files-found: error - - name: List run: ls -R dist/ - - name: Prune artifacts excluded from unsigned macOS release - if: ${{ env.SIGN_MACOS == 'false' }} - run: | - find dist -mindepth 1 -maxdepth 1 -type d \ - ! -name '*-apple-darwin*-unsigned' \ - ! -name 'aarch64-unknown-linux-musl' \ - ! -name 'aarch64-unknown-linux-musl-app-server' \ - ! -name 'x86_64-unknown-linux-musl' \ - ! -name 'x86_64-unknown-linux-musl-app-server' \ - ! -name 'aarch64-pc-windows-msvc' \ - ! -name 'x86_64-pc-windows-msvc' \ - -exec rm -rf {} + - - if ! find dist -type f -name '*-apple-darwin*-unsigned*' | grep -q .; then - echo "No unsigned macOS artifacts found in downloaded workflow artifacts." - exit 1 - fi - + # This is a temporary fix: we should modify shell-tool-mcp.yml so these + # files do not end up in dist/ in the first place. - name: Delete entries from dist/ that should not go in the release run: | - rm -rf dist/windows-binaries* - # cargo-timing.html appears under multiple target-specific directories. - # If included in files: dist/**, release upload races on duplicate - # asset names and can fail with 404s. - find dist -type f -name 'cargo-timing.html' -delete - find dist -type d -empty -delete + rm -rf dist/shell-tool-mcp* ls -R dist/ - - name: Add Codex package checksum manifest - run: | - set -euo pipefail - - manifest="dist/codex-package_SHA256SUMS" - tmp_manifest="$(mktemp)" - find dist -type f \ - \( -name 'codex-package-*.tar.gz' -o -name 'codex-app-server-package-*.tar.gz' \) \ - -print | - sort | - while IFS= read -r archive; do - sha256sum "$archive" | - awk -v name="$(basename "$archive")" '{ print $1 " " name }' - done > "$tmp_manifest" - - if [[ ! -s "$tmp_manifest" ]]; then - echo "No Codex package archives found for checksum manifest" - exit 1 - fi - - mv "$tmp_manifest" "$manifest" - cat "$manifest" - - name: Add config schema release asset run: | cp codex-rs/core/config.schema.json dist/config-schema.json @@ -1145,12 +1048,6 @@ jobs: set -euo pipefail version="${VERSION}" - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - echo "npm_tag=" >> "$GITHUB_OUTPUT" - exit 0 - fi - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "should_publish=true" >> "$GITHUB_OUTPUT" echo "npm_tag=" >> "$GITHUB_OUTPUT" @@ -1162,130 +1059,55 @@ jobs: echo "npm_tag=" >> "$GITHUB_OUTPUT" fi - - name: Determine Python runtime publish settings - id: python_runtime_publish_settings - env: - VERSION: ${{ steps.release_name.outputs.name }} - run: | - set -euo pipefail - version="${VERSION}" - - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - else - echo "should_publish=false" >> "$GITHUB_OUTPUT" - fi - - name: Setup pnpm - if: ${{ env.SIGN_MACOS == 'true' }} - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + uses: pnpm/action-setup@v4 with: run_install: false - name: Setup Node.js for npm packaging - if: ${{ env.SIGN_MACOS == 'true' }} - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: node-version: 22 - name: Install dependencies - if: ${{ env.SIGN_MACOS == 'true' }} run: pnpm install --frozen-lockfile + # stage_npm_packages.py requires DotSlash when staging releases. + - uses: facebook/install-dotslash@v2 - name: Stage npm packages - if: ${{ env.SIGN_MACOS == 'true' }} + if: github.repository_owner == 'openai' env: GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ steps.release_name.outputs.name }} run: | - workflow_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" ./scripts/stage_npm_packages.py \ - --release-version "$RELEASE_VERSION" \ - --workflow-url "$workflow_url" \ + --release-version "${{ steps.release_name.outputs.name }}" \ --package codex \ --package codex-responses-api-proxy \ --package codex-sdk - - name: Stage installer scripts - if: ${{ env.SIGN_MACOS == 'true' }} - run: | - cp scripts/install/install.sh dist/install.sh - cp scripts/install/install.ps1 dist/install.ps1 - - name: Create GitHub Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@v2 with: name: ${{ steps.release_name.outputs.name }} tag_name: ${{ github.ref_name }} body_path: ${{ steps.release_notes.outputs.path }} files: dist/** - overwrite_files: true - make_latest: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} # Mark as prerelease only when the version has a suffix after x.y.z # (e.g. -alpha, -beta). Otherwise publish a normal release. prerelease: ${{ contains(steps.release_name.outputs.name, '-') }} - - name: Clean up signed promotion handoff assets - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" --jq '.id')" - gh api --paginate "repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets" \ - --jq '.[] | [.id, .name] | @tsv' | - while IFS=$'\t' read -r asset_id asset_name; do - if [[ -z "$asset_id" || -z "$asset_name" ]]; then - continue - fi - - delete_asset=false - if [[ "$asset_name" == *unsigned* || "$asset_name" == "$SIGNED_MACOS_ASSET" ]]; then - delete_asset=true - fi - - if [[ "$delete_asset" == "true" ]]; then - echo "Deleting release asset ${asset_name}" - gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" - fi - done - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 + - if: github.repository_owner == 'openai' + uses: facebook/dotslash-publish-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag: ${{ github.ref_name }} config: .github/dotslash-config.json - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-zsh-config.json - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-argument-comment-lint-config.json - - name: Trigger developers.openai.com deploy # Only trigger the deploy if the release is not a pre-release. # The deploy is used to update the developers.openai.com website with the new config schema json file. - if: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} + if: ${{ !contains(steps.release_name.outputs.name, '-') && github.repository_owner == 'openai' }} continue-on-error: true env: DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }} @@ -1300,15 +1122,7 @@ jobs: # npm docs: https://docs.npmjs.com/trusted-publishers publish-npm: # Publish to npm for stable releases and alpha pre-releases with numeric suffixes. - # promote_signed intentionally skips build jobs that are ancestors of release; - # include the !cancelled() status function so Actions does not apply its implicit - # success() check to the whole dependency chain before evaluating release outputs. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_npm == 'true' - }} + if: ${{ needs.release.outputs.should_publish_npm == 'true' && github.repository_owner == 'openai' }} name: publish-npm needs: release runs-on: ubuntu-latest @@ -1318,37 +1132,36 @@ jobs: steps: - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: - # Node 24 bundles npm >= 11.5.1, which trusted publishing requires. - node-version: 24 + node-version: 22 registry-url: "https://registry.npmjs.org" scope: "@openai" + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + - name: Download npm tarballs from release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} run: | set -euo pipefail - version="$RELEASE_VERSION" - tag="$RELEASE_TAG" + version="${{ needs.release.outputs.version }}" + tag="${{ needs.release.outputs.tag }}" mkdir -p dist/npm - patterns=( - "codex-npm-${version}.tgz" - "codex-npm-linux-*-${version}.tgz" - "codex-npm-darwin-*-${version}.tgz" - "codex-npm-win32-*-${version}.tgz" - "codex-responses-api-proxy-npm-${version}.tgz" - "codex-sdk-npm-${version}.tgz" - ) - for pattern in "${patterns[@]}"; do - gh release download "$tag" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "$pattern" \ - --dir dist/npm - done + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-responses-api-proxy-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-sdk-npm-${version}.tgz" \ + --dir dist/npm # No NODE_AUTH_TOKEN needed because we use OIDC. - name: Publish to npm @@ -1357,193 +1170,23 @@ jobs: NPM_TAG: ${{ needs.release.outputs.npm_tag }} run: | set -euo pipefail - prefix="" + tag_args=() if [[ -n "${NPM_TAG}" ]]; then - prefix="${NPM_TAG}-" + tag_args+=(--tag "${NPM_TAG}") fi - root_tarball="dist/npm/codex-npm-${VERSION}.tgz" - sdk_tarball="dist/npm/codex-sdk-npm-${VERSION}.tgz" - # Keep this list in sync with CODEX_PLATFORM_PACKAGES in - # codex-cli/scripts/build_npm_package.py. The root wrapper advances - # @openai/codex@latest as soon as it publishes, so every platform - # package it aliases must already exist in the registry first. - platform_tarballs=( - "dist/npm/codex-npm-linux-x64-${VERSION}.tgz" - "dist/npm/codex-npm-linux-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-x64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-x64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-arm64-${VERSION}.tgz" - ) - - for required_tarball in "${platform_tarballs[@]}" "${root_tarball}"; do - if [[ ! -f "${required_tarball}" ]]; then - echo "Missing npm tarball: ${required_tarball}" - exit 1 - fi - done - - shopt -s nullglob - other_tarballs=() - for tarball in dist/npm/*-"${VERSION}".tgz; do - if [[ "${tarball}" == "${root_tarball}" || "${tarball}" == "${sdk_tarball}" ]]; then - continue - fi - - is_platform_tarball=false - for platform_tarball in "${platform_tarballs[@]}"; do - if [[ "${tarball}" == "${platform_tarball}" ]]; then - is_platform_tarball=true - break - fi - done - if [[ "${is_platform_tarball}" == true ]]; then - continue - fi - - other_tarballs+=("${tarball}") - done - - # Publish the platform packages before the root CLI wrapper. The root - # wrapper advances @openai/codex@latest, so it should only publish - # after the optional dependency versions it references exist. tarballs=( - "${platform_tarballs[@]}" - "${other_tarballs[@]}" - "${root_tarball}" + "codex-npm-${VERSION}.tgz" + "codex-responses-api-proxy-npm-${VERSION}.tgz" + "codex-sdk-npm-${VERSION}.tgz" ) - if [[ -f "${sdk_tarball}" ]]; then - tarballs+=("${sdk_tarball}") - fi for tarball in "${tarballs[@]}"; do - filename="$(basename "${tarball}")" - tag="" - - case "${filename}" in - codex-npm-linux-*-"${VERSION}".tgz|codex-npm-darwin-*-"${VERSION}".tgz|codex-npm-win32-*-"${VERSION}".tgz) - platform="${filename#codex-npm-}" - platform="${platform%-${VERSION}.tgz}" - tag="${prefix}${platform}" - ;; - codex-npm-"${VERSION}".tgz|codex-responses-api-proxy-npm-"${VERSION}".tgz|codex-sdk-npm-"${VERSION}".tgz) - tag="${NPM_TAG}" - ;; - *) - echo "Unexpected npm tarball: ${filename}" - exit 1 - ;; - esac - - publish_cmd=(npm publish "${GITHUB_WORKSPACE}/${tarball}") - if [[ -n "${tag}" ]]; then - publish_cmd+=(--tag "${tag}") - fi - - echo "+ ${publish_cmd[*]}" - set +e - publish_output="$("${publish_cmd[@]}" 2>&1)" - publish_status=$? - set -e - - echo "${publish_output}" - if [[ ${publish_status} -eq 0 ]]; then - continue - fi - - if grep -qiE "previously published|cannot publish over|version already exists" <<< "${publish_output}"; then - echo "Skipping already-published package version for ${filename}" - continue - fi - - exit "${publish_status}" + npm publish "${GITHUB_WORKSPACE}/dist/npm/${tarball}" "${tag_args[@]}" done - # Publish the platform-specific Python runtime wheels using PyPI trusted publishing. - # PyPI project configuration must trust this workflow and job. Keep this - # non-blocking while the Python runtime publishing path is new; failures still - # need release follow-up, but should not invalidate the Rust release itself. - publish-python-runtime: - # Publish to PyPI for stable releases and alpha pre-releases with numeric suffixes. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_python_runtime == 'true' - }} - name: publish-python-runtime - needs: release - runs-on: ubuntu-latest - continue-on-error: true - environment: pypi - permissions: - id-token: write # Required for PyPI trusted publishing. - contents: read - - steps: - - name: Download Python runtime wheels from release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} - run: | - set -euo pipefail - python_version="$RELEASE_VERSION" - python_version="${python_version/-alpha./a}" - python_version="${python_version/-beta./b}" - python_version="${python_version/-rc./rc}" - - mkdir -p dist/python-runtime - gh release download "$RELEASE_TAG" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "openai_codex_cli_bin-${python_version}-*.whl" \ - --dir dist/python-runtime - ls -lh dist/python-runtime - - - name: Publish Python runtime wheels to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - with: - packages-dir: dist/python-runtime - skip-existing: true - - winget: - name: winget - needs: release - # Only publish stable/mainline releases to WinGet; pre-releases include a - # '-' in the semver string (e.g., 1.2.3-alpha.1). - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' && - !contains(needs.release.outputs.version, '-') - }} - # This job only invokes a GitHub Action to open/update the winget-pkgs PR; - # it does not execute Windows-only tooling, so Linux is sufficient. - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Publish to WinGet - uses: vedantmgoyal9/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5 - with: - identifier: OpenAI.Codex - version: ${{ needs.release.outputs.version }} - release-tag: ${{ needs.release.outputs.tag }} - fork-user: openai-oss-forks - installers-regex: '^codex-(?:x86_64|aarch64)-pc-windows-msvc\.exe\.zip$' - token: ${{ secrets.WINGET_PUBLISH_PAT }} - update-branch: name: Update latest-alpha-cli branch - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' - }} permissions: contents: write needs: release diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml new file mode 100644 index 000000000000..66a76aa4d938 --- /dev/null +++ b/.github/workflows/shell-tool-mcp.yml @@ -0,0 +1,461 @@ +name: shell-tool-mcp + +on: + workflow_call: + inputs: + release-version: + description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v. + required: false + type: string + release-tag: + description: Tag name to use when downloading release artifacts (defaults to rust-v). + required: false + type: string + publish: + description: Whether to publish to npm when the version is releasable. + required: false + default: true + type: boolean + +env: + NODE_VERSION: 22 + +jobs: + metadata: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.compute.outputs.version }} + release_tag: ${{ steps.compute.outputs.release_tag }} + should_publish: ${{ steps.compute.outputs.should_publish }} + npm_tag: ${{ steps.compute.outputs.npm_tag }} + steps: + - name: Compute version and tags + id: compute + run: | + set -euo pipefail + + version="${{ inputs.release-version }}" + release_tag="${{ inputs.release-tag }}" + + if [[ -z "$version" ]]; then + if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then + version="${release_tag#rust-v}" + elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then + version="${GITHUB_REF_NAME#rust-v}" + release_tag="${GITHUB_REF_NAME}" + else + echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag." + exit 1 + fi + fi + + if [[ -z "$release_tag" ]]; then + release_tag="rust-v${version}" + fi + + npm_tag="" + should_publish="false" + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + should_publish="true" + elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then + should_publish="true" + npm_tag="alpha" + fi + + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" + echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT" + echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT" + + matrix-setup: + runs-on: ubuntu-latest + outputs: + rust-binaries: ${{ steps.compute.outputs.rust-binaries }} + bash-linux: ${{ steps.compute.outputs.bash-linux }} + bash-darwin: ${{ steps.compute.outputs.bash-darwin }} + steps: + - name: Compute matrices + id: compute + shell: bash + run: | + set -euo pipefail + is_openai="${{ github.repository_owner == 'openai' }}" + + # rust-binaries: always include x86_64-musl; conditionally include paid/fork runners + if [[ "$is_openai" == "true" ]]; then + rust='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","install_musl":true} + ]' + else + rust='[{"runner":"macos-latest","target":"aarch64-apple-darwin"}]' + fi + rust=$(echo "$rust" | jq -c '. + [{"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","install_musl":true}]') + echo "rust-binaries={\"include\":$rust}" >> "$GITHUB_OUTPUT" + + # bash-linux: always include x86_64 variants; add arm64 on openai + bash_linux='[ + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-24.04","image":"ubuntu:24.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-22.04","image":"ubuntu:22.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-12","image":"debian:12"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-11","image":"debian:11"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]' + if [[ "$is_openai" == "true" ]]; then + bash_linux=$(echo "$bash_linux" | jq -c '. + [ + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-24.04","image":"arm64v8/ubuntu:24.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-22.04","image":"arm64v8/ubuntu:22.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-20.04","image":"arm64v8/ubuntu:20.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-12","image":"arm64v8/debian:12"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-11","image":"arm64v8/debian:11"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]') + fi + echo "bash-linux={\"include\":$(echo "$bash_linux" | jq -c)}" >> "$GITHUB_OUTPUT" + + # bash-darwin: always include macos-14; add macos-15-xlarge on openai + bash_darwin='[{"runner":"macos-14","target":"aarch64-apple-darwin","variant":"macos-14"}]' + if [[ "$is_openai" == "true" ]]; then + bash_darwin=$(echo "$bash_darwin" | jq -c '. + [ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin","variant":"macos-15"} + ]') + fi + echo "bash-darwin={\"include\":$(echo "$bash_darwin" | jq -c)}" >> "$GITHUB_OUTPUT" + + rust-binaries: + name: Build Rust - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + defaults: + run: + working-directory: codex-rs + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.rust-binaries) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install UBSan runtime (musl) + if: ${{ matrix.install_musl }} + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -y + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 + fi + + - uses: dtolnay/rust-toolchain@1.93 + with: + targets: ${{ matrix.target }} + + - if: ${{ matrix.install_musl }} + name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.14.0 + + - if: ${{ matrix.install_musl }} + name: Install musl build dependencies + env: + TARGET: ${{ matrix.target }} + run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh" + + - if: ${{ matrix.install_musl }} + name: Configure rustc UBSan wrapper (musl host) + shell: bash + run: | + set -euo pipefail + ubsan="" + if command -v ldconfig >/dev/null 2>&1; then + ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')" + fi + wrapper_root="${RUNNER_TEMP:-/tmp}" + wrapper="${wrapper_root}/rustc-ubsan-wrapper" + cat > "${wrapper}" <> "$GITHUB_ENV" + echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV" + + - if: ${{ matrix.install_musl }} + name: Clear sanitizer flags (musl) + shell: bash + run: | + set -euo pipefail + # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. + echo "RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV" + # Override any runner-level Cargo config rustflags as well. + echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + + sanitize_flags() { + local input="$1" + input="${input//-fsanitize=undefined/}" + input="${input//-fno-sanitize-recover=undefined/}" + input="${input//-fno-sanitize-trap=undefined/}" + echo "$input" + } + + cflags="$(sanitize_flags "${CFLAGS-}")" + cxxflags="$(sanitize_flags "${CXXFLAGS-}")" + echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" + echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + + - name: Build exec server binaries + run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper + + - name: Stage exec server binaries + run: | + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}" + mkdir -p "$dest" + cp "target/${{ matrix.target }}/release/codex-exec-mcp-server" "$dest/" + cp "target/${{ matrix.target }}/release/codex-execve-wrapper" "$dest/" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-rust-${{ matrix.target }} + path: artifacts/** + if-no-files-found: error + + bash-linux: + name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + container: + image: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-linux) }} + steps: + - name: Install build prerequisites + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext + elif command -v dnf >/dev/null 2>&1; then + dnf install -y git gcc gcc-c++ make bison autoconf gettext + elif command -v yum >/dev/null 2>&1; then + yum install -y git gcc gcc-c++ make bison autoconf gettext + else + echo "Unsupported package manager in container" + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + bash-darwin: + name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-darwin) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + package: + name: Package npm module + needs: + - metadata + - rust-binaries + - bash-linux + - bash-darwin + runs-on: ubuntu-latest + env: + PACKAGE_VERSION: ${{ needs.metadata.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install JavaScript dependencies + run: pnpm install --frozen-lockfile + + - name: Build (shell-tool-mcp) + run: pnpm --filter @openai/codex-shell-tool-mcp run build + + - name: Download build artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts + + - name: Assemble staging directory + id: staging + shell: bash + run: | + set -euo pipefail + staging="${STAGING_DIR}" + mkdir -p "$staging" "$staging/vendor" + cp shell-tool-mcp/README.md "$staging/" + cp shell-tool-mcp/package.json "$staging/" + cp -R shell-tool-mcp/bin "$staging/" + + found_vendor="false" + shopt -s nullglob + for vendor_dir in artifacts/*/vendor; do + rsync -av "$vendor_dir/" "$staging/vendor/" + found_vendor="true" + done + if [[ "$found_vendor" == "false" ]]; then + echo "No vendor payloads were downloaded." + exit 1 + fi + + node - <<'NODE' + import fs from "node:fs"; + import path from "node:path"; + + const stagingDir = process.env.STAGING_DIR; + const version = process.env.PACKAGE_VERSION; + const pkgPath = path.join(stagingDir, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + pkg.version = version; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + NODE + + echo "dir=$staging" >> "$GITHUB_OUTPUT" + env: + STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp + + - name: Ensure binaries are executable + run: | + set -euo pipefail + staging="${{ steps.staging.outputs.dir }}" + chmod +x \ + "$staging"/vendor/*/codex-exec-mcp-server \ + "$staging"/vendor/*/codex-execve-wrapper \ + "$staging"/vendor/*/bash/*/bash + + - name: Create npm tarball + shell: bash + run: | + set -euo pipefail + mkdir -p dist/npm + staging="${{ steps.staging.outputs.dir }}" + pack_info=$(cd "$staging" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm") + filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);') + mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz" + + - uses: actions/upload-artifact@v6 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz + if-no-files-found: error + + publish: + name: Publish npm package + needs: + - metadata + - package + if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }} + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: https://registry.npmjs.org + scope: "@openai" + + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + + - name: Download npm tarball + uses: actions/download-artifact@v7 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm + + - name: Publish to npm + if: github.repository == 'openai/codex' + env: + NPM_TAG: ${{ needs.metadata.outputs.npm_tag }} + VERSION: ${{ needs.metadata.outputs.version }} + shell: bash + run: | + set -euo pipefail + tag_args=() + if [[ -n "${NPM_TAG}" ]]; then + tag_args+=(--tag "${NPM_TAG}") + fi + npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}" diff --git a/.github/workflows/termux-release-checkpoint.yml b/.github/workflows/termux-release-checkpoint.yml new file mode 100644 index 000000000000..0347e6f1b3d6 --- /dev/null +++ b/.github/workflows/termux-release-checkpoint.yml @@ -0,0 +1,103 @@ +name: termux-release-checkpoint + +on: + workflow_dispatch: + inputs: + source_branch: + description: "Release branch to checkpoint from, for example release/0.123.0" + required: false + type: string + default: "" + source_sha: + description: "Specific source commit SHA to checkpoint; defaults to the source branch tip" + required: false + type: string + default: "" + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + type: string + default: "wallentx/termux-target" + reviewer: + description: "GitHub username to request as reviewer" + required: false + type: string + default: "wallentx" + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-checkpoint-${{ inputs.source_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + checkpoint: + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + env: + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + REQUESTED_SOURCE_BRANCH: ${{ inputs.source_branch }} + REQUESTED_SOURCE_SHA: ${{ inputs.source_sha }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ inputs.source_branch || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + source_branch="${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${source_branch}" + + - name: Create checkpoint PR + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" diff --git a/.github/workflows/termux-release-deploy.yml b/.github/workflows/termux-release-deploy.yml new file mode 100644 index 000000000000..2071611dacc4 --- /dev/null +++ b/.github/workflows/termux-release-deploy.yml @@ -0,0 +1,243 @@ +name: termux-release-deploy + +on: + push: + branches: + - "release/**" + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to deploy, for example release/0.124.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to deploy. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged release PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged release PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + default: "wallentx/termux-target" + type: string + reviewer: + description: "GitHub username to request as reviewer on the checkpoint PR" + required: false + default: "wallentx" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-deploy-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + deploy: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' || !startsWith(github.event.head_commit.message, 'Seed Termux release automation') }} + permissions: + actions: read + contents: write + deployments: write + issues: write + pull-requests: write + env: + REQUESTED_RELEASE_BRANCH: ${{ inputs.release_branch }} + REQUESTED_RELEASE_SHA: ${{ inputs.release_sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + release_branch="${REQUESTED_RELEASE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${release_branch}" + + - name: Resolve release ref + id: release-ref + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: deploy + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Create deployment + if: steps.metadata.outputs.deploy == 'true' + id: deployment + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + deployment_id="$( + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments" \ + -f ref="${RELEASE_SHA}" \ + -f environment="termux-release" \ + -F auto_merge=false \ + -F required_contexts[] \ + -f description="Termux release deployment for ${TERMUX_TAG}" \ + --jq '.id' + )" + echo "id=${deployment_id}" >> "$GITHUB_OUTPUT" + + - name: Mark deployment in progress + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="in_progress" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Promoting Termux release artifact and preparing checkpoint PR" \ + >/dev/null + + - name: Locate merged pull request + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" + + - name: Ensure checkpoint PR + if: steps.metadata.outputs.deploy == 'true' + id: checkpoint + env: + SOURCE_BRANCH: ${{ steps.release-ref.outputs.branch }} + SOURCE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" + + - name: Mark deployment success + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="success" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment completed for ${TERMUX_TAG}" \ + >/dev/null + + - name: Mark deployment failure + if: failure() && steps.deployment.outputs.id != '' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="failure" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment failed" \ + >/dev/null diff --git a/.github/workflows/termux-release-promote.yml b/.github/workflows/termux-release-promote.yml new file mode 100644 index 000000000000..22c277c21a70 --- /dev/null +++ b/.github/workflows/termux-release-promote.yml @@ -0,0 +1,135 @@ +name: termux-release-promote + +on: + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to promote, for example release/0.122.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to promote. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-promote-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + promote: + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: write + pull-requests: read + env: + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Resolve release ref + id: release-ref + env: + INPUT_RELEASE_BRANCH: ${{ inputs.release_branch }} + INPUT_RELEASE_SHA: ${{ inputs.release_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: promote + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Locate merged pull request + if: steps.metadata.outputs.promote == 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.promote == 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.promote == 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" diff --git a/scripts/termux-configure-git.sh b/scripts/termux-configure-git.sh new file mode 100755 index 000000000000..fae3abcbab48 --- /dev/null +++ b/scripts/termux-configure-git.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Configure the explicit bot identity used by release automation and fetch any +# refs requested by the calling workflow. + +set -euo pipefail + +git config user.name "github-actions[bot]" +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + +while (($#)); do + case "$1" in + --origin) + shift + origin_refs=() + while (($#)) && [[ "$1" != --* ]]; do + origin_refs+=("$1") + shift + done + if ((${#origin_refs[@]})); then + git fetch --prune origin "${origin_refs[@]}" + fi + ;; + --upstream-tag) + if (($# < 3)); then + echo "--upstream-tag requires ." >&2 + exit 1 + fi + upstream_repo="$2" + upstream_tag="$3" + git remote add upstream "https://github.com/${upstream_repo}.git" 2>/dev/null || true + git fetch --prune --no-tags upstream "+refs/tags/${upstream_tag}:refs/tags/${upstream_tag}" + shift 3 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done diff --git a/scripts/termux-create-checkpoint-pr.sh b/scripts/termux-create-checkpoint-pr.sh new file mode 100755 index 000000000000..f315a5b91822 --- /dev/null +++ b/scripts/termux-create-checkpoint-pr.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/termux-release-paths.sh +source "${script_dir}/termux-release-paths.sh" + +source_branch="${SOURCE_BRANCH:-${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}}" +source_sha="${SOURCE_SHA:-${REQUESTED_SOURCE_SHA:-}}" +if [[ -z "${source_sha}" ]]; then + if [[ "${GITHUB_EVENT_NAME:-}" == "push" && "${source_branch}" == "${GITHUB_REF_NAME:-}" ]]; then + source_sha="${GITHUB_SHA}" + else + source_sha="$(git rev-parse "origin/${source_branch}")" + fi +fi + +if [[ -z "${DESTINATION_BRANCH:-}" ]]; then + echo "DESTINATION_BRANCH is required." >&2 + exit 1 +fi + +release_only_checkpoint_paths() { + printf '%s\n' "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +} + +resolve_source_version_conflicts() { + local path="$1" + local resolved_path + resolved_path="$(mktemp)" + + if ! awk ' + function normalize_versions(text) { + gsub(/version = "[^"]+"/, "version = \"\"", text) + return text + } + + BEGIN { + in_block = 0 + side = "" + ours = "" + theirs = "" + blocks = 0 + } + + /^<<<<<<< / { + if (in_block) { + exit 1 + } + in_block = 1 + side = "ours" + ours = "" + theirs = "" + blocks++ + next + } + + /^=======$/ && in_block { + side = "theirs" + next + } + + /^>>>>>>> / && in_block { + if (normalize_versions(ours) != normalize_versions(theirs)) { + exit 1 + } + printf "%s", theirs + in_block = 0 + side = "" + next + } + + { + if (!in_block) { + print + } else if (side == "ours") { + ours = ours $0 ORS + } else if (side == "theirs") { + theirs = theirs $0 ORS + } else { + exit 1 + } + } + + END { + if (in_block || blocks == 0) { + exit 1 + } + } + ' "${path}" > "${resolved_path}"; then + rm -f "${resolved_path}" + return 1 + fi + + cp "${resolved_path}" "${path}" + rm -f "${resolved_path}" +} + +short_sha="${source_sha:0:12}" +source_slug="${source_branch//\//_}" +dest_slug="${DESTINATION_BRANCH//\//_}" +checkpoint_branch="checkpoint/${dest_slug}_from_${source_slug}_${short_sha}" +pr_title="checkpoint: into ${DESTINATION_BRANCH} from ${source_branch} @ ${short_sha}" +merge_conflicted=false +conflict_summary="" + +existing_pr="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --head "${checkpoint_branch}" \ + --state all \ + --json number,state,mergedAt,url \ + --jq '[.[] | select(.state == "OPEN" or .mergedAt != null)] | .[0] // empty' +)" +if [[ -n "${existing_pr}" ]]; then + existing_url="$(jq -r '.url' <<< "${existing_pr}")" + existing_state="$(jq -r '.state' <<< "${existing_pr}")" + echo "Checkpoint PR already exists for ${checkpoint_branch}: ${existing_url} (${existing_state})." + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${existing_url}" >> "${GITHUB_OUTPUT}" + fi + exit 0 +fi + +git checkout -B "${checkpoint_branch}" "origin/${DESTINATION_BRANCH}" + +if ! git merge --no-ff --no-edit "${source_sha}"; then + mapfile -t conflicted_paths < <(git diff --name-only --diff-filter=U) + for conflicted_path in "${conflicted_paths[@]}"; do + if termux_is_checkpoint_release_only_path "${conflicted_path}"; then + echo "Auto-resolving release-only checkpoint conflict in ${conflicted_path} by keeping ${DESTINATION_BRANCH}." + if git cat-file -e "HEAD:${conflicted_path}" 2>/dev/null; then + git checkout --ours -- "${conflicted_path}" + git add "${conflicted_path}" + else + git rm -f --ignore-unmatch "${conflicted_path}" + fi + fi + done + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if ((${#remaining_conflicts[@]})); then + cargo_version_conflicts=true + for remaining_conflict in "${remaining_conflicts[@]}"; do + case "${remaining_conflict}" in + codex-rs/Cargo.toml|codex-rs/Cargo.lock) + ;; + *) + cargo_version_conflicts=false + ;; + esac + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + for remaining_conflict in "${remaining_conflicts[@]}"; do + if ! resolve_source_version_conflicts "${remaining_conflict}"; then + cargo_version_conflicts=false + break + fi + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + echo "Auto-resolving recurring Cargo version checkpoint conflicts by keeping ${source_branch} versions." + git add -- "${remaining_conflicts[@]}" + fi + fi + fi + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if [[ "${#remaining_conflicts[@]}" -eq 0 ]]; then + git commit --no-edit + else + merge_conflicted=true + conflict_summary="$( + printf '%s\n' "${remaining_conflicts[@]}" | awk '{ print "- `" $0 "`" }' + )" + echo "Automatic checkpoint merge failed; creating a manual-resolution PR instead." >&2 + if git rev-parse -q --verify MERGE_HEAD >/dev/null; then + git merge --abort + fi + git checkout -B "${checkpoint_branch}" "${source_sha}" + fi +fi + +if git cat-file -e "origin/${DESTINATION_BRANCH}:.github" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- .github + mapfile -t added_github_paths < <( + git diff --name-only --diff-filter=A "origin/${DESTINATION_BRANCH}" -- .github + ) + if ((${#added_github_paths[@]})); then + git rm -f --ignore-unmatch -- "${added_github_paths[@]}" + fi +else + git rm -r --ignore-unmatch .github +fi + +while IFS= read -r release_only_path; do + if git cat-file -e "origin/${DESTINATION_BRANCH}:${release_only_path}" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- "${release_only_path}" + else + git rm -f --ignore-unmatch -- "${release_only_path}" + fi +done < <(release_only_checkpoint_paths) + +if ! git diff --quiet || ! git diff --cached --quiet; then + if [[ -e .github || -L .github ]] || git ls-files --error-unmatch -- .github >/dev/null 2>&1; then + git add -A .github + fi + while IFS= read -r release_only_path; do + if [[ -e "${release_only_path}" || -L "${release_only_path}" ]] || git ls-files --error-unmatch -- "${release_only_path}" >/dev/null 2>&1; then + git add -A -- "${release_only_path}" + fi + done < <(release_only_checkpoint_paths) + if [[ "${merge_conflicted}" == "true" ]]; then + git commit -m "checkpoint: prepare ${source_branch} for ${DESTINATION_BRANCH}" + else + git commit --amend --no-edit + fi +fi + +if git diff --quiet "origin/${DESTINATION_BRANCH}" HEAD; then + echo "Checkpoint merge produced no destination changes after release-only files were restored." + exit 0 +fi + +git push --force-with-lease origin "${checkpoint_branch}" + +remaining="$( + git log --first-parent --pretty=format:%H "${source_sha}..origin/${source_branch}" | wc -w +)" + +body_path="${RUNNER_TEMP}/termux-checkpoint-pr.md" +{ + echo "## Termux release checkpoint" + echo + echo "- Source branch: \`${source_branch}\`" + echo "- Source hash: \`${source_sha}\`" + echo "- Destination branch: \`${DESTINATION_BRANCH}\`" + echo "- Remaining first-parent commits on source: ${remaining}" + echo + echo "This PR carries release-train conflict fixes and follow-up changes back into the reusable Termux patch branch." + if [[ "${merge_conflicted}" == "true" ]]; then + echo + echo "## Merge conflicts" + echo + echo "GitHub Actions could not create the checkpoint merge commit automatically, so this PR was created from the source branch state for manual conflict resolution." + echo + echo "Conflicted paths from the failed merge attempt:" + if [[ -n "${conflict_summary}" ]]; then + printf '%s\n' "${conflict_summary}" + else + echo "- Conflict details unavailable" + fi + fi + echo + echo "Release-only workflow files and metadata under \`.github\` were restored to the destination branch versions before opening this PR." +} > "${body_path}" + +pr_url="$( + gh pr create \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${DESTINATION_BRANCH}" \ + --head "${checkpoint_branch}" \ + --title "${pr_title}" \ + --body-file "${body_path}" +)" +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-reviewer "${REVIEWER}" || true +gh label create checkpoint --repo "${GITHUB_REPOSITORY}" --color c5def5 --description "Checkpoint merge" --force +gh label create termux-release --repo "${GITHUB_REPOSITORY}" --color 0e8a16 --description "Termux release automation" --force +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-label "checkpoint" --add-label "termux-release" + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${pr_url}" >> "${GITHUB_OUTPUT}" +fi diff --git a/scripts/termux-create-or-update-mirrored-release.sh b/scripts/termux-create-or-update-mirrored-release.sh new file mode 100755 index 000000000000..5787e894582b --- /dev/null +++ b/scripts/termux-create-or-update-mirrored-release.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +# Create a mirrored Termux release or repair an existing release that is missing +# the Android tarball. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" +: "${RUNNER_TEMP:?RUNNER_TEMP is required}" +: "${UPSTREAM_TAG:?UPSTREAM_TAG is required}" +: "${UPSTREAM_REPO:?UPSTREAM_REPO is required}" +: "${UPSTREAM_NAME?UPSTREAM_NAME is required}" +: "${TERMUX_TAG:?TERMUX_TAG is required}" +: "${UPSTREAM_PRERELEASE:?UPSTREAM_PRERELEASE is required}" +: "${UPSTREAM_HTML_URL?UPSTREAM_HTML_URL is required}" +: "${RELEASE_TRAIN?RELEASE_TRAIN is required}" +: "${RELEASE_EXISTS:?RELEASE_EXISTS is required}" +: "${PR_NUMBER:?PR_NUMBER is required}" +: "${HEAD_SHA:?HEAD_SHA is required}" +: "${RELEASE_SHA:?RELEASE_SHA is required}" + +promoted_dir="${PROMOTED_DIR:-promoted}" +asset_path="${promoted_dir}/codex-aarch64-linux-android.tar.gz" + +body_path="${RUNNER_TEMP}/release-body.md" +upstream_body_path="${RUNNER_TEMP}/upstream-release-body.md" +upstream_body_without_changelog_path="${RUNNER_TEMP}/upstream-release-body-without-changelog.md" +if gh release view "${UPSTREAM_TAG}" \ + --repo "${UPSTREAM_REPO}" \ + --json body \ + --jq '.body // ""' > "${upstream_body_path}"; then + awk ' + function heading_level(line, text) { + if (match(line, /^(#{1,6})[[:space:]]+(.+)$/, parts)) { + text = tolower(parts[2]) + sub(/[[:space:]]+#+[[:space:]]*$/, "", text) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", text) + if (text == "changelog" || text == "change log") { + return length(parts[1]) + } + } + return 0 + } + + { + if (!skip) { + level = heading_level($0) + if (level > 0) { + skip = 1 + skip_level = level + next + } + print + next + } + + if (match($0, /^(#{1,6})[[:space:]]+/, parts) && length(parts[1]) <= skip_level) { + skip = 0 + print + } + } + ' "${upstream_body_path}" > "${upstream_body_without_changelog_path}" +else + echo "::warning title=Upstream release notes unavailable::Could not read ${UPSTREAM_REPO} release ${UPSTREAM_TAG}." + : > "${upstream_body_without_changelog_path}" +fi + +{ + echo "Termux Android build for ${UPSTREAM_TAG}." + echo + echo "- Upstream release: ${UPSTREAM_HTML_URL}" + echo "- Release train: \`${RELEASE_TRAIN}\`" + echo "- Promoted PR: #${PR_NUMBER}" + echo "- Promoted PR head SHA: \`${HEAD_SHA}\`" +} > "${body_path}" +if [[ -s "${upstream_body_without_changelog_path}" ]]; then + { + echo + echo "## Upstream release notes" + echo + cat "${upstream_body_without_changelog_path}" + } >> "${body_path}" +fi + +release_title="${UPSTREAM_NAME}" +if [[ -z "${release_title}" || "${release_title}" == "null" ]]; then + release_title="${TERMUX_TAG}" +fi + +if [[ "${RELEASE_EXISTS}" == "true" ]]; then + gh release upload \ + "${TERMUX_TAG}" \ + "${asset_path}#codex-termux" \ + --repo "${GITHUB_REPOSITORY}" \ + --clobber + exit 0 +fi + +release_args=( + gh release create "${TERMUX_TAG}" + --repo "${GITHUB_REPOSITORY}" + --target "${RELEASE_SHA}" + --title "${release_title}" + --notes-file "${body_path}" +) +if [[ "${UPSTREAM_PRERELEASE}" == "true" ]]; then + release_args+=(--prerelease) +fi +release_args+=("${asset_path}#codex-termux") +"${release_args[@]}" diff --git a/scripts/termux-download-release-artifact.sh b/scripts/termux-download-release-artifact.sh new file mode 100755 index 000000000000..2acdba9025a0 --- /dev/null +++ b/scripts/termux-download-release-artifact.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${HEAD_SHA:-}" ]]; then + echo "HEAD_SHA is required." >&2 + exit 1 +fi + +if [[ -z "${PR_ARTIFACT_NAME:-}" ]]; then + echo "PR_ARTIFACT_NAME is required." >&2 + exit 1 +fi + +promoted_dir="${PROMOTED_DIR:-promoted}" + +find_run_id() { + local event="$1" + gh run list \ + --repo "${GITHUB_REPOSITORY}" \ + --workflow rust-release.yml \ + --event "${event}" \ + --status success \ + --commit "${HEAD_SHA}" \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId // empty' +} + +find_artifact_run_id() { + local artifact_name="$1" + + gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=${artifact_name}" \ + | jq -rs \ + --arg artifact_name "${artifact_name}" \ + --arg head_sha "${HEAD_SHA}" \ + ' + [ + .[].artifacts[] + | select(.name == $artifact_name) + | select(.expired == false) + | select(.workflow_run.head_sha == $head_sha) + ] + | sort_by(.created_at) + | reverse + | .[0].workflow_run.id // empty + ' +} + +artifact_name="${PR_ARTIFACT_NAME}" +run_id="$(find_artifact_run_id "${artifact_name}")" +mkdir -p "${promoted_dir}" +if [[ -n "${run_id}" ]] && gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}"; then + : +else + artifact_name="aarch64-linux-android" + run_id="$(find_run_id workflow_dispatch)" + if [[ -z "${run_id}" ]]; then + echo "No successful rust-release run found for ${HEAD_SHA}" >&2 + exit 1 + fi + gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}" +fi + +ls -la "${promoted_dir}" +if [[ -f "${promoted_dir}/SHA256SUMS" ]]; then + (cd "${promoted_dir}" && sha256sum -c SHA256SUMS) +fi +if [[ ! -f "${promoted_dir}/codex-aarch64-linux-android.tar.gz" ]]; then + echo "Expected ${promoted_dir}/codex-aarch64-linux-android.tar.gz in the downloaded artifact." >&2 + exit 1 +fi diff --git a/scripts/termux-find-release-pr.sh b/scripts/termux-find-release-pr.sh new file mode 100755 index 000000000000..3e35fa42eabe --- /dev/null +++ b/scripts/termux-find-release-pr.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -n "${INPUT_PR_NUMBER:-}" || -n "${INPUT_PR_HEAD_SHA:-}" ]]; then + if [[ -z "${INPUT_PR_NUMBER:-}" || -z "${INPUT_PR_HEAD_SHA:-}" ]]; then + echo "workflow_dispatch inputs pr_number and pr_head_sha must be provided together." >&2 + exit 1 + fi + + pr_number="${INPUT_PR_NUMBER}" + head_sha="${INPUT_PR_HEAD_SHA}" + head_ref="" +else + if [[ -z "${RELEASE_BRANCH:-}" || -z "${RELEASE_SHA:-}" ]]; then + echo "RELEASE_BRANCH and RELEASE_SHA are required." >&2 + exit 1 + fi + + pr_json="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${RELEASE_BRANCH}" \ + --state merged \ + --limit 100 \ + --json number,headRefOid,headRefName,mergeCommit \ + --jq "[.[] | select(.mergeCommit.oid == \"${RELEASE_SHA}\")] | sort_by(.number) | reverse | .[0] // empty" + )" + if [[ -z "${pr_json}" ]]; then + echo "Unable to find merged PR for ${RELEASE_SHA} into ${RELEASE_BRANCH}" >&2 + exit 1 + fi + + pr_number="$(jq -r '.number' <<< "${pr_json}")" + head_sha="$(jq -r '.headRefOid // .head.sha' <<< "${pr_json}")" + head_ref="$(jq -r '.headRefName // .head.ref' <<< "${pr_json}")" +fi + +artifact_name="termux-android-pr-${pr_number}-${head_sha}" +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "number=${pr_number}" + echo "head_sha=${head_sha}" + echo "head_ref=${head_ref}" + echo "artifact_name=${artifact_name}" + } >> "${GITHUB_OUTPUT}" +else + printf 'number=%s\nhead_sha=%s\nhead_ref=%s\nartifact_name=%s\n' \ + "${pr_number}" \ + "${head_sha}" \ + "${head_ref}" \ + "${artifact_name}" +fi diff --git a/scripts/termux-read-release-metadata.sh b/scripts/termux-read-release-metadata.sh new file mode 100755 index 000000000000..519a0635a368 --- /dev/null +++ b/scripts/termux-read-release-metadata.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# Parse .github/termux-release.json and emit workflow outputs for deploy or +# promote jobs. TERMUX_RELEASE_ACTION must be "deploy" or "promote". + +set -euo pipefail + +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +metadata=".github/termux-release.json" +action="${TERMUX_RELEASE_ACTION:-deploy}" +case "${action}" in + deploy) + missing_context="deployment" + ;; + promote) + missing_context="promotion" + ;; + *) + echo "TERMUX_RELEASE_ACTION must be deploy or promote." >&2 + exit 1 + ;; +esac + +if [[ ! -f "${metadata}" ]]; then + echo "No ${metadata}; this push is not a Termux release ${missing_context}." + echo "${action}=false" >> "${GITHUB_OUTPUT}" + exit 0 +fi + +upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" +upstream_name="$(jq -r '.upstream_name // .upstream_tag // empty' "${metadata}")" +termux_tag="$(jq -r '.termux_tag // empty' "${metadata}")" +upstream_version="${upstream_tag#rust-v}" +upstream_version="${upstream_version%-termux}" +upstream_prerelease=false +if [[ "${upstream_version}" == *-* ]]; then + upstream_prerelease=true +fi +upstream_html_url="$(jq -r '.upstream_html_url // ""' "${metadata}")" +upstream_repo="$(jq -r '.upstream_repo // "openai/codex"' "${metadata}")" +release_train="$(jq -r '.release_train // ""' "${metadata}")" +if [[ -z "${upstream_tag}" || -z "${termux_tag}" ]]; then + echo "Missing upstream_tag or termux_tag in ${metadata}" >&2 + exit 1 +fi + +release_state="$(TERMUX_TAG="${termux_tag}" "${script_dir}/termux-release-asset-state.sh")" +release_exists="$(awk -F= '$1 == "release_exists" { print $2 }' <<< "${release_state}")" +asset_exists="$(awk -F= '$1 == "asset_exists" { print $2 }' <<< "${release_state}")" + +if [[ "${action}" == "promote" ]]; then + if [[ "${asset_exists}" == "true" ]]; then + echo "${termux_tag} already exists with codex-aarch64-linux-android.tar.gz; skipping promotion." + echo "promote=false" >> "${GITHUB_OUTPUT}" + exit 0 + fi + if [[ "${release_exists}" == "true" ]]; then + echo "${termux_tag} exists but is missing codex-aarch64-linux-android.tar.gz; repairing promotion." + fi +fi + +{ + echo "${action}=true" + echo "upstream_tag=${upstream_tag}" + echo "upstream_name=${upstream_name}" + echo "termux_tag=${termux_tag}" + echo "upstream_prerelease=${upstream_prerelease}" + echo "upstream_html_url=${upstream_html_url}" + echo "upstream_repo=${upstream_repo}" + echo "release_train=${release_train}" + echo "release_exists=${release_exists}" + echo "asset_exists=${asset_exists}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-release-asset-state.sh b/scripts/termux-release-asset-state.sh new file mode 100755 index 000000000000..8694480b6a36 --- /dev/null +++ b/scripts/termux-release-asset-state.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +termux_tag="${1:-${TERMUX_TAG:-}}" +release_repo="${RELEASE_REPO:-${GITHUB_REPOSITORY:-}}" +asset_name="${ASSET_NAME:-codex-aarch64-linux-android.tar.gz}" + +if [[ -z "${termux_tag}" ]]; then + echo "TERMUX_TAG or tag argument is required." >&2 + exit 1 +fi + +if [[ -z "${release_repo}" ]]; then + echo "GITHUB_REPOSITORY or RELEASE_REPO is required." >&2 + exit 1 +fi + +release_exists=false +asset_exists=false + +if gh release view "${termux_tag}" --repo "${release_repo}" >/dev/null 2>&1; then + release_exists=true + release_asset_exists="$( + gh release view "${termux_tag}" \ + --repo "${release_repo}" \ + --json assets \ + | jq -r --arg asset_name "${asset_name}" \ + '.assets | map(.name) | any(. == $asset_name)' + )" + if [[ "${release_asset_exists}" == "true" ]]; then + asset_exists=true + fi +fi + +printf 'release_exists=%s\n' "${release_exists}" +printf 'asset_exists=%s\n' "${asset_exists}" diff --git a/scripts/termux-release-paths.sh b/scripts/termux-release-paths.sh new file mode 100755 index 000000000000..6990f323ec44 --- /dev/null +++ b/scripts/termux-release-paths.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# Sourceable path lists for files owned by the Termux release automation. + +set -euo pipefail + +readonly -a TERMUX_RELEASE_WORKFLOW_PATHS=( + .github/workflows/rust-release.yml + .github/workflows/shell-tool-mcp.yml + .github/workflows/termux-release-checkpoint.yml + .github/workflows/termux-release-deploy.yml + .github/workflows/termux-release-promote.yml +) + +readonly -a TERMUX_RELEASE_BRANCH_SCRIPT_PATHS=( + scripts/termux-configure-git.sh + scripts/termux-create-checkpoint-pr.sh + scripts/termux-create-or-update-mirrored-release.sh + scripts/termux-download-release-artifact.sh + scripts/termux-find-release-pr.sh + scripts/termux-read-release-metadata.sh + scripts/termux-release-asset-state.sh + scripts/termux-release-paths.sh + scripts/termux-resolve-release-ref.sh + scripts/termux-validate-gh-env.sh +) + +readonly -a TERMUX_RELEASE_AUTOMATION_PATHS=( + "${TERMUX_RELEASE_WORKFLOW_PATHS[@]}" + "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +) + +readonly -a TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS=( + "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" + .github/termux-release.json +) + +termux_path_in_list() { + local candidate="$1" + shift + local listed_path + + for listed_path in "$@"; do + [[ "${candidate}" != "${listed_path}" ]] || return 0 + done + return 1 +} + +termux_is_release_automation_path() { + termux_path_in_list "$1" "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" +} + +termux_is_checkpoint_release_only_path() { + termux_path_in_list "$1" "${TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS[@]}" +} diff --git a/scripts/termux-resolve-release-ref.sh b/scripts/termux-resolve-release-ref.sh new file mode 100755 index 000000000000..194773d45bed --- /dev/null +++ b/scripts/termux-resolve-release-ref.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Resolve the release branch and SHA for deploy/promote workflows. + +set -euo pipefail + +: "${GITHUB_EVENT_NAME:?GITHUB_EVENT_NAME is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" + +input_release_branch="${INPUT_RELEASE_BRANCH:-${REQUESTED_RELEASE_BRANCH:-}}" +input_release_sha="${INPUT_RELEASE_SHA:-${REQUESTED_RELEASE_SHA:-}}" + +if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then + if [[ -z "${input_release_branch}" ]]; then + echo "release_branch is required for workflow_dispatch." >&2 + exit 1 + fi + + release_branch="${input_release_branch}" + if [[ -n "${input_release_sha}" ]]; then + git checkout --detach "${input_release_sha}" + release_sha="${input_release_sha}" + else + release_sha="$(git rev-parse HEAD)" + fi +else + : "${GITHUB_REF_NAME:?GITHUB_REF_NAME is required}" + : "${GITHUB_SHA:?GITHUB_SHA is required}" + release_branch="${GITHUB_REF_NAME}" + release_sha="${GITHUB_SHA}" +fi + +{ + echo "branch=${release_branch}" + echo "sha=${release_sha}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-validate-gh-env.sh b/scripts/termux-validate-gh-env.sh new file mode 100755 index 000000000000..02581fdebceb --- /dev/null +++ b/scripts/termux-validate-gh-env.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Lightweight post-toolbox check for jobs that rely on authenticated gh calls. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +command -v gh +gh auth status --hostname github.com + +printf 'GITHUB_REPOSITORY=%s\n' "${GITHUB_REPOSITORY}" +printf 'REPO=%s\n' "${REPO:-}" +printf 'GH_REPO_URL=%s\n' "${GH_REPO_URL:-}" +printf 'GH_WORKFLOW_URL=%s\n' "${GH_WORKFLOW_URL:-}" From 113163fa19fe46110866a3bf22664805226cf395 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 21:58:26 +0000 Subject: [PATCH 25/25] Prepare Termux rust-v0.134.0 --- .github/termux-release.json | 15 + .github/workflows/rust-release.yml | 1798 +++++++---------- .github/workflows/shell-tool-mcp.yml | 461 +++++ .../workflows/termux-release-checkpoint.yml | 103 + .github/workflows/termux-release-deploy.yml | 243 +++ .github/workflows/termux-release-promote.yml | 135 ++ codex-rs/Cargo.toml | 2 +- scripts/termux-configure-git.sh | 40 + scripts/termux-create-checkpoint-pr.sh | 275 +++ ...ermux-create-or-update-mirrored-release.sh | 111 + scripts/termux-download-release-artifact.sh | 78 + scripts/termux-find-release-pr.sh | 53 + scripts/termux-read-release-metadata.sh | 77 + scripts/termux-release-asset-state.sh | 37 + scripts/termux-release-paths.sh | 55 + scripts/termux-resolve-release-ref.sh | 36 + scripts/termux-validate-gh-env.sh | 16 + 17 files changed, 2460 insertions(+), 1075 deletions(-) create mode 100644 .github/termux-release.json create mode 100644 .github/workflows/shell-tool-mcp.yml create mode 100644 .github/workflows/termux-release-checkpoint.yml create mode 100644 .github/workflows/termux-release-deploy.yml create mode 100644 .github/workflows/termux-release-promote.yml create mode 100755 scripts/termux-configure-git.sh create mode 100755 scripts/termux-create-checkpoint-pr.sh create mode 100755 scripts/termux-create-or-update-mirrored-release.sh create mode 100755 scripts/termux-download-release-artifact.sh create mode 100755 scripts/termux-find-release-pr.sh create mode 100755 scripts/termux-read-release-metadata.sh create mode 100755 scripts/termux-release-asset-state.sh create mode 100755 scripts/termux-release-paths.sh create mode 100755 scripts/termux-resolve-release-ref.sh create mode 100755 scripts/termux-validate-gh-env.sh diff --git a/.github/termux-release.json b/.github/termux-release.json new file mode 100644 index 000000000000..6d7721b50792 --- /dev/null +++ b/.github/termux-release.json @@ -0,0 +1,15 @@ +{ + "upstream_repo": "openai/codex", + "upstream_tag": "rust-v0.134.0", + "upstream_name": "0.134.0", + "upstream_html_url": "https://github.com/openai/codex/releases/tag/rust-v0.134.0", + "upstream_target": "main", + "upstream_release_id": "329640454", + "upstream_prerelease": false, + "release_train": "0.134.0", + "release_branch": "release/0.134.0", + "work_branch": "upstream/rust-v0.134.0", + "patch_branch": "wallentx/termux-target", + "patch_source_sha": "f008348be7cf8ec3bf5ed1b7afa1517c3a5aabbe", + "termux_tag": "rust-v0.134.0-termux" +} diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index c55337ecfe6e..56d0977e6e67 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -4,46 +4,49 @@ # git tag -a rust-v0.1.0 -m "Release 0.1.0" # git push origin rust-v0.1.0 # ``` -# -# To use external macOS signing, manually dispatch `release_mode=build_unsigned`, -# sign the unsigned macOS artifacts in a secure enclave, upload the signed handoff -# archive as a GitHub Release asset, then manually dispatch -# `release_mode=promote_signed` with `unsigned_run_id` and `signed_macos_asset`. -# The signed handoff archive should contain target or artifact directories such -# as `aarch64-apple-darwin/` with signed binaries. name: rust-release on: - push: - tags: - - "rust-v*.*.*" + pull_request: + branches: + - "release/**" workflow_dispatch: inputs: - release_mode: - description: "build_unsigned creates unsigned macOS handoff artifacts; promote_signed finishes a release from signed macOS handoff artifacts." + source_ref: + description: "Branch, tag, or commit SHA to build (code only; keeps .github from dispatched ref)" required: false + type: string + default: "" + build_target: + description: "Target to build" + required: true type: choice - default: build_unsigned + default: all options: - - build_unsigned - - promote_signed - sign_macos: - description: "Deprecated compatibility input; use release_mode instead." - required: false - type: boolean - default: false - unsigned_run_id: - description: "For promote_signed: workflow run id from the build_unsigned run." - required: false - type: string - signed_macos_asset: - description: "For promote_signed: exact GitHub Release asset name containing signed macOS handoff artifacts." - required: false - type: string - signed_macos_sha256: - description: "For promote_signed: optional SHA-256 of signed_macos_asset." - required: false - type: string + - all + - aarch64-apple-darwin + - x86_64-apple-darwin + - x86_64-unknown-linux-musl + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-musl + - aarch64-unknown-linux-gnu + - aarch64-linux-android + - x86_64-pc-windows-msvc + - aarch64-pc-windows-msvc + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read concurrency: group: ${{ github.workflow }} @@ -51,72 +54,26 @@ concurrency: jobs: tag-check: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - uses: dtolnay/rust-toolchain@1.92 - name: Validate tag matches Cargo.toml version shell: bash - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - REQUESTED_SIGN_MACOS: ${{ inputs.sign_macos }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} run: | set -euo pipefail echo "::group::Tag validation" - case "${RELEASE_MODE}" in - signed) - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - echo "❌ Manual rust-release runs must use release_mode=build_unsigned or release_mode=promote_signed" - exit 1 - fi - ;; - build_unsigned) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=build_unsigned is only valid for manual runs" - exit 1 - fi - ;; - promote_signed) - if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then - echo "❌ release_mode=promote_signed is only valid for manual runs" - exit 1 - fi - if [[ ! "${UNSIGNED_RUN_ID}" =~ ^[0-9]+$ ]]; then - echo "❌ release_mode=promote_signed requires unsigned_run_id to be a workflow run id" - exit 1 - fi - if [[ -z "${SIGNED_MACOS_ASSET}" ]]; then - echo "❌ release_mode=promote_signed requires signed_macos_asset" - exit 1 - fi - if [[ "${SIGNED_MACOS_ASSET}" == */* || "${SIGNED_MACOS_ASSET}" == *"*"* || "${SIGNED_MACOS_ASSET}" == *"?"* || "${SIGNED_MACOS_ASSET}" == *"["* ]]; then - echo "❌ signed_macos_asset must be an exact release asset name, not a path or glob" - exit 1 - fi - if [[ "${UNSIGNED_RUN_ID}" == "${GITHUB_RUN_ID}" ]]; then - echo "❌ unsigned_run_id must refer to the earlier build_unsigned run, not this run" - exit 1 - fi - ;; - *) - echo "❌ Unknown release_mode '${RELEASE_MODE}'" - exit 1 - ;; - esac - - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${REQUESTED_SIGN_MACOS}" == "true" ]]; then - echo "::warning title=Deprecated sign_macos input ignored::Use release_mode=build_unsigned or release_mode=promote_signed instead." - fi - # 1. Must be a tag and match the regex [[ "${GITHUB_REF_TYPE}" == "tag" ]] \ || { echo "❌ Not a tag push"; exit 1; } - [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \ + [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?(-termux)?$ ]] \ || { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; } # 2. Extract versions @@ -131,115 +88,198 @@ jobs: echo "✅ Tag and Cargo.toml agree (${tag_ver})" echo "::endgroup::" - build: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} + select-build-matrix: + if: always() && (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') needs: tag-check - name: Build - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: ${{ matrix.runs_on || matrix.runner }} - # Release builds can take a long time, so leave some headroom to avoid - # having to restart the full workflow due to a timeout. - timeout-minutes: 90 - permissions: - contents: read - id-token: write + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.select.outputs.matrix }} + build_version: ${{ steps.select.outputs.build_version }} + steps: + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + - id: select + shell: bash + env: + REPO_OWNER: ${{ github.repository_owner }} + EVENT_NAME: ${{ github.event_name }} + BUILD_TARGET: ${{ inputs.build_target }} + run: | + set -euo pipefail + + matrix='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-gnu"}, + {"runner":"ubuntu-24.04","target":"aarch64-linux-android"}, + {"runner":"windows-latest","target":"x86_64-pc-windows-msvc"}, + {"runner":"windows-11-arm","target":"aarch64-pc-windows-msvc"} + ]' + + if [[ "${REPO_OWNER}" != "openai" ]]; then + matrix="$(jq -c '[.[] | select(.runner | startswith("macos") | not)]' <<< "${matrix}")" + fi + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + matrix="$(jq -c '[.[] | select(.target == "aarch64-linux-android")]' <<< "${matrix}")" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" && -n "${BUILD_TARGET}" && "${BUILD_TARGET}" != "all" ]]; then + matrix="$(jq -c --arg build_target "${BUILD_TARGET}" '[.[] | select(.target == $build_target)]' <<< "${matrix}")" + fi + + if [[ "$(jq 'length' <<< "${matrix}")" -eq 0 ]]; then + echo "No build targets selected after applying owner/dispatch filters." >&2 + exit 1 + fi + + echo "matrix={\"include\":${matrix}}" >> "$GITHUB_OUTPUT" + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + metadata=".github/termux-release.json" + if [[ ! -f "${metadata}" ]]; then + echo "${metadata} is required for release train PR builds" >&2 + exit 1 + fi + upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" + if [[ -z "${upstream_tag}" || "${upstream_tag}" != rust-v* ]]; then + echo "Unable to determine upstream rust tag from ${metadata}" >&2 + exit 1 + fi + build_version="${upstream_tag#rust-v}" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + elif [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then + latest_release_tag="$( + GH_TOKEN="${{ github.token }}" gh api repos/openai/codex/releases/latest --jq '.tag_name' + )" + + if [[ -z "${latest_release_tag}" || "${latest_release_tag}" != rust-v* ]]; then + echo "Unable to determine latest stable release tag from openai/codex" >&2 + exit 1 + fi + + latest_release_version="${latest_release_tag#rust-v}" + build_version="${latest_release_version}-dev" + echo "build_version=${build_version}" >> "$GITHUB_OUTPUT" + fi + + build: + if: >- + always() && + (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || needs.tag-check.result == 'success') && + needs.select-build-matrix.result == 'success' + needs: + - tag-check + - select-build-matrix + name: Build - ${{ matrix.runner }} - ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + permissions: write-all + timeout-minutes: 120 defaults: run: working-directory: codex-rs env: - # 2026-03-04: temporarily change releases to use thin LTO because - # Ubuntu ARM is timing out at 60 minutes. - CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' }} + CODEX_BWRAP_ENABLE_FFI: ${{ contains(matrix.target, 'unknown-linux') && '1' || '0' }} + CODEX_BUILD_VERSION: ${{ needs.select-build-matrix.outputs.build_version }} + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" strategy: fail-fast: false - matrix: - include: - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "true" - - runner: macos-15-xlarge - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - # Release artifacts intentionally ship MUSL-linked Linux binaries. - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - bundle: primary - artifact_name: x86_64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - bundle: app-server - artifact_name: x86_64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - bundle: primary - artifact_name: aarch64-unknown-linux-musl - binaries: "codex codex-responses-api-proxy bwrap" - build_dmg: "false" - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - bundle: app-server - artifact_name: aarch64-unknown-linux-musl-app-server - binaries: "codex-app-server" - build_dmg: "false" + matrix: ${{ fromJson(needs.select-build-matrix.outputs.matrix) }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@v6 + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main + with: + verbose: true + - name: Checkout selected source ref + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + uses: actions/checkout@v6 with: - persist-credentials: false - - name: Print runner specs (Linux) - if: ${{ runner.os == 'Linux' }} + ref: ${{ inputs.source_ref }} + path: __source__ + fetch-depth: 1 + - name: Overlay selected source ref (excluding .github) + if: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref != '' }} + shell: bash + working-directory: ${{ github.workspace }} + run: | + set -euo pipefail + echo "Using source ref: ${{ inputs.source_ref }}" + + # Keep workflow/actions from the dispatch ref while replacing all other files. + find . -mindepth 1 -maxdepth 1 \ + ! -name .git \ + ! -name .github \ + ! -name __source__ \ + -exec rm -rf -- {} + + tar --exclude='.github' -C __source__ -cf - . | tar -xf - + rm -rf __source__ + + - name: Apply selected build version + if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} shell: bash + working-directory: ${{ github.workspace }} run: | set -euo pipefail - cpu_model="$(lscpu | awk -F: '/Model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}')" - total_ram="$(awk '/MemTotal/ {printf "%.1f GiB\n", $2 / 1024 / 1024}' /proc/meminfo)" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(uname -a)" - echo "CPU model: ${cpu_model}" - echo "Logical CPUs: $(nproc)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . - - name: Print runner specs (macOS) - if: ${{ runner.os == 'macOS' }} + if [[ -z "${CODEX_BUILD_VERSION}" ]]; then + echo "CODEX_BUILD_VERSION is empty for workflow_dispatch" >&2 + exit 1 + fi + echo "Using build version: ${CODEX_BUILD_VERSION}" + BUILD_VERSION="${CODEX_BUILD_VERSION}" perl -0777 -i -pe \ + 's/(\[workspace\.package\][^\[]*?version = ")([^"]+)(")/${1}$ENV{BUILD_VERSION}$3/s' \ + codex-rs/Cargo.toml + + grep -n "^\[workspace.package\]\|^version = " codex-rs/Cargo.toml | head -n 4 + + - name: Select Android NDK + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail - total_ram="$(sysctl -n hw.memsize | awk '{printf "%.1f GiB\n", $1 / 1024 / 1024 / 1024}')" - echo "Runner: ${RUNNER_NAME:-unknown}" - echo "OS: $(sw_vers -productName) $(sw_vers -productVersion)" - echo "Hardware model: $(sysctl -n hw.model)" - echo "CPU architecture: $(uname -m)" - echo "Logical CPUs: $(sysctl -n hw.logicalcpu)" - echo "Physical CPUs: $(sysctl -n hw.physicalcpu)" - echo "Total RAM: ${total_ram}" - echo "Disk usage:" - df -h . + + fallback_version="29.0.13113456" + ndk_root="${ANDROID_HOME}/ndk" + selected_ndk="" + + if [[ -d "${ndk_root}" ]]; then + while IFS= read -r candidate; do + toolchain="${candidate}/toolchains/llvm/prebuilt/linux-x86_64" + if [[ -x "${toolchain}/bin/aarch64-linux-android24-clang" ]]; then + selected_ndk="${candidate}" + break + fi + done < <(find "${ndk_root}" -mindepth 1 -maxdepth 1 -type d | sort -Vr) + fi + + if [[ -z "${selected_ndk}" ]]; then + echo "No usable preinstalled Android NDK found; installing ${fallback_version}" + yes | sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses || true + sudo "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" "ndk;${fallback_version}" + selected_ndk="${ndk_root}/${fallback_version}" + fi + + if [[ ! -x "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" ]]; then + echo "Selected Android NDK does not contain a usable aarch64 API 24 clang: ${selected_ndk}" >&2 + exit 1 + fi + + echo "Selected Android NDK: ${selected_ndk}" + "${selected_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" --version + echo "ANDROID_NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" + echo "NDK_HOME=${selected_ndk}" >> "$GITHUB_ENV" - name: Install Linux bwrap build dependencies - if: ${{ runner.os == 'Linux' }} + if: ${{ runner.os == 'Linux' && matrix.target != 'aarch64-linux-android' }} shell: bash run: | set -euo pipefail @@ -254,9 +294,173 @@ jobs: sudo apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 fi - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@1.93 with: targets: ${{ matrix.target }} + - name: Set up sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + uses: mozilla-actions/sccache-action@main + - name: Enable sccache (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} + shell: bash + run: | + set -euo pipefail + echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" + echo "SCCACHE_GHA_VERSION=android-release-${{ matrix.target }}" >> "$GITHUB_ENV" + echo "SCCACHE_CACHE_SIZE=5G" >> "$GITHUB_ENV" + - name: Ensure Rust target is installed + shell: bash + run: | + set -euo pipefail + rustup target add "${{ matrix.target }}" + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android build environment + shell: bash + run: | + set -euo pipefail + ndk="${ANDROID_NDK_HOME}" + target="${{ matrix.target }}" + + # Set up the Android toolchain + toolchain="${ndk}/toolchains/llvm/prebuilt/linux-x86_64" + + # Use API level 24 to follow termux-build convention + api_level="24" + + # Configure Cargo to use the NDK linker + cargo_target_var="CARGO_TARGET_${target^^}_LINKER" + cargo_target_var="${cargo_target_var//-/_}" + echo "${cargo_target_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + # Set CC and AR for the target + target_cc_var="CC_${target}" + target_cc_var="${target_cc_var//-/_}" + echo "${target_cc_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang" >> "$GITHUB_ENV" + + target_cxx_var="CXX_${target}" + target_cxx_var="${target_cxx_var//-/_}" + echo "${target_cxx_var}=${toolchain}/bin/aarch64-linux-android${api_level}-clang++" >> "$GITHUB_ENV" + + target_ar_var="AR_${target}" + target_ar_var="${target_ar_var//-/_}" + echo "${target_ar_var}=${toolchain}/bin/llvm-ar" >> "$GITHUB_ENV" + + # Add toolchain to PATH + echo "${toolchain}/bin" >> "$GITHUB_PATH" + + tls_align_source="${RUNNER_TEMP}/codex-android-tls-align.S" + tls_align_object="${RUNNER_TEMP}/codex-android-tls-align.o" + cat > "${tls_align_source}" <<'EOF' + .section .tdata.codex_android_tls_align,"awT",%progbits + .p2align 6 + .globl __codex_android_tls_align + .hidden __codex_android_tls_align + .type __codex_android_tls_align, %object + __codex_android_tls_align: + .byte 0 + .size __codex_android_tls_align, 1 + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${tls_align_source}" -o "${tls_align_object}" + + v8_compat_source="${RUNNER_TEMP}/codex-android-v8-compat.c" + v8_compat_object="${RUNNER_TEMP}/codex-android-v8-compat.o" + cat > "${v8_compat_source}" <<'EOF' + #include + #include + + extern int posix_memalign(void **memptr, size_t alignment, size_t size); + extern double strtod(const char *nptr, char **endptr); + extern float strtof(const char *nptr, char **endptr); + + void *aligned_alloc(size_t alignment, size_t size) { + void *ptr = 0; + if (posix_memalign(&ptr, alignment, size) != 0) { + return 0; + } + return ptr; + } + + double strtod_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtod(nptr, endptr); + } + + float strtof_l(const char *nptr, char **endptr, locale_t locale) { + (void)locale; + return strtof(nptr, endptr); + } + EOF + "${toolchain}/bin/aarch64-linux-android${api_level}-clang" -c "${v8_compat_source}" -o "${v8_compat_object}" + + builtins_archive="$( + find "${toolchain}/lib/clang" \ + -path "*/lib/linux/libclang_rt.builtins-aarch64-android.a" \ + -print -quit + )" + if [[ -z "${builtins_archive}" ]]; then + echo "Could not find Android compiler-rt builtins archive under ${toolchain}/lib/clang" >&2 + exit 1 + fi + builtins_dir="$(dirname "${builtins_archive}")" + + # Provide compatibility symbols and C++ ABI needed by the Android rusty_v8 archive. + rustflags_var="CARGO_TARGET_${target^^}_RUSTFLAGS" + rustflags_var="${rustflags_var//-/_}" + echo "${rustflags_var}=-C link-arg=${tls_align_object} -C link-arg=${v8_compat_object} -C link-arg=-Wl,-u,__codex_android_tls_align -C link-arg=-L${builtins_dir} -C link-arg=-lclang_rt.builtins-aarch64-android -C link-arg=-lc++abi -C link-arg=-Wl,-z,max-page-size=65536" >> "$GITHUB_ENV" + + + # Configure for vendored OpenSSL build + echo "CARGO_BUILD_TARGET_APPLIES_TO_HOST=false" >> "$GITHUB_ENV" + echo "CARGO_BUILD_TARGET=${{ matrix.target }}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Configure Android rusty_v8 artifact overrides + env: + GH_TOKEN: ${{ github.token }} + TARGET: ${{ matrix.target }} + shell: bash + run: | + set -euo pipefail + binding_dir="${RUNNER_TEMP}/rusty_v8" + archive="${binding_dir}/librusty_v8_release_${TARGET}.a.gz" + binding_path="${binding_dir}/src_binding_release_${TARGET}.rs" + checksums_path="${binding_dir}/rusty_v8_release_${TARGET}.sha256" + artifact_repository="wallentx/codex-termux" + release_tag="rusty-v8-v147.4.0" + mkdir -p "${binding_dir}" + echo "Downloading Android rusty_v8 artifacts from ${artifact_repository}@${release_tag}" + gh release download "${release_tag}" \ + --repo "${artifact_repository}" \ + --dir "${binding_dir}" \ + --pattern "librusty_v8_release_${TARGET}.a.gz" \ + --pattern "src_binding_release_${TARGET}.rs" \ + --pattern "rusty_v8_release_${TARGET}.sha256" + (cd "${binding_dir}" && sha256sum -c "$(basename "${checksums_path}")") + echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV" + echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Install termux-elf-cleaner + shell: bash + run: | + set -euo pipefail + version="v3.0.1" + src_dir="${RUNNER_TEMP}/termux-elf-cleaner-src" + build_dir="${RUNNER_TEMP}/termux-elf-cleaner-build" + bin_dir="${RUNNER_TEMP}/termux-elf-cleaner-bin" + + rm -rf "${src_dir}" "${build_dir}" "${bin_dir}" + mkdir -p "${src_dir}" "${build_dir}" "${bin_dir}" + + curl -fsSL "https://github.com/termux/termux-elf-cleaner/archive/refs/tags/${version}.tar.gz" \ + | tar -xzf - --strip-components=1 -C "${src_dir}" + + cmake -S "${src_dir}" -B "${build_dir}" -DCMAKE_BUILD_TYPE=Release + cmake --build "${build_dir}" --parallel + + install "${build_dir}/termux-elf-cleaner" "${bin_dir}/termux-elf-cleaner" + echo "${bin_dir}" >> "$GITHUB_PATH" - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Use hermetic Cargo home (musl) @@ -269,12 +473,25 @@ jobs: echo "${cargo_home}/bin" >> "$GITHUB_PATH" : > "${cargo_home}/config.toml" + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ${{ github.workspace }}/.cargo-home/bin/ + ${{ github.workspace }}/.cargo-home/registry/index/ + ${{ github.workspace }}/.cargo-home/registry/cache/ + ${{ github.workspace }}/.cargo-home/git/db/ + ${{ github.workspace }}/codex-rs/target/ + key: cargo-${{ matrix.runner }}-${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }} + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install Zig - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + uses: mlugg/setup-zig@v2 with: version: 0.14.0 - use-cache: false - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Install musl build tools @@ -310,12 +527,6 @@ jobs: shell: bash run: | set -euo pipefail - # Avoid problematic aws-lc jitter entropy code path on musl builders. - echo "AWS_LC_SYS_NO_JITTER_ENTROPY=1" >> "$GITHUB_ENV" - target_no_jitter="AWS_LC_SYS_NO_JITTER_ENTROPY_${{ matrix.target }}" - target_no_jitter="${target_no_jitter//-/_}" - echo "${target_no_jitter}=1" >> "$GITHUB_ENV" - # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. echo "RUSTFLAGS=" >> "$GITHUB_ENV" echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" @@ -340,94 +551,70 @@ jobs: echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" - - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }} - name: Configure musl rusty_v8 artifact overrides and verify checksums - uses: ./.github/actions/setup-rusty-v8-musl - with: - target: ${{ matrix.target }} - - - if: ${{ contains(matrix.target, 'linux') && matrix.bundle == 'primary' }} - name: Build bwrap and export digest + - name: Cargo build shell: bash run: | set -euo pipefail - target="${{ matrix.target }}" - cargo build --target "$target" --release --timings --bin bwrap - bwrap_path="target/${target}/release/bwrap" - if [[ ! -f "$bwrap_path" ]]; then - echo "bwrap binary ${bwrap_path} not found" - exit 1 + if [[ "${{ matrix.target }}" == 'aarch64-linux-android' ]]; then + export CARGO_BUILD_JOBS=4 + export CARGO_PROFILE_RELEASE_LTO=thin + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 + cargo build --target ${{ matrix.target }} --release --bin codex + exit 0 fi - digest="$(sha256sum "$bwrap_path" | awk '{print $1}')" - echo "CODEX_BWRAP_SHA256=${digest}" >> "$GITHUB_ENV" - echo "Built bwrap ${bwrap_path} with sha256:${digest}" - - - name: Cargo build + if [[ "${{ contains(matrix.target, 'windows') }}" == 'true' ]]; then + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy --bin codex-windows-sandbox-setup --bin codex-command-runner + else + cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy + fi + - name: sccache stats (Android) + if: ${{ matrix.target == 'aarch64-linux-android' }} shell: bash - run: | - build_args=() - for binary in ${{ matrix.binaries }}; do - build_args+=(--bin "$binary") - done - echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}" - cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}" + run: ${SCCACHE_PATH:-sccache} --show-stats || true - - name: Upload Cargo timings - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }} - path: codex-rs/target/**/cargo-timings/cargo-timing.html - if-no-files-found: warn - - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Stage unsigned macOS artifacts + - if: ${{ matrix.target == 'aarch64-linux-android' }} + name: Normalize Android ELF for Termux shell: bash run: | set -euo pipefail - - target="${{ matrix.target }}" - release_dir="target/${target}/release" - dest="unsigned-dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - unsigned_name="${binary}-${target}-unsigned" - unsigned_path="${dest}/${unsigned_name}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 + for binary in codex codex-responses-api-proxy; do + binary_path="target/${{ matrix.target }}/release/${binary}" + if [[ -f "${binary_path}" ]]; then + termux-elf-cleaner --api-level 24 "${binary_path}" + chmod +x "${binary_path}" + # if [[ "${binary}" == "codex" ]]; then + # # Patch PT_TLS (0x7) to PT_NULL (0x0) at offset 400 to bypass Bionic alignment checks. + # printf '\x00\x00\x00\x00' | dd of="${binary_path}" bs=1 seek=400 count=4 conv=notrunc + # fi fi - - cp "${binary_path}" "${unsigned_path}" - tar -C "$dest" -czf "${unsigned_path}.tar.gz" "${unsigned_name}" - zstd -T0 -19 --rm "${unsigned_path}" done - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} - name: Upload unsigned macOS artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }}-unsigned - path: codex-rs/unsigned-dist/${{ matrix.target }}/* - if-no-files-found: error - - - if: ${{ contains(matrix.target, 'linux') }} + - if: ${{ contains(matrix.target, 'linux') && !contains(matrix.target, 'android') && github.repository_owner == 'openai' }} name: Cosign Linux artifacts uses: ./.github/actions/linux-code-sign with: target: ${{ matrix.target }} artifacts-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release - binaries: ${{ matrix.binaries }} - - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS == 'true' }} + - if: ${{ contains(matrix.target, 'windows') && github.repository_owner == 'openai' }} + name: Sign Windows binaries with Azure Trusted Signing + uses: ./.github/actions/windows-code-sign + with: + target: ${{ matrix.target }} + client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID }} + endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} + account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} + + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (binaries) uses: ./.github/actions/macos-code-sign with: target: ${{ matrix.target }} - binaries: ${{ matrix.binaries }} sign-binaries: "true" sign-dmg: "false" apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }} @@ -436,7 +623,7 @@ jobs: apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: Build macOS dmg shell: bash run: | @@ -451,17 +638,23 @@ jobs: # The previous "MacOS code signing (binaries)" step signs + notarizes the # built artifacts in `${release_dir}`. This step packages *those same* # signed binaries into a dmg. + codex_binary_path="${release_dir}/codex" + proxy_binary_path="${release_dir}/codex-responses-api-proxy" + rm -rf "$dmg_root" mkdir -p "$dmg_root" - for binary in ${{ matrix.binaries }}; do - binary_path="${release_dir}/${binary}" - if [[ ! -f "${binary_path}" ]]; then - echo "Binary ${binary_path} not found" - exit 1 - fi - ditto "${binary_path}" "${dmg_root}/${binary}" - done + if [[ ! -f "$codex_binary_path" ]]; then + echo "Binary $codex_binary_path not found" + exit 1 + fi + if [[ ! -f "$proxy_binary_path" ]]; then + echo "Binary $proxy_binary_path not found" + exit 1 + fi + + ditto "$codex_binary_path" "${dmg_root}/codex" + ditto "$proxy_binary_path" "${dmg_root}/codex-responses-api-proxy" rm -f "$dmg_path" hdiutil create \ @@ -476,7 +669,7 @@ jobs: exit 1 fi - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} + - if: ${{ runner.os == 'macOS' && github.repository_owner == 'openai' }} name: MacOS code signing (dmg) uses: ./.github/actions/macos-code-sign with: @@ -490,121 +683,65 @@ jobs: apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - name: Stage artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | dest="dist/${{ matrix.target }}" mkdir -p "$dest" - for binary in ${{ matrix.binaries }}; do - cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" - if [[ "${{ matrix.target }}" == *linux* ]]; then - cp "target/${{ matrix.target }}/release/${binary}.sigstore" \ - "$dest/${binary}-${{ matrix.target }}.sigstore" + if [[ "${{ matrix.runner }}" == windows* ]]; then + cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe" + else + # For Android, we want the binary to be named just 'codex' in the archive. + if [[ "${{ matrix.target }}" == "aarch64-linux-android" ]]; then + cp target/${{ matrix.target }}/release/codex "$dest/codex" + else + cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" + fi + if [[ -f target/${{ matrix.target }}/release/codex-responses-api-proxy ]]; then + cp target/${{ matrix.target }}/release/codex-responses-api-proxy "$dest/codex-responses-api-proxy-${{ matrix.target }}" fi - done - - if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.bundle }}" == "primary" ]]; then - bundle_root="${RUNNER_TEMP}/codex-${{ matrix.target }}-bundle" - rm -rf "$bundle_root" - mkdir -p "$bundle_root/codex-resources" - cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex" - cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap" - chmod 0755 "$bundle_root/codex" "$bundle_root/codex-resources/bwrap" - tar -C "$bundle_root" -cf - codex codex-resources/bwrap | - zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst" fi - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" + if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.target }}" != *android* ]]; then + cp target/${{ matrix.target }}/release/codex.sigstore "$dest/codex-${{ matrix.target }}.sigstore" + cp target/${{ matrix.target }}/release/codex-responses-api-proxy.sigstore "$dest/codex-responses-api-proxy-${{ matrix.target }}.sigstore" fi - - name: Build Codex package archive - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "target/${TARGET}/release" \ - --archive-dir "dist/${TARGET}" - - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - aarch64-unknown-linux-musl) - platform_tag="manylinux_2_17_aarch64" - ;; - x86_64-unknown-linux-musl) - platform_tag="manylinux_2_17_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - # Do not install into the runner's system Python; macOS runners mark - # the Homebrew Python as externally managed under PEP 668. - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - stage_runtime_args=( - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" - stage-runtime - "$stage_dir" - "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/codex" - --codex-version "${GITHUB_REF_NAME}" - --platform-tag "$platform_tag" - ) - if [[ "${{ matrix.target }}" == *linux* ]]; then - # Keep bwrap in the runtime wheel so Linux sandbox fallback behavior - # matches the standalone release bundle on hosts without system bwrap. - stage_runtime_args+=( - --resource-binary - "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/bwrap" - ) + if [[ "${{ matrix.target }}" == *apple-darwin ]]; then + cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" fi - python3 "${stage_runtime_args[@]}" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error + - if: ${{ matrix.runner == 'windows-11-arm' }} + name: Install zstd + shell: powershell + run: choco install -y zstandard - name: Compress artifacts - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | # Path that contains the uncompressed binaries for the current # ${{ matrix.target }} dest="dist/${{ matrix.target }}" + repo_root=$PWD + + # We want to ship the raw Windows executables in the GitHub Release + # in addition to the compressed archives. Keep the originals for + # Windows targets; remove them elsewhere to limit the number of + # artifacts that end up in the GitHub Release. + keep_originals=false + if [[ "${{ matrix.runner }}" == windows* ]]; then + keep_originals=true + fi # For compatibility with environments that lack the `zstd` tool we - # additionally create a `.tar.gz` alongside every binary we publish. - # The end result is: + # additionally create a `.tar.gz` for all platforms and `.zip` for + # Windows alongside every single binary that we publish. The end result is: # codex-.zst (existing) # codex-.tar.gz (new) + # codex-.zip (only for Windows) # 1. Produce a .tar.gz for every file in the directory *before* we # run `zstd --rm`, because that flag deletes the original files. @@ -612,7 +749,7 @@ jobs: base="$(basename "$f")" # Skip files that are already archives (shouldn't happen, but be # safe). - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then + if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then continue fi @@ -622,344 +759,239 @@ jobs: fi # Create per-binary tar.gz - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - - # Also create .zst and remove the uncompressed binaries to keep - # non-Windows artifact directories small. - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} - with: - name: ${{ matrix.artifact_name }} - # Upload the per-binary .zst files, .tar.gz equivalents, and any - # prebuilt archives staged above. - path: | - codex-rs/dist/${{ matrix.target }}/* - - stage-signed-macos: - if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode == 'promote_signed' }} - needs: tag-check - name: Stage signed macOS handoff - ${{ matrix.target }} - ${{ matrix.bundle }} - runs-on: macos-15-xlarge - timeout-minutes: 30 - permissions: - contents: read - defaults: - run: - working-directory: codex-rs + # For Android, we want the archive to have the target suffix even though the binary is just 'codex'. + archive_name="${base}" + if [[ "${{ matrix.target }}" == "aarch64-linux-android" && "${base}" == "codex" ]]; then + archive_name="codex-${{ matrix.target }}" + fi + tar -C "$dest" -czf "$dest/${archive_name}.tar.gz" "$base" + + # Create zip archive for Windows binaries + # Must run from inside the dest dir so 7z won't + # embed the directory path inside the zip. + if [[ "${{ matrix.runner }}" == windows* ]]; then + if [[ "$base" == "codex-${{ matrix.target }}.exe" ]]; then + # Bundle the sandbox helper binaries into the main codex zip so + # WinGet installs include the required helpers next to codex.exe. + # Fall back to the single-binary zip if the helpers are missing + # to avoid breaking releases. + bundle_dir="$(mktemp -d)" + runner_src="$dest/codex-command-runner-${{ matrix.target }}.exe" + setup_src="$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" + if [[ -f "$runner_src" && -f "$setup_src" ]]; then + cp "$dest/$base" "$bundle_dir/$base" + cp "$runner_src" "$bundle_dir/codex-command-runner.exe" + cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" + # Use an absolute path so bundle zips land in the real dist + # dir even when 7z runs from a temp directory. + (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) + else + echo "warning: missing sandbox binaries; falling back to single-binary zip" + echo "warning: expected $runner_src and $setup_src" + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + rm -rf "$bundle_dir" + else + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + fi - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-apple-darwin - bundle: primary - artifact_name: aarch64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: aarch64-apple-darwin - bundle: app-server - artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: primary - artifact_name: x86_64-apple-darwin - binaries: "codex codex-responses-api-proxy" - build_dmg: "false" - - target: x86_64-apple-darwin - bundle: app-server - artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server" - build_dmg: "false" + # Also create .zst (existing behaviour) *and* remove the original + # uncompressed binary to keep the directory small. + zstd_args=(-T0 -19) + if [[ "${keep_originals}" == false ]]; then + zstd_args+=(--rm) + fi - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + if [[ "${archive_name}" != "${base}" ]]; then + zstd "${zstd_args[@]}" "$dest/$base" -o "$dest/${archive_name}.zst" + else + zstd "${zstd_args[@]}" "$dest/$base" + fi + done - - name: Download signed macOS handoff + - name: Add Termux release metadata + if: ${{ (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && matrix.target == 'aarch64-linux-android' }} shell: bash - env: - GH_TOKEN: ${{ github.token }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - SIGNED_MACOS_SHA256: ${{ inputs.signed_macos_sha256 }} run: | set -euo pipefail - - download_dir="${RUNNER_TEMP}/signed-macos-download" - handoff_dir="${RUNNER_TEMP}/signed-macos-handoff" - rm -rf "$download_dir" "$handoff_dir" - mkdir -p "$download_dir" "$handoff_dir" - - gh release download "$GITHUB_REF_NAME" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern "$SIGNED_MACOS_ASSET" \ - --dir "$download_dir" - - asset_count="$(find "$download_dir" -maxdepth 1 -type f | wc -l | tr -d '[:space:]')" - if [[ "$asset_count" != "1" ]]; then - echo "Expected exactly one signed macOS handoff asset named ${SIGNED_MACOS_ASSET}; found ${asset_count}" - find "$download_dir" -maxdepth 1 -type f -print - exit 1 - fi - - asset_path="$(find "$download_dir" -maxdepth 1 -type f -print -quit)" - if [[ -n "${SIGNED_MACOS_SHA256}" ]]; then - expected_sha="$(printf '%s' "$SIGNED_MACOS_SHA256" | tr '[:upper:]' '[:lower:]')" - actual_sha="$(shasum -a 256 "$asset_path" | awk '{print $1}')" - if [[ "$actual_sha" != "$expected_sha" ]]; then - echo "signed_macos_sha256 mismatch for ${SIGNED_MACOS_ASSET}" - echo "expected: ${expected_sha}" - echo "actual: ${actual_sha}" - exit 1 - fi + if [[ ! -f "${GITHUB_WORKSPACE}/.github/termux-release.json" ]]; then + echo "No Termux release metadata found; skipping metadata attachment." + exit 0 fi + dest="dist/${{ matrix.target }}" + cp "${GITHUB_WORKSPACE}/.github/termux-release.json" "${dest}/termux-release.json" + ( + cd "${dest}" + sha256sum ./* > SHA256SUMS + ) - asset_name="$(basename "$asset_path")" - case "$asset_name" in - *.tar.zst) - zstd -dc "$asset_path" | tar -C "$handoff_dir" -xf - - ;; - *.tar.gz|*.tgz) - tar -C "$handoff_dir" -xzf "$asset_path" - ;; - *.zip) - ditto -x -k "$asset_path" "$handoff_dir" - ;; - *) - echo "Unsupported signed macOS handoff archive format: ${asset_name}" - exit 1 - ;; - esac - - echo "SIGNED_MACOS_HANDOFF_DIR=$handoff_dir" >> "$GITHUB_ENV" + - id: upload-artifact + uses: actions/upload-artifact@v6 + with: + name: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + # Upload the per-binary .zst files as well as the new .tar.gz + # equivalents we generated in the previous step. + path: | + codex-rs/dist/${{ matrix.target }}/* - - name: Stage signed macOS artifacts + - name: Publish Termux artifact notification + if: ${{ github.event_name == 'pull_request' && startsWith(github.base_ref, 'release/') && matrix.target == 'aarch64-linux-android' }} shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_ID: ${{ steps.upload-artifact.outputs.artifact-id }} + ARTIFACT_NAME: ${{ github.event_name == 'pull_request' && format('termux-android-pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || matrix.target }} + PR_NUMBER: ${{ github.event.pull_request.number }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | set -euo pipefail - - target="${{ matrix.target }}" - artifact_name="${{ matrix.artifact_name }}" - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${target}" - fi - if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" ]]; then - source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - fi - if [[ ! -d "$source_dir" ]]; then - echo "Signed macOS handoff is missing ${artifact_name}/" - echo "Expected either:" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/${target}" - echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" - find "$SIGNED_MACOS_HANDOFF_DIR" -maxdepth 3 -type f -print + if [[ -z "${ARTIFACT_ID}" ]]; then + echo "upload-artifact did not return an artifact id" >&2 exit 1 fi - dest="dist/${target}" - mkdir -p "$dest" - - for binary in ${{ matrix.binaries }}; do - source_path="${source_dir}/${binary}" - if [[ ! -f "$source_path" ]]; then - source_path="${source_dir}/${binary}-${target}" - fi - if [[ ! -f "$source_path" ]]; then - echo "Signed macOS handoff is missing ${binary} for ${artifact_name}" - exit 1 - fi - - release_path="${dest}/${binary}-${target}" - ditto "$source_path" "$release_path" - chmod 0755 "$release_path" - codesign --verify --strict --verbose=2 "$release_path" - done - - # DMG staging is disabled for signed promotion because we no longer - # distribute DMGs from this release path. Keep the branch here so the - # handoff can opt back in by flipping matrix.build_dmg if needed. - if [[ "${{ matrix.build_dmg }}" == "true" ]]; then - dmg_name="codex-${target}.dmg" - dmg_source="${source_dir}/${dmg_name}" - if [[ ! -f "$dmg_source" ]]; then - echo "Signed macOS handoff is missing ${dmg_name} for ${artifact_name}" - exit 1 - fi - - codesign --verify --strict --verbose=2 "$dmg_source" - xcrun stapler validate "$dmg_source" - cp "$dmg_source" "$dest/$dmg_name" + marker="" + artifact_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts/${ARTIFACT_ID}" + run_url="${GH_WORKFLOW_URL:-https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + body_path="${RUNNER_TEMP}/termux-artifact-comment.md" + { + echo "${marker}" + echo "Termux Android artifact ready for testing:" + echo + echo "- Artifact: [${ARTIFACT_NAME}](${artifact_url})" + echo "- Workflow run: [${GITHUB_RUN_ID}](${run_url})" + echo + echo "You must be signed in to GitHub with repository access to download Actions artifacts." + } > "${body_path}" + + existing_comment_id="$( + gh pr view "${PR_NUMBER}" \ + --repo "${GITHUB_REPOSITORY}" \ + --json comments \ + --jq ".comments | map(select(.author.is_bot == true and (.body | contains(\"${marker}\")))) | .[-1].id // \"\"" + )" + if [[ -n "${existing_comment_id}" ]]; then + gh api graphql \ + -f query=' + mutation($id: ID!, $body: String!) { + updateIssueComment(input: {id: $id, body: $body}) { + issueComment { + id + } + } + } + ' \ + -f id="${existing_comment_id}" \ + -f body="$(cat "${body_path}")" \ + >/dev/null + else + gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body-file "${body_path}" fi - - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - shell: bash - run: | - set -euo pipefail - - case "${{ matrix.target }}" in - aarch64-apple-darwin) - platform_tag="macosx_11_0_arm64" - ;; - x86_64-apple-darwin) - platform_tag="macosx_10_9_x86_64" - ;; - *) - echo "No Python runtime wheel platform tag for ${{ matrix.target }}" - exit 1 - ;; - esac - - python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build - - stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" - wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" - python3 \ - "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \ - stage-runtime \ - "$stage_dir" \ - "${GITHUB_WORKSPACE}/codex-rs/dist/${{ matrix.target }}/codex-${{ matrix.target }}" \ - --codex-version "${GITHUB_REF_NAME}" \ - --platform-tag "$platform_tag" - "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - - - name: Build Codex package archive - shell: bash - env: - TARGET: ${{ matrix.target }} - BUNDLE: ${{ matrix.bundle }} - run: | - set -euo pipefail - bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ - --target "$TARGET" \ - --bundle "$BUNDLE" \ - --entrypoint-dir "dist/${TARGET}" \ - --archive-dir "dist/${TARGET}" \ - --target-suffixed-entrypoint - - - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-runtime-wheel-${{ matrix.target }} - path: python-runtime-dist/${{ matrix.target }}/*.whl - if-no-files-found: error - - - name: Compress artifacts - shell: bash - run: | - set -euo pipefail - - dest="dist/${{ matrix.target }}" - for f in "$dest"/*; do - base="$(basename "$f")" - if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then - continue + gh label create binary-ready \ + --repo "${GITHUB_REPOSITORY}" \ + --color 0e8a16 \ + --description "Android binary is ready for testing" \ + --force + gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --add-label binary-ready + + if [[ -n "${SLACK_WEBHOOK_URL:-}" ]]; then + slack_payload_path="${RUNNER_TEMP}/termux-artifact-slack.json" + pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}" + jq -n \ + --arg artifact_name "${ARTIFACT_NAME}" \ + --arg artifact_url "${artifact_url}" \ + --arg pr_number "${PR_NUMBER}" \ + --arg pr_url "${pr_url}" \ + --arg run_id "${GITHUB_RUN_ID}" \ + --arg run_url "${run_url}" \ + '{ + text: ("Termux Android artifact ready for testing: " + $artifact_url), + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: ("*Termux Android artifact ready for testing*\n" + $artifact_url) + } + }, + { + type: "section", + fields: [ + { + type: "mrkdwn", + text: ("*Artifact:*\n<" + $artifact_url + "|" + $artifact_name + ">") + }, + { + type: "mrkdwn", + text: ("*Pull request:*\n<" + $pr_url + "|#" + $pr_number + ">") + }, + { + type: "mrkdwn", + text: ("*Workflow run:*\n<" + $run_url + "|" + $run_id + ">") + } + ] + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: "You must be signed in to GitHub with repository access to download Actions artifacts." + } + ] + } + ] + }' > "${slack_payload_path}" + + if ! curl -fsS \ + -X POST \ + -H "Content-type: application/json" \ + --data @"${slack_payload_path}" \ + "${SLACK_WEBHOOK_URL}"; then + echo "::warning::Failed to send Slack artifact notification" fi + fi - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - zstd -T0 -19 --rm "$dest/$base" - done - - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.artifact_name }} - path: | - codex-rs/dist/${{ matrix.target }}/* - - build-windows: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - needs: tag-check - uses: ./.github/workflows/rust-release-windows.yml - with: - release-lto: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }} - secrets: inherit - - argument-comment-lint-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: argument-comment-lint release assets + shell-tool-mcp: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'openai' + name: shell-tool-mcp needs: tag-check - uses: ./.github/workflows/rust-release-argument-comment-lint.yml + permissions: + contents: read + id-token: write + uses: ./.github/workflows/shell-tool-mcp.yml with: + release-tag: ${{ github.ref_name }} publish: true - - zsh-release-assets: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} - name: zsh release assets - needs: tag-check - uses: ./.github/workflows/rust-release-zsh.yml + secrets: inherit release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') needs: - - tag-check - build - - stage-signed-macos - - build-windows - - argument-comment-lint-release-assets - - zsh-release-assets - if: >- - ${{ - always() && - needs.tag-check.result == 'success' && - ( - ( - github.event_name == 'workflow_dispatch' && - inputs.release_mode == 'promote_signed' && - needs.stage-signed-macos.result == 'success' && - needs.build.result == 'skipped' && - needs.build-windows.result == 'skipped' && - needs.argument-comment-lint-release-assets.result == 'skipped' && - needs.zsh-release-assets.result == 'skipped' - ) || - ( - (github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed') && - needs.build.result == 'success' && - needs.stage-signed-macos.result == 'skipped' && - needs.build-windows.result == 'success' && - needs.argument-comment-lint-release-assets.result == 'success' && - needs.zsh-release-assets.result == 'success' - ) - ) - }} + - shell-tool-mcp name: release runs-on: ubuntu-latest permissions: contents: write actions: read - env: - RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} - SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode == 'promote_signed' }} - SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} - UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} outputs: version: ${{ steps.release_name.outputs.name }} tag: ${{ github.ref_name }} - sign_macos: ${{ steps.release_mode.outputs.sign_macos }} should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }} npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }} - should_publish_python_runtime: ${{ steps.python_runtime_publish_settings.outputs.should_publish }} steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + uses: actions/checkout@v6 - - name: Define release mode - id: release_mode - run: | - echo "release_mode=${RELEASE_MODE}" >> "$GITHUB_OUTPUT" - echo "sign_macos=${SIGN_MACOS}" >> "$GITHUB_OUTPUT" + - name: 🧰 Actions Toolbox + # This is required for the GitHub CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: wallentx/gh-actions/composite/actions-toolbox@main - name: Generate release notes from tag commit message id: release_notes @@ -981,138 +1013,18 @@ jobs: echo "path=${notes_path}" >> "${GITHUB_OUTPUT}" - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@v7 with: path: dist - - name: Validate unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - run_summary="$(gh run view "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --json conclusion,event,headBranch,headSha,status,workflowName,url \ - --jq '[.workflowName, .event, .headBranch, .headSha, .status, .conclusion, .url] | @tsv')" - IFS=$'\t' read -r workflow_name event head_branch head_sha status conclusion run_url <<< "$run_summary" - expected_head_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")" - - if [[ "$workflow_name" != "$GITHUB_WORKFLOW" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is for workflow '${workflow_name}', expected '${GITHUB_WORKFLOW}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$event" != "workflow_dispatch" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} was triggered by '${event}', expected 'workflow_dispatch'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_branch" != "$GITHUB_REF_NAME" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used ref '${head_branch}', expected '${GITHUB_REF_NAME}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$head_sha" != "$expected_head_sha" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} used head SHA '${head_sha}', expected '${expected_head_sha}'" - echo "Run URL: ${run_url}" - exit 1 - fi - - if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then - echo "unsigned_run_id ${UNSIGNED_RUN_ID} is ${status}/${conclusion}, expected completed/success" - echo "Run URL: ${run_url}" - exit 1 - fi - - - name: Download artifacts from unsigned build run - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - gh run download "$UNSIGNED_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --dir dist - - - name: Remove unsigned macOS staging artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - run: | - set -euo pipefail - find dist -mindepth 1 -maxdepth 1 -type d \ - -name '*-apple-darwin*-unsigned' \ - -exec rm -rf {} + - - - name: Re-upload promoted Linux x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-unknown-linux-musl - path: dist/x86_64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Linux arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-unknown-linux-musl - path: dist/aarch64-unknown-linux-musl/* - if-no-files-found: error - - - name: Re-upload promoted Windows x64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: x86_64-pc-windows-msvc - path: dist/x86_64-pc-windows-msvc/* - if-no-files-found: error - - - name: Re-upload promoted Windows arm64 artifacts - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aarch64-pc-windows-msvc - path: dist/aarch64-pc-windows-msvc/* - if-no-files-found: error - - name: List run: ls -R dist/ - - name: Prune artifacts excluded from unsigned macOS release - if: ${{ env.SIGN_MACOS == 'false' }} - run: | - find dist -mindepth 1 -maxdepth 1 -type d \ - ! -name '*-apple-darwin*-unsigned' \ - ! -name 'aarch64-unknown-linux-musl' \ - ! -name 'aarch64-unknown-linux-musl-app-server' \ - ! -name 'x86_64-unknown-linux-musl' \ - ! -name 'x86_64-unknown-linux-musl-app-server' \ - ! -name 'aarch64-pc-windows-msvc' \ - ! -name 'x86_64-pc-windows-msvc' \ - -exec rm -rf {} + - - if ! find dist -type f -name '*-apple-darwin*-unsigned*' | grep -q .; then - echo "No unsigned macOS artifacts found in downloaded workflow artifacts." - exit 1 - fi - + # This is a temporary fix: we should modify shell-tool-mcp.yml so these + # files do not end up in dist/ in the first place. - name: Delete entries from dist/ that should not go in the release run: | - rm -rf dist/windows-binaries* - # cargo-timing.html appears under multiple target-specific directories. - # If included in files: dist/**, release upload races on duplicate - # asset names and can fail with 404s. - find dist -type f -name 'cargo-timing.html' -delete - # Keep package-builder sidecar archives as workflow artifacts only - # until distribution channels are ready to consume them. - find dist -type f \ - \( -name 'codex-package-*' -o -name 'codex-app-server-package-*' \) \ - -delete - find dist -type d -empty -delete + rm -rf dist/shell-tool-mcp* ls -R dist/ @@ -1136,12 +1048,6 @@ jobs: set -euo pipefail version="${VERSION}" - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - echo "npm_tag=" >> "$GITHUB_OUTPUT" - exit 0 - fi - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "should_publish=true" >> "$GITHUB_OUTPUT" echo "npm_tag=" >> "$GITHUB_OUTPUT" @@ -1153,132 +1059,55 @@ jobs: echo "npm_tag=" >> "$GITHUB_OUTPUT" fi - - name: Determine Python runtime publish settings - id: python_runtime_publish_settings - env: - VERSION: ${{ steps.release_name.outputs.name }} - run: | - set -euo pipefail - version="${VERSION}" - - if [[ "${SIGN_MACOS}" != "true" ]]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - else - echo "should_publish=false" >> "$GITHUB_OUTPUT" - fi - - name: Setup pnpm - if: ${{ env.SIGN_MACOS == 'true' }} - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 + uses: pnpm/action-setup@v4 with: run_install: false - name: Setup Node.js for npm packaging - if: ${{ env.SIGN_MACOS == 'true' }} - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: node-version: 22 - name: Install dependencies - if: ${{ env.SIGN_MACOS == 'true' }} run: pnpm install --frozen-lockfile # stage_npm_packages.py requires DotSlash when staging releases. - - uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2 + - uses: facebook/install-dotslash@v2 - name: Stage npm packages - if: ${{ env.SIGN_MACOS == 'true' }} + if: github.repository_owner == 'openai' env: GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ steps.release_name.outputs.name }} run: | - workflow_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" ./scripts/stage_npm_packages.py \ - --release-version "$RELEASE_VERSION" \ - --workflow-url "$workflow_url" \ + --release-version "${{ steps.release_name.outputs.name }}" \ --package codex \ --package codex-responses-api-proxy \ --package codex-sdk - - name: Stage installer scripts - if: ${{ env.SIGN_MACOS == 'true' }} - run: | - cp scripts/install/install.sh dist/install.sh - cp scripts/install/install.ps1 dist/install.ps1 - - name: Create GitHub Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@v2 with: name: ${{ steps.release_name.outputs.name }} tag_name: ${{ github.ref_name }} body_path: ${{ steps.release_notes.outputs.path }} files: dist/** - overwrite_files: true - make_latest: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} # Mark as prerelease only when the version has a suffix after x.y.z # (e.g. -alpha, -beta). Otherwise publish a normal release. prerelease: ${{ contains(steps.release_name.outputs.name, '-') }} - - name: Clean up signed promotion handoff assets - if: ${{ env.RELEASE_MODE == 'promote_signed' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" --jq '.id')" - gh api --paginate "repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets" \ - --jq '.[] | [.id, .name] | @tsv' | - while IFS=$'\t' read -r asset_id asset_name; do - if [[ -z "$asset_id" || -z "$asset_name" ]]; then - continue - fi - - delete_asset=false - if [[ "$asset_name" == *unsigned* || "$asset_name" == "$SIGNED_MACOS_ASSET" ]]; then - delete_asset=true - fi - - if [[ "$delete_asset" == "true" ]]; then - echo "Deleting release asset ${asset_name}" - gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" - fi - done - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 + - if: github.repository_owner == 'openai' + uses: facebook/dotslash-publish-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag: ${{ github.ref_name }} config: .github/dotslash-config.json - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-zsh-config.json - - - if: ${{ env.SIGN_MACOS == 'true' }} - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-argument-comment-lint-config.json - - name: Trigger developers.openai.com deploy # Only trigger the deploy if the release is not a pre-release. # The deploy is used to update the developers.openai.com website with the new config schema json file. - if: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} + if: ${{ !contains(steps.release_name.outputs.name, '-') && github.repository_owner == 'openai' }} continue-on-error: true env: DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }} @@ -1293,15 +1122,7 @@ jobs: # npm docs: https://docs.npmjs.com/trusted-publishers publish-npm: # Publish to npm for stable releases and alpha pre-releases with numeric suffixes. - # promote_signed intentionally skips build jobs that are ancestors of release; - # include the !cancelled() status function so Actions does not apply its implicit - # success() check to the whole dependency chain before evaluating release outputs. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_npm == 'true' - }} + if: ${{ needs.release.outputs.should_publish_npm == 'true' && github.repository_owner == 'openai' }} name: publish-npm needs: release runs-on: ubuntu-latest @@ -1311,37 +1132,36 @@ jobs: steps: - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@v6 with: - # Node 24 bundles npm >= 11.5.1, which trusted publishing requires. - node-version: 24 + node-version: 22 registry-url: "https://registry.npmjs.org" scope: "@openai" + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + - name: Download npm tarballs from release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} run: | set -euo pipefail - version="$RELEASE_VERSION" - tag="$RELEASE_TAG" + version="${{ needs.release.outputs.version }}" + tag="${{ needs.release.outputs.tag }}" mkdir -p dist/npm - patterns=( - "codex-npm-${version}.tgz" - "codex-npm-linux-*-${version}.tgz" - "codex-npm-darwin-*-${version}.tgz" - "codex-npm-win32-*-${version}.tgz" - "codex-responses-api-proxy-npm-${version}.tgz" - "codex-sdk-npm-${version}.tgz" - ) - for pattern in "${patterns[@]}"; do - gh release download "$tag" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "$pattern" \ - --dir dist/npm - done + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-responses-api-proxy-npm-${version}.tgz" \ + --dir dist/npm + gh release download "$tag" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "codex-sdk-npm-${version}.tgz" \ + --dir dist/npm # No NODE_AUTH_TOKEN needed because we use OIDC. - name: Publish to npm @@ -1350,193 +1170,23 @@ jobs: NPM_TAG: ${{ needs.release.outputs.npm_tag }} run: | set -euo pipefail - prefix="" + tag_args=() if [[ -n "${NPM_TAG}" ]]; then - prefix="${NPM_TAG}-" + tag_args+=(--tag "${NPM_TAG}") fi - root_tarball="dist/npm/codex-npm-${VERSION}.tgz" - sdk_tarball="dist/npm/codex-sdk-npm-${VERSION}.tgz" - # Keep this list in sync with CODEX_PLATFORM_PACKAGES in - # codex-cli/scripts/build_npm_package.py. The root wrapper advances - # @openai/codex@latest as soon as it publishes, so every platform - # package it aliases must already exist in the registry first. - platform_tarballs=( - "dist/npm/codex-npm-linux-x64-${VERSION}.tgz" - "dist/npm/codex-npm-linux-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-x64-${VERSION}.tgz" - "dist/npm/codex-npm-darwin-arm64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-x64-${VERSION}.tgz" - "dist/npm/codex-npm-win32-arm64-${VERSION}.tgz" - ) - - for required_tarball in "${platform_tarballs[@]}" "${root_tarball}"; do - if [[ ! -f "${required_tarball}" ]]; then - echo "Missing npm tarball: ${required_tarball}" - exit 1 - fi - done - - shopt -s nullglob - other_tarballs=() - for tarball in dist/npm/*-"${VERSION}".tgz; do - if [[ "${tarball}" == "${root_tarball}" || "${tarball}" == "${sdk_tarball}" ]]; then - continue - fi - - is_platform_tarball=false - for platform_tarball in "${platform_tarballs[@]}"; do - if [[ "${tarball}" == "${platform_tarball}" ]]; then - is_platform_tarball=true - break - fi - done - if [[ "${is_platform_tarball}" == true ]]; then - continue - fi - - other_tarballs+=("${tarball}") - done - - # Publish the platform packages before the root CLI wrapper. The root - # wrapper advances @openai/codex@latest, so it should only publish - # after the optional dependency versions it references exist. tarballs=( - "${platform_tarballs[@]}" - "${other_tarballs[@]}" - "${root_tarball}" + "codex-npm-${VERSION}.tgz" + "codex-responses-api-proxy-npm-${VERSION}.tgz" + "codex-sdk-npm-${VERSION}.tgz" ) - if [[ -f "${sdk_tarball}" ]]; then - tarballs+=("${sdk_tarball}") - fi for tarball in "${tarballs[@]}"; do - filename="$(basename "${tarball}")" - tag="" - - case "${filename}" in - codex-npm-linux-*-"${VERSION}".tgz|codex-npm-darwin-*-"${VERSION}".tgz|codex-npm-win32-*-"${VERSION}".tgz) - platform="${filename#codex-npm-}" - platform="${platform%-${VERSION}.tgz}" - tag="${prefix}${platform}" - ;; - codex-npm-"${VERSION}".tgz|codex-responses-api-proxy-npm-"${VERSION}".tgz|codex-sdk-npm-"${VERSION}".tgz) - tag="${NPM_TAG}" - ;; - *) - echo "Unexpected npm tarball: ${filename}" - exit 1 - ;; - esac - - publish_cmd=(npm publish "${GITHUB_WORKSPACE}/${tarball}") - if [[ -n "${tag}" ]]; then - publish_cmd+=(--tag "${tag}") - fi - - echo "+ ${publish_cmd[*]}" - set +e - publish_output="$("${publish_cmd[@]}" 2>&1)" - publish_status=$? - set -e - - echo "${publish_output}" - if [[ ${publish_status} -eq 0 ]]; then - continue - fi - - if grep -qiE "previously published|cannot publish over|version already exists" <<< "${publish_output}"; then - echo "Skipping already-published package version for ${filename}" - continue - fi - - exit "${publish_status}" + npm publish "${GITHUB_WORKSPACE}/dist/npm/${tarball}" "${tag_args[@]}" done - # Publish the platform-specific Python runtime wheels using PyPI trusted publishing. - # PyPI project configuration must trust this workflow and job. Keep this - # non-blocking while the Python runtime publishing path is new; failures still - # need release follow-up, but should not invalidate the Rust release itself. - publish-python-runtime: - # Publish to PyPI for stable releases and alpha pre-releases with numeric suffixes. - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.should_publish_python_runtime == 'true' - }} - name: publish-python-runtime - needs: release - runs-on: ubuntu-latest - continue-on-error: true - environment: pypi - permissions: - id-token: write # Required for PyPI trusted publishing. - contents: read - - steps: - - name: Download Python runtime wheels from release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.release.outputs.tag }} - RELEASE_VERSION: ${{ needs.release.outputs.version }} - run: | - set -euo pipefail - python_version="$RELEASE_VERSION" - python_version="${python_version/-alpha./a}" - python_version="${python_version/-beta./b}" - python_version="${python_version/-rc./rc}" - - mkdir -p dist/python-runtime - gh release download "$RELEASE_TAG" \ - --repo "${GITHUB_REPOSITORY}" \ - --pattern "openai_codex_cli_bin-${python_version}-*.whl" \ - --dir dist/python-runtime - ls -lh dist/python-runtime - - - name: Publish Python runtime wheels to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - with: - packages-dir: dist/python-runtime - skip-existing: true - - winget: - name: winget - needs: release - # Only publish stable/mainline releases to WinGet; pre-releases include a - # '-' in the semver string (e.g., 1.2.3-alpha.1). - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' && - !contains(needs.release.outputs.version, '-') - }} - # This job only invokes a GitHub Action to open/update the winget-pkgs PR; - # it does not execute Windows-only tooling, so Linux is sufficient. - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Publish to WinGet - uses: vedantmgoyal9/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5 - with: - identifier: OpenAI.Codex - version: ${{ needs.release.outputs.version }} - release-tag: ${{ needs.release.outputs.tag }} - fork-user: openai-oss-forks - installers-regex: '^codex-(?:x86_64|aarch64)-pc-windows-msvc\.exe\.zip$' - token: ${{ secrets.WINGET_PUBLISH_PAT }} - update-branch: name: Update latest-alpha-cli branch - if: >- - ${{ - !cancelled() && - needs.release.result == 'success' && - needs.release.outputs.sign_macos == 'true' - }} permissions: contents: write needs: release diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml new file mode 100644 index 000000000000..66a76aa4d938 --- /dev/null +++ b/.github/workflows/shell-tool-mcp.yml @@ -0,0 +1,461 @@ +name: shell-tool-mcp + +on: + workflow_call: + inputs: + release-version: + description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v. + required: false + type: string + release-tag: + description: Tag name to use when downloading release artifacts (defaults to rust-v). + required: false + type: string + publish: + description: Whether to publish to npm when the version is releasable. + required: false + default: true + type: boolean + +env: + NODE_VERSION: 22 + +jobs: + metadata: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.compute.outputs.version }} + release_tag: ${{ steps.compute.outputs.release_tag }} + should_publish: ${{ steps.compute.outputs.should_publish }} + npm_tag: ${{ steps.compute.outputs.npm_tag }} + steps: + - name: Compute version and tags + id: compute + run: | + set -euo pipefail + + version="${{ inputs.release-version }}" + release_tag="${{ inputs.release-tag }}" + + if [[ -z "$version" ]]; then + if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then + version="${release_tag#rust-v}" + elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then + version="${GITHUB_REF_NAME#rust-v}" + release_tag="${GITHUB_REF_NAME}" + else + echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag." + exit 1 + fi + fi + + if [[ -z "$release_tag" ]]; then + release_tag="rust-v${version}" + fi + + npm_tag="" + should_publish="false" + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + should_publish="true" + elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then + should_publish="true" + npm_tag="alpha" + fi + + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" + echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT" + echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT" + + matrix-setup: + runs-on: ubuntu-latest + outputs: + rust-binaries: ${{ steps.compute.outputs.rust-binaries }} + bash-linux: ${{ steps.compute.outputs.bash-linux }} + bash-darwin: ${{ steps.compute.outputs.bash-darwin }} + steps: + - name: Compute matrices + id: compute + shell: bash + run: | + set -euo pipefail + is_openai="${{ github.repository_owner == 'openai' }}" + + # rust-binaries: always include x86_64-musl; conditionally include paid/fork runners + if [[ "$is_openai" == "true" ]]; then + rust='[ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin"}, + {"runner":"macos-15-xlarge","target":"x86_64-apple-darwin"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","install_musl":true} + ]' + else + rust='[{"runner":"macos-latest","target":"aarch64-apple-darwin"}]' + fi + rust=$(echo "$rust" | jq -c '. + [{"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","install_musl":true}]') + echo "rust-binaries={\"include\":$rust}" >> "$GITHUB_OUTPUT" + + # bash-linux: always include x86_64 variants; add arm64 on openai + bash_linux='[ + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-24.04","image":"ubuntu:24.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"ubuntu-22.04","image":"ubuntu:22.04"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-12","image":"debian:12"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"debian-11","image":"debian:11"}, + {"runner":"ubuntu-24.04","target":"x86_64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]' + if [[ "$is_openai" == "true" ]]; then + bash_linux=$(echo "$bash_linux" | jq -c '. + [ + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-24.04","image":"arm64v8/ubuntu:24.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-22.04","image":"arm64v8/ubuntu:22.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"ubuntu-20.04","image":"arm64v8/ubuntu:20.04"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-12","image":"arm64v8/debian:12"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"debian-11","image":"arm64v8/debian:11"}, + {"runner":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-musl","variant":"centos-9","image":"quay.io/centos/centos:stream9"} + ]') + fi + echo "bash-linux={\"include\":$(echo "$bash_linux" | jq -c)}" >> "$GITHUB_OUTPUT" + + # bash-darwin: always include macos-14; add macos-15-xlarge on openai + bash_darwin='[{"runner":"macos-14","target":"aarch64-apple-darwin","variant":"macos-14"}]' + if [[ "$is_openai" == "true" ]]; then + bash_darwin=$(echo "$bash_darwin" | jq -c '. + [ + {"runner":"macos-15-xlarge","target":"aarch64-apple-darwin","variant":"macos-15"} + ]') + fi + echo "bash-darwin={\"include\":$(echo "$bash_darwin" | jq -c)}" >> "$GITHUB_OUTPUT" + + rust-binaries: + name: Build Rust - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + defaults: + run: + working-directory: codex-rs + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.rust-binaries) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install UBSan runtime (musl) + if: ${{ matrix.install_musl }} + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -y + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 + fi + + - uses: dtolnay/rust-toolchain@1.93 + with: + targets: ${{ matrix.target }} + + - if: ${{ matrix.install_musl }} + name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.14.0 + + - if: ${{ matrix.install_musl }} + name: Install musl build dependencies + env: + TARGET: ${{ matrix.target }} + run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh" + + - if: ${{ matrix.install_musl }} + name: Configure rustc UBSan wrapper (musl host) + shell: bash + run: | + set -euo pipefail + ubsan="" + if command -v ldconfig >/dev/null 2>&1; then + ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')" + fi + wrapper_root="${RUNNER_TEMP:-/tmp}" + wrapper="${wrapper_root}/rustc-ubsan-wrapper" + cat > "${wrapper}" <> "$GITHUB_ENV" + echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV" + + - if: ${{ matrix.install_musl }} + name: Clear sanitizer flags (musl) + shell: bash + run: | + set -euo pipefail + # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. + echo "RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV" + # Override any runner-level Cargo config rustflags as well. + echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + + sanitize_flags() { + local input="$1" + input="${input//-fsanitize=undefined/}" + input="${input//-fno-sanitize-recover=undefined/}" + input="${input//-fno-sanitize-trap=undefined/}" + echo "$input" + } + + cflags="$(sanitize_flags "${CFLAGS-}")" + cxxflags="$(sanitize_flags "${CXXFLAGS-}")" + echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" + echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + + - name: Build exec server binaries + run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper + + - name: Stage exec server binaries + run: | + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}" + mkdir -p "$dest" + cp "target/${{ matrix.target }}/release/codex-exec-mcp-server" "$dest/" + cp "target/${{ matrix.target }}/release/codex-execve-wrapper" "$dest/" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-rust-${{ matrix.target }} + path: artifacts/** + if-no-files-found: error + + bash-linux: + name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + container: + image: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-linux) }} + steps: + - name: Install build prerequisites + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext + elif command -v dnf >/dev/null 2>&1; then + dnf install -y git gcc gcc-c++ make bison autoconf gettext + elif command -v yum >/dev/null 2>&1; then + yum install -y git gcc gcc-c++ make bison autoconf gettext + else + echo "Unsupported package manager in container" + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + bash-darwin: + name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: [metadata, matrix-setup] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix-setup.outputs.bash-darwin) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build patched Bash + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash + cd /tmp/bash + git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b + git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" + ./configure --without-bash-malloc + cores="$(getconf _NPROCESSORS_ONLN)" + make -j"${cores}" + + dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" + mkdir -p "$dest" + cp bash "$dest/bash" + + - uses: actions/upload-artifact@v6 + with: + name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} + path: artifacts/** + if-no-files-found: error + + package: + name: Package npm module + needs: + - metadata + - rust-binaries + - bash-linux + - bash-darwin + runs-on: ubuntu-latest + env: + PACKAGE_VERSION: ${{ needs.metadata.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install JavaScript dependencies + run: pnpm install --frozen-lockfile + + - name: Build (shell-tool-mcp) + run: pnpm --filter @openai/codex-shell-tool-mcp run build + + - name: Download build artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts + + - name: Assemble staging directory + id: staging + shell: bash + run: | + set -euo pipefail + staging="${STAGING_DIR}" + mkdir -p "$staging" "$staging/vendor" + cp shell-tool-mcp/README.md "$staging/" + cp shell-tool-mcp/package.json "$staging/" + cp -R shell-tool-mcp/bin "$staging/" + + found_vendor="false" + shopt -s nullglob + for vendor_dir in artifacts/*/vendor; do + rsync -av "$vendor_dir/" "$staging/vendor/" + found_vendor="true" + done + if [[ "$found_vendor" == "false" ]]; then + echo "No vendor payloads were downloaded." + exit 1 + fi + + node - <<'NODE' + import fs from "node:fs"; + import path from "node:path"; + + const stagingDir = process.env.STAGING_DIR; + const version = process.env.PACKAGE_VERSION; + const pkgPath = path.join(stagingDir, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + pkg.version = version; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + NODE + + echo "dir=$staging" >> "$GITHUB_OUTPUT" + env: + STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp + + - name: Ensure binaries are executable + run: | + set -euo pipefail + staging="${{ steps.staging.outputs.dir }}" + chmod +x \ + "$staging"/vendor/*/codex-exec-mcp-server \ + "$staging"/vendor/*/codex-execve-wrapper \ + "$staging"/vendor/*/bash/*/bash + + - name: Create npm tarball + shell: bash + run: | + set -euo pipefail + mkdir -p dist/npm + staging="${{ steps.staging.outputs.dir }}" + pack_info=$(cd "$staging" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm") + filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);') + mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz" + + - uses: actions/upload-artifact@v6 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz + if-no-files-found: error + + publish: + name: Publish npm package + needs: + - metadata + - package + if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }} + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: https://registry.npmjs.org + scope: "@openai" + + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + + - name: Download npm tarball + uses: actions/download-artifact@v7 + with: + name: codex-shell-tool-mcp-npm + path: dist/npm + + - name: Publish to npm + if: github.repository == 'openai/codex' + env: + NPM_TAG: ${{ needs.metadata.outputs.npm_tag }} + VERSION: ${{ needs.metadata.outputs.version }} + shell: bash + run: | + set -euo pipefail + tag_args=() + if [[ -n "${NPM_TAG}" ]]; then + tag_args+=(--tag "${NPM_TAG}") + fi + npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}" diff --git a/.github/workflows/termux-release-checkpoint.yml b/.github/workflows/termux-release-checkpoint.yml new file mode 100644 index 000000000000..0347e6f1b3d6 --- /dev/null +++ b/.github/workflows/termux-release-checkpoint.yml @@ -0,0 +1,103 @@ +name: termux-release-checkpoint + +on: + workflow_dispatch: + inputs: + source_branch: + description: "Release branch to checkpoint from, for example release/0.123.0" + required: false + type: string + default: "" + source_sha: + description: "Specific source commit SHA to checkpoint; defaults to the source branch tip" + required: false + type: string + default: "" + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + type: string + default: "wallentx/termux-target" + reviewer: + description: "GitHub username to request as reviewer" + required: false + type: string + default: "wallentx" + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-checkpoint-${{ inputs.source_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + checkpoint: + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + env: + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + REQUESTED_SOURCE_BRANCH: ${{ inputs.source_branch }} + REQUESTED_SOURCE_SHA: ${{ inputs.source_sha }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ inputs.source_branch || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + source_branch="${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${source_branch}" + + - name: Create checkpoint PR + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" diff --git a/.github/workflows/termux-release-deploy.yml b/.github/workflows/termux-release-deploy.yml new file mode 100644 index 000000000000..2071611dacc4 --- /dev/null +++ b/.github/workflows/termux-release-deploy.yml @@ -0,0 +1,243 @@ +name: termux-release-deploy + +on: + push: + branches: + - "release/**" + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to deploy, for example release/0.124.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to deploy. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged release PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged release PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + destination_branch: + description: "Destination patch branch to receive the checkpoint PR" + required: false + default: "wallentx/termux-target" + type: string + reviewer: + description: "GitHub username to request as reviewer on the checkpoint PR" + required: false + default: "wallentx" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-deploy-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + deploy: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' || !startsWith(github.event.head_commit.message, 'Seed Termux release automation') }} + permissions: + actions: read + contents: write + deployments: write + issues: write + pull-requests: write + env: + REQUESTED_RELEASE_BRANCH: ${{ inputs.release_branch }} + REQUESTED_RELEASE_SHA: ${{ inputs.release_sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} + REVIEWER: ${{ inputs.reviewer || 'wallentx' }} + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Configure git + run: | + set -euo pipefail + release_branch="${REQUESTED_RELEASE_BRANCH:-${GITHUB_REF_NAME}}" + bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-configure-git.sh" --origin "${DESTINATION_BRANCH}" "${release_branch}" + + - name: Resolve release ref + id: release-ref + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: deploy + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Create deployment + if: steps.metadata.outputs.deploy == 'true' + id: deployment + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + deployment_id="$( + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments" \ + -f ref="${RELEASE_SHA}" \ + -f environment="termux-release" \ + -F auto_merge=false \ + -F required_contexts[] \ + -f description="Termux release deployment for ${TERMUX_TAG}" \ + --jq '.id' + )" + echo "id=${deployment_id}" >> "$GITHUB_OUTPUT" + + - name: Mark deployment in progress + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="in_progress" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Promoting Termux release artifact and preparing checkpoint PR" \ + >/dev/null + + - name: Locate merged pull request + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" + + - name: Ensure checkpoint PR + if: steps.metadata.outputs.deploy == 'true' + id: checkpoint + env: + SOURCE_BRANCH: ${{ steps.release-ref.outputs.branch }} + SOURCE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-checkpoint-pr.sh" + + - name: Mark deployment success + if: steps.metadata.outputs.deploy == 'true' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="success" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment completed for ${TERMUX_TAG}" \ + >/dev/null + + - name: Mark deployment failure + if: failure() && steps.deployment.outputs.id != '' + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} + run: | + set -euo pipefail + log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ + -f state="failure" \ + -f environment="termux-release" \ + -f log_url="${log_url}" \ + -F auto_inactive=false \ + -f description="Termux release deployment failed" \ + >/dev/null diff --git a/.github/workflows/termux-release-promote.yml b/.github/workflows/termux-release-promote.yml new file mode 100644 index 000000000000..22c277c21a70 --- /dev/null +++ b/.github/workflows/termux-release-promote.yml @@ -0,0 +1,135 @@ +name: termux-release-promote + +on: + workflow_dispatch: + inputs: + release_branch: + description: "Release branch to promote, for example release/0.122.0" + required: true + type: string + release_sha: + description: "Release branch commit SHA to promote. Defaults to the branch head." + required: false + default: "" + type: string + pr_number: + description: "Merged PR number to promote. Optional; normally discovered automatically." + required: false + default: "" + type: string + pr_head_sha: + description: "Merged PR head SHA. Required only when pr_number is set." + required: false + default: "" + type: string + +permissions: + actions: read + attestations: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read + +concurrency: + group: termux-release-promote-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + promote: + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: write + pull-requests: read + env: + TERMUX_AUTOMATION_DIR: ${{ github.workspace }}/.termux-release-automation + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Export GitHub App token for gh + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: echo "GH_TOKEN=${APP_TOKEN}" >> "${GITHUB_ENV}" + + - name: Checkout release branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Checkout automation helpers + uses: actions/checkout@v6 + with: + fetch-depth: 1 + ref: ${{ github.workflow_sha }} + path: .termux-release-automation + token: ${{ steps.app-token.outputs.token }} + + - name: 🧰 Actions Toolbox + uses: wallentx/gh-actions/composite/actions-toolbox@main + + - name: Validate GitHub CLI environment + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-validate-gh-env.sh" + + - name: Resolve release ref + id: release-ref + env: + INPUT_RELEASE_BRANCH: ${{ inputs.release_branch }} + INPUT_RELEASE_SHA: ${{ inputs.release_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-resolve-release-ref.sh" + + - name: Read release metadata + id: metadata + env: + TERMUX_RELEASE_ACTION: promote + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-read-release-metadata.sh" + + - name: Locate merged pull request + if: steps.metadata.outputs.promote == 'true' + id: pr + env: + RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-find-release-pr.sh" + + - name: Download promoted PR artifact + if: steps.metadata.outputs.promote == 'true' + env: + PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-download-release-artifact.sh" + + - name: Create or update mirrored Termux release + if: steps.metadata.outputs.promote == 'true' + env: + UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} + UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} + UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} + TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} + UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} + UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} + RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} + RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} + run: bash "${TERMUX_AUTOMATION_DIR}/scripts/termux-create-or-update-mirrored-release.sh" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 44aeba13be78..da7fa5299a4a 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -118,7 +118,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.134.0-alpha.3" +version = "0.134.0" # Track the edition for all workspace crates in one place. Individual # crates can still override this value, but keeping it here means new # crates created with `cargo new -w ...` automatically inherit the 2024 diff --git a/scripts/termux-configure-git.sh b/scripts/termux-configure-git.sh new file mode 100755 index 000000000000..fae3abcbab48 --- /dev/null +++ b/scripts/termux-configure-git.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Configure the explicit bot identity used by release automation and fetch any +# refs requested by the calling workflow. + +set -euo pipefail + +git config user.name "github-actions[bot]" +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + +while (($#)); do + case "$1" in + --origin) + shift + origin_refs=() + while (($#)) && [[ "$1" != --* ]]; do + origin_refs+=("$1") + shift + done + if ((${#origin_refs[@]})); then + git fetch --prune origin "${origin_refs[@]}" + fi + ;; + --upstream-tag) + if (($# < 3)); then + echo "--upstream-tag requires ." >&2 + exit 1 + fi + upstream_repo="$2" + upstream_tag="$3" + git remote add upstream "https://github.com/${upstream_repo}.git" 2>/dev/null || true + git fetch --prune --no-tags upstream "+refs/tags/${upstream_tag}:refs/tags/${upstream_tag}" + shift 3 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done diff --git a/scripts/termux-create-checkpoint-pr.sh b/scripts/termux-create-checkpoint-pr.sh new file mode 100755 index 000000000000..f315a5b91822 --- /dev/null +++ b/scripts/termux-create-checkpoint-pr.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/termux-release-paths.sh +source "${script_dir}/termux-release-paths.sh" + +source_branch="${SOURCE_BRANCH:-${REQUESTED_SOURCE_BRANCH:-${GITHUB_REF_NAME}}}" +source_sha="${SOURCE_SHA:-${REQUESTED_SOURCE_SHA:-}}" +if [[ -z "${source_sha}" ]]; then + if [[ "${GITHUB_EVENT_NAME:-}" == "push" && "${source_branch}" == "${GITHUB_REF_NAME:-}" ]]; then + source_sha="${GITHUB_SHA}" + else + source_sha="$(git rev-parse "origin/${source_branch}")" + fi +fi + +if [[ -z "${DESTINATION_BRANCH:-}" ]]; then + echo "DESTINATION_BRANCH is required." >&2 + exit 1 +fi + +release_only_checkpoint_paths() { + printf '%s\n' "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +} + +resolve_source_version_conflicts() { + local path="$1" + local resolved_path + resolved_path="$(mktemp)" + + if ! awk ' + function normalize_versions(text) { + gsub(/version = "[^"]+"/, "version = \"\"", text) + return text + } + + BEGIN { + in_block = 0 + side = "" + ours = "" + theirs = "" + blocks = 0 + } + + /^<<<<<<< / { + if (in_block) { + exit 1 + } + in_block = 1 + side = "ours" + ours = "" + theirs = "" + blocks++ + next + } + + /^=======$/ && in_block { + side = "theirs" + next + } + + /^>>>>>>> / && in_block { + if (normalize_versions(ours) != normalize_versions(theirs)) { + exit 1 + } + printf "%s", theirs + in_block = 0 + side = "" + next + } + + { + if (!in_block) { + print + } else if (side == "ours") { + ours = ours $0 ORS + } else if (side == "theirs") { + theirs = theirs $0 ORS + } else { + exit 1 + } + } + + END { + if (in_block || blocks == 0) { + exit 1 + } + } + ' "${path}" > "${resolved_path}"; then + rm -f "${resolved_path}" + return 1 + fi + + cp "${resolved_path}" "${path}" + rm -f "${resolved_path}" +} + +short_sha="${source_sha:0:12}" +source_slug="${source_branch//\//_}" +dest_slug="${DESTINATION_BRANCH//\//_}" +checkpoint_branch="checkpoint/${dest_slug}_from_${source_slug}_${short_sha}" +pr_title="checkpoint: into ${DESTINATION_BRANCH} from ${source_branch} @ ${short_sha}" +merge_conflicted=false +conflict_summary="" + +existing_pr="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --head "${checkpoint_branch}" \ + --state all \ + --json number,state,mergedAt,url \ + --jq '[.[] | select(.state == "OPEN" or .mergedAt != null)] | .[0] // empty' +)" +if [[ -n "${existing_pr}" ]]; then + existing_url="$(jq -r '.url' <<< "${existing_pr}")" + existing_state="$(jq -r '.state' <<< "${existing_pr}")" + echo "Checkpoint PR already exists for ${checkpoint_branch}: ${existing_url} (${existing_state})." + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${existing_url}" >> "${GITHUB_OUTPUT}" + fi + exit 0 +fi + +git checkout -B "${checkpoint_branch}" "origin/${DESTINATION_BRANCH}" + +if ! git merge --no-ff --no-edit "${source_sha}"; then + mapfile -t conflicted_paths < <(git diff --name-only --diff-filter=U) + for conflicted_path in "${conflicted_paths[@]}"; do + if termux_is_checkpoint_release_only_path "${conflicted_path}"; then + echo "Auto-resolving release-only checkpoint conflict in ${conflicted_path} by keeping ${DESTINATION_BRANCH}." + if git cat-file -e "HEAD:${conflicted_path}" 2>/dev/null; then + git checkout --ours -- "${conflicted_path}" + git add "${conflicted_path}" + else + git rm -f --ignore-unmatch "${conflicted_path}" + fi + fi + done + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if ((${#remaining_conflicts[@]})); then + cargo_version_conflicts=true + for remaining_conflict in "${remaining_conflicts[@]}"; do + case "${remaining_conflict}" in + codex-rs/Cargo.toml|codex-rs/Cargo.lock) + ;; + *) + cargo_version_conflicts=false + ;; + esac + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + for remaining_conflict in "${remaining_conflicts[@]}"; do + if ! resolve_source_version_conflicts "${remaining_conflict}"; then + cargo_version_conflicts=false + break + fi + done + + if [[ "${cargo_version_conflicts}" == "true" ]]; then + echo "Auto-resolving recurring Cargo version checkpoint conflicts by keeping ${source_branch} versions." + git add -- "${remaining_conflicts[@]}" + fi + fi + fi + + mapfile -t remaining_conflicts < <(git diff --name-only --diff-filter=U) + if [[ "${#remaining_conflicts[@]}" -eq 0 ]]; then + git commit --no-edit + else + merge_conflicted=true + conflict_summary="$( + printf '%s\n' "${remaining_conflicts[@]}" | awk '{ print "- `" $0 "`" }' + )" + echo "Automatic checkpoint merge failed; creating a manual-resolution PR instead." >&2 + if git rev-parse -q --verify MERGE_HEAD >/dev/null; then + git merge --abort + fi + git checkout -B "${checkpoint_branch}" "${source_sha}" + fi +fi + +if git cat-file -e "origin/${DESTINATION_BRANCH}:.github" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- .github + mapfile -t added_github_paths < <( + git diff --name-only --diff-filter=A "origin/${DESTINATION_BRANCH}" -- .github + ) + if ((${#added_github_paths[@]})); then + git rm -f --ignore-unmatch -- "${added_github_paths[@]}" + fi +else + git rm -r --ignore-unmatch .github +fi + +while IFS= read -r release_only_path; do + if git cat-file -e "origin/${DESTINATION_BRANCH}:${release_only_path}" 2>/dev/null; then + git checkout "origin/${DESTINATION_BRANCH}" -- "${release_only_path}" + else + git rm -f --ignore-unmatch -- "${release_only_path}" + fi +done < <(release_only_checkpoint_paths) + +if ! git diff --quiet || ! git diff --cached --quiet; then + if [[ -e .github || -L .github ]] || git ls-files --error-unmatch -- .github >/dev/null 2>&1; then + git add -A .github + fi + while IFS= read -r release_only_path; do + if [[ -e "${release_only_path}" || -L "${release_only_path}" ]] || git ls-files --error-unmatch -- "${release_only_path}" >/dev/null 2>&1; then + git add -A -- "${release_only_path}" + fi + done < <(release_only_checkpoint_paths) + if [[ "${merge_conflicted}" == "true" ]]; then + git commit -m "checkpoint: prepare ${source_branch} for ${DESTINATION_BRANCH}" + else + git commit --amend --no-edit + fi +fi + +if git diff --quiet "origin/${DESTINATION_BRANCH}" HEAD; then + echo "Checkpoint merge produced no destination changes after release-only files were restored." + exit 0 +fi + +git push --force-with-lease origin "${checkpoint_branch}" + +remaining="$( + git log --first-parent --pretty=format:%H "${source_sha}..origin/${source_branch}" | wc -w +)" + +body_path="${RUNNER_TEMP}/termux-checkpoint-pr.md" +{ + echo "## Termux release checkpoint" + echo + echo "- Source branch: \`${source_branch}\`" + echo "- Source hash: \`${source_sha}\`" + echo "- Destination branch: \`${DESTINATION_BRANCH}\`" + echo "- Remaining first-parent commits on source: ${remaining}" + echo + echo "This PR carries release-train conflict fixes and follow-up changes back into the reusable Termux patch branch." + if [[ "${merge_conflicted}" == "true" ]]; then + echo + echo "## Merge conflicts" + echo + echo "GitHub Actions could not create the checkpoint merge commit automatically, so this PR was created from the source branch state for manual conflict resolution." + echo + echo "Conflicted paths from the failed merge attempt:" + if [[ -n "${conflict_summary}" ]]; then + printf '%s\n' "${conflict_summary}" + else + echo "- Conflict details unavailable" + fi + fi + echo + echo "Release-only workflow files and metadata under \`.github\` were restored to the destination branch versions before opening this PR." +} > "${body_path}" + +pr_url="$( + gh pr create \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${DESTINATION_BRANCH}" \ + --head "${checkpoint_branch}" \ + --title "${pr_title}" \ + --body-file "${body_path}" +)" +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-reviewer "${REVIEWER}" || true +gh label create checkpoint --repo "${GITHUB_REPOSITORY}" --color c5def5 --description "Checkpoint merge" --force +gh label create termux-release --repo "${GITHUB_REPOSITORY}" --color 0e8a16 --description "Termux release automation" --force +gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-label "checkpoint" --add-label "termux-release" + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "pr_url=${pr_url}" >> "${GITHUB_OUTPUT}" +fi diff --git a/scripts/termux-create-or-update-mirrored-release.sh b/scripts/termux-create-or-update-mirrored-release.sh new file mode 100755 index 000000000000..5787e894582b --- /dev/null +++ b/scripts/termux-create-or-update-mirrored-release.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +# Create a mirrored Termux release or repair an existing release that is missing +# the Android tarball. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" +: "${RUNNER_TEMP:?RUNNER_TEMP is required}" +: "${UPSTREAM_TAG:?UPSTREAM_TAG is required}" +: "${UPSTREAM_REPO:?UPSTREAM_REPO is required}" +: "${UPSTREAM_NAME?UPSTREAM_NAME is required}" +: "${TERMUX_TAG:?TERMUX_TAG is required}" +: "${UPSTREAM_PRERELEASE:?UPSTREAM_PRERELEASE is required}" +: "${UPSTREAM_HTML_URL?UPSTREAM_HTML_URL is required}" +: "${RELEASE_TRAIN?RELEASE_TRAIN is required}" +: "${RELEASE_EXISTS:?RELEASE_EXISTS is required}" +: "${PR_NUMBER:?PR_NUMBER is required}" +: "${HEAD_SHA:?HEAD_SHA is required}" +: "${RELEASE_SHA:?RELEASE_SHA is required}" + +promoted_dir="${PROMOTED_DIR:-promoted}" +asset_path="${promoted_dir}/codex-aarch64-linux-android.tar.gz" + +body_path="${RUNNER_TEMP}/release-body.md" +upstream_body_path="${RUNNER_TEMP}/upstream-release-body.md" +upstream_body_without_changelog_path="${RUNNER_TEMP}/upstream-release-body-without-changelog.md" +if gh release view "${UPSTREAM_TAG}" \ + --repo "${UPSTREAM_REPO}" \ + --json body \ + --jq '.body // ""' > "${upstream_body_path}"; then + awk ' + function heading_level(line, text) { + if (match(line, /^(#{1,6})[[:space:]]+(.+)$/, parts)) { + text = tolower(parts[2]) + sub(/[[:space:]]+#+[[:space:]]*$/, "", text) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", text) + if (text == "changelog" || text == "change log") { + return length(parts[1]) + } + } + return 0 + } + + { + if (!skip) { + level = heading_level($0) + if (level > 0) { + skip = 1 + skip_level = level + next + } + print + next + } + + if (match($0, /^(#{1,6})[[:space:]]+/, parts) && length(parts[1]) <= skip_level) { + skip = 0 + print + } + } + ' "${upstream_body_path}" > "${upstream_body_without_changelog_path}" +else + echo "::warning title=Upstream release notes unavailable::Could not read ${UPSTREAM_REPO} release ${UPSTREAM_TAG}." + : > "${upstream_body_without_changelog_path}" +fi + +{ + echo "Termux Android build for ${UPSTREAM_TAG}." + echo + echo "- Upstream release: ${UPSTREAM_HTML_URL}" + echo "- Release train: \`${RELEASE_TRAIN}\`" + echo "- Promoted PR: #${PR_NUMBER}" + echo "- Promoted PR head SHA: \`${HEAD_SHA}\`" +} > "${body_path}" +if [[ -s "${upstream_body_without_changelog_path}" ]]; then + { + echo + echo "## Upstream release notes" + echo + cat "${upstream_body_without_changelog_path}" + } >> "${body_path}" +fi + +release_title="${UPSTREAM_NAME}" +if [[ -z "${release_title}" || "${release_title}" == "null" ]]; then + release_title="${TERMUX_TAG}" +fi + +if [[ "${RELEASE_EXISTS}" == "true" ]]; then + gh release upload \ + "${TERMUX_TAG}" \ + "${asset_path}#codex-termux" \ + --repo "${GITHUB_REPOSITORY}" \ + --clobber + exit 0 +fi + +release_args=( + gh release create "${TERMUX_TAG}" + --repo "${GITHUB_REPOSITORY}" + --target "${RELEASE_SHA}" + --title "${release_title}" + --notes-file "${body_path}" +) +if [[ "${UPSTREAM_PRERELEASE}" == "true" ]]; then + release_args+=(--prerelease) +fi +release_args+=("${asset_path}#codex-termux") +"${release_args[@]}" diff --git a/scripts/termux-download-release-artifact.sh b/scripts/termux-download-release-artifact.sh new file mode 100755 index 000000000000..2acdba9025a0 --- /dev/null +++ b/scripts/termux-download-release-artifact.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${HEAD_SHA:-}" ]]; then + echo "HEAD_SHA is required." >&2 + exit 1 +fi + +if [[ -z "${PR_ARTIFACT_NAME:-}" ]]; then + echo "PR_ARTIFACT_NAME is required." >&2 + exit 1 +fi + +promoted_dir="${PROMOTED_DIR:-promoted}" + +find_run_id() { + local event="$1" + gh run list \ + --repo "${GITHUB_REPOSITORY}" \ + --workflow rust-release.yml \ + --event "${event}" \ + --status success \ + --commit "${HEAD_SHA}" \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId // empty' +} + +find_artifact_run_id() { + local artifact_name="$1" + + gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=${artifact_name}" \ + | jq -rs \ + --arg artifact_name "${artifact_name}" \ + --arg head_sha "${HEAD_SHA}" \ + ' + [ + .[].artifacts[] + | select(.name == $artifact_name) + | select(.expired == false) + | select(.workflow_run.head_sha == $head_sha) + ] + | sort_by(.created_at) + | reverse + | .[0].workflow_run.id // empty + ' +} + +artifact_name="${PR_ARTIFACT_NAME}" +run_id="$(find_artifact_run_id "${artifact_name}")" +mkdir -p "${promoted_dir}" +if [[ -n "${run_id}" ]] && gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}"; then + : +else + artifact_name="aarch64-linux-android" + run_id="$(find_run_id workflow_dispatch)" + if [[ -z "${run_id}" ]]; then + echo "No successful rust-release run found for ${HEAD_SHA}" >&2 + exit 1 + fi + gh run download "${run_id}" \ + --repo "${GITHUB_REPOSITORY}" \ + --name "${artifact_name}" \ + --dir "${promoted_dir}" +fi + +ls -la "${promoted_dir}" +if [[ -f "${promoted_dir}/SHA256SUMS" ]]; then + (cd "${promoted_dir}" && sha256sum -c SHA256SUMS) +fi +if [[ ! -f "${promoted_dir}/codex-aarch64-linux-android.tar.gz" ]]; then + echo "Expected ${promoted_dir}/codex-aarch64-linux-android.tar.gz in the downloaded artifact." >&2 + exit 1 +fi diff --git a/scripts/termux-find-release-pr.sh b/scripts/termux-find-release-pr.sh new file mode 100755 index 000000000000..3e35fa42eabe --- /dev/null +++ b/scripts/termux-find-release-pr.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -n "${INPUT_PR_NUMBER:-}" || -n "${INPUT_PR_HEAD_SHA:-}" ]]; then + if [[ -z "${INPUT_PR_NUMBER:-}" || -z "${INPUT_PR_HEAD_SHA:-}" ]]; then + echo "workflow_dispatch inputs pr_number and pr_head_sha must be provided together." >&2 + exit 1 + fi + + pr_number="${INPUT_PR_NUMBER}" + head_sha="${INPUT_PR_HEAD_SHA}" + head_ref="" +else + if [[ -z "${RELEASE_BRANCH:-}" || -z "${RELEASE_SHA:-}" ]]; then + echo "RELEASE_BRANCH and RELEASE_SHA are required." >&2 + exit 1 + fi + + pr_json="$( + gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --base "${RELEASE_BRANCH}" \ + --state merged \ + --limit 100 \ + --json number,headRefOid,headRefName,mergeCommit \ + --jq "[.[] | select(.mergeCommit.oid == \"${RELEASE_SHA}\")] | sort_by(.number) | reverse | .[0] // empty" + )" + if [[ -z "${pr_json}" ]]; then + echo "Unable to find merged PR for ${RELEASE_SHA} into ${RELEASE_BRANCH}" >&2 + exit 1 + fi + + pr_number="$(jq -r '.number' <<< "${pr_json}")" + head_sha="$(jq -r '.headRefOid // .head.sha' <<< "${pr_json}")" + head_ref="$(jq -r '.headRefName // .head.ref' <<< "${pr_json}")" +fi + +artifact_name="termux-android-pr-${pr_number}-${head_sha}" +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "number=${pr_number}" + echo "head_sha=${head_sha}" + echo "head_ref=${head_ref}" + echo "artifact_name=${artifact_name}" + } >> "${GITHUB_OUTPUT}" +else + printf 'number=%s\nhead_sha=%s\nhead_ref=%s\nartifact_name=%s\n' \ + "${pr_number}" \ + "${head_sha}" \ + "${head_ref}" \ + "${artifact_name}" +fi diff --git a/scripts/termux-read-release-metadata.sh b/scripts/termux-read-release-metadata.sh new file mode 100755 index 000000000000..519a0635a368 --- /dev/null +++ b/scripts/termux-read-release-metadata.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# Parse .github/termux-release.json and emit workflow outputs for deploy or +# promote jobs. TERMUX_RELEASE_ACTION must be "deploy" or "promote". + +set -euo pipefail + +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +metadata=".github/termux-release.json" +action="${TERMUX_RELEASE_ACTION:-deploy}" +case "${action}" in + deploy) + missing_context="deployment" + ;; + promote) + missing_context="promotion" + ;; + *) + echo "TERMUX_RELEASE_ACTION must be deploy or promote." >&2 + exit 1 + ;; +esac + +if [[ ! -f "${metadata}" ]]; then + echo "No ${metadata}; this push is not a Termux release ${missing_context}." + echo "${action}=false" >> "${GITHUB_OUTPUT}" + exit 0 +fi + +upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" +upstream_name="$(jq -r '.upstream_name // .upstream_tag // empty' "${metadata}")" +termux_tag="$(jq -r '.termux_tag // empty' "${metadata}")" +upstream_version="${upstream_tag#rust-v}" +upstream_version="${upstream_version%-termux}" +upstream_prerelease=false +if [[ "${upstream_version}" == *-* ]]; then + upstream_prerelease=true +fi +upstream_html_url="$(jq -r '.upstream_html_url // ""' "${metadata}")" +upstream_repo="$(jq -r '.upstream_repo // "openai/codex"' "${metadata}")" +release_train="$(jq -r '.release_train // ""' "${metadata}")" +if [[ -z "${upstream_tag}" || -z "${termux_tag}" ]]; then + echo "Missing upstream_tag or termux_tag in ${metadata}" >&2 + exit 1 +fi + +release_state="$(TERMUX_TAG="${termux_tag}" "${script_dir}/termux-release-asset-state.sh")" +release_exists="$(awk -F= '$1 == "release_exists" { print $2 }' <<< "${release_state}")" +asset_exists="$(awk -F= '$1 == "asset_exists" { print $2 }' <<< "${release_state}")" + +if [[ "${action}" == "promote" ]]; then + if [[ "${asset_exists}" == "true" ]]; then + echo "${termux_tag} already exists with codex-aarch64-linux-android.tar.gz; skipping promotion." + echo "promote=false" >> "${GITHUB_OUTPUT}" + exit 0 + fi + if [[ "${release_exists}" == "true" ]]; then + echo "${termux_tag} exists but is missing codex-aarch64-linux-android.tar.gz; repairing promotion." + fi +fi + +{ + echo "${action}=true" + echo "upstream_tag=${upstream_tag}" + echo "upstream_name=${upstream_name}" + echo "termux_tag=${termux_tag}" + echo "upstream_prerelease=${upstream_prerelease}" + echo "upstream_html_url=${upstream_html_url}" + echo "upstream_repo=${upstream_repo}" + echo "release_train=${release_train}" + echo "release_exists=${release_exists}" + echo "asset_exists=${asset_exists}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-release-asset-state.sh b/scripts/termux-release-asset-state.sh new file mode 100755 index 000000000000..8694480b6a36 --- /dev/null +++ b/scripts/termux-release-asset-state.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +termux_tag="${1:-${TERMUX_TAG:-}}" +release_repo="${RELEASE_REPO:-${GITHUB_REPOSITORY:-}}" +asset_name="${ASSET_NAME:-codex-aarch64-linux-android.tar.gz}" + +if [[ -z "${termux_tag}" ]]; then + echo "TERMUX_TAG or tag argument is required." >&2 + exit 1 +fi + +if [[ -z "${release_repo}" ]]; then + echo "GITHUB_REPOSITORY or RELEASE_REPO is required." >&2 + exit 1 +fi + +release_exists=false +asset_exists=false + +if gh release view "${termux_tag}" --repo "${release_repo}" >/dev/null 2>&1; then + release_exists=true + release_asset_exists="$( + gh release view "${termux_tag}" \ + --repo "${release_repo}" \ + --json assets \ + | jq -r --arg asset_name "${asset_name}" \ + '.assets | map(.name) | any(. == $asset_name)' + )" + if [[ "${release_asset_exists}" == "true" ]]; then + asset_exists=true + fi +fi + +printf 'release_exists=%s\n' "${release_exists}" +printf 'asset_exists=%s\n' "${asset_exists}" diff --git a/scripts/termux-release-paths.sh b/scripts/termux-release-paths.sh new file mode 100755 index 000000000000..6990f323ec44 --- /dev/null +++ b/scripts/termux-release-paths.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# Sourceable path lists for files owned by the Termux release automation. + +set -euo pipefail + +readonly -a TERMUX_RELEASE_WORKFLOW_PATHS=( + .github/workflows/rust-release.yml + .github/workflows/shell-tool-mcp.yml + .github/workflows/termux-release-checkpoint.yml + .github/workflows/termux-release-deploy.yml + .github/workflows/termux-release-promote.yml +) + +readonly -a TERMUX_RELEASE_BRANCH_SCRIPT_PATHS=( + scripts/termux-configure-git.sh + scripts/termux-create-checkpoint-pr.sh + scripts/termux-create-or-update-mirrored-release.sh + scripts/termux-download-release-artifact.sh + scripts/termux-find-release-pr.sh + scripts/termux-read-release-metadata.sh + scripts/termux-release-asset-state.sh + scripts/termux-release-paths.sh + scripts/termux-resolve-release-ref.sh + scripts/termux-validate-gh-env.sh +) + +readonly -a TERMUX_RELEASE_AUTOMATION_PATHS=( + "${TERMUX_RELEASE_WORKFLOW_PATHS[@]}" + "${TERMUX_RELEASE_BRANCH_SCRIPT_PATHS[@]}" +) + +readonly -a TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS=( + "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" + .github/termux-release.json +) + +termux_path_in_list() { + local candidate="$1" + shift + local listed_path + + for listed_path in "$@"; do + [[ "${candidate}" != "${listed_path}" ]] || return 0 + done + return 1 +} + +termux_is_release_automation_path() { + termux_path_in_list "$1" "${TERMUX_RELEASE_AUTOMATION_PATHS[@]}" +} + +termux_is_checkpoint_release_only_path() { + termux_path_in_list "$1" "${TERMUX_CHECKPOINT_RELEASE_ONLY_PATHS[@]}" +} diff --git a/scripts/termux-resolve-release-ref.sh b/scripts/termux-resolve-release-ref.sh new file mode 100755 index 000000000000..194773d45bed --- /dev/null +++ b/scripts/termux-resolve-release-ref.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Resolve the release branch and SHA for deploy/promote workflows. + +set -euo pipefail + +: "${GITHUB_EVENT_NAME:?GITHUB_EVENT_NAME is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" + +input_release_branch="${INPUT_RELEASE_BRANCH:-${REQUESTED_RELEASE_BRANCH:-}}" +input_release_sha="${INPUT_RELEASE_SHA:-${REQUESTED_RELEASE_SHA:-}}" + +if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then + if [[ -z "${input_release_branch}" ]]; then + echo "release_branch is required for workflow_dispatch." >&2 + exit 1 + fi + + release_branch="${input_release_branch}" + if [[ -n "${input_release_sha}" ]]; then + git checkout --detach "${input_release_sha}" + release_sha="${input_release_sha}" + else + release_sha="$(git rev-parse HEAD)" + fi +else + : "${GITHUB_REF_NAME:?GITHUB_REF_NAME is required}" + : "${GITHUB_SHA:?GITHUB_SHA is required}" + release_branch="${GITHUB_REF_NAME}" + release_sha="${GITHUB_SHA}" +fi + +{ + echo "branch=${release_branch}" + echo "sha=${release_sha}" +} >> "${GITHUB_OUTPUT}" diff --git a/scripts/termux-validate-gh-env.sh b/scripts/termux-validate-gh-env.sh new file mode 100755 index 000000000000..02581fdebceb --- /dev/null +++ b/scripts/termux-validate-gh-env.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Lightweight post-toolbox check for jobs that rely on authenticated gh calls. + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +command -v gh +gh auth status --hostname github.com + +printf 'GITHUB_REPOSITORY=%s\n' "${GITHUB_REPOSITORY}" +printf 'REPO=%s\n' "${REPO:-}" +printf 'GH_REPO_URL=%s\n' "${GH_REPO_URL:-}" +printf 'GH_WORKFLOW_URL=%s\n' "${GH_WORKFLOW_URL:-}"