From 171d9a2bf8b150f1bebdff4a3d0201ef98574354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Tue, 6 Oct 2026 10:08:54 +0500 Subject: [PATCH] Add coordinated release packaging and disposable update acceptance --- .../coa-core/examples/coordinated_package.rs | 38 +++++++++++++ .../examples/coordinated_update_e2e.rs | 54 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 crates/coa-core/examples/coordinated_package.rs create mode 100644 crates/coa-core/examples/coordinated_update_e2e.rs diff --git a/crates/coa-core/examples/coordinated_package.rs b/crates/coa-core/examples/coordinated_package.rs new file mode 100644 index 0000000..d7a3e6d --- /dev/null +++ b/crates/coa-core/examples/coordinated_package.rs @@ -0,0 +1,38 @@ +use std::{fs, path::PathBuf}; +use coa_core::{manifest::{Kind, Manifest}, package::{self, BuildOptions}, pkgsource::{self, Source}, release, signing, Result}; + +fn main() -> Result<()> { + let args: Vec = std::env::args().skip(1).collect(); + let source = Source::Dir(args[1].clone().into()); + let (previous, _) = pkgsource::fetch_manifest(&source, signing::EMBEDDED_PUBLIC_KEY)?; + let tree = PathBuf::from(&args[2]); + if args[0] == "extract" { + assert!(!tree.exists(), "Use a new release tree"); + package::extract(&PathBuf::from(&args[1]), &previous, &tree, &|_, _| {})?; + println!("Verified and extracted {}: {} historical migrations", previous.version, previous.migrations.len()); + return Ok(()); + } + assert_eq!(args[0], "pack"); + let out = PathBuf::from(&args[3]); + assert!(!out.exists(), "Release addresses must be immutable"); + for migration in &previous.migrations { + let entry = previous.files.iter().find(|file| file.path.starts_with("_migrations/") && file.sha256 == migration.sha256).expect("Historical SQL entry"); + assert_eq!(coa_core::fsx::sha256_file(&tree.join(&entry.path))?, entry.sha256, "Historical SQL changed: {}", migration.id); + } + let mut manifest = package::build(&tree, &out, &BuildOptions { + kind: Kind::Update, version: args[4].clone(), core_commit: Some(args[5].clone()), + bots_commit: previous.bots.and_then(|revision| revision.commit), + built_at: chrono::Utc::now().to_rfc3339(), part_size: package::DEFAULT_PART_SIZE, + migrations: previous.migrations.clone(), + }, &|line| println!("{line}"))?; + manifest.min_manager_version = "0.6.5".into(); + manifest.validate()?; + coa_core::fsx::atomic_write(&out.join("manifest.json"), &serde_json::to_vec_pretty(&manifest)?)?; + let key_path = PathBuf::from(std::env::var("USERPROFILE").unwrap()).join(".coa-manager/signing/manifest-signing.key"); + release::sign_manifest(&out, &fs::read_to_string(key_path)?)?; + coa_core::release_schema::verify_package(&out, signing::EMBEDDED_PUBLIC_KEY)?; + let reread: Manifest = serde_json::from_slice(&fs::read(out.join("manifest.json"))?)?; + assert_eq!(serde_json::to_value(reread.migrations)?, serde_json::to_value(previous.migrations)?); + println!("PASS: signed {}; all historical migration bytes, checksums, aliases and order preserved", manifest.version); + Ok(()) +} diff --git a/crates/coa-core/examples/coordinated_update_e2e.rs b/crates/coa-core/examples/coordinated_update_e2e.rs new file mode 100644 index 0000000..98b5f77 --- /dev/null +++ b/crates/coa-core/examples/coordinated_update_e2e.rs @@ -0,0 +1,54 @@ +use std::{collections::BTreeMap, path::PathBuf}; +use coa_core::{backup, driver, registry::MetaDir, update::{self, RepackEnv}, Result}; + +fn players(db: &coa_core::db::Db) -> Result { + db.query("SELECT CONCAT(id,':',username,':',HEX(salt),':',HEX(verifier)) FROM acore_auth.account WHERE id=900001; SELECT CONCAT(guid,':',account,':',name,':',level,':',money) FROM acore_characters.characters WHERE guid=900001; SELECT CONCAT(guid,':',itemEntry,':',owner_guid,':',enchantments) FROM acore_characters.item_instance WHERE guid=900001; SELECT CONCAT(guid,':',bag,':',slot,':',item) FROM acore_characters.character_inventory WHERE guid=900001;") +} + +fn exercise(root: &std::path::Path, package: &str) -> Result<()> { + let meta_dir = coa_core::registry::metadata_dir_for(root)?; + let (_, meta) = MetaDir::open(&meta_dir)?; + let expected = backup::with_database(root, |db| [coa_core::realms::Mode::Coa, coa_core::realms::Mode::Wildcard].into_iter().map(|mode| players(&db.clone().for_realm(mode))).collect::>>())?; + assert!(driver::run(root, driver::Verb::StopAll)?.ok); + let source = coa_core::pkgsource::Source::Dir(package.into()); + let preview = update::preview(root, &meta, &source, coa_core::signing::EMBEDDED_PUBLIC_KEY, &BTreeMap::new())?; + assert_eq!(preview.pending_migrations, 0, "Historical SQL must not replay"); + assert!(!preview.items.iter().any(|item| item.action == update::Action::Conflict), "Unresolved candidate files: {:?}", preview.items); + let environment = RepackEnv { root, meta_dir: &meta_dir }; + let outcome = update::apply(&update::Params { + root, meta_dir: &meta_dir, source, trusted_key: coa_core::signing::EMBEDDED_PUBLIC_KEY, + resolutions: BTreeMap::new(), cancel: Default::default(), env: &environment, fail_after_ops: None, + }, &|stage, progress| println!("{progress}% {stage}"))?; + assert_eq!(outcome.txn.state, update::State::Committed, "{:?}", outcome.txn); + backup::with_database(root, |db| { + for (index, mode) in [coa_core::realms::Mode::Coa, coa_core::realms::Mode::Wildcard].into_iter().enumerate() { + let realm = db.clone().for_realm(mode); + assert_eq!(players(&realm)?, expected[index], "Player data changed in {mode:?}"); + let problems = coa_core::schema_check::check(&realm, root)?; + assert!(problems.is_empty(), "{problems:?}"); + } + Ok(()) + })?; + let release = coa_core::squid::release(root).expect("SQUID release metadata"); + println!("SQUID metadata: {release:?}"); + assert_eq!(coa_core::squid::fields(root)?.len(), 40); + let (_, installed) = MetaDir::open(&meta_dir)?; + let repeated = update::preview(root, &installed, &coa_core::pkgsource::Source::Dir(package.into()), coa_core::signing::EMBEDDED_PUBLIC_KEY, &BTreeMap::new())?; + assert_eq!(repeated.pending_migrations, 0); + assert!(repeated.items.iter().all(|item| item.action == update::Action::Skip), "{:?}", repeated.items); + println!("PASS: coordinated update committed, both realms started, player fields preserved, historical SQL skipped, repeated preview clean"); + Ok(()) +} + +fn main() -> Result<()> { + let args: Vec = std::env::args().skip(1).collect(); + let root = PathBuf::from(&args[0]); + assert!(root.file_name().unwrap().to_string_lossy().starts_with("coa-schema-fixture-chaos-064-transition-")); + assert_eq!(std::fs::read(root.join(".transition-fixture"))?, b"disposable published-064 transition fixture"); + assert_eq!(std::fs::read(root.join(".release-schema-fixture"))?, b"disposable release-schema fixture"); + assert!(!root.join("Data").symlink_metadata()?.file_type().is_symlink()); + assert!(driver::run(&root, driver::Verb::StopAll)?.ok); + let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| exercise(&root, &args[1]))); + assert!(driver::run(&root, driver::Verb::StopAll)?.ok, "Fixture shutdown failed"); + outcome.map_err(|_| coa_core::Error::Invalid("Disposable coordinated acceptance failed; stopped services and retained recovery copies".into()))? +}