Add curated plugin catalog revision markers

This commit is contained in:
Matthew Zeng
2026-06-15 21:47:13 -07:00
parent 46f17930b6
commit f0ce10bbf3
2 changed files with 49 additions and 1 deletions
+28
View File
@@ -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);
@@ -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,
@@ -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);
}
@@ -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)
@@ -299,6 +306,7 @@ 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);
}
@@ -306,6 +314,7 @@ fn sync_openai_plugins_repo_via_http(
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)
@@ -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)
@@ -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,
@@ -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(
@@ -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!(
@@ -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())
@@ -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")
@@ -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()
@@ -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));
}
@@ -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]
@@ -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)