diff options
Diffstat (limited to 'release/src/main.rs')
| -rw-r--r-- | release/src/main.rs | 519 |
1 files changed, 519 insertions, 0 deletions
diff --git a/release/src/main.rs b/release/src/main.rs new file mode 100644 index 000000000..38bb728a8 --- /dev/null +++ b/release/src/main.rs | |||
| @@ -0,0 +1,519 @@ | |||
| 1 | use std::collections::{BTreeMap, HashMap}; | ||
| 2 | use std::fs; | ||
| 3 | use std::path::{Path, PathBuf}; | ||
| 4 | use std::process::Command as ProcessCommand; | ||
| 5 | |||
| 6 | use anyhow::{anyhow, Result}; | ||
| 7 | use clap::{Parser, Subcommand}; | ||
| 8 | use petgraph::graph::{Graph, NodeIndex}; | ||
| 9 | use petgraph::visit::Bfs; | ||
| 10 | use petgraph::{Directed, Direction}; | ||
| 11 | use serde::Deserialize; | ||
| 12 | use toml_edit::{DocumentMut, Item, Value}; | ||
| 13 | |||
| 14 | /// Tool to traverse and operate on intra-repo Rust crate dependencies | ||
| 15 | #[derive(Parser, Debug)] | ||
| 16 | #[command(author, version, about)] | ||
| 17 | struct Args { | ||
| 18 | /// Path to embassy repository | ||
| 19 | #[arg(short, long)] | ||
| 20 | repo: PathBuf, | ||
| 21 | |||
| 22 | /// Command to perform on each crate | ||
| 23 | #[command(subcommand)] | ||
| 24 | command: Command, | ||
| 25 | } | ||
| 26 | |||
| 27 | #[derive(Debug, Subcommand)] | ||
| 28 | enum Command { | ||
| 29 | /// All crates and their direct dependencies | ||
| 30 | List, | ||
| 31 | /// List all dependencies for a crate | ||
| 32 | Dependencies { | ||
| 33 | /// Crate name to print dependencies for. | ||
| 34 | #[arg(value_name = "CRATE")] | ||
| 35 | crate_name: String, | ||
| 36 | }, | ||
| 37 | /// List all dependencies for a crate | ||
| 38 | Dependents { | ||
| 39 | /// Crate name to print dependencies for. | ||
| 40 | #[arg(value_name = "CRATE")] | ||
| 41 | crate_name: String, | ||
| 42 | }, | ||
| 43 | |||
| 44 | /// SemverCheck | ||
| 45 | SemverCheck { | ||
| 46 | /// Crate to check. Will traverse that crate an it's dependents. If not specified checks all crates. | ||
| 47 | #[arg(value_name = "CRATE")] | ||
| 48 | crate_name: String, | ||
| 49 | }, | ||
| 50 | /// Prepare to release a crate and all dependents that needs updating | ||
| 51 | /// - Semver checks | ||
| 52 | /// - Bump versions and commit | ||
| 53 | /// - Create tag. | ||
| 54 | PrepareRelease { | ||
| 55 | /// Crate to release. Will traverse that crate an it's dependents. If not specified checks all crates. | ||
| 56 | #[arg(value_name = "CRATE")] | ||
| 57 | crate_name: String, | ||
| 58 | }, | ||
| 59 | } | ||
| 60 | |||
| 61 | #[derive(Debug, Subcommand, Clone, Copy, PartialEq)] | ||
| 62 | enum ReleaseKind { | ||
| 63 | Patch, | ||
| 64 | Minor, | ||
| 65 | } | ||
| 66 | |||
| 67 | #[derive(Debug, Deserialize)] | ||
| 68 | struct CargoToml { | ||
| 69 | package: Option<Package>, | ||
| 70 | dependencies: Option<Deps>, | ||
| 71 | } | ||
| 72 | |||
| 73 | #[derive(Debug, Deserialize)] | ||
| 74 | struct Package { | ||
| 75 | name: String, | ||
| 76 | version: Option<String>, | ||
| 77 | } | ||
| 78 | |||
| 79 | #[derive(Debug, Deserialize)] | ||
| 80 | #[serde(untagged)] | ||
| 81 | enum Dep { | ||
| 82 | Version(String), | ||
| 83 | DetailedTable(BTreeMap<String, toml::Value>), | ||
| 84 | } | ||
| 85 | |||
| 86 | type Deps = std::collections::BTreeMap<String, Dep>; | ||
| 87 | |||
| 88 | #[derive(Debug, Clone, Deserialize)] | ||
| 89 | struct CrateConfig { | ||
| 90 | features: Option<Vec<String>>, | ||
| 91 | target: Option<String>, | ||
| 92 | } | ||
| 93 | |||
| 94 | type ReleaseConfig = HashMap<String, CrateConfig>; | ||
| 95 | |||
| 96 | fn load_release_config(repo: &Path) -> ReleaseConfig { | ||
| 97 | let config_path = repo.join("release/config.toml"); | ||
| 98 | if !config_path.exists() { | ||
| 99 | return HashMap::new(); | ||
| 100 | } | ||
| 101 | let content = fs::read_to_string(&config_path).expect("Failed to read release/config.toml"); | ||
| 102 | toml::from_str(&content).expect("Invalid TOML format in release/config.toml") | ||
| 103 | } | ||
| 104 | |||
| 105 | fn update_version(c: &mut Crate, new_version: &str) -> Result<()> { | ||
| 106 | let path = &c.path; | ||
| 107 | c.id.version = new_version.to_string(); | ||
| 108 | let content = fs::read_to_string(&path)?; | ||
| 109 | let mut doc: DocumentMut = content.parse()?; | ||
| 110 | for section in ["package"] { | ||
| 111 | if let Some(Item::Table(dep_table)) = doc.get_mut(section) { | ||
| 112 | dep_table.insert("version", Item::Value(Value::from(new_version))); | ||
| 113 | } | ||
| 114 | } | ||
| 115 | fs::write(&path, doc.to_string())?; | ||
| 116 | Ok(()) | ||
| 117 | } | ||
| 118 | |||
| 119 | fn update_versions(to_update: &Crate, dep: &CrateId, new_version: &str) -> Result<()> { | ||
| 120 | let path = &to_update.path; | ||
| 121 | let content = fs::read_to_string(&path)?; | ||
| 122 | let mut doc: DocumentMut = content.parse()?; | ||
| 123 | let mut changed = false; | ||
| 124 | for section in ["dependencies", "dev-dependencies", "build-dependencies"] { | ||
| 125 | if let Some(Item::Table(dep_table)) = doc.get_mut(section) { | ||
| 126 | if let Some(item) = dep_table.get_mut(&dep.name) { | ||
| 127 | match item { | ||
| 128 | // e.g., foo = "0.1.0" | ||
| 129 | Item::Value(Value::String(_)) => { | ||
| 130 | *item = Item::Value(Value::from(new_version)); | ||
| 131 | changed = true; | ||
| 132 | } | ||
| 133 | // e.g., foo = { version = "...", ... } | ||
| 134 | Item::Value(Value::InlineTable(inline)) => { | ||
| 135 | if inline.contains_key("version") { | ||
| 136 | inline["version"] = Value::from(new_version); | ||
| 137 | changed = true; | ||
| 138 | } | ||
| 139 | } | ||
| 140 | _ => {} // Leave unusual formats untouched | ||
| 141 | } | ||
| 142 | } | ||
| 143 | } | ||
| 144 | } | ||
| 145 | |||
| 146 | if changed { | ||
| 147 | fs::write(&path, doc.to_string())?; | ||
| 148 | println!("🔧 Updated {} to {} in {}", dep.name, new_version, path.display()); | ||
| 149 | } | ||
| 150 | Ok(()) | ||
| 151 | } | ||
| 152 | |||
| 153 | #[derive(Debug, Clone)] | ||
| 154 | struct Crate { | ||
| 155 | id: CrateId, | ||
| 156 | path: PathBuf, | ||
| 157 | config: CrateConfig, | ||
| 158 | dependencies: Vec<CrateId>, | ||
| 159 | } | ||
| 160 | |||
| 161 | #[derive(Debug, Clone, PartialOrd, Ord)] | ||
| 162 | struct CrateId { | ||
| 163 | name: String, | ||
| 164 | version: String, | ||
| 165 | } | ||
| 166 | |||
| 167 | impl PartialEq for CrateId { | ||
| 168 | fn eq(&self, other: &CrateId) -> bool { | ||
| 169 | self.name == other.name | ||
| 170 | } | ||
| 171 | } | ||
| 172 | |||
| 173 | impl Eq for CrateId {} | ||
| 174 | impl std::hash::Hash for CrateId { | ||
| 175 | fn hash<H: std::hash::Hasher>(&self, state: &mut H) { | ||
| 176 | self.name.hash(state) | ||
| 177 | } | ||
| 178 | } | ||
| 179 | |||
| 180 | fn list_crates(path: &PathBuf) -> Result<BTreeMap<CrateId, Crate>> { | ||
| 181 | let d = std::fs::read_dir(path)?; | ||
| 182 | let release_config = load_release_config(path); | ||
| 183 | let mut crates = BTreeMap::new(); | ||
| 184 | for c in d { | ||
| 185 | let entry = c?; | ||
| 186 | let name = entry.file_name().to_str().unwrap().to_string(); | ||
| 187 | if entry.file_type()?.is_dir() && name.starts_with("embassy-") { | ||
| 188 | let entry = entry.path().join("Cargo.toml"); | ||
| 189 | if entry.exists() { | ||
| 190 | let content = fs::read_to_string(&entry).unwrap_or_else(|_| { | ||
| 191 | panic!("Failed to read {:?}", entry); | ||
| 192 | }); | ||
| 193 | let parsed: CargoToml = toml::from_str(&content).unwrap_or_else(|e| { | ||
| 194 | panic!("Failed to parse {:?}: {}", entry, e); | ||
| 195 | }); | ||
| 196 | let p = parsed.package.unwrap(); | ||
| 197 | let id = CrateId { | ||
| 198 | name: p.name.clone(), | ||
| 199 | version: p.version.unwrap(), | ||
| 200 | }; | ||
| 201 | |||
| 202 | let mut dependencies = Vec::new(); | ||
| 203 | if let Some(deps) = parsed.dependencies { | ||
| 204 | for (k, v) in deps { | ||
| 205 | if k.starts_with("embassy-") { | ||
| 206 | dependencies.push(CrateId { | ||
| 207 | name: k, | ||
| 208 | version: match v { | ||
| 209 | Dep::Version(v) => v, | ||
| 210 | Dep::DetailedTable(table) => { | ||
| 211 | table.get("version").unwrap().as_str().unwrap().to_string() | ||
| 212 | } | ||
| 213 | }, | ||
| 214 | }); | ||
| 215 | } | ||
| 216 | } | ||
| 217 | } | ||
| 218 | |||
| 219 | let path = path.join(entry); | ||
| 220 | if let Some(config) = release_config.get(&p.name) { | ||
| 221 | crates.insert( | ||
| 222 | id.clone(), | ||
| 223 | Crate { | ||
| 224 | id, | ||
| 225 | path, | ||
| 226 | dependencies, | ||
| 227 | config: config.clone(), | ||
| 228 | }, | ||
| 229 | ); | ||
| 230 | } | ||
| 231 | } | ||
| 232 | } | ||
| 233 | } | ||
| 234 | Ok(crates) | ||
| 235 | } | ||
| 236 | |||
| 237 | fn build_graph(crates: &BTreeMap<CrateId, Crate>) -> (Graph<CrateId, ()>, HashMap<CrateId, NodeIndex>) { | ||
| 238 | let mut graph = Graph::<CrateId, (), Directed>::new(); | ||
| 239 | let mut node_indices: HashMap<CrateId, NodeIndex> = HashMap::new(); | ||
| 240 | |||
| 241 | // Helper to insert or get existing node | ||
| 242 | let get_or_insert_node = |id: CrateId, graph: &mut Graph<CrateId, ()>, map: &mut HashMap<CrateId, NodeIndex>| { | ||
| 243 | if let Some(&idx) = map.get(&id) { | ||
| 244 | idx | ||
| 245 | } else { | ||
| 246 | let idx = graph.add_node(id.clone()); | ||
| 247 | map.insert(id, idx); | ||
| 248 | idx | ||
| 249 | } | ||
| 250 | }; | ||
| 251 | |||
| 252 | for krate in crates.values() { | ||
| 253 | get_or_insert_node(krate.id.clone(), &mut graph, &mut node_indices); | ||
| 254 | } | ||
| 255 | |||
| 256 | for krate in crates.values() { | ||
| 257 | // Insert crate node if not exists | ||
| 258 | let crate_idx = get_or_insert_node(krate.id.clone(), &mut graph, &mut node_indices); | ||
| 259 | |||
| 260 | // Insert dependencies and connect edges | ||
| 261 | for dep in krate.dependencies.iter() { | ||
| 262 | let dep_idx = get_or_insert_node(dep.clone(), &mut graph, &mut node_indices); | ||
| 263 | graph.add_edge(crate_idx, dep_idx, ()); | ||
| 264 | } | ||
| 265 | } | ||
| 266 | |||
| 267 | (graph, node_indices) | ||
| 268 | } | ||
| 269 | |||
| 270 | fn main() -> Result<()> { | ||
| 271 | let args = Args::parse(); | ||
| 272 | |||
| 273 | let root = args.repo.canonicalize()?; //.expect("Invalid root crate path"); | ||
| 274 | let mut crates = list_crates(&root)?; | ||
| 275 | //println!("Crates: {:?}", crates); | ||
| 276 | let (mut graph, indices) = build_graph(&crates); | ||
| 277 | |||
| 278 | // use petgraph::dot::{Config, Dot}; | ||
| 279 | // println!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel])); | ||
| 280 | |||
| 281 | match args.command { | ||
| 282 | Command::List => { | ||
| 283 | let ordered = petgraph::algo::toposort(&graph, None).unwrap(); | ||
| 284 | for node in ordered.iter() { | ||
| 285 | if graph.neighbors_directed(*node, Direction::Incoming).count() == 0 { | ||
| 286 | let start = graph.node_weight(*node).unwrap(); | ||
| 287 | let mut bfs = Bfs::new(&graph, *node); | ||
| 288 | while let Some(node) = bfs.next(&graph) { | ||
| 289 | let weight = graph.node_weight(node).unwrap(); | ||
| 290 | if weight.name == start.name { | ||
| 291 | println!("+ {}-{}", weight.name, weight.version); | ||
| 292 | } else { | ||
| 293 | println!("|- {}-{}", weight.name, weight.version); | ||
| 294 | } | ||
| 295 | } | ||
| 296 | println!(""); | ||
| 297 | } | ||
| 298 | } | ||
| 299 | } | ||
| 300 | Command::Dependencies { crate_name } => { | ||
| 301 | let idx = indices | ||
| 302 | .get(&CrateId { | ||
| 303 | name: crate_name.clone(), | ||
| 304 | version: "".to_string(), | ||
| 305 | }) | ||
| 306 | .expect("unable to find crate in tree"); | ||
| 307 | let mut bfs = Bfs::new(&graph, *idx); | ||
| 308 | while let Some(node) = bfs.next(&graph) { | ||
| 309 | let weight = graph.node_weight(node).unwrap(); | ||
| 310 | if weight.name == crate_name { | ||
| 311 | println!("+ {}-{}", weight.name, weight.version); | ||
| 312 | } else { | ||
| 313 | println!("|- {}-{}", weight.name, weight.version); | ||
| 314 | } | ||
| 315 | } | ||
| 316 | } | ||
| 317 | Command::Dependents { crate_name } => { | ||
| 318 | let idx = indices | ||
| 319 | .get(&CrateId { | ||
| 320 | name: crate_name.clone(), | ||
| 321 | version: "".to_string(), | ||
| 322 | }) | ||
| 323 | .expect("unable to find crate in tree"); | ||
| 324 | let node = graph.node_weight(*idx).unwrap(); | ||
| 325 | println!("+ {}-{}", node.name, node.version); | ||
| 326 | for parent in graph.neighbors_directed(*idx, Direction::Incoming) { | ||
| 327 | let weight = graph.node_weight(parent).unwrap(); | ||
| 328 | println!("|- {}-{}", weight.name, weight.version); | ||
| 329 | } | ||
| 330 | } | ||
| 331 | Command::SemverCheck { crate_name } => { | ||
| 332 | let c = crates | ||
| 333 | .get(&CrateId { | ||
| 334 | name: crate_name.to_string(), | ||
| 335 | version: "".to_string(), | ||
| 336 | }) | ||
| 337 | .unwrap(); | ||
| 338 | check_semver(&c)?; | ||
| 339 | } | ||
| 340 | Command::PrepareRelease { crate_name } => { | ||
| 341 | let start = indices | ||
| 342 | .get(&CrateId { | ||
| 343 | name: crate_name.clone(), | ||
| 344 | version: "".to_string(), | ||
| 345 | }) | ||
| 346 | .expect("unable to find crate in tree"); | ||
| 347 | |||
| 348 | graph.reverse(); | ||
| 349 | |||
| 350 | let mut bfs = Bfs::new(&graph, *start); | ||
| 351 | |||
| 352 | while let Some(node) = bfs.next(&graph) { | ||
| 353 | let weight = graph.node_weight(node).unwrap(); | ||
| 354 | println!("Preparing {}", weight.name); | ||
| 355 | let mut c = crates.get_mut(weight).unwrap(); | ||
| 356 | let ver = semver::Version::parse(&c.id.version)?; | ||
| 357 | let newver = if let Err(_) = check_semver(&c) { | ||
| 358 | println!("Semver check failed, bumping minor!"); | ||
| 359 | semver::Version::new(ver.major, ver.minor + 1, 0) | ||
| 360 | } else { | ||
| 361 | semver::Version::new(ver.major, ver.minor, ver.patch + 1) | ||
| 362 | }; | ||
| 363 | |||
| 364 | println!( | ||
| 365 | "Updating {} from {} -> {}", | ||
| 366 | weight.name, | ||
| 367 | c.id.version, | ||
| 368 | newver.to_string() | ||
| 369 | ); | ||
| 370 | let newver = newver.to_string(); | ||
| 371 | |||
| 372 | update_version(&mut c, &newver)?; | ||
| 373 | let c = crates.get(weight).unwrap(); | ||
| 374 | |||
| 375 | // Update all nodes further down the tree | ||
| 376 | let mut bfs = Bfs::new(&graph, node); | ||
| 377 | while let Some(dep_node) = bfs.next(&graph) { | ||
| 378 | let dep_weight = graph.node_weight(dep_node).unwrap(); | ||
| 379 | let dep = crates.get(dep_weight).unwrap(); | ||
| 380 | update_versions(dep, &c.id, &newver)?; | ||
| 381 | } | ||
| 382 | |||
| 383 | // Update changelog | ||
| 384 | update_changelog(&root, &c)?; | ||
| 385 | } | ||
| 386 | |||
| 387 | let weight = graph.node_weight(*start).unwrap(); | ||
| 388 | let c = crates.get(weight).unwrap(); | ||
| 389 | publish_release(&root, &c, false)?; | ||
| 390 | |||
| 391 | println!("# Please inspect changes and run the following commands when happy:"); | ||
| 392 | |||
| 393 | println!("git commit -a -m 'chore: prepare crate releases'"); | ||
| 394 | let mut bfs = Bfs::new(&graph, *start); | ||
| 395 | while let Some(node) = bfs.next(&graph) { | ||
| 396 | let weight = graph.node_weight(node).unwrap(); | ||
| 397 | println!("git tag {}-v{}", weight.name, weight.version); | ||
| 398 | } | ||
| 399 | |||
| 400 | println!(""); | ||
| 401 | println!("# Run these commands to publish the crate and dependents:"); | ||
| 402 | |||
| 403 | let mut bfs = Bfs::new(&graph, *start); | ||
| 404 | while let Some(node) = bfs.next(&graph) { | ||
| 405 | let weight = graph.node_weight(node).unwrap(); | ||
| 406 | let c = crates.get(weight).unwrap(); | ||
| 407 | |||
| 408 | let mut args: Vec<String> = vec![ | ||
| 409 | "publish".to_string(), | ||
| 410 | "--manifest-path".to_string(), | ||
| 411 | c.path.display().to_string(), | ||
| 412 | ]; | ||
| 413 | |||
| 414 | if let Some(features) = &c.config.features { | ||
| 415 | args.push("--features".into()); | ||
| 416 | args.push(features.join(",")); | ||
| 417 | } | ||
| 418 | |||
| 419 | if let Some(target) = &c.config.target { | ||
| 420 | args.push("--target".into()); | ||
| 421 | args.push(target.clone()); | ||
| 422 | } | ||
| 423 | |||
| 424 | /* | ||
| 425 | let mut dry_run = args.clone(); | ||
| 426 | dry_run.push("--dry-run".to_string()); | ||
| 427 | |||
| 428 | println!("cargo {}", dry_run.join(" ")); | ||
| 429 | */ | ||
| 430 | println!("cargo {}", args.join(" ")); | ||
| 431 | } | ||
| 432 | |||
| 433 | println!(""); | ||
| 434 | println!("# Run this command to push changes and tags:"); | ||
| 435 | println!("git push --tags"); | ||
| 436 | } | ||
| 437 | } | ||
| 438 | Ok(()) | ||
| 439 | } | ||
| 440 | |||
| 441 | fn check_semver(c: &Crate) -> Result<()> { | ||
| 442 | let mut args: Vec<String> = vec![ | ||
| 443 | "semver-checks".to_string(), | ||
| 444 | "--manifest-path".to_string(), | ||
| 445 | c.path.display().to_string(), | ||
| 446 | "--default-features".to_string(), | ||
| 447 | ]; | ||
| 448 | if let Some(features) = &c.config.features { | ||
| 449 | args.push("--features".into()); | ||
| 450 | args.push(features.join(",")); | ||
| 451 | } | ||
| 452 | |||
| 453 | let status = ProcessCommand::new("cargo").args(&args).output()?; | ||
| 454 | |||
| 455 | println!("{}", core::str::from_utf8(&status.stdout).unwrap()); | ||
| 456 | eprintln!("{}", core::str::from_utf8(&status.stderr).unwrap()); | ||
| 457 | if !status.status.success() { | ||
| 458 | return Err(anyhow!("semver check failed")); | ||
| 459 | } else { | ||
| 460 | Ok(()) | ||
| 461 | } | ||
| 462 | } | ||
| 463 | |||
| 464 | fn update_changelog(repo: &Path, c: &Crate) -> Result<()> { | ||
| 465 | let args: Vec<String> = vec![ | ||
| 466 | "release".to_string(), | ||
| 467 | "replace".to_string(), | ||
| 468 | "--config".to_string(), | ||
| 469 | repo.join("release").join("release.toml").display().to_string(), | ||
| 470 | "--manifest-path".to_string(), | ||
| 471 | c.path.display().to_string(), | ||
| 472 | "--execute".to_string(), | ||
| 473 | "--no-confirm".to_string(), | ||
| 474 | ]; | ||
| 475 | |||
| 476 | let status = ProcessCommand::new("cargo").args(&args).output()?; | ||
| 477 | |||
| 478 | println!("{}", core::str::from_utf8(&status.stdout).unwrap()); | ||
| 479 | eprintln!("{}", core::str::from_utf8(&status.stderr).unwrap()); | ||
| 480 | if !status.status.success() { | ||
| 481 | return Err(anyhow!("release replace failed")); | ||
| 482 | } else { | ||
| 483 | Ok(()) | ||
| 484 | } | ||
| 485 | } | ||
| 486 | |||
| 487 | fn publish_release(_repo: &Path, c: &Crate, push: bool) -> Result<()> { | ||
| 488 | let mut args: Vec<String> = vec![ | ||
| 489 | "publish".to_string(), | ||
| 490 | "--manifest-path".to_string(), | ||
| 491 | c.path.display().to_string(), | ||
| 492 | ]; | ||
| 493 | |||
| 494 | if let Some(features) = &c.config.features { | ||
| 495 | args.push("--features".into()); | ||
| 496 | args.push(features.join(",")); | ||
| 497 | } | ||
| 498 | |||
| 499 | if let Some(target) = &c.config.target { | ||
| 500 | args.push("--target".into()); | ||
| 501 | args.push(target.clone()); | ||
| 502 | } | ||
| 503 | |||
| 504 | if !push { | ||
| 505 | args.push("--dry-run".to_string()); | ||
| 506 | args.push("--allow-dirty".to_string()); | ||
| 507 | args.push("--keep-going".to_string()); | ||
| 508 | } | ||
| 509 | |||
| 510 | let status = ProcessCommand::new("cargo").args(&args).output()?; | ||
| 511 | |||
| 512 | println!("{}", core::str::from_utf8(&status.stdout).unwrap()); | ||
| 513 | eprintln!("{}", core::str::from_utf8(&status.stderr).unwrap()); | ||
| 514 | if !status.status.success() { | ||
| 515 | return Err(anyhow!("publish failed")); | ||
| 516 | } else { | ||
| 517 | Ok(()) | ||
| 518 | } | ||
| 519 | } | ||
