lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 6c27a358c36fc8027d2919a2325ce91a5d0d1791
parent cc57eabbc1e8bcd58bdd03d2a4d1ba46b4695a15
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Jul 2026 13:18:02 +0000

release: close public dependency graph

- enforce the versioned strict publication policy schema
- derive every dependency edge and publish order from Cargo metadata
- publish the transport-neutral crate as the public delivery boundary
- remove public feature paths into internal adapters and stores

Diffstat:
MCargo.lock | 4----
Mcontracts/releases/publish_policy.toml | 6+++++-
Mcrates/nostr_accounts/Cargo.toml | 7+------
Mcrates/nostr_accounts/README | 3+--
Mcrates/nostr_accounts/src/lib.rs | 4----
Mcrates/outbox/contracts/migration_authority_v1.manifest.json | 4++--
Mcrates/outbox/contracts/migration_authority_v1.manifest.sha256 | 2+-
Mcrates/outbox/contracts/phase1_publication_v1.manifest.json | 4++--
Mcrates/outbox/contracts/phase1_publication_v1.manifest.sha256 | 2+-
Mcrates/runtime/Cargo.toml | 2--
Mcrates/runtime/src/transport.rs | 71-----------------------------------------------------------------------
Mcrates/trade/Cargo.toml | 15---------------
Mcrates/trade/src/operational_listing/draft.rs | 57++++++++++++---------------------------------------------
Mcrates/transport/Cargo.toml | 2+-
Mtools/xtask/src/contract.rs | 1049++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mtools/xtask/src/contract/release_provenance.rs | 16++++++++++++++++
16 files changed, 631 insertions(+), 617 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -4815,7 +4815,6 @@ version = "1.0.0-alpha.1" dependencies = [ "radroots_identity", "radroots_nostr_signer", - "radroots_nostrdb", "radroots_protected_store", "radroots_runtime", "radroots_secret_vault", @@ -4984,7 +4983,6 @@ dependencies = [ "radroots_runtime_paths", "radroots_secret_vault", "radroots_transport", - "radroots_transport_reticulum", "serde", "serde_json", "tempfile", @@ -5184,11 +5182,9 @@ dependencies = [ "dto_bindgen_core", "hex", "nostr", - "radroots_authority", "radroots_core", "radroots_event", "radroots_event_codec", - "radroots_event_store", "radroots_nostr", "radroots_test_fixtures", "radroots_transport", diff --git a/contracts/releases/publish_policy.toml b/contracts/releases/publish_policy.toml @@ -1,3 +1,6 @@ +[schema] +version = 1 + [release] version = "1.0.0-alpha.1" @@ -28,6 +31,7 @@ public = [ "radroots_secret_vault", "radroots_sql_core", "radroots_trade", + "radroots_transport", ] internal = [ "radroots_authority", @@ -51,7 +55,6 @@ internal = [ "radroots_test_fixtures", "radroots_trade_sp1_guest", "radroots_trade_sp1_host", - "radroots_transport", "radroots_transport_nostr", "radroots_transport_publish_protocol", "radroots_transport_reticulum", @@ -63,6 +66,7 @@ yank_only = [] [publish_order] crates = [ + "radroots_transport", "radroots_runtime_distribution", "radroots_replica_schema", "radroots_protocol_contract_v1", diff --git a/crates/nostr_accounts/Cargo.toml b/crates/nostr_accounts/Cargo.toml @@ -11,6 +11,7 @@ repository.workspace = true homepage.workspace = true documentation = "https://docs.rs/radroots_nostr_accounts" readme = "README" +exclude = ["src/nostrdb_bridge.rs"] [features] default = ["std", "file-store", "memory-vault"] @@ -26,7 +27,6 @@ std = [ file-store = ["std"] memory-vault = ["std", "radroots_secret_vault/memory-vault"] os-keyring = ["std", "radroots_secret_vault/os-keyring"] -nostrdb-bridge = ["std", "dep:radroots_nostrdb"] [dependencies] radroots_identity = { workspace = true, optional = true, default-features = false, features = [ @@ -34,11 +34,6 @@ radroots_identity = { workspace = true, optional = true, default-features = fals "json-file", ] } radroots_nostr_signer = { workspace = true, optional = true } -radroots_nostrdb = { workspace = true, optional = true, default-features = false, features = [ - "nostrdb", - "giftwrap", - "rt", -] } radroots_protected_store = { workspace = true, optional = true, default-features = false, features = [ "std", ] } diff --git a/crates/nostr_accounts/README b/crates/nostr_accounts/README @@ -11,8 +11,7 @@ and vault-backed stores for the `radroots` core libraries. * shared bootstrap helpers for local account stores backed by protected files and runtime-selected secret vaults; * vault integrations through `radroots_secret_vault`, including memory and OS - keyring backends; - * optional nostrdb bridge helpers for account registration and lookup. + keyring backends. ## Copyright diff --git a/crates/nostr_accounts/src/lib.rs b/crates/nostr_accounts/src/lib.rs @@ -6,8 +6,6 @@ pub mod error; pub mod manager; #[cfg(feature = "std")] pub mod model; -#[cfg(feature = "nostrdb-bridge")] -pub mod nostrdb_bridge; #[cfg(feature = "std")] pub mod store; #[cfg(feature = "std")] @@ -22,8 +20,6 @@ pub mod prelude { RADROOTS_NOSTR_ACCOUNTS_STORE_VERSION, RadrootsNostrAccountRecord, RadrootsNostrAccountStatus, RadrootsNostrAccountStoreState, }; - #[cfg(feature = "nostrdb-bridge")] - pub use crate::nostrdb_bridge::radroots_nostr_accounts_register_default_secret_with_nostrdb; #[cfg(feature = "std")] pub use crate::store::{ RadrootsNostrAccountStore, RadrootsNostrFileAccountStore, RadrootsNostrMemoryAccountStore, diff --git a/crates/outbox/contracts/migration_authority_v1.manifest.json b/crates/outbox/contracts/migration_authority_v1.manifest.json @@ -305,10 +305,10 @@ }, { "file": { - "byte_length": 486359, + "byte_length": 495349, "hash_algorithm": "sha256_bytes_v1", "path": "tools/xtask/src/contract.rs", - "sha256": "96468090685099e9d3722d45cf05170f3da83376150cbf58fee776157e6be649" + "sha256": "fc6c2e794ef5c787ed134f38982b1c18518cde30fb21643669428fa05a3eccd3" }, "role": "contract_dispatch" }, diff --git a/crates/outbox/contracts/migration_authority_v1.manifest.sha256 b/crates/outbox/contracts/migration_authority_v1.manifest.sha256 @@ -1 +1 @@ -7756d99889ee5875664d1c831c0a430d3352dac2ab958b9abf4bdb8cd5a360a4 +168348114a174c2f745ae388182d604832dee24efa2c24781d03a3a10d64462a diff --git a/crates/outbox/contracts/phase1_publication_v1.manifest.json b/crates/outbox/contracts/phase1_publication_v1.manifest.json @@ -112,9 +112,9 @@ }, { "file": { - "byte_length": 486359, + "byte_length": 495349, "path": "tools/xtask/src/contract.rs", - "sha256": "96468090685099e9d3722d45cf05170f3da83376150cbf58fee776157e6be649" + "sha256": "fc6c2e794ef5c787ed134f38982b1c18518cde30fb21643669428fa05a3eccd3" }, "role": "contract_dispatch" }, diff --git a/crates/outbox/contracts/phase1_publication_v1.manifest.sha256 b/crates/outbox/contracts/phase1_publication_v1.manifest.sha256 @@ -1 +1 @@ -b9e4b58a8745555d0bcd66128de509e788b90dcb1d49ec73b9b6fa81a0ed0307 +e8fea684b1e1f37d99e0ffbc974ec4fb1b069f5ab9b50b5c8bdafa624c1c7246 diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml @@ -17,7 +17,6 @@ default = [] cli = ["dep:clap"] transport = ["dep:radroots_event", "dep:radroots_transport"] transport-nostr = ["transport"] -transport-reticulum = ["transport", "dep:radroots_transport_reticulum"] transport-workers = ["transport"] [dependencies] @@ -38,7 +37,6 @@ radroots_secret_vault = { workspace = true, features = ["std"] } radroots_transport = { workspace = true, optional = true, default-features = false, features = [ "serde", ] } -radroots_transport_reticulum = { workspace = true, optional = true, default-features = false } serde = { workspace = true } serde_json = { workspace = true } tempfile = { workspace = true } diff --git a/crates/runtime/src/transport.rs b/crates/runtime/src/transport.rs @@ -1339,10 +1339,6 @@ mod tests { RadrootsTransportSatisfactionPolicy, RadrootsTransportStatus, RadrootsTransportTarget, RadrootsTransportTargetReceipt, RadrootsTransportTargetSet, }; - #[cfg(feature = "transport-reticulum")] - use radroots_transport::{ - RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity, - }; #[cfg(feature = "transport-workers")] use std::sync::{Arc, Mutex}; @@ -1776,73 +1772,6 @@ mod tests { assert_eq!(delivery_request.now_ms(), 123_456); } - #[cfg(feature = "transport-reticulum")] - #[tokio::test] - async fn registry_preserves_default_reticulum_reject_behavior() { - let mut registry = RadrootsRuntimeTransportRegistry::new(); - registry - .register(radroots_transport_reticulum::RadrootsReticulumTransport::default()) - .expect("register"); - let transport = registry - .transport(&RadrootsTransportKind::Reticulum) - .expect("reticulum transport"); - let request = RadrootsRuntimeTransportDispatchRequest::new( - "reticulum-delivery", - opaque_payload(), - vec![target(RadrootsTransportKind::Reticulum, "reticulum:local")], - RadrootsTransportSatisfactionPolicy::any_accepted(), - 1_000, - ) - .expect("request"); - let receipt = transport - .deliver( - request - .transport_delivery_request() - .expect("delivery request"), - ) - .await - .expect("receipt"); - let status = transport.status().await.expect("status"); - assert_eq!( - status.implementation(), - RadrootsTransportImplementationState::Real - ); - assert_eq!( - status.maturity(), - RadrootsTransportCapabilityMaturity::Preview - ); - assert_eq!( - status.availability(), - RadrootsTransportCapabilityAvailability::Unavailable - ); - assert!(!status.capabilities().can_deliver()); - assert!(!status.capabilities().can_fetch()); - let fetch = transport - .fetch( - RadrootsTransportFetchRequest::new( - "reticulum-fetch", - RadrootsTransportTargetSet::new(vec![target( - RadrootsTransportKind::Reticulum, - "reticulum:local", - )]) - .expect("target set"), - ) - .expect("fetch request"), - ) - .await - .expect("fetch"); - assert_eq!(fetch.fetched_count(), 0); - - assert_eq!( - receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), - 0 - ); - assert_eq!( - receipt.target_receipts()[0].status(), - RadrootsTransportDeliveryTargetStatus::FailedRetryable - ); - } - #[test] fn bounded_queue_tracks_capacity_inflight_and_shutdown() { let mut queue = RadrootsRuntimeBoundedQueue::new(2).expect("queue"); diff --git a/crates/trade/Cargo.toml b/crates/trade/Cargo.toml @@ -23,21 +23,11 @@ dto-bindgen = [ "radroots_event/dto-bindgen", ] std = [ - "radroots_authority/std", "radroots_core/std", "radroots_event/std", "radroots_event_codec/std", "radroots_event_codec/nostr", ] -event_store = [ - "std", - "serde_json", - "dep:radroots_event_store", - "dep:sqlx", - "radroots_event_store/sqlite", - "radroots_event_store/runtime-tokio", - "sqlx/sqlite-bundled", -] serde = [ "dep:serde", "radroots_core/serde", @@ -54,11 +44,9 @@ serde_json = [ ] [dependencies] -radroots_authority = { workspace = true, default-features = false } radroots_core = { workspace = true, default-features = false } radroots_event = { workspace = true, default-features = false } radroots_event_codec = { workspace = true, default-features = false } -radroots_event_store = { workspace = true, optional = true, default-features = false } base64 = { workspace = true, optional = true } dto_bindgen = { workspace = true, optional = true } dto_bindgen_core = { workspace = true, optional = true } @@ -71,9 +59,6 @@ serde_json = { workspace = true, default-features = false, features = [ "alloc", ], optional = true } sha2 = { workspace = true, default-features = false, optional = true } -sqlx = { workspace = true, default-features = false, features = [ - "derive", -], optional = true } [dev-dependencies] nostr = { workspace = true } diff --git a/crates/trade/src/operational_listing/draft.rs b/crates/trade/src/operational_listing/draft.rs @@ -10,9 +10,7 @@ use alloc::{format, string::ToString, vec::Vec}; #[cfg(feature = "std")] use std::{string::ToString, vec::Vec}; -use radroots_authority::RadrootsActorContext; use radroots_event::{ - contract::RadrootsActorRole, ids::{ RadrootsClassifiedListingAddress, RadrootsIdParseError, RadrootsInventoryBinId, RadrootsPublicKey, @@ -94,9 +92,6 @@ pub enum RadrootsOperationalListingEditError { InvalidFarmPubkey(RadrootsIdParseError), InvalidClassifiedListingAddress(RadrootsIdParseError), InvalidModel(RadrootsOperationalListingValidationError), - ActorRoleUnsatisfied { - required_role: RadrootsActorRole, - }, FarmPubkeyMismatch { expected_pubkey: RadrootsPublicKey, actual_pubkey: RadrootsPublicKey, @@ -121,10 +116,6 @@ impl fmt::Display for RadrootsOperationalListingEditError { Self::InvalidModel(error) => { write!(f, "invalid listing edit model: {error}") } - Self::ActorRoleUnsatisfied { required_role } => write!( - f, - "listing edit actor does not satisfy required role {required_role:?}" - ), Self::FarmPubkeyMismatch { .. } => { f.write_str("listing edit farm pubkey does not match seller") } @@ -172,17 +163,13 @@ fn listing_addr( .expect("typed listing identity must form a listing address") } +/// Canonicalizes an operational listing edit after the caller has authorized +/// `authorized_seller_pubkey` for the seller-owned listing boundary. pub fn canonicalize_operational_listing_edit( - actor: &RadrootsActorContext, + authorized_seller_pubkey: &RadrootsPublicKey, mut document: RadrootsOperationalListingEditDocumentV1, ) -> Result<RadrootsOperationalListingCanonicalEdit, RadrootsOperationalListingEditError> { - if !actor.satisfies(RadrootsActorRole::Seller) { - return Err(RadrootsOperationalListingEditError::ActorRoleUnsatisfied { - required_role: RadrootsActorRole::Seller, - }); - } - - let seller_pubkey = actor.pubkey().clone(); + let seller_pubkey = authorized_seller_pubkey.clone(); let farm_pubkey = document.listing.farm.pubkey.as_str(); if farm_pubkey.is_empty() { document.listing.farm.pubkey = seller_pubkey.as_str().to_string(); @@ -193,13 +180,11 @@ pub fn canonicalize_operational_listing_edit( #[cfg(test)] mod tests { - use radroots_authority::RadrootsActorContext; use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; use radroots_event::{ - contract::RadrootsActorRole, farm::RadrootsFarmRef, ids::{ RadrootsClassifiedListingAddress, RadrootsDTag, RadrootsInventoryBinId, @@ -292,12 +277,8 @@ mod tests { } } - fn seller_actor() -> RadrootsActorContext { - RadrootsActorContext::explicit_pubkey(SELLER, [RadrootsActorRole::Seller]).expect("actor") - } - - fn buyer_actor() -> RadrootsActorContext { - RadrootsActorContext::explicit_pubkey(SELLER, [RadrootsActorRole::Buyer]).expect("actor") + fn seller_pubkey() -> RadrootsPublicKey { + RadrootsPublicKey::parse(SELLER).expect("seller") } #[test] @@ -316,7 +297,7 @@ mod tests { let document: RadrootsOperationalListingEditDocumentV1 = serde_json::from_str(&json).expect("deserialize document"); - let canonical = canonicalize_operational_listing_edit(&seller_actor(), document) + let canonical = canonicalize_operational_listing_edit(&seller_pubkey(), document) .expect("canonical draft"); assert_eq!(canonical.seller_pubkey().as_str(), SELLER); @@ -362,7 +343,7 @@ mod tests { listing.farm.pubkey.clear(); let document = RadrootsOperationalListingEditDocumentV1::new(listing); - let canonical = canonicalize_operational_listing_edit(&seller_actor(), document) + let canonical = canonicalize_operational_listing_edit(&seller_pubkey(), document) .expect("canonical draft"); assert_eq!(canonical.seller_pubkey().as_str(), SELLER); @@ -374,26 +355,12 @@ mod tests { } #[test] - fn canonicalize_operational_listing_edit_rejects_non_seller_actor() { - let document = RadrootsOperationalListingEditDocumentV1::new(listing()); - - let error = canonicalize_operational_listing_edit(&buyer_actor(), document).unwrap_err(); - - assert_eq!( - error, - RadrootsOperationalListingEditError::ActorRoleUnsatisfied { - required_role: RadrootsActorRole::Seller - } - ); - } - - #[test] fn canonicalize_operational_listing_edit_rejects_mismatched_farm_pubkey() { let mut listing = listing(); listing.farm.pubkey = OTHER.to_string(); let document = RadrootsOperationalListingEditDocumentV1::new(listing); - let error = canonicalize_operational_listing_edit(&seller_actor(), document).unwrap_err(); + let error = canonicalize_operational_listing_edit(&seller_pubkey(), document).unwrap_err(); assert!(matches!( error, @@ -407,7 +374,7 @@ mod tests { listing.farm.pubkey = "bad".to_string(); let document = RadrootsOperationalListingEditDocumentV1::new(listing); - let error = canonicalize_operational_listing_edit(&seller_actor(), document).unwrap_err(); + let error = canonicalize_operational_listing_edit(&seller_pubkey(), document).unwrap_err(); assert!(matches!( error, @@ -472,7 +439,7 @@ mod tests { listing.primary_bin_id = bin_id("bin-2"); let document = RadrootsOperationalListingEditDocumentV1::new(listing); - let error = canonicalize_operational_listing_edit(&seller_actor(), document).unwrap_err(); + let error = canonicalize_operational_listing_edit(&seller_pubkey(), document).unwrap_err(); assert_eq!( error, @@ -507,7 +474,7 @@ mod tests { listing.bins.push(listing.bins[0].clone()); let document = RadrootsOperationalListingEditDocumentV1::new(listing); - let error = canonicalize_operational_listing_edit(&seller_actor(), document).unwrap_err(); + let error = canonicalize_operational_listing_edit(&seller_pubkey(), document).unwrap_err(); assert_eq!( error, diff --git a/crates/transport/Cargo.toml b/crates/transport/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "radroots_transport" -publish = false +publish = ["crates-io"] version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs @@ -79,6 +79,8 @@ use sha2::{Digest, Sha256}; use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::path::{Path, PathBuf}; +#[cfg(not(test))] +use std::process::Command; pub(crate) fn validate_artifact_contracts(workspace_root: &Path) -> Result<(), String> { validate_event_contract_registry_v7_inventory(workspace_root)?; @@ -3211,42 +3213,76 @@ const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 44] = [ ]; #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct ReleaseContractFile { + schema: ReleasePolicySchema, release: ReleaseSection, - #[serde(default)] classification: ReleaseClassification, - #[serde(default)] - publish: Option<ReleaseCrateSet>, - #[serde(default)] - internal: Option<ReleaseCrateSet>, publish_order: ReleaseCrateSet, } -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ReleasePolicySchema { + version: u32, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct ReleaseClassification { - #[serde(default)] public: Vec<String>, - #[serde(default)] internal: Vec<String>, - #[serde(default)] deferred: Vec<String>, - #[serde(default)] retired: Vec<String>, - #[serde(default)] yank_only: Vec<String>, } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct ReleaseSection { version: String, } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct ReleaseCrateSet { crates: Vec<String>, } #[derive(Debug, Deserialize)] +struct CargoMetadataDocument { + packages: Vec<CargoMetadataPackage>, + workspace_members: Vec<String>, +} + +#[derive(Debug, Deserialize)] +struct CargoMetadataPackage { + id: String, + name: String, + publish: Option<Vec<String>>, + dependencies: Vec<CargoMetadataDependency>, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +struct CargoMetadataDependency { + name: String, + source: Option<String>, + kind: Option<String>, + rename: Option<String>, + optional: bool, + target: Option<String>, + path: Option<PathBuf>, +} + +#[derive(Debug)] +struct ReleaseWorkspacePackage { + publish: Option<Vec<String>>, + dependencies: Vec<CargoMetadataDependency>, +} + +type ReleaseWorkspacePackages = BTreeMap<String, ReleaseWorkspacePackage>; + +#[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] struct ConformanceVectorFile { suite: String, @@ -3265,32 +3301,12 @@ struct ConformanceVectorEntry { } impl ReleaseContractFile { - fn uses_classification(&self) -> bool { - !self.classification.public.is_empty() - || !self.classification.internal.is_empty() - || !self.classification.deferred.is_empty() - || !self.classification.retired.is_empty() - || !self.classification.yank_only.is_empty() - } - fn public_crates(&self) -> Vec<String> { - if self.uses_classification() { - return self.classification.public.clone(); - } - self.publish - .as_ref() - .map(|set| set.crates.clone()) - .unwrap_or_default() + self.classification.public.clone() } fn internal_crates(&self) -> Vec<String> { - if self.uses_classification() { - return self.classification.internal.clone(); - } - self.internal - .as_ref() - .map(|set| set.crates.clone()) - .unwrap_or_default() + self.classification.internal.clone() } fn deferred_crates(&self) -> Vec<String> { @@ -4805,6 +4821,7 @@ struct WorkspacePackageRecord { #[cfg_attr(not(test), allow(dead_code))] manifest_path: PathBuf, publish_enabled: bool, + #[cfg(test)] publish: Option<PackagePublish>, manifest_value: toml::Value, } @@ -4829,12 +4846,12 @@ fn workspace_package_records(workspace_root: &Path) -> Result<Vec<WorkspacePacka }; let name = package_manifest.package.name; let publish_enabled = package_publish_enabled(package_manifest.package.publish.as_ref()); - let publish = package_manifest.package.publish.clone(); records.push(WorkspacePackageRecord { name, manifest_path, publish_enabled, - publish, + #[cfg(test)] + publish: package_manifest.package.publish.clone(), manifest_value, }); } @@ -4992,6 +5009,7 @@ fn workspace_package_publish_flags( Ok(flags) } +#[cfg(test)] fn workspace_package_publish_configs( workspace_root: &Path, ) -> Result<BTreeMap<String, Option<PackagePublish>>, String> { @@ -5007,6 +5025,7 @@ fn workspace_package_publish_configs( Ok(configs) } +#[cfg(test)] fn read_workspace_package_dependencies( workspace_root: &Path, ) -> Result<BTreeMap<String, BTreeSet<String>>, String> { @@ -5039,6 +5058,176 @@ fn read_workspace_package_dependencies( Ok(deps) } +#[cfg(not(test))] +fn load_release_workspace_packages( + workspace_root: &Path, +) -> Result<ReleaseWorkspacePackages, String> { + const COMMAND: &str = "cargo metadata --locked --format-version 1"; + let output = Command::new("cargo") + .args(["metadata", "--locked", "--format-version", "1"]) + .current_dir(workspace_root) + .output() + .map_err(|error| format!("run {COMMAND}: {error}"))?; + if !output.status.success() { + return Err(format!( + "{COMMAND} failed: {}", + String::from_utf8_lossy(&output.stderr).trim() + )); + } + let metadata = serde_json::from_slice::<CargoMetadataDocument>(&output.stdout) + .map_err(|error| format!("parse {COMMAND} output: {error}"))?; + release_workspace_packages_from_metadata(metadata) +} + +#[cfg(test)] +fn load_release_workspace_packages( + workspace_root: &Path, +) -> Result<ReleaseWorkspacePackages, String> { + let dependencies = read_workspace_package_dependencies(workspace_root)?; + let mut packages = BTreeMap::new(); + for record in workspace_package_records(workspace_root)? { + let package_dependencies = dependencies + .get(&record.name) + .into_iter() + .flatten() + .map(|name| CargoMetadataDependency { + name: name.clone(), + source: None, + kind: None, + rename: None, + optional: false, + target: None, + path: Some(PathBuf::from(name)), + }) + .collect(); + let publish = match record.publish { + None => None, + Some(PackagePublish::Bool(false)) => Some(Vec::new()), + Some(PackagePublish::Bool(true)) => None, + Some(PackagePublish::Registries(registries)) => Some(registries), + }; + if packages + .insert( + record.name.clone(), + ReleaseWorkspacePackage { + publish, + dependencies: package_dependencies, + }, + ) + .is_some() + { + return Err(format!("duplicate workspace package name {}", record.name)); + } + } + Ok(packages) +} + +fn release_workspace_packages_from_metadata( + metadata: CargoMetadataDocument, +) -> Result<ReleaseWorkspacePackages, String> { + let workspace_ids = collect_unique_set( + &metadata.workspace_members, + "cargo metadata workspace_members", + )?; + let mut packages = BTreeMap::new(); + let mut found_ids = BTreeSet::new(); + for package in metadata.packages { + if !workspace_ids.contains(&package.id) { + continue; + } + found_ids.insert(package.id); + let dependencies = package + .dependencies + .into_iter() + .filter(|dependency| dependency.kind.as_deref() != Some("dev")) + .collect(); + if packages + .insert( + package.name.clone(), + ReleaseWorkspacePackage { + publish: package.publish, + dependencies, + }, + ) + .is_some() + { + return Err(format!( + "cargo metadata contains duplicate workspace package name {}", + package.name + )); + } + } + if found_ids != workspace_ids { + let missing = workspace_ids + .difference(&found_ids) + .cloned() + .collect::<BTreeSet<_>>(); + return Err(format!( + "cargo metadata workspace_members reference missing package ids: {}", + join_set(&missing) + )); + } + Ok(packages) +} + +fn validate_public_dependency_closure( + packages: &ReleaseWorkspacePackages, + public_packages: &BTreeSet<String>, +) -> Result<(), String> { + for package_name in public_packages { + let package = packages + .get(package_name) + .ok_or_else(|| format!("cargo metadata is missing public package {package_name}"))?; + for dependency in &package.dependencies { + if packages.contains_key(&dependency.name) { + if !public_packages.contains(&dependency.name) { + return Err(format!( + "public package {package_name} has non-public {} dependency {}{}", + dependency.kind.as_deref().unwrap_or("normal"), + dependency.name, + release_dependency_context(dependency) + )); + } + continue; + } + if dependency.path.is_some() { + return Err(format!( + "public package {package_name} has unversioned path dependency {} outside the workspace{}", + dependency.name, + release_dependency_context(dependency) + )); + } + match dependency.source.as_deref() { + Some(source) if source.starts_with("registry+") => {} + Some(source) => { + return Err(format!( + "public package {package_name} has non-registry dependency {} from {source}{}", + dependency.name, + release_dependency_context(dependency) + )); + } + None => { + return Err(format!( + "public package {package_name} has dependency {} without a registry source{}", + dependency.name, + release_dependency_context(dependency) + )); + } + } + } + } + Ok(()) +} + +fn release_dependency_context(dependency: &CargoMetadataDependency) -> String { + format!( + " (rename={}, optional={}, target={})", + dependency.rename.as_deref().unwrap_or("none"), + dependency.optional, + dependency.target.as_deref().unwrap_or("all") + ) +} + fn validate_publishable_dto_tooling_sources( workspace_root: &Path, public_crates: &BTreeSet<String>, @@ -8277,6 +8466,7 @@ fn validate_coverage_policy_parity( Ok(()) } +#[cfg(test)] fn publish_config_is_public(publish: Option<&PackagePublish>) -> bool { matches!( publish, @@ -8285,10 +8475,19 @@ fn publish_config_is_public(publish: Option<&PackagePublish>) -> bool { ) } +#[cfg(test)] fn publish_config_is_non_public(publish: Option<&PackagePublish>) -> bool { matches!(publish, Some(PackagePublish::Bool(false))) } +fn metadata_publish_config_is_public(publish: Option<&Vec<String>>) -> bool { + matches!(publish, Some(registries) if registries.as_slice() == ["crates-io"]) +} + +fn metadata_publish_config_is_non_public(publish: Option<&Vec<String>>) -> bool { + matches!(publish, Some(registries) if registries.is_empty()) +} + #[cfg(test)] fn validate_release_publish_policy( workspace_root: &Path, @@ -8296,6 +8495,9 @@ fn validate_release_publish_policy( contract_version: &str, ) -> Result<(), String> { let release = load_release_contract(workspace_root, contract_version)?; + if release.schema.version != 1 { + return Err("release policy schema.version must be 1".to_string()); + } if release.release.version.trim().is_empty() { return Err("release.version must not be empty".to_string()); } @@ -8306,23 +8508,11 @@ fn validate_release_publish_policy( )); } - let workspace_packages = workspace_package_names(workspace_root)? - .into_iter() - .collect::<BTreeSet<_>>(); - let uses_classification = release.uses_classification(); - let public_field = if uses_classification { - "classification.public" - } else { - "publish.crates" - }; - let internal_field = if uses_classification { - "classification.internal" - } else { - "internal.crates" - }; + let metadata_packages = load_release_workspace_packages(workspace_root)?; + let workspace_packages = metadata_packages.keys().cloned().collect::<BTreeSet<_>>(); - let public_set = collect_unique_set(&release.public_crates(), public_field)?; - let internal_set = collect_unique_set(&release.internal_crates(), internal_field)?; + let public_set = collect_unique_set(&release.public_crates(), "classification.public")?; + let internal_set = collect_unique_set(&release.internal_crates(), "classification.internal")?; let deferred_set = collect_unique_set(&release.deferred_crates(), "classification.deferred")?; let retired_set = collect_unique_set(&release.retired_crates(), "classification.retired")?; let yank_only_set = @@ -8397,30 +8587,27 @@ fn validate_release_publish_policy( .enumerate() .map(|(idx, name)| (name.clone(), idx)) .collect::<BTreeMap<_, _>>(); - let dependencies = read_workspace_package_dependencies(workspace_root) - .expect("workspace package manifests were already parsed"); + validate_public_dependency_closure(&metadata_packages, &public_set)?; for crate_name in &public_set { - let crate_deps = &dependencies[crate_name]; + let crate_deps = &metadata_packages[crate_name].dependencies; let crate_order = order_index[crate_name]; for dep in crate_deps { - if !public_set.contains(dep) { + if !public_set.contains(&dep.name) { continue; } - let dep_order = order_index[dep]; + let dep_order = order_index[&dep.name]; if dep_order >= crate_order { return Err(format!( "publish order must place dependency {} before {}", - dep, crate_name + dep.name, crate_name )); } } } - let publish_configs = workspace_package_publish_configs(workspace_root) - .expect("workspace publish configs are stable"); for crate_name in &public_set { - let publish = publish_configs[crate_name].as_ref(); - if !publish_config_is_public(publish) { + let publish = metadata_packages[crate_name].publish.as_ref(); + if !metadata_publish_config_is_public(publish) { return Err(format!( "public crate {} must set publish = [\"crates-io\"]", crate_name @@ -8433,8 +8620,8 @@ fn validate_release_publish_policy( .chain(retired_set.iter()) .chain(yank_only_set.iter()) { - let publish = publish_configs[crate_name].as_ref(); - if !publish_config_is_non_public(publish) { + let publish = metadata_packages[crate_name].publish.as_ref(); + if !metadata_publish_config_is_non_public(publish) { return Err(format!( "non-public crate {} must set publish = false", crate_name @@ -8484,15 +8671,8 @@ fn validate_release_preflight_with_override_and_profile( )?; let policy = load_coverage_policy(&bundle.root).expect("validated contract includes coverage policy"); - let publish_crates = collect_unique_set( - &release.public_crates(), - if release.uses_classification() { - "classification.public" - } else { - "publish.crates" - }, - ) - .expect("validated contract enforces unique public crates"); + let publish_crates = collect_unique_set(&release.public_crates(), "classification.public") + .expect("validated contract enforces unique public crates"); let required_crate_list = policy .required_crates() .expect("validated contract includes required crates"); @@ -8598,6 +8778,9 @@ fn validate_release_publish_policy_with_override( contract_version, release_policy_override, )?; + if release.schema.version != 1 { + return Err("release policy schema.version must be 1".to_string()); + } if release.release.version.trim().is_empty() { return Err("release.version must not be empty".to_string()); } @@ -8608,23 +8791,11 @@ fn validate_release_publish_policy_with_override( )); } - let workspace_packages = workspace_package_names(workspace_root)? - .into_iter() - .collect::<BTreeSet<_>>(); - let uses_classification = release.uses_classification(); - let public_field = if uses_classification { - "classification.public" - } else { - "publish.crates" - }; - let internal_field = if uses_classification { - "classification.internal" - } else { - "internal.crates" - }; + let metadata_packages = load_release_workspace_packages(workspace_root)?; + let workspace_packages = metadata_packages.keys().cloned().collect::<BTreeSet<_>>(); - let public_set = collect_unique_set(&release.public_crates(), public_field)?; - let internal_set = collect_unique_set(&release.internal_crates(), internal_field)?; + let public_set = collect_unique_set(&release.public_crates(), "classification.public")?; + let internal_set = collect_unique_set(&release.internal_crates(), "classification.internal")?; let deferred_set = collect_unique_set(&release.deferred_crates(), "classification.deferred")?; let retired_set = collect_unique_set(&release.retired_crates(), "classification.retired")?; let yank_only_set = @@ -8699,30 +8870,27 @@ fn validate_release_publish_policy_with_override( .enumerate() .map(|(idx, name)| (name.clone(), idx)) .collect::<BTreeMap<_, _>>(); - let dependencies = read_workspace_package_dependencies(workspace_root) - .expect("workspace package manifests were already parsed"); + validate_public_dependency_closure(&metadata_packages, &public_set)?; for crate_name in &public_set { - let crate_deps = &dependencies[crate_name]; + let crate_deps = &metadata_packages[crate_name].dependencies; let crate_order = order_index[crate_name]; for dep in crate_deps { - if !public_set.contains(dep) { + if !public_set.contains(&dep.name) { continue; } - let dep_order = order_index[dep]; + let dep_order = order_index[&dep.name]; if dep_order >= crate_order { return Err(format!( "publish order must place dependency {} before {}", - dep, crate_name + dep.name, crate_name )); } } } - let publish_configs = workspace_package_publish_configs(workspace_root) - .expect("workspace publish configs are stable"); for crate_name in &public_set { - let publish = publish_configs[crate_name].as_ref(); - if !publish_config_is_public(publish) { + let publish = metadata_packages[crate_name].publish.as_ref(); + if !metadata_publish_config_is_public(publish) { return Err(format!( "public crate {} must set publish = [\"crates-io\"]", crate_name @@ -8735,8 +8903,8 @@ fn validate_release_publish_policy_with_override( .chain(retired_set.iter()) .chain(yank_only_set.iter()) { - let publish = publish_configs[crate_name].as_ref(); - if !publish_config_is_non_public(publish) { + let publish = metadata_packages[crate_name].publish.as_ref(); + if !metadata_publish_config_is_non_public(publish) { return Err(format!( "non-public crate {} must set publish = false", crate_name @@ -9271,14 +9439,18 @@ summary = "Exercise synthetic major release governance." ); write_file( &root_release_policy_path(&root), - r#"[release] -version = "1.0.0" + r#"[schema] +version = 1 -[publish] -crates = ["radroots_a"] +[release] +version = "1.0.0" -[internal] -crates = ["radroots_b"] +[classification] +public = ["radroots_a"] +internal = ["radroots_b"] +deferred = [] +retired = [] +yank_only = [] [publish_order] crates = ["radroots_a"] @@ -9454,7 +9626,199 @@ vector = "contracts/conformance/vectors/operational_listing/build_draft.v1.json" } fn write_root_release_policy(root: &Path, raw: &str) { - write_file(&root_release_policy_path(root), raw); + write_file( + &root_release_policy_path(root), + &format!("[schema]\nversion = 1\n\n{raw}"), + ); + } + + fn write_basic_release_policy( + root: &Path, + version: &str, + public: &[&str], + internal: &[&str], + publish_order: &[&str], + ) { + let render = |values: &[&str]| { + values + .iter() + .map(|value| format!("{value:?}")) + .collect::<Vec<_>>() + .join(", ") + }; + write_root_release_policy( + root, + &format!( + "[release]\nversion = {version:?}\n\n[classification]\npublic = [{}]\ninternal = [{}]\ndeferred = []\nretired = []\nyank_only = []\n\n[publish_order]\ncrates = [{}]\n", + render(public), + render(internal), + render(publish_order) + ), + ); + } + + fn metadata_dependency( + name: &str, + source: Option<&str>, + kind: Option<&str>, + rename: Option<&str>, + optional: bool, + target: Option<&str>, + path: Option<&str>, + ) -> CargoMetadataDependency { + CargoMetadataDependency { + name: name.to_string(), + source: source.map(str::to_string), + kind: kind.map(str::to_string), + rename: rename.map(str::to_string), + optional, + target: target.map(str::to_string), + path: path.map(PathBuf::from), + } + } + + #[test] + fn release_policy_metadata_covers_all_dependency_forms() { + let metadata = CargoMetadataDocument { + workspace_members: vec![ + "path+file:///workspace/a#radroots_a@1.0.0".to_string(), + "path+file:///workspace/b#radroots_b@1.0.0".to_string(), + "path+file:///workspace/c#radroots_c@1.0.0".to_string(), + ], + packages: vec![ + CargoMetadataPackage { + id: "path+file:///workspace/a#radroots_a@1.0.0".to_string(), + name: "radroots_a".to_string(), + publish: Some(vec!["crates-io".to_string()]), + dependencies: vec![ + metadata_dependency( + "radroots_b", + None, + Some("build"), + Some("renamed_b"), + true, + Some("cfg(target_os = \"ios\")"), + Some("/workspace/b"), + ), + metadata_dependency( + "radroots_c", + None, + Some("dev"), + None, + false, + None, + Some("/workspace/c"), + ), + metadata_dependency( + "serde", + Some("registry+https://github.com/rust-lang/crates.io-index"), + None, + None, + false, + None, + None, + ), + ], + }, + CargoMetadataPackage { + id: "path+file:///workspace/b#radroots_b@1.0.0".to_string(), + name: "radroots_b".to_string(), + publish: Some(vec!["crates-io".to_string()]), + dependencies: Vec::new(), + }, + CargoMetadataPackage { + id: "path+file:///workspace/c#radroots_c@1.0.0".to_string(), + name: "radroots_c".to_string(), + publish: Some(Vec::new()), + dependencies: Vec::new(), + }, + ], + }; + let mut packages = + release_workspace_packages_from_metadata(metadata).expect("parse cargo metadata"); + assert_eq!(packages["radroots_a"].dependencies.len(), 2); + + let public = BTreeSet::from(["radroots_a".to_string(), "radroots_b".to_string()]); + validate_public_dependency_closure(&packages, &public) + .expect("renamed optional target build dependency must be covered"); + + packages + .get_mut("radroots_a") + .expect("public package") + .dependencies + .push(metadata_dependency( + "radroots_c", + None, + None, + Some("internal_alias"), + true, + Some("cfg(unix)"), + Some("/workspace/c"), + )); + let internal_err = validate_public_dependency_closure(&packages, &public) + .expect_err("public dependency on internal package must fail"); + assert!(internal_err.contains("non-public normal dependency radroots_c")); + assert!(internal_err.contains("rename=internal_alias, optional=true, target=cfg(unix)")); + + let dependencies = &mut packages + .get_mut("radroots_a") + .expect("public package") + .dependencies; + dependencies.pop(); + dependencies.push(metadata_dependency( + "external_path", + None, + None, + None, + false, + None, + Some("/outside/workspace"), + )); + assert!( + validate_public_dependency_closure(&packages, &public) + .expect_err("external path dependency must fail") + .contains("unversioned path dependency external_path") + ); + + let dependencies = &mut packages + .get_mut("radroots_a") + .expect("public package") + .dependencies; + dependencies.pop(); + dependencies.push(metadata_dependency( + "git_dep", + Some("git+https://example.com/dependency"), + None, + None, + false, + None, + None, + )); + assert!( + validate_public_dependency_closure(&packages, &public) + .expect_err("git dependency must fail") + .contains("non-registry dependency git_dep") + ); + + let dependencies = &mut packages + .get_mut("radroots_a") + .expect("public package") + .dependencies; + dependencies.pop(); + dependencies.push(metadata_dependency( + "source_less", + None, + None, + None, + false, + None, + None, + )); + assert!( + validate_public_dependency_closure(&packages, &public) + .expect_err("source-less external dependency must fail") + .contains("without a registry source") + ); } fn configure_root_release_policy_workspace(root: &Path) { @@ -11383,122 +11747,30 @@ readme = { workspace = true } } #[test] - fn release_contract_helpers_cover_classification_and_env_override_paths() { - let release = ReleaseSection { - version: "1.0.0".to_string(), - }; - let empty_order = ReleaseCrateSet { crates: Vec::new() }; - - let legacy = ReleaseContractFile { - release: ReleaseSection { - version: release.version.clone(), - }, - classification: ReleaseClassification::default(), - publish: Some(ReleaseCrateSet { - crates: vec!["radroots_public".to_string()], - }), - internal: Some(ReleaseCrateSet { - crates: vec!["radroots_internal".to_string()], - }), - publish_order: ReleaseCrateSet { - crates: empty_order.crates.clone(), - }, - }; - assert!(!legacy.uses_classification()); - assert_eq!(legacy.public_crates(), vec!["radroots_public".to_string()]); - assert_eq!( - legacy.internal_crates(), - vec!["radroots_internal".to_string()] - ); - - let empty_legacy = ReleaseContractFile { - release: ReleaseSection { - version: release.version.clone(), - }, - classification: ReleaseClassification::default(), - publish: None, - internal: None, - publish_order: ReleaseCrateSet { - crates: empty_order.crates.clone(), - }, - }; - assert!(!empty_legacy.uses_classification()); - assert_eq!(empty_legacy.public_crates(), Vec::<String>::new()); - assert_eq!(empty_legacy.internal_crates(), Vec::<String>::new()); - - let internal = ReleaseContractFile { - release: ReleaseSection { - version: release.version.clone(), - }, - classification: ReleaseClassification { - internal: vec!["radroots_internal_only".to_string()], - ..ReleaseClassification::default() - }, - publish: None, - internal: None, - publish_order: ReleaseCrateSet { - crates: empty_order.crates.clone(), - }, - }; - assert!(internal.uses_classification()); - - let deferred = ReleaseContractFile { + fn release_contract_helpers_cover_strict_classification_and_override_paths() { + let release = ReleaseContractFile { + schema: ReleasePolicySchema { version: 1 }, release: ReleaseSection { - version: release.version.clone(), + version: "1.0.0".to_string(), }, classification: ReleaseClassification { + public: vec!["radroots_public".to_string()], + internal: vec!["radroots_internal".to_string()], deferred: vec!["radroots_deferred".to_string()], - ..ReleaseClassification::default() - }, - publish: None, - internal: None, - publish_order: ReleaseCrateSet { - crates: empty_order.crates.clone(), - }, - }; - assert!(deferred.uses_classification()); - assert_eq!( - deferred.deferred_crates(), - vec!["radroots_deferred".to_string()] - ); - - let retired = ReleaseContractFile { - release: ReleaseSection { - version: release.version.clone(), - }, - classification: ReleaseClassification { retired: vec!["radroots_retired".to_string()], - ..ReleaseClassification::default() + yank_only: vec!["radroots_yank_only".to_string()], }, - publish: None, - internal: None, publish_order: ReleaseCrateSet { - crates: empty_order.crates.clone(), - }, - }; - assert!(retired.uses_classification()); - assert_eq!( - retired.retired_crates(), - vec!["radroots_retired".to_string()] - ); - - let yank_only = ReleaseContractFile { - release, - classification: ReleaseClassification { - yank_only: vec!["radroots_yank_only".to_string()], - ..ReleaseClassification::default() + crates: vec!["radroots_public".to_string()], }, - publish: None, - internal: None, - publish_order: empty_order, }; - assert!(yank_only.uses_classification()); - assert_eq!( - yank_only.yank_only_crates(), - vec!["radroots_yank_only".to_string()] - ); + assert_eq!(release.public_crates(), vec!["radroots_public"]); + assert_eq!(release.internal_crates(), vec!["radroots_internal"]); + assert_eq!(release.deferred_crates(), vec!["radroots_deferred"]); + assert_eq!(release.retired_crates(), vec!["radroots_retired"]); + assert_eq!(release.yank_only_crates(), vec!["radroots_yank_only"]); - let root = create_synthetic_workspace("release_contract_env_override"); + let root = create_synthetic_workspace("release_contract_strict_override"); let policy_path = root_release_policy_path(&root); let resolved = resolve_release_contract_path_with_override(&root, "1.0.0", Some(policy_path.clone())) @@ -11514,7 +11786,6 @@ readme = { workspace = true } .expect_err("missing fixture policy should fail"); assert!(err.contains("release policy override points to a missing file")); assert!(err.contains(&missing_policy.display().to_string())); - let _ = fs::remove_dir_all(&root); } @@ -11810,117 +12081,50 @@ crates = ["unknown"] fn release_publish_policy_reports_contract_errors() { let root = create_synthetic_workspace("release_policy_errors"); let contract_root = root.join("contracts"); - let release_policy_path = root_release_policy_path(&root); - - write_file( - &release_policy_path, - r#"[release] -version = "" - -[publish] -crates = ["radroots_a"] -[internal] -crates = ["radroots_b"] - -[publish_order] -crates = ["radroots_a"] -"#, - ); + write_basic_release_policy(&root, "", &["radroots_a"], &["radroots_b"], &["radroots_a"]); let empty_version = validate_release_publish_policy(&root, &contract_root, "1.0.0") .expect_err("empty release version"); assert!(empty_version.contains("must not be empty")); - write_file( - &release_policy_path, - r#"[release] -version = "2.0.0" - -[publish] -crates = ["radroots_a"] - -[internal] -crates = ["radroots_b"] - -[publish_order] -crates = ["radroots_a"] -"#, + write_basic_release_policy( + &root, + "2.0.0", + &["radroots_a"], + &["radroots_b"], + &["radroots_a"], ); let version_mismatch = validate_release_publish_policy(&root, &contract_root, "1.0.0") .expect_err("release version mismatch"); assert!(version_mismatch.contains("must match contract version")); - write_file( - &release_policy_path, - r#"[release] -version = "1.0.0" - -[publish] -crates = ["radroots_a"] - -[internal] -crates = ["radroots_a"] - -[publish_order] -crates = ["radroots_a"] -"#, + write_basic_release_policy( + &root, + "1.0.0", + &["radroots_a"], + &["radroots_a"], + &["radroots_a"], ); let overlap = validate_release_publish_policy(&root, &contract_root, "1.0.0") .expect_err("publish/internal overlap"); assert!(overlap.contains("overlap is not allowed")); - write_file( - &release_policy_path, - r#"[release] -version = "1.0.0" - -[publish] -crates = ["radroots_a"] - -[internal] -crates = [] - -[publish_order] -crates = ["radroots_a"] -"#, - ); + write_basic_release_policy(&root, "1.0.0", &["radroots_a"], &[], &["radroots_a"]); let missing_workspace = validate_release_publish_policy(&root, &contract_root, "1.0.0") .expect_err("missing workspace crate"); assert!(missing_workspace.contains("missing workspace crates")); - write_file( - &release_policy_path, - r#"[release] -version = "1.0.0" - -[publish] -crates = ["radroots_a"] - -[internal] -crates = ["radroots_b"] - -[publish_order] -crates = [] -"#, - ); + write_basic_release_policy(&root, "1.0.0", &["radroots_a"], &["radroots_b"], &[]); let missing_publish_order = validate_release_publish_policy(&root, &contract_root, "1.0.0") .expect_err("missing publish order entries"); assert!(missing_publish_order.contains("missing publish crates")); - write_file( - &release_policy_path, - r#"[release] -version = "1.0.0" - -[publish] -crates = ["radroots_a"] - -[internal] -crates = ["radroots_b"] - -[publish_order] -crates = ["radroots_a", "radroots_b"] -"#, + write_basic_release_policy( + &root, + "1.0.0", + &["radroots_a"], + &["radroots_b"], + &["radroots_a", "radroots_b"], ); let extra_publish_order = validate_release_publish_policy(&root, &contract_root, "1.0.0") .expect_err("extra publish order entries"); @@ -11956,39 +12160,23 @@ documentation = "https://docs.example.com/b" readme = "README" "#, ); - write_file( - &release_policy_path, - r#"[release] -version = "1.0.0" - -[publish] -crates = ["radroots_a", "radroots_b"] - -[internal] -crates = [] - -[publish_order] -crates = ["radroots_a", "radroots_b"] -"#, + write_basic_release_policy( + &root, + "1.0.0", + &["radroots_a", "radroots_b"], + &[], + &["radroots_a", "radroots_b"], ); let dependency_order = validate_release_publish_policy(&root, &contract_root, "1.0.0") .expect_err("dependency order violation"); assert!(dependency_order.contains("must place dependency")); - write_file( - &release_policy_path, - r#"[release] -version = "1.0.0" - -[publish] -crates = ["radroots_a"] - -[internal] -crates = ["radroots_b"] - -[publish_order] -crates = ["radroots_a"] -"#, + write_basic_release_policy( + &root, + "1.0.0", + &["radroots_a"], + &["radroots_b"], + &["radroots_a"], ); write_file( &root.join("crates").join("b").join("Cargo.toml"), @@ -11999,8 +12187,9 @@ edition = "2024" publish = false "#, ); - validate_release_publish_policy(&root, &contract_root, "1.0.0") - .expect("internal dependency should be ignored in publish ordering"); + let internal_dependency = validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect_err("public dependency on internal package must fail"); + assert!(internal_dependency.contains("non-public normal dependency radroots_b")); write_file( &root.join("crates").join("a").join("Cargo.toml"), @@ -12011,20 +12200,12 @@ edition = "2024" publish = false "#, ); - write_file( - &release_policy_path, - r#"[release] -version = "1.0.0" - -[publish] -crates = ["radroots_a"] - -[internal] -crates = ["radroots_b"] - -[publish_order] -crates = ["radroots_a"] -"#, + write_basic_release_policy( + &root, + "1.0.0", + &["radroots_a"], + &["radroots_b"], + &["radroots_a"], ); let publish_flag = validate_release_publish_policy(&root, &contract_root, "1.0.0") .expect_err("publish crate must be publishable"); @@ -12776,20 +12957,12 @@ crates = ["radroots_a", "radroots_b"] ); let missing_release = temp_root("release_missing_workspace_manifest"); - write_root_release_policy( + write_basic_release_policy( &missing_release, - r#"[release] -version = "1.0.0" - -[publish] -crates = ["radroots_a"] - -[internal] -crates = ["radroots_b"] - -[publish_order] -crates = ["radroots_a"] -"#, + "1.0.0", + &["radroots_a"], + &["radroots_b"], + &["radroots_a"], ); let release_workspace_err = validate_release_publish_policy(&missing_release, &contract_root, "1.0.0") @@ -12802,77 +12975,45 @@ crates = ["radroots_a"] .expect_err("release contract read error"); assert!(release_load_err.contains(RELEASE_POLICY_RELATIVE)); - write_file( - &release_policy_path, - r#"[release] -version = "1.0.0" - -[publish] -crates = ["radroots_a", "radroots_a"] - -[internal] -crates = ["radroots_b"] - -[publish_order] -crates = ["radroots_a"] -"#, + write_basic_release_policy( + &root, + "1.0.0", + &["radroots_a", "radroots_a"], + &["radroots_b"], + &["radroots_a"], ); let duplicate_publish = validate_release_publish_policy(&root, &contract_root, "1.0.0") .expect_err("duplicate publish crates"); - assert!(duplicate_publish.contains("publish.crates has duplicate crate")); - - write_file( - &release_policy_path, - r#"[release] -version = "1.0.0" + assert!(duplicate_publish.contains("classification.public has duplicate crate")); -[publish] -crates = ["radroots_a"] - -[internal] -crates = ["radroots_b", "radroots_b"] - -[publish_order] -crates = ["radroots_a"] -"#, + write_basic_release_policy( + &root, + "1.0.0", + &["radroots_a"], + &["radroots_b", "radroots_b"], + &["radroots_a"], ); let duplicate_internal = validate_release_publish_policy(&root, &contract_root, "1.0.0") .expect_err("duplicate internal crates"); - assert!(duplicate_internal.contains("internal.crates has duplicate crate")); - - write_file( - &release_policy_path, - r#"[release] -version = "1.0.0" - -[publish] -crates = ["radroots_a"] + assert!(duplicate_internal.contains("classification.internal has duplicate crate")); -[internal] -crates = ["radroots_b"] - -[publish_order] -crates = ["radroots_a", "radroots_a"] -"#, + write_basic_release_policy( + &root, + "1.0.0", + &["radroots_a"], + &["radroots_b"], + &["radroots_a", "radroots_a"], ); let duplicate_order = validate_release_publish_policy(&root, &contract_root, "1.0.0") .expect_err("duplicate publish order"); assert!(duplicate_order.contains("publish_order.crates has duplicate crate")); - write_file( - &release_policy_path, - r#"[release] -version = "1.0.0" - -[publish] -crates = ["radroots_a"] - -[internal] -crates = ["radroots_b"] - -[publish_order] -crates = ["radroots_a"] -"#, + write_basic_release_policy( + &root, + "1.0.0", + &["radroots_a"], + &["radroots_b"], + &["radroots_a"], ); write_file( &root.join("crates").join("a").join("Cargo.toml"), @@ -13071,14 +13212,18 @@ require_conformance_vectors = true let duplicate_publish = create_synthetic_workspace("preflight_duplicate_publish"); write_file( &root_release_policy_path(&duplicate_publish), - r#"[release] -version = "1.0.0" + r#"[schema] +version = 1 -[publish] -crates = ["radroots_a", "radroots_a"] +[release] +version = "1.0.0" -[internal] -crates = ["radroots_b"] +[classification] +public = ["radroots_a", "radroots_a"] +internal = ["radroots_b"] +deferred = [] +retired = [] +yank_only = [] [publish_order] crates = ["radroots_a"] @@ -13086,7 +13231,7 @@ crates = ["radroots_a"] ); let duplicate_publish_err = validate_generic_release_preflight(&duplicate_publish) .expect_err("duplicate publish crates"); - assert!(duplicate_publish_err.contains("publish.crates has duplicate crate")); + assert!(duplicate_publish_err.contains("classification.public has duplicate crate")); let _ = fs::remove_dir_all(&duplicate_publish); let duplicate_required = create_synthetic_workspace("preflight_duplicate_required"); @@ -13336,7 +13481,6 @@ Volume, let root = create_synthetic_workspace("coverage_release_extra_errors"); let contract_root = root.join("contracts"); let coverage_root = coverage_root(&contract_root); - let release_policy_path = root_release_policy_path(&root); write_file( &coverage_root.join("coverage.toml"), @@ -13372,39 +13516,23 @@ crates = ["radroots_b"] .expect_err("required list must match workspace crates"); assert!(required_list_mismatch.contains("missing workspace crates")); - write_file( - &release_policy_path, - r#"[release] -version = "1.0.0" - -[publish] -crates = ["radroots_a", "radroots_b", "radroots_extra"] - -[internal] -crates = [] - -[publish_order] -crates = ["radroots_a", "radroots_b"] -"#, + write_basic_release_policy( + &root, + "1.0.0", + &["radroots_a", "radroots_b", "radroots_extra"], + &[], + &["radroots_a", "radroots_b"], ); let release_extra = validate_release_publish_policy(&root, &contract_root, "1.0.0") .expect_err("release extra crate"); assert!(release_extra.contains("include unknown crates")); - write_file( - &release_policy_path, - r#"[release] -version = "1.0.0" - -[publish] -crates = ["radroots_a"] - -[internal] -crates = ["radroots_b"] - -[publish_order] -crates = ["radroots_a", "radroots_b"] -"#, + write_basic_release_policy( + &root, + "1.0.0", + &["radroots_a"], + &["radroots_b"], + &["radroots_a", "radroots_b"], ); let publish_order_extra = validate_release_publish_policy(&root, &contract_root, "1.0.0") .expect_err("publish order non-publish crate"); @@ -13416,16 +13544,17 @@ crates = ["radroots_a", "radroots_b"] #[test] fn validate_contract_bundle_reports_release_policy_errors() { let release_error_root = create_synthetic_workspace("bundle_release_policy_error"); - write_file( - &root_release_policy_path(&release_error_root), + write_root_release_policy( + &release_error_root, r#"[release] version = "1.0.0" -[publish] -crates = ["radroots_a"] - -[internal] -crates = ["radroots_b"] +[classification] +public = ["radroots_a"] +internal = ["radroots_b"] +deferred = [] +retired = [] +yank_only = [] [publish_order] crates = [] diff --git a/tools/xtask/src/contract/release_provenance.rs b/tools/xtask/src/contract/release_provenance.rs @@ -11,6 +11,7 @@ use std::path::{Component, Path, PathBuf}; use std::process::Command; const SCHEMA_VERSION: u32 = 1; +const PUBLISH_POLICY_SCHEMA_VERSION: u32 = 1; const CONTRACT_ID: &str = "radroots.release.phase1_publication_provenance.v1"; const AUTHORITY: &str = "candidate_release_evidence_not_protocol_authority_v1"; const HASH_ALGORITHM: &str = "sha256_bytes_v1"; @@ -173,6 +174,7 @@ struct TestProfiles { #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] struct PublishPolicy { + schema: PublishPolicySchema, release: ReleaseVersion, classification: ReleaseClassification, publish_order: PublishOrder, @@ -180,6 +182,12 @@ struct PublishPolicy { #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] +struct PublishPolicySchema { + version: u32, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct ReleaseVersion { version: String, } @@ -414,6 +422,11 @@ fn validate_test_profile(label: &str, profile: &TestProfile) -> Result<(), Strin } fn validate_publish_policy(policy: &PublishPolicy) -> Result<(), String> { + if policy.schema.version != PUBLISH_POLICY_SCHEMA_VERSION { + return Err(format!( + "release publish policy schema version must be {PUBLISH_POLICY_SCHEMA_VERSION}" + )); + } if policy.release.version.trim().is_empty() { return Err("release.version must not be empty".to_owned()); } @@ -1288,6 +1301,9 @@ mod tests { fn test_policy() -> PublishPolicy { PublishPolicy { + schema: PublishPolicySchema { + version: PUBLISH_POLICY_SCHEMA_VERSION, + }, release: ReleaseVersion { version: "1.2.3".to_owned(), },