Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions codex-rs/core-plugins/src/startup_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const OPENAI_PLUGINS_GIT_URL: &str = "https://github.com/openai/plugins.git";
const CURATED_PLUGINS_FETCH_REF: &str = "refs/codex/curated-sync";
const CURATED_PLUGINS_RELATIVE_DIR: &str = ".tmp/plugins";
const CURATED_PLUGINS_SHA_FILE: &str = ".tmp/plugins.sha";
const CURATED_PLUGINS_CATALOG_REVISION_FILE: &str = ".codex-plugin-catalog-revision";
const CURATED_PLUGINS_SYNC_LOCK_FILE: &str = ".tmp/plugins.sync.lock";
const CURATED_PLUGINS_BACKUP_ARCHIVE_FALLBACK_VERSION: &str = "export-backup";
const CURATED_PLUGINS_GIT_TIMEOUT: Duration = Duration::from_secs(30);
Expand Down Expand Up @@ -67,6 +68,10 @@ fn curated_plugins_sha_path(codex_home: &Path) -> PathBuf {
codex_home.join(CURATED_PLUGINS_SHA_FILE)
}

pub(crate) fn curated_plugins_catalog_revision_path(repo_path: &Path) -> PathBuf {
repo_path.join(CURATED_PLUGINS_CATALOG_REVISION_FILE)
}

pub fn sync_openai_plugins_repo(codex_home: &Path) -> Result<String, String> {
sync_openai_plugins_repo_with_transport_overrides(
codex_home,
Expand Down Expand Up @@ -164,6 +169,7 @@ fn sync_openai_plugins_repo_via_git(codex_home: &Path, git_binary: &str) -> Resu
let local_sha = read_local_git_or_sha_file(&repo_path, &sha_path, git_binary);

if local_sha.as_deref() == Some(remote_sha.as_str()) && repo_path.join(".git").is_dir() {
ensure_curated_plugins_catalog_revision(&repo_path, &remote_sha)?;
return Ok(remote_sha);
}

Expand Down Expand Up @@ -196,6 +202,7 @@ fn sync_openai_plugins_repo_via_git(codex_home: &Path, git_binary: &str) -> Resu
}

ensure_marketplace_manifest_exists(staged_repo_dir.path())?;
write_curated_plugins_catalog_revision(staged_repo_dir.path(), &remote_sha)?;
activate_curated_repo(&repo_path, staged_repo_dir)?;
write_curated_plugins_sha(&sha_path, &remote_sha)?;
Ok(remote_sha)
Expand Down Expand Up @@ -299,13 +306,15 @@ fn sync_openai_plugins_repo_via_http(
let local_sha = read_sha_file(&sha_path);

if local_sha.as_deref() == Some(remote_sha.as_str()) && repo_path.is_dir() {
ensure_curated_plugins_catalog_revision(&repo_path, &remote_sha)?;
return Ok(remote_sha);
}

let staged_repo_dir = prepare_curated_repo_parent_and_temp_dir(&repo_path)?;
let zipball_bytes = runtime.block_on(fetch_curated_repo_zipball(api_base_url, &remote_sha))?;
extract_zipball_to_dir(&zipball_bytes, staged_repo_dir.path())?;
ensure_marketplace_manifest_exists(staged_repo_dir.path())?;
write_curated_plugins_catalog_revision(staged_repo_dir.path(), &remote_sha)?;
activate_curated_repo(&repo_path, staged_repo_dir)?;
write_curated_plugins_sha(&sha_path, &remote_sha)?;
Ok(remote_sha)
Expand All @@ -329,6 +338,7 @@ fn sync_openai_plugins_repo_via_backup_archive(
ensure_marketplace_manifest_exists(staged_repo_dir.path())?;
let export_version = read_extracted_backup_archive_git_sha(staged_repo_dir.path())?
.unwrap_or_else(|| CURATED_PLUGINS_BACKUP_ARCHIVE_FALLBACK_VERSION.to_string());
write_curated_plugins_catalog_revision(staged_repo_dir.path(), &export_version)?;
activate_curated_repo(&repo_path, staged_repo_dir)?;
write_curated_plugins_sha(&sha_path, &export_version)?;
Ok(export_version)
Expand Down Expand Up @@ -564,6 +574,24 @@ fn write_curated_plugins_sha(sha_path: &Path, remote_sha: &str) -> Result<(), St
})
}

fn ensure_curated_plugins_catalog_revision(repo_path: &Path, revision: &str) -> Result<(), String> {
let revision_path = curated_plugins_catalog_revision_path(repo_path);
if read_sha_file(&revision_path).as_deref() == Some(revision) {
return Ok(());
}
write_curated_plugins_catalog_revision(repo_path, revision)
}

fn write_curated_plugins_catalog_revision(repo_path: &Path, revision: &str) -> Result<(), String> {
let revision_path = curated_plugins_catalog_revision_path(repo_path);
std::fs::write(&revision_path, format!("{revision}\n")).map_err(|err| {
format!(
"failed to write curated plugins catalog revision file {}: {err}",
revision_path.display()
)
})
}

fn read_local_git_or_sha_file(
repo_path: &Path,
sha_path: &Path,
Expand Down
22 changes: 21 additions & 1 deletion codex-rs/core-plugins/src/startup_sync_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ fn write_file(path: &Path, contents: &str) {
std::fs::write(path, contents).unwrap();
}

fn assert_curated_catalog_revision(repo_path: &Path, expected_revision: &str) {
assert_eq!(
std::fs::read_to_string(curated_plugins_catalog_revision_path(repo_path))
.expect("read catalog revision"),
format!("{expected_revision}\n")
);
}

fn write_curated_plugin(root: &Path, plugin_name: &str) {
let plugin_root = root.join("plugins").join(plugin_name);
write_file(
Expand Down Expand Up @@ -343,6 +351,7 @@ exit 1
let repo_path = curated_plugins_repo_path(tmp.path());
assert!(repo_path.join(".git").is_dir());
assert_curated_gmail_repo(&repo_path);
assert_curated_catalog_revision(&repo_path, sha);
assert_eq!(read_curated_plugins_sha(tmp.path()).as_deref(), Some(sha));
let invocations = std::fs::read_to_string(invocation_log).expect("read invocation log");
assert_eq!(
Expand Down Expand Up @@ -451,7 +460,9 @@ fn sync_openai_plugins_repo_via_git_succeeds_with_local_rewritten_remote() {
.expect("git sync should succeed");

assert_eq!(synced_sha, sha);
assert_curated_gmail_repo(&curated_plugins_repo_path(tmp.path()));
let curated_repo_path = curated_plugins_repo_path(tmp.path());
assert_curated_gmail_repo(&curated_repo_path);
assert_curated_catalog_revision(&curated_repo_path, &sha);
assert_eq!(
read_curated_plugins_sha(tmp.path()).as_deref(),
Some(sha.as_str())
Expand Down Expand Up @@ -505,6 +516,7 @@ fn sync_openai_plugins_repo_via_git_succeeds_with_local_rewritten_remote() {
.expect("incremental git sync should succeed");

assert_eq!(synced_sha, updated_sha);
assert_curated_catalog_revision(&curated_plugins_repo_path(tmp.path()), &updated_sha);
assert!(
curated_plugins_repo_path(tmp.path())
.join("plugins/linear/.codex-plugin/plugin.json")
Expand Down Expand Up @@ -556,11 +568,16 @@ fn sync_openai_plugins_repo_via_git_succeeds_with_local_rewritten_remote() {
assert!(!has_plugins_clone_dirs(tmp.path()));

let unchanged_sync_invocation_count = invocation_log_contents.lines().count();
std::fs::remove_file(curated_plugins_catalog_revision_path(
&curated_plugins_repo_path(tmp.path()),
))
.expect("remove catalog revision before unchanged sync");
let synced_sha =
sync_openai_plugins_repo_via_git(tmp.path(), git_wrapper.to_str().expect("utf8 path"))
.expect("unchanged git sync should succeed");

assert_eq!(synced_sha, updated_sha);
assert_curated_catalog_revision(&curated_plugins_repo_path(tmp.path()), &updated_sha);
let invocation_log = std::fs::read_to_string(&invocation_log).expect("read sync invocations");
let unchanged_sync_invocations = invocation_log
.lines()
Expand Down Expand Up @@ -599,6 +616,7 @@ async fn sync_openai_plugins_repo_falls_back_to_http_when_git_is_unavailable() {
let repo_path = curated_plugins_repo_path(tmp.path());
assert_eq!(synced_sha, sha);
assert_curated_gmail_repo(&repo_path);
assert_curated_catalog_revision(&repo_path, sha);
assert_eq!(read_curated_plugins_sha(tmp.path()).as_deref(), Some(sha));
}

Expand Down Expand Up @@ -796,6 +814,7 @@ async fn sync_openai_plugins_repo_skips_archive_download_when_sha_matches() {

assert_eq!(read_curated_plugins_sha(tmp.path()).as_deref(), Some(sha));
assert!(repo_path.join(".agents/plugins/marketplace.json").is_file());
assert_curated_catalog_revision(&repo_path, sha);
}

#[tokio::test]
Expand Down Expand Up @@ -824,6 +843,7 @@ async fn sync_openai_plugins_repo_falls_back_to_export_archive_when_no_snapshot_
let repo_path = curated_plugins_repo_path(tmp.path());
assert_eq!(synced_sha, export_sha);
assert_curated_gmail_repo(&repo_path);
assert_curated_catalog_revision(&repo_path, export_sha);
assert_eq!(
read_curated_plugins_sha(tmp.path()).as_deref(),
Some(export_sha)
Expand Down
Loading