Auto-upgrade configured marketplaces (#17425)

## Summary
- Add best-effort auto-upgrade for user-configured Git marketplaces
recorded in `config.toml`.
- Track the last activated Git revision with `last_revision` so
unchanged marketplace sources skip clone work.
- Trigger the upgrade from plugin startup and `plugin/list`, while
preserving existing fail-open plugin behavior with warning logs rather
than new user-visible errors.

## Details
- Remote configured marketplaces use `git ls-remote` to compare the
source/ref against the recorded revision.
- Upgrades clone into a staging directory, validate that
`.agents/plugins/marketplace.json` exists and that the manifest name
matches the configured marketplace key, then atomically activate the new
root.
- Local `.agents/plugins/marketplace.json` marketplaces remain live
filesystem state and are not auto-pulled.
- Existing non-curated plugin cache refresh is kicked after successful
marketplace root upgrades.

## Validation
- `just write-config-schema`
- `cargo test -p codex-core marketplace_upgrade`
- `cargo check -p codex-cli -p codex-app-server`
- `just fix -p codex-core`

Did not run the complete `cargo test` suite because the repo
instructions require asking before a full core workspace run.
This commit is contained in:
xli-oai
2026-04-16 10:36:34 -07:00
committed by GitHub
Unverified
parent 109b22a8d0
commit faf48489f3
17 changed files with 1094 additions and 34 deletions
+75 -6
View File
@@ -1,8 +1,12 @@
use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use clap::Parser;
use codex_core::config::Config;
use codex_core::config::find_codex_home;
use codex_core::plugins::MarketplaceAddRequest;
use codex_core::plugins::PluginMarketplaceUpgradeOutcome;
use codex_core::plugins::PluginsManager;
use codex_core::plugins::add_marketplace;
use codex_utils_cli::CliConfigOverrides;
@@ -17,8 +21,8 @@ pub struct MarketplaceCli {
#[derive(Debug, clap::Subcommand)]
enum MarketplaceSubcommand {
/// Add a remote marketplace repository.
Add(AddMarketplaceArgs),
Upgrade(UpgradeMarketplaceArgs),
}
#[derive(Debug, Parser)]
@@ -27,11 +31,9 @@ struct AddMarketplaceArgs {
/// or local marketplace root directories.
source: String,
/// Git ref to check out. Overrides any @ref or #ref suffix in SOURCE.
#[arg(long = "ref", value_name = "REF")]
ref_name: Option<String>,
/// Sparse-checkout path to use while cloning git sources. Repeat to include multiple paths.
#[arg(
long = "sparse",
value_name = "PATH",
@@ -40,6 +42,11 @@ struct AddMarketplaceArgs {
sparse_paths: Vec<String>,
}
#[derive(Debug, Parser)]
struct UpgradeMarketplaceArgs {
marketplace_name: Option<String>,
}
impl MarketplaceCli {
pub async fn run(self) -> Result<()> {
let MarketplaceCli {
@@ -47,14 +54,13 @@ impl MarketplaceCli {
subcommand,
} = self;
// Validate overrides now. This command writes to CODEX_HOME only; marketplace discovery
// happens from that cache root after the next plugin/list or app-server start.
config_overrides
let overrides = config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
match subcommand {
MarketplaceSubcommand::Add(args) => run_add(args).await?,
MarketplaceSubcommand::Upgrade(args) => run_upgrade(overrides, args).await?,
}
Ok(())
@@ -98,6 +104,60 @@ async fn run_add(args: AddMarketplaceArgs) -> Result<()> {
Ok(())
}
async fn run_upgrade(
overrides: Vec<(String, toml::Value)>,
args: UpgradeMarketplaceArgs,
) -> Result<()> {
let UpgradeMarketplaceArgs { marketplace_name } = args;
let config = Config::load_with_cli_overrides(overrides)
.await
.context("failed to load configuration")?;
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
let manager = PluginsManager::new(codex_home.to_path_buf());
let outcome = manager
.upgrade_configured_marketplaces_for_config(&config, marketplace_name.as_deref())
.map_err(anyhow::Error::msg)?;
print_upgrade_outcome(&outcome, marketplace_name.as_deref())
}
fn print_upgrade_outcome(
outcome: &PluginMarketplaceUpgradeOutcome,
marketplace_name: Option<&str>,
) -> Result<()> {
for error in &outcome.errors {
eprintln!(
"Failed to upgrade marketplace `{}`: {}",
error.marketplace_name, error.message
);
}
if !outcome.all_succeeded() {
bail!("{} upgrade failure(s) occurred.", outcome.errors.len());
}
let selection_label = marketplace_name.unwrap_or("all configured Git marketplaces");
if outcome.selected_marketplaces.is_empty() {
println!("No configured Git marketplaces to upgrade.");
} else if outcome.upgraded_roots.is_empty() {
if marketplace_name.is_some() {
println!("Marketplace `{selection_label}` is already up to date.");
} else {
println!("All configured Git marketplaces are already up to date.");
}
} else if marketplace_name.is_some() {
println!("Upgraded marketplace `{selection_label}` to the latest configured revision.");
for root in &outcome.upgraded_roots {
println!("Installed marketplace root: {}", root.display());
}
} else {
println!("Upgraded {} marketplace(s).", outcome.upgraded_roots.len());
for root in &outcome.upgraded_roots {
println!("Installed marketplace root: {}", root.display());
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -132,4 +192,13 @@ mod tests {
vec!["plugins/foo", "skills/bar"]
);
}
#[test]
fn upgrade_subcommand_parses_optional_marketplace_name() {
let upgrade_all = UpgradeMarketplaceArgs::try_parse_from(["upgrade"]).unwrap();
assert_eq!(upgrade_all.marketplace_name, None);
let upgrade_one = UpgradeMarketplaceArgs::try_parse_from(["upgrade", "debug"]).unwrap();
assert_eq!(upgrade_one.marketplace_name.as_deref(), Some("debug"));
}
}